From 78bf4702f02866a83ed16506e5077cef5c6b879f Mon Sep 17 00:00:00 2001 From: Peter Stace Date: Thu, 5 Feb 2026 20:19:47 +1100 Subject: [PATCH 1/9] Add Buffer operation Adds the Buffer operation by porting the relevant code from the JTS library and exposing it via a thin wrapper in the `geom` package. The buffer operation is configured via the "optional parameters" Go pattern, which was the same approach as taken for the buffer operation when exposed from the GEOS package. --- CHANGELOG.md | 5 + geom/alg_buffer.go | 159 ++++ geom/alg_buffer_test.go | 167 +++++ internal/jtsport/MANIFEST.csv | 87 ++- internal/jtsport/TRANSLITERATION_GUIDE.md | 7 + ...ithm_distance_discrete_frechet_distance.go | 461 ++++++++++++ ...distance_discrete_frechet_distance_test.go | 78 ++ ...hm_distance_discrete_hausdorff_distance.go | 198 +++++ ...stance_discrete_hausdorff_distance_test.go | 57 ++ .../algorithm_distance_distance_to_point.go | 60 ++ .../algorithm_distance_point_pair_distance.go | 93 +++ .../jtsport/jts/geom_geometry_overlay_test.go | 24 +- internal/jtsport/jts/geom_point.go | 4 +- .../jts/geomgraph_edge_noding_validator.go | 6 +- internal/jtsport/jts/geomgraph_node_map.go | 16 +- internal/jtsport/jts/index_strtree_strtree.go | 6 + ...tstest_geomop_geometry_method_operation.go | 50 +- .../jts/noding_fast_noding_validator.go | 111 +++ .../jts/noding_fast_noding_validator_test.go | 130 ++++ .../jts/noding_noding_intersection_finder.go | 284 ++++++++ internal/jtsport/jts/noding_scaled_noder.go | 109 +++ .../jtsport/jts/noding_validating_noder.go | 8 +- .../jts/operation_buffer_buffer_builder.go | 246 +++++++ ...eration_buffer_buffer_curve_set_builder.go | 371 ++++++++++ ...ion_buffer_buffer_input_line_simplifier.go | 188 +++++ .../jtsport/jts/operation_buffer_buffer_op.go | 300 ++++++++ .../operation_buffer_buffer_parameter_test.go | 170 +++++ .../jts/operation_buffer_buffer_parameters.go | 213 ++++++ ...ion_buffer_buffer_result_validator_test.go | 27 + .../jts/operation_buffer_buffer_subgraph.go | 223 ++++++ .../jts/operation_buffer_buffer_test.go | 689 ++++++++++++++++++ .../operation_buffer_buffer_validator_test.go | 263 +++++++ .../operation_buffer_depth_segment_test.go | 52 ++ .../jts/operation_buffer_offset_curve.go | 522 +++++++++++++ .../operation_buffer_offset_curve_builder.go | 274 +++++++ .../operation_buffer_offset_curve_section.go | 119 +++ .../jts/operation_buffer_offset_curve_test.go | 390 ++++++++++ ...eration_buffer_offset_segment_generator.go | 515 +++++++++++++ .../operation_buffer_offset_segment_string.go | 100 +++ .../operation_buffer_rightmost_edge_finder.go | 143 ++++ .../jts/operation_buffer_segment_mc_index.go | 31 + ...operation_buffer_subgraph_depth_locater.go | 205 ++++++ ...lidate_buffer_curve_max_distance_finder.go | 126 ++++ ...ffer_validate_buffer_distance_validator.go | 181 +++++ ...buffer_validate_buffer_result_validator.go | 196 +++++ ...uffer_validate_distance_to_point_finder.go | 60 ++ ...ion_buffer_validate_point_pair_distance.go | 91 +++ .../jts/operation_buffer_variable_buffer.go | 387 ++++++++++ .../operation_buffer_variable_buffer_test.go | 134 ++++ .../operation_distance_base_distance_test.go | 109 +++ ...tance_connected_element_location_filter.go | 46 ++ ...distance_connected_element_point_filter.go | 40 + .../jts/operation_distance_distance_op.go | 380 ++++++++++ .../jts/operation_distance_distance_test.go | 78 ++ .../operation_distance_geometry_location.go | 67 ++ .../jts/operation_overlayng_overlay_graph.go | 9 + .../jts/operation_relateng_relate_edge.go | 3 +- ...ration_valid_indexed_nested_hole_tester.go | 76 ++ ...ion_valid_indexed_nested_polygon_tester.go | 156 ++++ .../jts/operation_valid_is_valid_op.go | 510 +++++++++++++ .../jts/operation_valid_is_valid_test.go | 186 +++++ ...ion_valid_polygon_intersection_analyzer.go | 223 ++++++ .../jts/operation_valid_polygon_ring.go | 361 +++++++++ ...eration_valid_polygon_topology_analyzer.go | 322 ++++++++ ...eration_valid_topology_validation_error.go | 120 +++ .../operation_valid_valid_closed_ring_test.go | 65 ++ ...ion_valid_valid_self_touching_ring_test.go | 143 ++++ internal/jtsport/jts/stubs.go | 106 --- internal/jtsport/jts/util_debug.go | 277 +++++++ internal/jtsport/jts/util_stopwatch.go | 78 ++ internal/jtsport/xmltest/runner_test.go | 25 +- internal/test/test.go | 19 +- 72 files changed, 11573 insertions(+), 162 deletions(-) create mode 100644 geom/alg_buffer.go create mode 100644 geom/alg_buffer_test.go create mode 100644 internal/jtsport/jts/algorithm_distance_discrete_frechet_distance.go create mode 100644 internal/jtsport/jts/algorithm_distance_discrete_frechet_distance_test.go create mode 100644 internal/jtsport/jts/algorithm_distance_discrete_hausdorff_distance.go create mode 100644 internal/jtsport/jts/algorithm_distance_discrete_hausdorff_distance_test.go create mode 100644 internal/jtsport/jts/algorithm_distance_distance_to_point.go create mode 100644 internal/jtsport/jts/algorithm_distance_point_pair_distance.go create mode 100644 internal/jtsport/jts/noding_fast_noding_validator.go create mode 100644 internal/jtsport/jts/noding_fast_noding_validator_test.go create mode 100644 internal/jtsport/jts/noding_noding_intersection_finder.go create mode 100644 internal/jtsport/jts/noding_scaled_noder.go create mode 100644 internal/jtsport/jts/operation_buffer_buffer_builder.go create mode 100644 internal/jtsport/jts/operation_buffer_buffer_curve_set_builder.go create mode 100644 internal/jtsport/jts/operation_buffer_buffer_input_line_simplifier.go create mode 100644 internal/jtsport/jts/operation_buffer_buffer_op.go create mode 100644 internal/jtsport/jts/operation_buffer_buffer_parameter_test.go create mode 100644 internal/jtsport/jts/operation_buffer_buffer_parameters.go create mode 100644 internal/jtsport/jts/operation_buffer_buffer_result_validator_test.go create mode 100644 internal/jtsport/jts/operation_buffer_buffer_subgraph.go create mode 100644 internal/jtsport/jts/operation_buffer_buffer_test.go create mode 100644 internal/jtsport/jts/operation_buffer_buffer_validator_test.go create mode 100644 internal/jtsport/jts/operation_buffer_depth_segment_test.go create mode 100644 internal/jtsport/jts/operation_buffer_offset_curve.go create mode 100644 internal/jtsport/jts/operation_buffer_offset_curve_builder.go create mode 100644 internal/jtsport/jts/operation_buffer_offset_curve_section.go create mode 100644 internal/jtsport/jts/operation_buffer_offset_curve_test.go create mode 100644 internal/jtsport/jts/operation_buffer_offset_segment_generator.go create mode 100644 internal/jtsport/jts/operation_buffer_offset_segment_string.go create mode 100644 internal/jtsport/jts/operation_buffer_rightmost_edge_finder.go create mode 100644 internal/jtsport/jts/operation_buffer_segment_mc_index.go create mode 100644 internal/jtsport/jts/operation_buffer_subgraph_depth_locater.go create mode 100644 internal/jtsport/jts/operation_buffer_validate_buffer_curve_max_distance_finder.go create mode 100644 internal/jtsport/jts/operation_buffer_validate_buffer_distance_validator.go create mode 100644 internal/jtsport/jts/operation_buffer_validate_buffer_result_validator.go create mode 100644 internal/jtsport/jts/operation_buffer_validate_distance_to_point_finder.go create mode 100644 internal/jtsport/jts/operation_buffer_validate_point_pair_distance.go create mode 100644 internal/jtsport/jts/operation_buffer_variable_buffer.go create mode 100644 internal/jtsport/jts/operation_buffer_variable_buffer_test.go create mode 100644 internal/jtsport/jts/operation_distance_base_distance_test.go create mode 100644 internal/jtsport/jts/operation_distance_connected_element_location_filter.go create mode 100644 internal/jtsport/jts/operation_distance_connected_element_point_filter.go create mode 100644 internal/jtsport/jts/operation_distance_distance_op.go create mode 100644 internal/jtsport/jts/operation_distance_distance_test.go create mode 100644 internal/jtsport/jts/operation_distance_geometry_location.go create mode 100644 internal/jtsport/jts/operation_valid_indexed_nested_hole_tester.go create mode 100644 internal/jtsport/jts/operation_valid_indexed_nested_polygon_tester.go create mode 100644 internal/jtsport/jts/operation_valid_is_valid_op.go create mode 100644 internal/jtsport/jts/operation_valid_is_valid_test.go create mode 100644 internal/jtsport/jts/operation_valid_polygon_intersection_analyzer.go create mode 100644 internal/jtsport/jts/operation_valid_polygon_ring.go create mode 100644 internal/jtsport/jts/operation_valid_polygon_topology_analyzer.go create mode 100644 internal/jtsport/jts/operation_valid_topology_validation_error.go create mode 100644 internal/jtsport/jts/operation_valid_valid_closed_ring_test.go create mode 100644 internal/jtsport/jts/operation_valid_valid_self_touching_ring_test.go create mode 100644 internal/jtsport/jts/util_debug.go create mode 100644 internal/jtsport/jts/util_stopwatch.go diff --git a/CHANGELOG.md b/CHANGELOG.md index 65dd60cc..40cad904 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,11 @@ ## Unreleased +- Add `Buffer` function that computes the buffer of a geometry at a given + radius. Options are available for controlling quad segments, end cap style + (round, flat, square), join style (round, mitre, bevel), single-sided mode, + and simplify factor. The implementation is based on a port of JTS. + - Change `GeometryCollection.Dimension()` to return -1 for empty geometry collections (previously returned 0). This change is consistent with GEOS. diff --git a/geom/alg_buffer.go b/geom/alg_buffer.go new file mode 100644 index 00000000..85705655 --- /dev/null +++ b/geom/alg_buffer.go @@ -0,0 +1,159 @@ +package geom + +import ( + "github.com/peterstace/simplefeatures/internal/jtsport/jts" +) + +// Buffer returns a geometry that contains all points within the given radius +// of the input geometry. +// +// In GIS, the positive (or negative) buffer of a geometry is defined as +// the Minkowski sum (or difference) of the geometry with a circle of radius +// equal to the absolute value of the buffer distance. +// +// The buffer operation always returns a polygonal result. +// The negative or zero-distance buffer of lines and points is always an empty +// Polygon. +// +// Since true buffer curves may contain circular arcs, computed buffer polygons +// are only approximations to the true geometry. The user can control the +// accuracy of the approximation by specifying the number of segments used to +// approximate quarter circles (via BufferQuadSegments). +// +// An error may be returned in pathological cases of numerical degeneracy. +func Buffer(g Geometry, radius float64, opts ...BufferOption) (Geometry, error) { + optSet := newBufferOptionSet(opts) + + params := jts.OperationBuffer_NewBufferParameters() + params.SetQuadrantSegments(optSet.quadSegments) + params.SetEndCapStyle(optSet.endCapStyle) + params.SetJoinStyle(optSet.joinStyle) + params.SetMitreLimit(optSet.mitreLimit) + params.SetSingleSided(optSet.isSingleSided) + params.SetSimplifyFactor(optSet.simplifyFactor) + + var result Geometry + err := catch(func() error { + wkbReader := jts.Io_NewWKBReader() + jtsG, err := wkbReader.ReadBytes(g.AsBinary()) + if err != nil { + return wrap(err, "converting geometry to JTS") + } + jtsResult := jts.OperationBuffer_BufferOp_BufferOpWithParams(jtsG, radius, params) + wkbWriter := jts.Io_NewWKBWriter() + result, err = UnmarshalWKB(wkbWriter.Write(jtsResult), NoValidate{}) + return wrap(err, "converting JTS buffer result to simplefeatures") + }) + return result, err +} + +// BufferOption allows the behaviour of the Buffer operation to be modified. +type BufferOption func(*bufferOptionSet) + +type bufferOptionSet struct { + quadSegments int + endCapStyle int + joinStyle int + mitreLimit float64 + isSingleSided bool + simplifyFactor float64 +} + +func newBufferOptionSet(opts []BufferOption) bufferOptionSet { + bos := bufferOptionSet{ + quadSegments: jts.OperationBuffer_BufferParameters_DEFAULT_QUADRANT_SEGMENTS, + endCapStyle: jts.OperationBuffer_BufferParameters_CAP_ROUND, + joinStyle: jts.OperationBuffer_BufferParameters_JOIN_ROUND, + mitreLimit: jts.OperationBuffer_BufferParameters_DEFAULT_MITRE_LIMIT, + isSingleSided: false, + simplifyFactor: jts.OperationBuffer_BufferParameters_DEFAULT_SIMPLIFY_FACTOR, + } + for _, opt := range opts { + opt(&bos) + } + return bos +} + +// BufferQuadSegments sets the number of segments used to approximate a quarter +// circle. It defaults to 8. +func BufferQuadSegments(quadSegments int) BufferOption { + return func(bos *bufferOptionSet) { + bos.quadSegments = quadSegments + } +} + +// BufferEndCapRound sets the end cap style to 'round'. It is 'round' by +// default. +func BufferEndCapRound() BufferOption { + return func(bos *bufferOptionSet) { + bos.endCapStyle = jts.OperationBuffer_BufferParameters_CAP_ROUND + } +} + +// BufferEndCapFlat sets the end cap style to 'flat'. It is 'round' by default. +func BufferEndCapFlat() BufferOption { + return func(bos *bufferOptionSet) { + bos.endCapStyle = jts.OperationBuffer_BufferParameters_CAP_FLAT + } +} + +// BufferEndCapSquare sets the end cap style to 'square'. It is 'round' by +// default. +func BufferEndCapSquare() BufferOption { + return func(bos *bufferOptionSet) { + bos.endCapStyle = jts.OperationBuffer_BufferParameters_CAP_SQUARE + } +} + +// BufferJoinStyleRound sets the join style to 'round'. It is 'round' by +// default. +func BufferJoinStyleRound() BufferOption { + return func(bos *bufferOptionSet) { + bos.joinStyle = jts.OperationBuffer_BufferParameters_JOIN_ROUND + } +} + +// BufferJoinStyleMitre sets the join style to 'mitre'. It is 'round' by +// default. The mitreLimit controls how far a mitre join can extend from the +// join point. Corners with a ratio which exceed the limit will be beveled. +func BufferJoinStyleMitre(mitreLimit float64) BufferOption { + return func(bos *bufferOptionSet) { + bos.joinStyle = jts.OperationBuffer_BufferParameters_JOIN_MITRE + bos.mitreLimit = mitreLimit + } +} + +// BufferJoinStyleBevel sets the join style to 'bevel'. It is 'round' by +// default. +func BufferJoinStyleBevel() BufferOption { + return func(bos *bufferOptionSet) { + bos.joinStyle = jts.OperationBuffer_BufferParameters_JOIN_BEVEL + } +} + +// BufferSingleSided sets whether the computed buffer should be single-sided. +// A single-sided buffer is constructed on only one side of each input line. +// The side is determined by the sign of the buffer distance: positive +// indicates the left-hand side, negative indicates the right-hand side. +// The end cap style is ignored for single-sided buffers and forced to flat. +func BufferSingleSided() BufferOption { + return func(bos *bufferOptionSet) { + bos.isSingleSided = true + } +} + +// BufferSimplifyFactor sets the factor used to determine the simplify distance +// tolerance for input simplification. The factor is multiplied by the buffer +// distance to get the simplification tolerance. Simplifying can increase the +// performance of computing buffers. Values between 0.01 and 0.1 produce +// relatively good accuracy for the generated buffer. Larger values sacrifice +// accuracy in return for performance. The default is 0.01. +func BufferSimplifyFactor(factor float64) BufferOption { + return func(bos *bufferOptionSet) { + if factor < 0 { + bos.simplifyFactor = 0 + } else { + bos.simplifyFactor = factor + } + } +} diff --git a/geom/alg_buffer_test.go b/geom/alg_buffer_test.go new file mode 100644 index 00000000..ed624a02 --- /dev/null +++ b/geom/alg_buffer_test.go @@ -0,0 +1,167 @@ +package geom_test + +import ( + "strconv" + "testing" + + "github.com/peterstace/simplefeatures/geom" + "github.com/peterstace/simplefeatures/internal/test" +) + +func TestBuffer(t *testing.T) { + for i, tc := range []struct { + name string + input string + dist float64 + opts []geom.BufferOption + want string + }{ + { + name: "point with defaults", + input: "POINT(0 0)", + dist: 1, + opts: nil, + want: "POLYGON((1 0,0.9807852804032304 -0.19509032201612825,0.9238795325112867 -0.3826834323650898,0.8314696123025453 -0.5555702330196022,0.7071067811865476 -0.7071067811865475,0.5555702330196023 -0.8314696123025452,0.38268343236508984 -0.9238795325112867,0.1950903220161283 -0.9807852804032304,0 -1,-0.1950903220161282 -0.9807852804032304,-0.3826834323650897 -0.9238795325112867,-0.555570233019602 -0.8314696123025453,-0.7071067811865475 -0.7071067811865476,-0.8314696123025453 -0.5555702330196021,-0.9238795325112867 -0.38268343236508984,-0.9807852804032303 -0.19509032201612858,-1 0,-0.9807852804032304 0.19509032201612836,-0.9238795325112868 0.38268343236508967,-0.8314696123025453 0.555570233019602,-0.7071067811865477 0.7071067811865475,-0.5555702330196022 0.8314696123025453,-0.38268343236509034 0.9238795325112865,-0.19509032201612866 0.9807852804032303,0 1,0.19509032201612828 0.9807852804032304,0.38268343236509 0.9238795325112866,0.5555702330196018 0.8314696123025455,0.7071067811865475 0.7071067811865477,0.8314696123025453 0.5555702330196022,0.9238795325112865 0.3826834323650904,0.9807852804032303 0.19509032201612872,1 0))", + }, + { + name: "distance of 2", + input: "POINT(0 0)", + dist: 2, + opts: nil, + want: "POLYGON((2 0,1.9615705608064609 -0.3901806440322565,1.8477590650225735 -0.7653668647301796,1.6629392246050907 -1.1111404660392044,1.4142135623730951 -1.414213562373095,1.1111404660392046 -1.6629392246050905,0.7653668647301797 -1.8477590650225735,0.3901806440322566 -1.9615705608064609,0 -2,-0.3901806440322564 -1.9615705608064609,-0.7653668647301795 -1.8477590650225735,-1.111140466039204 -1.6629392246050907,-1.414213562373095 -1.4142135623730951,-1.6629392246050907 -1.1111404660392041,-1.8477590650225735 -0.7653668647301797,-1.9615705608064606 -0.39018064403225716,-2 0,-1.9615705608064609 0.3901806440322567,-1.8477590650225737 0.7653668647301793,-1.6629392246050907 1.111140466039204,-1.4142135623730954 1.414213562373095,-1.1111404660392044 1.6629392246050907,-0.7653668647301807 1.847759065022573,-0.39018064403225733 1.9615705608064606,0 2,0.39018064403225655 1.9615705608064609,0.76536686473018 1.8477590650225733,1.1111404660392037 1.662939224605091,1.414213562373095 1.4142135623730954,1.6629392246050907 1.1111404660392044,1.847759065022573 0.7653668647301808,1.9615705608064606 0.39018064403225744,2 0))", + }, + { + name: "reduced quads", + input: "POINT(0 0)", + dist: 1, + opts: []geom.BufferOption{geom.BufferQuadSegments(2)}, + want: "POLYGON((1 0,0.7071067811865476 -0.7071067811865475,0 -1,-0.7071067811865475 -0.7071067811865476,-1 0,-0.7071067811865477 0.7071067811865475,0 1,0.7071067811865475 0.7071067811865477,1 0))", + }, + { + name: "multipoint with distance apart greater than distance", + input: "MULTIPOINT(0 0,3 0)", + dist: 1, + opts: nil, + want: "MULTIPOLYGON(((4 0,3.9807852804032304 -0.19509032201612825,3.923879532511287 -0.3826834323650898,3.8314696123025453 -0.5555702330196022,3.7071067811865475 -0.7071067811865475,3.555570233019602 -0.8314696123025452,3.3826834323650896 -0.9238795325112867,3.1950903220161284 -0.9807852804032304,3 -1,2.804909677983872 -0.9807852804032304,2.6173165676349104 -0.9238795325112867,2.444429766980398 -0.8314696123025453,2.2928932188134525 -0.7071067811865476,2.1685303876974547 -0.5555702330196021,2.076120467488713 -0.38268343236508984,2.0192147195967696 -0.19509032201612858,2 0,2.0192147195967696 0.19509032201612836,2.076120467488713 0.38268343236508967,2.1685303876974547 0.555570233019602,2.292893218813452 0.7071067811865475,2.444429766980398 0.8314696123025453,2.6173165676349095 0.9238795325112865,2.804909677983871 0.9807852804032303,3 1,3.1950903220161284 0.9807852804032304,3.38268343236509 0.9238795325112866,3.5555702330196017 0.8314696123025455,3.7071067811865475 0.7071067811865477,3.8314696123025453 0.5555702330196022,3.9238795325112865 0.3826834323650904,3.9807852804032304 0.19509032201612872,4 0)),((1 0,0.9807852804032304 -0.19509032201612825,0.9238795325112867 -0.3826834323650898,0.8314696123025453 -0.5555702330196022,0.7071067811865476 -0.7071067811865475,0.5555702330196023 -0.8314696123025452,0.38268343236508984 -0.9238795325112867,0.1950903220161283 -0.9807852804032304,0 -1,-0.1950903220161282 -0.9807852804032304,-0.3826834323650897 -0.9238795325112867,-0.555570233019602 -0.8314696123025453,-0.7071067811865475 -0.7071067811865476,-0.8314696123025453 -0.5555702330196021,-0.9238795325112867 -0.38268343236508984,-0.9807852804032303 -0.19509032201612858,-1 0,-0.9807852804032304 0.19509032201612836,-0.9238795325112868 0.38268343236508967,-0.8314696123025453 0.555570233019602,-0.7071067811865477 0.7071067811865475,-0.5555702330196022 0.8314696123025453,-0.38268343236509034 0.9238795325112865,-0.19509032201612866 0.9807852804032303,0 1,0.19509032201612828 0.9807852804032304,0.38268343236509 0.9238795325112866,0.5555702330196018 0.8314696123025455,0.7071067811865475 0.7071067811865477,0.8314696123025453 0.5555702330196022,0.9238795325112865 0.3826834323650904,0.9807852804032303 0.19509032201612872,1 0)))", + }, + { + name: "multipoint with distance apart less than distance", + input: "MULTIPOINT(0 0,1 0)", + dist: 1, + opts: nil, + want: "POLYGON((0.49999999999999994 0.861172520678903,0.6173165676349097 0.9238795325112865,0.8049096779838714 0.9807852804032303,1 1,1.1950903220161282 0.9807852804032304,1.38268343236509 0.9238795325112866,1.5555702330196017 0.8314696123025455,1.7071067811865475 0.7071067811865477,1.8314696123025453 0.5555702330196022,1.9238795325112865 0.3826834323650904,1.9807852804032304 0.19509032201612872,2 0,1.9807852804032304 -0.19509032201612825,1.9238795325112867 -0.3826834323650898,1.8314696123025453 -0.5555702330196022,1.7071067811865475 -0.7071067811865475,1.5555702330196022 -0.8314696123025452,1.3826834323650898 -0.9238795325112867,1.1950903220161284 -0.9807852804032304,1 -1,0.8049096779838718 -0.9807852804032304,0.6173165676349103 -0.9238795325112867,0.5000000000000001 -0.861172520678903,0.38268343236508984 -0.9238795325112867,0.1950903220161283 -0.9807852804032304,0 -1,-0.1950903220161282 -0.9807852804032304,-0.3826834323650897 -0.9238795325112867,-0.555570233019602 -0.8314696123025453,-0.7071067811865475 -0.7071067811865476,-0.8314696123025453 -0.5555702330196021,-0.9238795325112867 -0.38268343236508984,-0.9807852804032303 -0.19509032201612858,-1 0,-0.9807852804032304 0.19509032201612836,-0.9238795325112868 0.38268343236508967,-0.8314696123025453 0.555570233019602,-0.7071067811865477 0.7071067811865475,-0.5555702330196022 0.8314696123025453,-0.38268343236509034 0.9238795325112865,-0.19509032201612866 0.9807852804032303,0 1,0.19509032201612828 0.9807852804032304,0.38268343236509 0.9238795325112866,0.49999999999999994 0.861172520678903))", + }, + { + name: "linestring with defaults", + input: "LINESTRING(0 0,1 0)", + dist: 1, + opts: nil, + want: "POLYGON((1 1,1.1950903220161284 0.9807852804032304,1.3826834323650898 0.9238795325112867,1.5555702330196022 0.8314696123025452,1.7071067811865475 0.7071067811865475,1.8314696123025453 0.5555702330196022,1.9238795325112867 0.3826834323650898,1.9807852804032304 0.19509032201612825,2 0,1.9807852804032304 -0.19509032201612825,1.9238795325112867 -0.3826834323650898,1.8314696123025453 -0.5555702330196022,1.7071067811865475 -0.7071067811865475,1.5555702330196022 -0.8314696123025452,1.3826834323650898 -0.9238795325112867,1.1950903220161284 -0.9807852804032304,1 -1,0 -1,-0.19509032201612866 -0.9807852804032303,-0.38268343236509034 -0.9238795325112865,-0.5555702330196022 -0.8314696123025453,-0.7071067811865477 -0.7071067811865475,-0.8314696123025453 -0.555570233019602,-0.9238795325112868 -0.38268343236508967,-0.9807852804032304 -0.19509032201612836,-1 0,-0.9807852804032303 0.19509032201612858,-0.9238795325112867 0.38268343236508984,-0.8314696123025453 0.5555702330196021,-0.7071067811865475 0.7071067811865476,-0.555570233019602 0.8314696123025453,-0.3826834323650897 0.9238795325112867,-0.1950903220161282 0.9807852804032304,0 1,1 1))", + }, + { + name: "endcap flat", + input: "LINESTRING(0 0,1 0)", + dist: 1, + opts: []geom.BufferOption{geom.BufferEndCapFlat()}, + want: "POLYGON((1 1,1 -1,0 -1,0 1,1 1))", + }, + { + name: "endcap square", + input: "LINESTRING(0 0,1 0)", + dist: 1, + opts: []geom.BufferOption{geom.BufferEndCapSquare()}, + want: "POLYGON((1 1,2 1,2 -1,0 -1,-1 -1,-1 1,1 1))", + }, + { + name: "single sided positive", + input: "LINESTRING(0 0,1 0)", + dist: +1, + opts: []geom.BufferOption{geom.BufferSingleSided()}, + want: "POLYGON((1 0,0 0,0 1,1 1,1 0))", + }, + { + name: "single sided negative", + input: "LINESTRING(0 0,1 0)", + dist: -1, + opts: []geom.BufferOption{geom.BufferSingleSided()}, + want: "POLYGON((0 0,1 0,1 -1,0 -1,0 0))", + }, + { + name: "join style miter below limit", + input: "LINESTRING(0 0,3 0,3 3)", + dist: 1, + opts: []geom.BufferOption{geom.BufferEndCapFlat(), geom.BufferJoinStyleMitre(2)}, + want: "POLYGON((2 1,2 3,4 3,4 -1,0 -1,0 1,2 1))", + }, + { + name: "join style miter above limit", + input: "LINESTRING(0 0,3 0,3 3)", + dist: 1, + opts: []geom.BufferOption{geom.BufferEndCapFlat(), geom.BufferJoinStyleMitre(1)}, + want: "POLYGON((2 1,2 3,4 3,4 -0.4142135623730951,3.414213562373095 -1,0 -1,0 1,2 1))", + }, + { + name: "join style bevel", + input: "LINESTRING(0 0,3 0,3 3)", + dist: 1, + opts: []geom.BufferOption{geom.BufferEndCapFlat(), geom.BufferJoinStyleBevel()}, + want: "POLYGON((2 1,2 3,4 3,4 0,3 -1,0 -1,0 1,2 1))", + }, + { + name: "simplify factor high", + input: "LINESTRING(0 0,0.25 0.01,0.5 0,0.75 0.01,1 0)", + dist: 0.1, + opts: []geom.BufferOption{geom.BufferSimplifyFactor(1.0), geom.BufferEndCapFlat()}, + want: "POLYGON((0.25 0.11,0.75 0.11,1.0039968038348872 0.09992009587217894,0.9960031961651128 -0.09992009587217894,0.5 -0.1,0.25 -0.09007996802557443,0.003996803834887157 -0.09992009587217894,-0.003996803834887157 0.09992009587217894,0.25 0.11))", + }, + { + name: "simplify factor low", + input: "LINESTRING(0 0,0.25 0.01,0.5 0,0.75 0.01,1 0)", + dist: 0.1, + opts: []geom.BufferOption{geom.BufferSimplifyFactor(0.01), geom.BufferEndCapFlat()}, + want: "POLYGON((0.24600319616511285 0.10992009587217894,0.25399680383488715 0.10992009587217894,0.5 0.10007996802557442,0.7460031961651128 0.10992009587217894,0.7539968038348872 0.10992009587217894,1.0039968038348872 0.09992009587217894,0.9960031961651128 -0.09992009587217894,0.75 -0.09007996802557443,0.5039968038348872 -0.09992009587217894,0.49600319616511285 -0.09992009587217894,0.25 -0.09007996802557443,0.003996803834887157 -0.09992009587217894,-0.003996803834887157 0.09992009587217894,0.24600319616511285 0.10992009587217894))", + }, + { + name: "polygon with hole closed by buffer", + input: "POLYGON((0 0,10 0,10 10,0 10,0 0),(4 4,6 4,6 6,4 6,4 4))", + dist: 1, + opts: []geom.BufferOption{geom.BufferQuadSegments(2)}, + want: "POLYGON((-1 0,-1 10,-0.7071067811865475 10.707106781186548,0 11,10 11,10.707106781186548 10.707106781186548,11 10,11 0,10.707106781186548 -0.7071067811865475,10 -1,0 -1,-0.7071067811865475 -0.7071067811865476,-1 0))", + }, + { + name: "polygon with hole not closed by buffer", + input: "POLYGON((0 0,10 0,10 10,0 10,0 0),(3 3,7 3,7 7,3 7,3 3))", + dist: 1, + opts: []geom.BufferOption{geom.BufferQuadSegments(2)}, + want: "POLYGON((-1 0,-1 10,-0.7071067811865475 10.707106781186548,0 11,10 11,10.707106781186548 10.707106781186548,11 10,11 0,10.707106781186548 -0.7071067811865475,10 -1,0 -1,-0.7071067811865475 -0.7071067811865476,-1 0),(4 4,6 4,6 6,4 6,4 4))", + }, + { + name: "negative buffer", + input: "POLYGON((0 0,10 0,10 10,0 10,0 0))", + dist: -1, + opts: []geom.BufferOption{geom.BufferQuadSegments(2)}, + want: "POLYGON((1 1,1 9,9 9,9 1,1 1))", + }, + { + name: "negative buffer causing disappearance", + input: "POLYGON((0 0,2 0,2 2,0 2,0 0))", + dist: -2, + opts: []geom.BufferOption{geom.BufferQuadSegments(2)}, + want: "POLYGON EMPTY", + }, + { + name: "geometry collection", + input: "GEOMETRYCOLLECTION(POLYGON((0 0,4 0,4 4,0 4,0 0)),LINESTRING(6 2,10 2),POINT(12 2))", + dist: 1, + opts: []geom.BufferOption{geom.BufferQuadSegments(2)}, + want: "MULTIPOLYGON(((-1 0,-1 4,-0.7071067811865475 4.707106781186548,0 5,4 5,4.707106781186548 4.707106781186548,5 4,5 2,5 0,4.707106781186548 -0.7071067811865475,4 -1,0 -1,-0.7071067811865475 -0.7071067811865476,-1 0)),((5 2,5.292893218813452 2.7071067811865475,6 3,10 3,10.707106781186548 2.7071067811865475,11 2,10.707106781186548 1.2928932188134525,10 1,6 1,5.292893218813452 1.2928932188134525,5 2)),((11 2,11.292893218813452 2.7071067811865475,12 3,12.707106781186548 2.707106781186548,13 2,12.707106781186548 1.2928932188134525,12 1,11.292893218813452 1.2928932188134525,11 2)))", + }, + } { + t.Run(strconv.Itoa(i), func(t *testing.T) { + in := test.FromWKT(t, tc.input) + got, err := geom.Buffer(in, tc.dist, tc.opts...) + test.NoErr(t, err) + test.ExactEqualsWKT(t, got, tc.want) + }) + } +} diff --git a/internal/jtsport/MANIFEST.csv b/internal/jtsport/MANIFEST.csv index 927f0936..9f912229 100644 --- a/internal/jtsport/MANIFEST.csv +++ b/internal/jtsport/MANIFEST.csv @@ -7,6 +7,12 @@ algorithm/BoundaryNodeRule.java,algorithm_boundary_node_rule.go,ported algorithm/CGAlgorithmsDD.java,algorithm_cgalgorithms_dd.go,ported algorithm/CGAlgorithmsDDTest.java,algorithm_cgalgorithms_dd_test.go,ported algorithm/Distance.java,algorithm_distance.go,ported +algorithm/distance/DiscreteFrechetDistance.java,algorithm_distance_discrete_frechet_distance.go,ported +algorithm/distance/DiscreteFrechetDistanceTest.java,algorithm_distance_discrete_frechet_distance_test.go,ported +algorithm/distance/DiscreteHausdorffDistance.java,algorithm_distance_discrete_hausdorff_distance.go,ported +algorithm/distance/DiscreteHausdorffDistanceTest.java,algorithm_distance_discrete_hausdorff_distance_test.go,ported +algorithm/distance/DistanceToPoint.java,algorithm_distance_distance_to_point.go,ported +algorithm/distance/PointPairDistance.java,algorithm_distance_point_pair_distance.go,ported algorithm/DistanceTest.java,algorithm_distance_test.go,ported algorithm/HCoordinate.java,algorithm_hcoordinate.go,ported algorithm/Intersection.java,algorithm_intersection.go,ported @@ -21,6 +27,7 @@ algorithm/locate/SimplePointInAreaLocator.java,algorithm_locate_simple_point_in_ algorithm/locate/SimplePointInAreaLocatorTest.java,algorithm_locate_simple_point_in_area_locator_test.go,ported algorithm/NotRepresentableException.java,algorithm_not_representable_exception.go,ported algorithm/Orientation.java,algorithm_orientation.go,ported +algorithm/OrientationTest.java,algorithm_orientation_test.go,ported algorithm/PointLocation.java,algorithm_point_location.go,ported algorithm/PointLocationTest.java,algorithm_point_location_test.go,ported algorithm/PointLocator.java,algorithm_point_locator.go,ported @@ -58,6 +65,7 @@ geom/EnvelopeTest.java,geom_envelope_test.go,ported geom/GeometryCollectionIterator.java,geom_geometry_collection_iterator.go,ported geom/GeometryCollectionIteratorTest.java,geom_geometry_collection_iterator_test.go,ported geom/GeometryCollection.java,geom_geometry_collection.go,ported +geom/GeometryCollectionTest.java,geom_geometry_collection_test.go,ported geom/GeometryComponentFilter.java,geom_geometry_component_filter.go,ported geom/GeometryFactory.java,geom_geometry_factory.go,ported geom/GeometryFactoryTest.java,geom_geometry_factory_test.go,ported @@ -113,11 +121,14 @@ geom/LinearRing.java,geom_linear_ring.go,ported geom/LineSegment.java,geom_line_segment.go,ported geom/LineSegmentTest.java,geom_line_segment_test.go,ported geom/LineString.java,geom_line_string.go,ported +geom/LineStringTest.java,geom_line_string_test.go,ported geom/Location.java,geom_location.go,ported geom/MultiLineString.java,geom_multi_line_string.go,ported geom/MultiPoint.java,geom_multi_point.go,ported +geom/MultiPointTest.java,geom_multi_point_test.go,ported geom/MultiPolygon.java,geom_multi_polygon.go,ported geom/Point.java,geom_point.go,ported +geom/PointTest.java,geom_point_test.go,ported geom/Polygonal.java,geom_polygonal.go,ported geom/Polygon.java,geom_polygon.go,ported geom/Position.java,geom_position.go,ported @@ -206,7 +217,8 @@ math/DDTest.java,math_dd_test.go,reviewed math/MathUtil.java,math_math_util.go,reviewed noding/BasicSegmentString.java,noding_basic_segment_string.go,ported noding/BoundaryChainNoder.java,noding_boundary_chain_noder.go,ported -noding/FastNodingValidator.java,noding_fast_noding_validator.go,pending +noding/FastNodingValidator.java,noding_fast_noding_validator.go,ported +noding/FastNodingValidatorTest.java,noding_fast_noding_validator_test.go,ported noding/InteriorIntersectionFinderAdder.java,noding_interior_intersection_finder_adder.go,ported noding/IntersectionAdder.java,noding_intersection_adder.go,ported noding/IntersectionFinderAdder.java,noding_intersection_finder_adder.go,ported @@ -216,9 +228,10 @@ noding/NodableSegmentString.java,noding_nodable_segment_string.go,ported noding/NodedSegmentString.java,noding_noded_segment_string.go,ported noding/NodedSegmentStringTest.java,noding_noded_segment_string_test.go,ported noding/Noder.java,noding_noder.go,ported +noding/NodingIntersectionFinder.java,noding_noding_intersection_finder.go,ported noding/NodingValidator.java,noding_noding_validator.go,ported noding/Octant.java,noding_octant.go,ported -noding/ScaledNoder.java,noding_scaled_noder.go,pending +noding/ScaledNoder.java,noding_scaled_noder.go,ported noding/SegmentExtractingNoder.java,noding_segment_extracting_noder.go,ported noding/SegmentIntersector.java,noding_segment_intersector.go,ported noding/SegmentNode.java,noding_segment_node.go,ported @@ -228,6 +241,7 @@ noding/SegmentPointComparatorTest.java,noding_segment_point_comparator_test.go,p noding/SegmentSetMutualIntersector.java,noding_segment_set_mutual_intersector.go,ported noding/SegmentString.java,noding_segment_string.go,ported noding/SinglePassNoder.java,noding_single_pass_noder.go,ported +noding/TestUtil.java,noding_test_util_test.go,ported noding/snapround/GeometryNoder.java,noding_snapround_geometry_noder.go,ported noding/snapround/HotPixelIndex.java,noding_snapround_hot_pixel_index.go,ported noding/snapround/HotPixel.java,noding_snapround_hot_pixel.go,ported @@ -245,26 +259,39 @@ noding/snap/SnappingNoderTest.java,noding_snap_snapping_noder_test.go,ported noding/snap/SnappingPointIndex.java,noding_snap_snapping_point_index.go,ported noding/ValidatingNoder.java,noding_validating_noder.go,ported operation/BoundaryOp.java,operation_boundary_op.go,ported -operation/buffer/BufferBuilder.java,operation_buffer_buffer_builder.go,pending -operation/buffer/BufferCurveSetBuilder.java,operation_buffer_buffer_curve_set_builder.go,pending -operation/buffer/BufferInputLineSimplifier.java,operation_buffer_buffer_input_line_simplifier.go,pending -operation/buffer/BufferOp.java,operation_buffer_buffer_op.go,pending -operation/buffer/BufferParameters.java,operation_buffer_buffer_parameters.go,pending -operation/buffer/BufferSubgraph.java,operation_buffer_buffer_subgraph.go,pending -operation/buffer/OffsetCurveBuilder.java,operation_buffer_offset_curve_builder.go,pending -operation/buffer/OffsetCurve.java,operation_buffer_offset_curve.go,pending -operation/buffer/OffsetCurveSection.java,operation_buffer_offset_curve_section.go,pending -operation/buffer/OffsetSegmentGenerator.java,operation_buffer_offset_segment_generator.go,pending -operation/buffer/OffsetSegmentString.java,operation_buffer_offset_segment_string.go,pending -operation/buffer/RightmostEdgeFinder.java,operation_buffer_rightmost_edge_finder.go,pending -operation/buffer/SegmentMCIndex.java,operation_buffer_segment_mc_index.go,pending -operation/buffer/SubgraphDepthLocater.java,operation_buffer_subgraph_depth_locater.go,pending -operation/buffer/validate/BufferCurveMaximumDistanceFinder.java,operation_buffer_validate_buffer_curve_max_distance_finder.go,pending -operation/buffer/validate/BufferDistanceValidator.java,operation_buffer_validate_buffer_distance_validator.go,pending -operation/buffer/validate/BufferResultValidator.java,operation_buffer_validate_buffer_result_validator.go,pending -operation/buffer/validate/DistanceToPointFinder.java,operation_buffer_validate_distance_to_point_finder.go,pending -operation/buffer/validate/PointPairDistance.java,operation_buffer_validate_point_pair_distance.go,pending -operation/buffer/VariableBuffer.java,operation_buffer_variable_buffer.go,pending +operation/buffer/BufferBuilder.java,operation_buffer_buffer_builder.go,ported +operation/buffer/BufferCurveSetBuilder.java,operation_buffer_buffer_curve_set_builder.go,ported +operation/buffer/BufferInputLineSimplifier.java,operation_buffer_buffer_input_line_simplifier.go,ported +operation/buffer/BufferOp.java,operation_buffer_buffer_op.go,ported +operation/buffer/BufferParameters.java,operation_buffer_buffer_parameters.go,ported +operation/buffer/BufferParameterTest.java,operation_buffer_buffer_parameter_test.go,ported +operation/buffer/BufferResultValidatorTest.java,operation_buffer_buffer_result_validator_test.go,ported +operation/buffer/BufferValidator.java,operation_buffer_buffer_validator_test.go,ported +operation/buffer/BufferSubgraph.java,operation_buffer_buffer_subgraph.go,ported +operation/buffer/BufferTest.java,operation_buffer_buffer_test.go,ported +operation/buffer/DepthSegmentTest.java,operation_buffer_depth_segment_test.go,ported +operation/buffer/OffsetCurve.java,operation_buffer_offset_curve.go,ported +operation/buffer/OffsetCurveBuilder.java,operation_buffer_offset_curve_builder.go,ported +operation/buffer/OffsetCurveSection.java,operation_buffer_offset_curve_section.go,ported +operation/buffer/OffsetCurveTest.java,operation_buffer_offset_curve_test.go,ported +operation/buffer/OffsetSegmentGenerator.java,operation_buffer_offset_segment_generator.go,ported +operation/buffer/OffsetSegmentString.java,operation_buffer_offset_segment_string.go,ported +operation/buffer/RightmostEdgeFinder.java,operation_buffer_rightmost_edge_finder.go,ported +operation/buffer/SegmentMCIndex.java,operation_buffer_segment_mc_index.go,ported +operation/buffer/SubgraphDepthLocater.java,operation_buffer_subgraph_depth_locater.go,ported +operation/buffer/VariableBuffer.java,operation_buffer_variable_buffer.go,ported +operation/buffer/VariableBufferTest.java,operation_buffer_variable_buffer_test.go,ported +operation/buffer/validate/BufferCurveMaximumDistanceFinder.java,operation_buffer_validate_buffer_curve_max_distance_finder.go,ported +operation/buffer/validate/BufferDistanceValidator.java,operation_buffer_validate_buffer_distance_validator.go,ported +operation/buffer/validate/BufferResultValidator.java,operation_buffer_validate_buffer_result_validator.go,ported +operation/buffer/validate/DistanceToPointFinder.java,operation_buffer_validate_distance_to_point_finder.go,ported +operation/buffer/validate/PointPairDistance.java,operation_buffer_validate_point_pair_distance.go,ported +operation/distance/BaseDistanceTest.java,operation_distance_base_distance_test.go,ported +operation/distance/ConnectedElementLocationFilter.java,operation_distance_connected_element_location_filter.go,ported +operation/distance/ConnectedElementPointFilter.java,operation_distance_connected_element_point_filter.go,ported +operation/distance/DistanceOp.java,operation_distance_distance_op.go,ported +operation/distance/DistanceTest.java,operation_distance_distance_test.go,ported +operation/distance/GeometryLocation.java,operation_distance_geometry_location.go,ported operation/GeometryGraphOperation.java,operation_geometry_graph_operation.go,ported operation/linemerge/EdgeString.java,operation_linemerge_edge_string.go,ported operation/linemerge/LineMergeDirectedEdge.java,operation_linemerge_line_merge_directed_edge.go,ported @@ -276,9 +303,11 @@ operation/linemerge/LineSequencer.java,operation_linemerge_line_sequencer.go,por operation/linemerge/LineSequencerTest.java,operation_linemerge_line_sequencer_test.go,ported operation/overlay/ConsistentPolygonRingChecker.java,operation_overlay_consistent_polygon_ring_checker.go,ported operation/overlay/EdgeSetNoder.java,operation_overlay_edge_set_noder.go,ported +operation/overlay/FixedPrecisionSnapRoundingTest.java,operation_overlay_fixed_precision_snapping_test.go,ported operation/overlay/LineBuilder.java,operation_overlay_line_builder.go,ported operation/overlay/MaximalEdgeRing.java,operation_overlay_maximal_edge_ring.go,ported operation/overlay/MinimalEdgeRing.java,operation_overlay_minimal_edge_ring.go,ported +operation/overlay/OverlayOpTest.java,operation_overlay_overlay_op_test.go,ported operation/overlayng/CoverageUnion.java,operation_overlayng_coverage_union.go,ported operation/overlayng/CoverageUnionTest.java,operation_overlayng_coverage_union_test.go,ported operation/overlayng/Edge.java,operation_overlayng_edge.go,ported @@ -374,9 +403,21 @@ operation/union/OverlapUnion.java,operation_union_overlap_union.go,ported operation/union/OverlapUnionTest.java,operation_union_overlap_union_test.go,ported operation/union/PointGeometryUnion.java,operation_union_point_geometry_union.go,ported operation/union/UnaryUnionOp.java,operation_union_unary_union_op.go,ported +operation/union/UnaryUnionOpTest.java,operation_union_unary_union_op_test.go,ported operation/union/UnionInteracting.java,operation_union_union_interacting.go,ported operation/union/UnionStrategy.java,operation_union_union_strategy.go,ported +operation/valid/IndexedNestedHoleTester.java,operation_valid_indexed_nested_hole_tester.go,ported +operation/valid/IndexedNestedPolygonTester.java,operation_valid_indexed_nested_polygon_tester.go,ported operation/valid/IsSimpleOp.java,operation_valid_is_simple_op.go,ported +operation/valid/IsSimpleOpTest.java,operation_valid_is_simple_op_test.go,ported +operation/valid/IsValidOp.java,operation_valid_is_valid_op.go,ported +operation/valid/IsValidTest.java,operation_valid_is_valid_test.go,ported +operation/valid/ValidClosedRingTest.java,operation_valid_valid_closed_ring_test.go,ported +operation/valid/ValidSelfTouchingRingTest.java,operation_valid_valid_self_touching_ring_test.go,ported +operation/valid/PolygonIntersectionAnalyzer.java,operation_valid_polygon_intersection_analyzer.go,ported +operation/valid/PolygonRing.java,operation_valid_polygon_ring.go,ported +operation/valid/PolygonTopologyAnalyzer.java,operation_valid_polygon_topology_analyzer.go,ported +operation/valid/TopologyValidationError.java,operation_valid_topology_validation_error.go,ported planargraph/algorithm/ConnectedSubgraphFinder.java,planargraph_algorithm_connected_subgraph_finder.go,ported planargraph/DirectedEdge.java,planargraph_directed_edge.go,ported planargraph/DirectedEdgeStar.java,planargraph_directed_edge_star.go,ported @@ -405,5 +446,7 @@ testrunner/TestReader.java,jtstest_testrunner_test_reader.go,ported testrunner/TestRun.java,jtstest_testrunner_test_run.go,ported util/AssertionFailedException.java,util_assertion_failed_exception.go,reviewed util/Assert.java,util_assert.go,reviewed +util/Debug.java,util_debug.go,ported util/IntArrayList.java,util_int_array_list.go,reviewed util/IntArrayListTest.java,util_int_array_list_test.go,reviewed +util/Stopwatch.java,util_stopwatch.go,ported diff --git a/internal/jtsport/TRANSLITERATION_GUIDE.md b/internal/jtsport/TRANSLITERATION_GUIDE.md index 30b083e6..dde9ff4e 100644 --- a/internal/jtsport/TRANSLITERATION_GUIDE.md +++ b/internal/jtsport/TRANSLITERATION_GUIDE.md @@ -532,6 +532,13 @@ junit.AssertEquals(t, 3, iar.Size()) junit.AssertTrue(t, result.IsValid()) ``` +## JTS Util Package + +JTS has utility classes in `org.locationtech.jts.util` (e.g., `Assert`, +`Debug`). These are ported in `util_*.go` files. Use the ported Go functions +(e.g., `Util_Assert_IsTrue`) wherever the Java code uses them, rather than +reimplementing the logic inline. + ## Marker Interfaces Java marker interfaces (empty interfaces for categorization) become Go diff --git a/internal/jtsport/jts/algorithm_distance_discrete_frechet_distance.go b/internal/jtsport/jts/algorithm_distance_discrete_frechet_distance.go new file mode 100644 index 00000000..7a305cb7 --- /dev/null +++ b/internal/jtsport/jts/algorithm_distance_discrete_frechet_distance.go @@ -0,0 +1,461 @@ +package jts + +import ( + "math" + "sort" +) + +// AlgorithmDistance_DiscreteFrechetDistance computes the Discrete Fréchet Distance +// between two geometries. +// +// The Fréchet distance is a measure of similarity between curves. Thus, it can +// be used like the Hausdorff distance. +// +// An analogy for the Fréchet distance taken from +// Computing Discrete Fréchet Distance: +// A man is walking a dog on a leash: the man can move +// on one curve, the dog on the other; both may vary their +// speed, but backtracking is not allowed. +// +// Its metric is better than the Hausdorff distance +// because it takes the directions of the curves into account. +// It is possible that two curves have a small Hausdorff but a large +// Fréchet distance. +// +// This implementation is based on the following optimized Fréchet distance algorithm: +// Thomas Devogele, Maxence Esnault, Laurent Etienne. Distance discrète de Fréchet optimisée. Spatial +// Analysis and Geomatics (SAGEO), Nov 2016, Nice, France. hal-02110055 +// +// Several matrix storage implementations are provided. +type AlgorithmDistance_DiscreteFrechetDistance struct { + g0 *Geom_Geometry + g1 *Geom_Geometry + ptDist *AlgorithmDistance_PointPairDistance +} + +// AlgorithmDistance_DiscreteFrechetDistance_Distance computes the Discrete Fréchet Distance between two Geometries +// using a Cartesian distance computation function. +func AlgorithmDistance_DiscreteFrechetDistance_Distance(g0, g1 *Geom_Geometry) float64 { + dist := AlgorithmDistance_NewDiscreteFrechetDistance(g0, g1) + return dist.distance() +} + +// AlgorithmDistance_NewDiscreteFrechetDistance creates an instance of this class using the provided geometries. +func AlgorithmDistance_NewDiscreteFrechetDistance(g0, g1 *Geom_Geometry) *AlgorithmDistance_DiscreteFrechetDistance { + return &AlgorithmDistance_DiscreteFrechetDistance{ + g0: g0, + g1: g1, + } +} + +// distance computes the Discrete Fréchet Distance between the input geometries. +func (dfd *AlgorithmDistance_DiscreteFrechetDistance) distance() float64 { + coords0 := dfd.g0.GetCoordinates() + coords1 := dfd.g1.GetCoordinates() + + distances := algorithmDistance_createMatrixStorage(len(coords0), len(coords1)) + diagonal := algorithmDistance_bresenhamDiagonal(len(coords0), len(coords1)) + + distanceToPair := make(map[float64][]int) + dfd.computeCoordinateDistances(coords0, coords1, diagonal, distances, distanceToPair) + dfd.ptDist = algorithmDistance_computeFrechet(coords0, coords1, diagonal, distances, distanceToPair) + + return dfd.ptDist.GetDistance() +} + +// algorithmDistance_createMatrixStorage creates a matrix to store the computed distances. +func algorithmDistance_createMatrixStorage(rows, cols int) algorithmDistance_matrixStorage { + max := rows + if cols > max { + max = cols + } + // NOTE: these constraints need to be verified + if max < 1024 { + return algorithmDistance_newRectMatrix(rows, cols, math.Inf(1)) + } + + return algorithmDistance_newCsrMatrix(rows, cols, math.Inf(1)) +} + +// GetCoordinates gets the pair of Coordinates at which the distance is obtained. +func (dfd *AlgorithmDistance_DiscreteFrechetDistance) GetCoordinates() []*Geom_Coordinate { + if dfd.ptDist == nil { + dfd.distance() + } + + return dfd.ptDist.GetCoordinates() +} + +// algorithmDistance_computeFrechet computes the Fréchet Distance for the given distance matrix. +func algorithmDistance_computeFrechet(coords0, coords1 []*Geom_Coordinate, diagonal []int, + distances algorithmDistance_matrixStorage, distanceToPair map[float64][]int) *AlgorithmDistance_PointPairDistance { + for d := 0; d < len(diagonal); d += 2 { + i0 := diagonal[d] + j0 := diagonal[d+1] + + for i := i0; i < len(coords0); i++ { + if distances.isValueSet(i, j0) { + dist := algorithmDistance_getMinDistanceAtCorner(distances, i, j0) + if dist > distances.get(i, j0) { + distances.set(i, j0, dist) + } + } else { + break + } + } + for j := j0 + 1; j < len(coords1); j++ { + if distances.isValueSet(i0, j) { + dist := algorithmDistance_getMinDistanceAtCorner(distances, i0, j) + if dist > distances.get(i0, j) { + distances.set(i0, j, dist) + } + } else { + break + } + } + } + + result := AlgorithmDistance_NewPointPairDistance() + distance := distances.get(len(coords0)-1, len(coords1)-1) + index := distanceToPair[distance] + if index == nil { + panic("Pair of points not recorded for computed distance") + } + result.InitializeWithCoordinatesAndDistance(coords0[index[0]], coords1[index[1]], distance) + return result +} + +// algorithmDistance_getMinDistanceAtCorner returns the minimum distance at the corner (i, j). +func algorithmDistance_getMinDistanceAtCorner(matrix algorithmDistance_matrixStorage, i, j int) float64 { + if i > 0 && j > 0 { + d0 := matrix.get(i-1, j-1) + d1 := matrix.get(i-1, j) + d2 := matrix.get(i, j-1) + return math.Min(math.Min(d0, d1), d2) + } + if i == 0 && j == 0 { + return matrix.get(0, 0) + } + + if i == 0 { + return matrix.get(0, j-1) + } + + // j == 0 + return matrix.get(i-1, 0) +} + +// computeCoordinateDistances computes relevant distances between pairs of Coordinates for the +// computation of the Discrete Fréchet Distance. +func (dfd *AlgorithmDistance_DiscreteFrechetDistance) computeCoordinateDistances(coords0, coords1 []*Geom_Coordinate, diagonal []int, + distances algorithmDistance_matrixStorage, distanceToPair map[float64][]int) { + numDiag := len(diagonal) + maxDistOnDiag := 0.0 + imin, jmin := 0, 0 + numCoords0 := len(coords0) + numCoords1 := len(coords1) + + // First compute all the distances along the diagonal. + // Record the maximum distance. + + for k := 0; k < numDiag; k += 2 { + i0 := diagonal[k] + j0 := diagonal[k+1] + diagDist := coords0[i0].Distance(coords1[j0]) + if diagDist > maxDistOnDiag { + maxDistOnDiag = diagDist + } + distances.set(i0, j0, diagDist) + if _, exists := distanceToPair[diagDist]; !exists { + distanceToPair[diagDist] = []int{i0, j0} + } + } + + // Check for distances shorter than maxDistOnDiag along the diagonal + for k := 0; k < numDiag-2; k += 2 { + // Decode index + i0 := diagonal[k] + j0 := diagonal[k+1] + + // Get reference coordinates for col and row + coord0 := coords0[i0] + coord1 := coords1[j0] + + // Check for shorter distances in this row + i := i0 + 1 + for ; i < numCoords0; i++ { + if !distances.isValueSet(i, j0) { + dist := coords0[i].Distance(coord1) + if dist < maxDistOnDiag || i < imin { + distances.set(i, j0, dist) + if _, exists := distanceToPair[dist]; !exists { + distanceToPair[dist] = []int{i, j0} + } + } else { + break + } + } else { + break + } + } + imin = i + + // Check for shorter distances in this column + j := j0 + 1 + for ; j < numCoords1; j++ { + if !distances.isValueSet(i0, j) { + dist := coord0.Distance(coords1[j]) + if dist < maxDistOnDiag || j < jmin { + distances.set(i0, j, dist) + if _, exists := distanceToPair[dist]; !exists { + distanceToPair[dist] = []int{i0, j} + } + } else { + break + } + } else { + break + } + } + jmin = j + } + + //System.out.println(distances.toString()); +} + +// algorithmDistance_bresenhamDiagonal computes the indices for the diagonal of a numCols x numRows grid +// using the Bresenham line algorithm. +func algorithmDistance_bresenhamDiagonal(numCols, numRows int) []int { + dim := numCols + if numRows > dim { + dim = numRows + } + diagXY := make([]int, 2*dim) + + dx := numCols - 1 + dy := numRows - 1 + var err int + i := 0 + if numCols > numRows { + y := 0 + err = 2*dy - dx + for x := 0; x < numCols; x++ { + diagXY[i] = x + i++ + diagXY[i] = y + i++ + if err > 0 { + y += 1 + err -= 2 * dx + } + err += 2 * dy + } + } else { + x := 0 + err = 2*dx - dy + for y := 0; y < numRows; y++ { + diagXY[i] = x + i++ + diagXY[i] = y + i++ + if err > 0 { + x += 1 + err -= 2 * dy + } + err += 2 * dx + } + } + return diagXY +} + +// algorithmDistance_matrixStorage is an abstract base class for storing 2d matrix data. +type algorithmDistance_matrixStorage interface { + get(i, j int) float64 + set(i, j int, value float64) + isValueSet(i, j int) bool +} + +// algorithmDistance_rectMatrix is a straightforward implementation of a rectangular matrix. +type algorithmDistance_rectMatrix struct { + numRows int + numCols int + defaultValue float64 + matrix []float64 +} + +// algorithmDistance_newRectMatrix creates an instance of this matrix using the given number of rows and columns. +// A default value can be specified. +func algorithmDistance_newRectMatrix(numRows, numCols int, defaultValue float64) *algorithmDistance_rectMatrix { + matrix := make([]float64, numRows*numCols) + for i := range matrix { + matrix[i] = defaultValue + } + return &algorithmDistance_rectMatrix{ + numRows: numRows, + numCols: numCols, + defaultValue: defaultValue, + matrix: matrix, + } +} + +func (m *algorithmDistance_rectMatrix) get(i, j int) float64 { + return m.matrix[i*m.numCols+j] +} + +func (m *algorithmDistance_rectMatrix) set(i, j int, value float64) { + m.matrix[i*m.numCols+j] = value +} + +func (m *algorithmDistance_rectMatrix) isValueSet(i, j int) bool { + return math.Float64bits(m.get(i, j)) != math.Float64bits(m.defaultValue) +} + +// algorithmDistance_csrMatrix is a matrix implementation that adheres to the +// Compressed sparse row format. +// Note: Unfortunately not as fast as expected. +type algorithmDistance_csrMatrix struct { + numRows int + numCols int + defaultValue float64 + v []float64 + ri []int + ci []int +} + +func algorithmDistance_newCsrMatrix(numRows, numCols int, defaultValue float64) *algorithmDistance_csrMatrix { + return algorithmDistance_newCsrMatrixWithExpectedValues(numRows, numCols, defaultValue, algorithmDistance_expectedValuesHeuristic(numRows, numCols)) +} + +func algorithmDistance_newCsrMatrixWithExpectedValues(numRows, numCols int, defaultValue float64, expectedValues int) *algorithmDistance_csrMatrix { + return &algorithmDistance_csrMatrix{ + numRows: numRows, + numCols: numCols, + defaultValue: defaultValue, + v: make([]float64, expectedValues), + ci: make([]int, expectedValues), + ri: make([]int, numRows+1), + } +} + +// algorithmDistance_expectedValuesHeuristic computes an initial value for the number of expected values. +func algorithmDistance_expectedValuesHeuristic(numRows, numCols int) int { + max := numRows + if numCols > max { + max = numCols + } + return max * max / 10 +} + +func (m *algorithmDistance_csrMatrix) indexOf(i, j int) int { + cLow := m.ri[i] + cHigh := m.ri[i+1] + if cHigh <= cLow { + return ^cLow + } + + idx := sort.SearchInts(m.ci[cLow:cHigh], j) + if idx < cHigh-cLow && m.ci[cLow+idx] == j { + return cLow + idx + } + return ^(cLow + idx) +} + +func (m *algorithmDistance_csrMatrix) get(i, j int) float64 { + // get the index in the vector + vi := m.indexOf(i, j) + + // if the vector index is negative, return default value + if vi < 0 { + return m.defaultValue + } + + return m.v[vi] +} + +func (m *algorithmDistance_csrMatrix) set(i, j int, value float64) { + // get the index in the vector + vi := m.indexOf(i, j) + + // do we already have a value? + if vi < 0 { + // no, we don't, we need to ensure space! + m.ensureCapacity(m.ri[m.numRows] + 1) + + // update row indices + for ii := i + 1; ii <= m.numRows; ii++ { + m.ri[ii] += 1 + } + + // move and update column indices, move values + vi = ^vi + for ii := m.ri[m.numRows]; ii > vi; ii-- { + m.ci[ii] = m.ci[ii-1] + m.v[ii] = m.v[ii-1] + } + + // insert column index + m.ci[vi] = j + } + + // set the new value + m.v[vi] = value +} + +func (m *algorithmDistance_csrMatrix) isValueSet(i, j int) bool { + return m.indexOf(i, j) >= 0 +} + +// ensureCapacity ensures that the column index vector (ci) and value vector (v) are sufficiently large. +func (m *algorithmDistance_csrMatrix) ensureCapacity(required int) { + if required < len(m.v) { + return + } + + increment := m.numRows + if m.numCols > increment { + increment = m.numCols + } + newV := make([]float64, len(m.v)+increment) + copy(newV, m.v) + m.v = newV + newCi := make([]int, len(m.v)+increment) + copy(newCi, m.ci) + m.ci = newCi +} + +// algorithmDistance_hashMapMatrix is a sparse matrix based on Go's map. +type algorithmDistance_hashMapMatrix struct { + numRows int + numCols int + defaultValue float64 + matrix map[int64]float64 +} + +// algorithmDistance_newHashMapMatrix creates an instance of this class. +func algorithmDistance_newHashMapMatrix(numRows, numCols int, defaultValue float64) *algorithmDistance_hashMapMatrix { + return &algorithmDistance_hashMapMatrix{ + numRows: numRows, + numCols: numCols, + defaultValue: defaultValue, + matrix: make(map[int64]float64), + } +} + +func (m *algorithmDistance_hashMapMatrix) get(i, j int) float64 { + key := int64(i)<<32 | int64(j) + if v, ok := m.matrix[key]; ok { + return v + } + return m.defaultValue +} + +func (m *algorithmDistance_hashMapMatrix) set(i, j int, value float64) { + key := int64(i)<<32 | int64(j) + m.matrix[key] = value +} + +func (m *algorithmDistance_hashMapMatrix) isValueSet(i, j int) bool { + key := int64(i)<<32 | int64(j) + _, ok := m.matrix[key] + return ok +} diff --git a/internal/jtsport/jts/algorithm_distance_discrete_frechet_distance_test.go b/internal/jtsport/jts/algorithm_distance_discrete_frechet_distance_test.go new file mode 100644 index 00000000..9f8dffed --- /dev/null +++ b/internal/jtsport/jts/algorithm_distance_discrete_frechet_distance_test.go @@ -0,0 +1,78 @@ +package jts + +import ( + "testing" + + "github.com/peterstace/simplefeatures/internal/jtsport/junit" +) + +func TestDiscreteFrechetDistance_LineSegments(t *testing.T) { + algorithmDistance_discreteFrechetDistance_runTest(t, + "LINESTRING(0 0, 1 0.0, 2 0.0, 3 0.0, 4 0)", + "LINESTRING(0 1, 1 1.1, 2 1.2, 3 1.1, 4 1)", 1.2) +} + +func TestDiscreteFrechetDistance_Orientation(t *testing.T) { + algorithmDistance_discreteFrechetDistance_runTest(t, + "LINESTRING(0 0, 10 10, 20 15)", + "LINESTRING(0 1, 8 9, 12 11, 21 15)", 2.23606797749979) +} + +func TestDiscreteFrechetDistance_FromDHD(t *testing.T) { + algorithmDistance_discreteFrechetDistance_runTest(t, + "LINESTRING (130 0, 0 0, 0 150)", + "LINESTRING (10 10, 10 150, 130 10)", 191.049731745428) +} + +// TRANSLITERATION NOTE: testDevogeleEtAlPaper lacks @Test annotation in Java, +// so it's not run as a JUnit test. Porting as a non-test function to match. +func algorithmDistance_discreteFrechetDistance_testDevogeleEtAlPaper(t *testing.T) { + algorithmDistance_discreteFrechetDistance_runTest(t, "LINESTRING(0.2 2.0, 1.5 2.8, 2.3 1.6, 2.9 1.8, 4.1 3.1, 5.6 2.9, 7.2 1.3, 8.2 1.1)", + "LINESTRING(0.3 1.6, 3.2 3.0, 3.8 1.8, 5.2 3.1, 6.5 2.8, 7.0 0.8, 8.9 0.6)", 1.697056) +} + +// TRANSLITERATION NOTE: testLongEifelWalk lacks @Test annotation in Java, +// so it's not run as a JUnit test. Porting as a non-test function to match. +func algorithmDistance_discreteFrechetDistance_testLongEifelWalk(t *testing.T) { + algorithmDistance_discreteFrechetDistance_runTest(t, + "MultiLineStringZ ((3334206.2900000000372529 5575410.37999999988824129 442.10000000000002274, 3334209.33999999985098839 5575407.62000000011175871 442.80000000000001137, 3334210.75 5575409.12999999988824129 442.69999999999998863, 3334190.06000000005587935 5575386.17999999970197678 443.60000000000002274, 3334184.52000000001862645 5575375.44000000040978193 444.69999999999998863, 3334176.30000000027939677 5575352.2099999999627471 447, 3334149.60999999986961484 5575360.71999999973922968 446.90000000000003411, 3334137.68000000016763806 5575366.88999999966472387 446.5, 3334125.9599999999627471 5575379.83000000007450581 445.30000000000001137, 3334115.97000000020489097 5575386.7099999999627471 444.80000000000001137, 3334106.01000000024214387 5575391.91999999992549419 444.40000000000003411, 3334096.97000000020489097 5575394.66000000014901161 444.30000000000001137, 3334087.49000000022351742 5575394.83999999985098839 444.40000000000003411, 3334078.58999999985098839 5575393 444.69999999999998863, 3334071.85999999986961484 5575390.10000000055879354 444.80000000000001137, 3334059.89999999990686774 5575404.5 442.13999999999998636, 3334046.47999999998137355 5575415.37999999988824129 438.81000000000000227, 3334129.45000000018626451 5575530.35000000055879354 437.62000000000000455, 3334179.47999999998137355 5575497.06000000052154064 439.01999999999998181, 3334196.87000000011175871 5575537.81000000052154064 440.03000000000002956, 3334229.08000000007450581 5575541.70000000018626451 441.62999999999999545, 3334209 5575622.25999999977648258 442.37999999999999545, 3334134.66000000014901161 5575601.10000000055879354 438.60000000000002274, 3334134.07000000029802322 5575602.57000000029802322 438.60000000000002274, 3334131.07000000029802322 5575609.45000000018626451 438.69999999999998863, 3334118.20000000018626451 5575638.02000000048428774 438.90000000000003411, 3334114.66999999992549419 5575645.81000000052154064 438.90000000000003411, 3334102.81000000005587935 5575667.88999999966472387 439.40000000000003411, 3334100.2900000000372529 5575676.42999999970197678 439.69999999999998863, 3334098.2900000000372529 5575683.16999999992549419 440, 3334095.64999999990686774 5575696.71999999973922968 440.69999999999998863, 3334095.16999999992549419 5575710.99000000022351742 441.40000000000003411, 3334095.97999999998137355 5575727.77000000048428774 442.10000000000002274, 3334098.89000000013038516 5575747.71999999973922968 442.90000000000003411, 3334099.78000000026077032 5575751.37000000011175871 443.10000000000002274, 3334101.35000000009313226 5575758 443.30000000000001137, 3334097.9599999999627471 5575759.21999999973922968 443.5, 3334093.58999999985098839 5575760.91000000014901161 443.69999999999998863, 3334081.22999999998137355 5575773.66000000014901161 445.30000000000001137, 3334081.82000000029802322 5575776.53000000026077032 445.40000000000003411, 3334081.87999999988824129 5575783.2099999999627471 445.69999999999998863, 3334072.4599999999627471 5575817.00999999977648258 447.60000000000002274, 3334070.62000000011175871 5575831.32000000029802322 447.90000000000003411, 3334074.35000000009313226 5575863.92999999970197678 447.60000000000002274, 3334078.08000000007450581 5575896.54999999981373549 447.10000000000002274, 3334076.18999999994412065 5575897.61000000033527613 447.19999999999998863, 3334070.14999999990686774 5575905.03000000026077032 447.40000000000003411, 3334062.60999999986961484 5575953.58999999985098839 446.5, 3334055.07000000029802322 5576002.03000000026077032 447.90000000000003411, 3334047.60000000009313226 5576050.4599999999627471 450, 3334039.10000000009313226 5576059.08000000007450581 449.90000000000003411, 3334003.41000000014901161 5576080.90000000037252903 450.19999999999998863, 3333973.05000000027939677 5576099.44000000040978193 453.10000000000002274, 3333967.72999999998137355 5576102.73000000044703484 453.60000000000002274, 3334005.68999999994412065 5576130.92999999970197678 453.5, 3334024.99000000022351742 5576141.4599999999627471 453.30000000000001137, 3334015.52000000001862645 5576159.90000000037252903 455.30000000000001137, 3334010.81000000005587935 5576166.83999999985098839 455.5, 3333996.70000000018626451 5576178.52000000048428774 455.69999999999998863, 3333994.28000000026077032 5576183.16000000014901161 455.80000000000001137, 3333992.4599999999627471 5576186.56000000052154064 456, 3333982.58000000007450581 5576198.88999999966472387 457.10000000000002274, 3333964.35999999986961484 5576218.38999999966472387 460.5, 3333933.55000000027939677 5576245.41000000014901161 466.69999999999998863, 3333902.72999999998137355 5576272.41999999992549419 470.69999999999998863, 3333876.7900000000372529 5576293.27000000048428774 472.19999999999998863, 3333850.85000000009313226 5576314.24000000022351742 474.10000000000002274, 3333845.35000000009313226 5576318.63999999966472387 474.69999999999998863, 3333819.91999999992549419 5576337.69000000040978193 476.30000000000001137, 3333844.28000000026077032 5576355.08000000007450581 477.30000000000001137, 3333868.64999999990686774 5576372.57000000029802322 477.80000000000001137, 3333897.91000000014901161 5576391.46999999973922968 478.60000000000002274, 3333927.08999999985098839 5576410.37000000011175871 479, 3333933.47999999998137355 5576416.17999999970197678 479.10000000000002274, 3333946.58999999985098839 5576438.48000000044703484 479.5, 3333961.68000000016763806 5576472.07000000029802322 479.60000000000002274, 3333976.85999999986961484 5576505.66999999992549419 478.40000000000003411, 3333977.74000000022351742 5576506.53000000026077032 478.30000000000001137, 3333982.37000000011175871 5576510.83999999985098839 478, 3333937.39000000013038516 5576531.83999999985098839 479.80000000000001137, 3333892.47999999998137355 5576552.73000000044703484 481.80000000000001137, 3333853.27000000001862645 5576571.10000000055879354 483.80000000000001137, 3333847.57000000029802322 5576573.73000000044703484 484.10000000000002274, 3333877.45000000018626451 5576603.17999999970197678 482.5, 3333907.32000000029802322 5576632.75 481.10000000000002274, 3333937.18999999994412065 5576662.2099999999627471 480.19999999999998863, 3333967.07000000029802322 5576691.78000000026077032 479.30000000000001137, 3333996.93999999994412065 5576721.23000000044703484 478.69999999999998863, 3334017.08999999985098839 5576736.07000000029802322 478.40000000000003411, 3334053.24000000022351742 5576759.10000000055879354 477, 3334089.33000000007450581 5576782.02000000048428774 473.90000000000003411, 3334133.33999999985098839 5576805.58000000007450581 471, 3334161.47000000020489097 5576824.74000000022351742 469.30000000000001137, 3334189.66000000014901161 5576843.88999999966472387 467.5, 3334196.74000000022351742 5576849.12999999988824129 467.5, 3334191.56000000005587935 5576854.52000000048428774 467.5, 3334200.33000000007450581 5576856.69000000040978193 467.5, 3334224.60999999986961484 5576862.73000000044703484 467.30000000000001137, 3334257.60000000009313226 5576870.92999999970197678 466, 3334286.28000000026077032 5576876.04999999981373549 464.69999999999998863, 3334315.03000000026077032 5576881.16000000014901161 464.80000000000001137, 3334321.03000000026077032 5576881.53000000026077032 464.90000000000003411, 3334328.2099999999627471 5576880.86000000033527613 464.80000000000001137, 3334366.72999999998137355 5576867.62999999988824129 464.19999999999998863, 3334405.85000000009313226 5576855.71999999973922968 463.10000000000002274, 3334445.05000000027939677 5576843.81000000052154064 461.69999999999998863, 3334450.10999999986961484 5576841.42999999970197678 461.60000000000002274, 3334488.43999999994412065 5576863.27000000048428774 462, 3334526.70000000018626451 5576885.12000000011175871 462.69999999999998863, 3334528.83999999985098839 5576887.40000000037252903 462.69999999999998863, 3334530.37999999988824129 5576888.67999999970197678 462.80000000000001137, 3334558.78000000026077032 5576912.2900000000372529 463.40000000000003411, 3334588.7099999999627471 5576937.17999999970197678 464.90000000000003411, 3334618.64999999990686774 5576962.07000000029802322 465.5, 3334648.66000000014901161 5576986.96999999973922968 466.60000000000002274, 3334654.95000000018626451 5576989.78000000026077032 466.80000000000001137, 3334679.01000000024214387 5577002.27000000048428774 466.90000000000003411, 3334703.07000000029802322 5577014.77000000048428774 467.30000000000001137, 3334745.20000000018626451 5577037.38999999966472387 467.5, 3334784.43999999994412065 5577058.87999999988824129 468.80000000000001137, 3334823.60999999986961484 5577080.37000000011175871 469.90000000000003411, 3334862.85000000009313226 5577101.74000000022351742 471.30000000000001137, 3334900.35000000009313226 5577122.28000000026077032 472, 3334902.08999999985098839 5577123.23000000044703484 472.10000000000002274, 3334877.7900000000372529 5577153.27000000048428774 472.30000000000001137, 3334853.47999999998137355 5577183.19000000040978193 471, 3334835.93000000016763806 5577205.56000000052154064 471.69999999999998863, 3334796.37000000011175871 5577235.50999999977648258 472.30000000000001137, 3334756.72999999998137355 5577265.36000000033527613 472.30000000000001137, 3334718.41000000014901161 5577293.83000000007450581 473.10000000000002274, 3334680.08000000007450581 5577322.29999999981373549 473.60000000000002274, 3334665.62999999988824129 5577334.44000000040978193 474, 3334660.56000000005587935 5577338.71999999973922968 474.10000000000002274, 3334688.05000000027939677 5577360.4599999999627471 474.69999999999998863, 3334715.60000000009313226 5577382.20000000018626451 476.19999999999998863, 3334743.16000000014901161 5577403.94000000040978193 478.40000000000003411, 3334776.10999999986961484 5577427.2900000000372529 480.69999999999998863, 3334789.16999999992549419 5577441.36000000033527613 481.10000000000002274, 3334798.64000000013038516 5577454.87000000011175871 481.69999999999998863, 3334823.64000000013038516 5577490.60000000055879354 482.5, 3334850.62999999988824129 5577530.95000000018626451 483.90000000000003411, 3334863.64000000013038516 5577550.36000000033527613 483.90000000000003411, 3334879.57000000029802322 5577574.02000000048428774 482.90000000000003411, 3334895.49000000022351742 5577597.67999999970197678 482, 3334915.58000000007450581 5577627.23000000044703484 481.19999999999998863, 3334932.68999999994412065 5577652.41000000014901161 481.69999999999998863, 3334739.87000000011175871 5577803.70000000018626451 472.40000000000003411, 3334741.5400000000372529 5577811.54999999981373549 472.40000000000003411, 3334745.35000000009313226 5577817.11000000033527613 472.60000000000002274, 3334751.37000000011175871 5577822.7099999999627471 472.90000000000003411, 3334764.9599999999627471 5577833.20000000018626451 473.60000000000002274, 3334775.78000000026077032 5577841.87999999988824129 474.30000000000001137, 3334802.14999999990686774 5577866.77000000048428774 476.10000000000002274, 3334824.31000000005587935 5577884.78000000026077032 477.5, 3334846.39999999990686774 5577902.79999999981373549 478.69999999999998863, 3334858 5577913.7900000000372529 479.30000000000001137, 3334866.75 5577922.08999999985098839 479.69999999999998863, 3334886.20000000018626451 5577935.62000000011175871 480.60000000000002274, 3334917.02000000001862645 5577963.94000000040978193 481.60000000000002274, 3334947.83999999985098839 5577992.25999999977648258 482.69999999999998863, 3334980.27000000001862645 5578019.29999999981373549 483.90000000000003411, 3334997.02000000001862645 5578035.48000000044703484 484.5, 3335011.66000000014901161 5578050.04999999981373549 485.10000000000002274, 3335018.89999999990686774 5578055.83999999985098839 485.5, 3335026.52000000001862645 5578060.28000000026077032 485.80000000000001137, 3335034.10000000009313226 5578063.37999999988824129 486.19999999999998863, 3335042.83000000007450581 5578066.56000000052154064 486.69999999999998863, 3335051.32000000029802322 5578069.29999999981373549 487.19999999999998863, 3335062.22000000020489097 5578071.29999999981373549 487.69999999999998863, 3335071.83000000007450581 5578071 488.30000000000001137, 3335072.49000000022351742 5578075.87999999988824129 488.30000000000001137, 3335069.37000000011175871 5578081.10000000055879354 488, 3335068.56000000005587935 5578082.4599999999627471 488, 3335061.85000000009313226 5578089.12000000011175871 487.5, 3335057.62999999988824129 5578095.49000000022351742 487.19999999999998863, 3335052.24000000022351742 5578105.67999999970197678 486.80000000000001137, 3335049.64000000013038516 5578118.11000000033527613 486.60000000000002274, 3335050.78000000026077032 5578125.20000000018626451 486.60000000000002274, 3335067.66000000014901161 5578145.27000000048428774 487.40000000000003411, 3335069.05000000027939677 5578146.33999999985098839 487.40000000000003411, 3335070.51000000024214387 5578147.52000000048428774 487.5, 3335071.70000000018626451 5578155.83999999985098839 487.5, 3335070.89999999990686774 5578162.2099999999627471 487.40000000000003411, 3335058.05000000027939677 5578200.67999999970197678 486.19999999999998863, 3335055.60999999986961484 5578213.78000000026077032 485.90000000000003411, 3335055.58000000007450581 5578231.25999999977648258 485.5, 3335060.80000000027939677 5578250.02000000048428774 485.19999999999998863, 3335070.4599999999627471 5578262.52000000048428774 485.30000000000001137, 3335088.83000000007450581 5578273.53000000026077032 485.80000000000001137, 3335122.58000000007450581 5578283.83000000007450581 487.19999999999998863, 3335129.10999999986961484 5578287.53000000026077032 487.40000000000003411, 3335135.32000000029802322 5578292.33999999985098839 487.60000000000002274, 3335142.18000000016763806 5578304.37999999988824129 487.60000000000002274, 3335153.49000000022351742 5578328.74000000022351742 487.5, 3335157.57000000029802322 5578333.73000000044703484 487.60000000000002274, 3335166.31000000005587935 5578339.81000000052154064 487.90000000000003411, 3335176.18999999994412065 5578343.62000000011175871 488.40000000000003411, 3335188.18000000016763806 5578348.58999999985098839 489, 3335198.06000000005587935 5578352.17999999970197678 489.5, 3335212.2900000000372529 5578356.19000000040978193 490.30000000000001137, 3335232.75 5578358.56000000052154064 491.60000000000002274, 3335246.12999999988824129 5578360.03000000026077032 492.40000000000003411, 3335261.60000000009313226 5578364.78000000026077032 493.40000000000003411, 3335269.58000000007450581 5578369.2099999999627471 493.80000000000001137, 3335272.35999999986961484 5578373.91000000014901161 493.90000000000003411, 3335273.53000000026077032 5578379.44000000040978193 493.90000000000003411, 3335268.93000000016763806 5578394.28000000026077032 493.30000000000001137, 3335258.22000000020489097 5578407.29999999981373549 492.30000000000001137, 3335250.87999999988824129 5578423.56000000052154064 491.60000000000002274, 3335247.0400000000372529 5578441.94000000040978193 491.19999999999998863, 3335244.08000000007450581 5578470.75 490.90000000000003411, 3335241.12999999988824129 5578499.57000000029802322 490.90000000000003411, 3335241.02000000001862645 5578528.07000000029802322 491.10000000000002274, 3335240.97999999998137355 5578556.4599999999627471 491.40000000000003411, 3335242.97999999998137355 5578568.20000000018626451 491.60000000000002274, 3335250.39999999990686774 5578588.90000000037252903 492.10000000000002274, 3335267.2900000000372529 5578623.44000000040978193 493, 3335282.62999999988824129 5578646.90000000037252903 493.5, 3335293.02000000001862645 5578660.15000000037252903 493.80000000000001137, 3335299.7099999999627471 5578671.19000000040978193 493.90000000000003411, 3335313.74000000022351742 5578689.23000000044703484 494, 3335332.68000000016763806 5578706.90000000037252903 494.30000000000001137, 3335351.55000000027939677 5578724.67999999970197678 494.40000000000003411, 3335359.97999999998137355 5578732.10000000055879354 494.60000000000002274, 3335371.70000000018626451 5578739.98000000044703484 494.80000000000001137, 3335382.33999999985098839 5578742.99000000022351742 495.19999999999998863, 3335393.41000000014901161 5578743.75999999977648258 495.80000000000001137, 3335401.12000000011175871 5578741.74000000022351742 496.19999999999998863, 3335425.22999999998137355 5578728.74000000022351742 497.60000000000002274, 3335444.9599999999627471 5578716.87999999988824129 498.80000000000001137, 3335448.28000000026077032 5578715.66999999992549419 499, 3335452.37000000011175871 5578712.08999999985098839 499.19999999999998863, 3335457.45000000018626451 5578710.37000000011175871 499.5, 3335464.91999999992549419 5578710.13999999966472387 499.90000000000003411, 3335472.25 5578711.81000000052154064 500.10000000000002274, 3335474.93000000016763806 5578713.38999999966472387 500.19999999999998863, 3335487.43999999994412065 5578721.4599999999627471 500.69999999999998863, 3335482.37999999988824129 5578728.29999999981373549 500.10000000000002274, 3335480.02000000001862645 5578730.37999999988824129 499.90000000000003411, 3335482.37999999988824129 5578728.29999999981373549 500.10000000000002274, 3335487.43999999994412065 5578721.4599999999627471 500.69999999999998863, 3335498.16000000014901161 5578711 501.90000000000003411, 3335535.64999999990686774 5578681.11000000033527613 504.69999999999998863, 3335554.72999999998137355 5578668.71999999973922968 505.40000000000003411, 3335562.2900000000372529 5578666.58999999985098839 505.60000000000002274, 3335572.64999999990686774 5578665.27000000048428774 505.90000000000003411, 3335617.82000000029802322 5578662.31000000052154064 506.80000000000001137, 3335630.16000000014901161 5578662.48000000044703484 507.10000000000002274, 3335657.70000000018626451 5578665.96999999973922968 508, 3335682.35999999986961484 5578672.66999999992549419 509.10000000000002274, 3335707.02000000001862645 5578679.46999999973922968 510.60000000000002274, 3335716.41000000014901161 5578683.62999999988824129 511.10000000000002274, 3335737.83999999985098839 5578696.54999999981373549 512.39999999999997726, 3335772.5 5578727.08999999985098839 514.29999999999995453, 3335807.56000000005587935 5578759.17999999970197678 516.20000000000004547, 3335842.62000000011175871 5578791.27000000048428774 518.10000000000002274, 3335877.76000000024214387 5578823.35000000055879354 520.10000000000002274, 3335912.10999999986961484 5578855.4599999999627471 522, 3335946.4599999999627471 5578887.67999999970197678 523.79999999999995453, 3335980.74000000022351742 5578919.91000000014901161 525.10000000000002274, 3336015.08999999985098839 5578952.02000000048428774 526.10000000000002274, 3336041.95000000018626451 5578974.7900000000372529 526.70000000000004547, 3336068.74000000022351742 5578997.56000000052154064 527.20000000000004547, 3336088.93999999994412065 5579019.20000000018626451 527.29999999999995453, 3336130.89999999990686774 5579027.81000000052154064 528.89999999999997726, 3336172.7900000000372529 5579036.41999999992549419 530.60000000000002274, 3336214.66999999992549419 5579044.91999999992549419 532.79999999999995453, 3336256.62999999988824129 5579053.53000000026077032 535.29999999999995453, 3336256.07000000029802322 5579051.44000000040978193 535.29999999999995453, 3336256.62999999988824129 5579053.53000000026077032 535.29999999999995453, 3336302.26000000024214387 5579063.37000000011175871 537.60000000000002274, 3336347.89000000013038516 5579073.08999999985098839 539.10000000000002274, 3336393.52000000001862645 5579082.91999999992549419 540, 3336439.08000000007450581 5579092.75999999977648258 540.5, 3336448.77000000001862645 5579092.90000000037252903 540.5, 3336473.87999999988824129 5579070.53000000026077032 540.20000000000004547, 3336498.99000000022351742 5579048.27000000048428774 539.60000000000002274, 3336504.80000000027939677 5579044.75 539.5, 3336535.35000000009313226 5579025.54999999981373549 538.20000000000004547, 3336549.68000000016763806 5579018.77000000048428774 537.10000000000002274, 3336583.76000000024214387 5578998.23000000044703484 534.29999999999995453, 3336617.89999999990686774 5578977.70000000018626451 531, 3336627.97999999998137355 5578971.60000000055879354 529.89999999999997726, 3336660.87000000011175871 5578951.99000000022351742 526.20000000000004547, 3336687.24000000022351742 5578931.36000000033527613 523, 3336695.5400000000372529 5578923.20000000018626451 521.79999999999995453, 3336703.18999999994412065 5578916.83999999985098839 520.60000000000002274, 3336719.33999999985098839 5578902.2099999999627471 518.10000000000002274, 3336726.20000000018626451 5578893.5400000000372529 517, 3336746.83000000007450581 5578866.74000000022351742 513.5, 3336751.32000000029802322 5578859.70000000018626451 512.79999999999995453, 3336754.32000000029802322 5578853.25999999977648258 512.10000000000002274, 3336756.2900000000372529 5578847.74000000022351742 511.60000000000002274, 3336757.47000000020489097 5578835.02000000048428774 510.69999999999998863, 3336758.87999999988824129 5578806.91999999992549419 509, 3336760.22000000020489097 5578778.7099999999627471 507.69999999999998863, 3336775.47000000020489097 5578769.45000000018626451 505.69999999999998863, 3336787 5578768.75999999977648258 504.40000000000003411, 3336828.33999999985098839 5578771.27000000048428774 500.5, 3336833.91000000014901161 5578771.66000000014901161 500, 3336871.18999999994412065 5578771.83999999985098839 496.10000000000002274, 3336882.47000000020489097 5578769.94000000040978193 495.10000000000002274, 3336905.57000000029802322 5578763.21999999973922968 493.80000000000001137, 3336909.60000000009313226 5578762.08999999985098839 493.60000000000002274, 3336915.16000000014901161 5578760.03000000026077032 493.40000000000003411, 3336920.07000000029802322 5578757.31000000052154064 493.30000000000001137, 3336938.26000000024214387 5578755.53000000026077032 493, 3336963.85000000009313226 5578753.52000000048428774 492.60000000000002274, 3336995.78000000026077032 5578748.63999999966472387 491.90000000000003411, 3337027.70000000018626451 5578743.65000000037252903 490.40000000000003411, 3337037.77000000001862645 5578739.78000000026077032 490, 3337054.26000000024214387 5578729.25 489.60000000000002274, 3337062.87999999988824129 5578721.96999999973922968 489.40000000000003411, 3337070.01000000024214387 5578712.9599999999627471 489.40000000000003411, 3337079.2099999999627471 5578706.21999999973922968 489, 3337103.58000000007450581 5578685.2099999999627471 486.80000000000001137, 3337128.03000000026077032 5578664.20000000018626451 485.19999999999998863, 3337136.2099999999627471 5578654.48000000044703484 484.69999999999998863, 3337149.72999999998137355 5578639.92999999970197678 484.19999999999998863, 3337169.16000000014901161 5578618.07000000029802322 484.80000000000001137, 3337188.58000000007450581 5578596.10000000055879354 484.69999999999998863, 3337199.12999999988824129 5578582.19000000040978193 484.5, 3337206.60000000009313226 5578567.92999999970197678 484.30000000000001137, 3337210.70000000018626451 5578559.90000000037252903 484, 3337217.64999999990686774 5578540.10000000055879354 483.40000000000003411, 3337220.58000000007450581 5578530.87999999988824129 483.10000000000002274, 3337225.10999999986961484 5578509.13999999966472387 482.80000000000001137, 3337228.07000000029802322 5578503.49000000022351742 482.80000000000001137, 3337233 5578498.99000000022351742 482.60000000000002274, 3337238.93000000016763806 5578494.91000000014901161 482.5, 3337246.08000000007450581 5578493.25 482.40000000000003411, 3337253.07000000029802322 5578493.48000000044703484 482.30000000000001137, 3337259.5 5578494.06000000052154064 482.19999999999998863, 3337301.62000000011175871 5578505.57000000029802322 482.40000000000003411, 3337327.77000000001862645 5578512.11000000033527613 482.80000000000001137, 3337371.64000000013038516 5578522.57000000029802322 483.60000000000002274, 3337412.07000000029802322 5578530.12000000011175871 482.69999999999998863, 3337426.83999999985098839 5578533.12000000011175871 482, 3337445.99000000022351742 5578534.75999999977648258 481.30000000000001137, 3337467.58000000007450581 5578534.5400000000372529 480.60000000000002274, 3337479.07000000029802322 5578532.74000000022351742 480.5, 3337502.45000000018626451 5578525.7900000000372529 480.19999999999998863, 3337536.2099999999627471 5578511.06000000052154064 480.19999999999998863, 3337569.2099999999627471 5578492.24000000022351742 480.5, 3337582.7099999999627471 5578481.69000000040978193 480, 3337586.41000000014901161 5578479.24000000022351742 479.90000000000003411, 3337611.68999999994412065 5578464.54999999981373549 477.80000000000001137, 3337636.97000000020489097 5578449.75 474.80000000000001137, 3337635.28000000026077032 5578445.90000000037252903 475.30000000000001137, 3337630.18999999994412065 5578433.15000000037252903 476.69999999999998863, 3337625.18000000016763806 5578414.0400000000372529 477.90000000000003411, 3337624.4599999999627471 5578409.04999999981373549 478, 3337623.97999999998137355 5578407.40000000037252903 478.10000000000002274, 3337174.80000000027939677 5577945.83999999985098839 515.5, 3337159.64999999990686774 5577937.62000000011175871 516.70000000000004547, 3337157.97999999998137355 5577936.78000000026077032 516.79999999999995453, 3337136.99000000022351742 5577924.07000000029802322 518.70000000000004547, 3337106.87999999988824129 5577904.50999999977648258 521.10000000000002274, 3337096.45000000018626451 5577899.15000000037252903 521.70000000000004547, 3337063.07000000029802322 5577875.24000000022351742 524.10000000000002274, 3337038.06000000005587935 5577854.75 525.5, 3337018.52000000001862645 5577840.77000000048428774 526.39999999999997726, 3336996.72999999998137355 5577822.9599999999627471 528, 3336975.85000000009313226 5577806.90000000037252903 529.29999999999995453, 3336937.47999999998137355 5577778.58000000007450581 530.70000000000004547, 3336928.76000000024214387 5577773.06000000052154064 530.89999999999997726, 3336918.9599999999627471 5577762.66999999992549419 531.29999999999995453, 3336916.62000000011175871 5577760.85000000055879354 531.39999999999997726, 3336897.55000000027939677 5577741.07000000029802322 532.29999999999995453, 3336883.32000000029802322 5577728.15000000037252903 532.70000000000004547, 3336874.9599999999627471 5577722.83999999985098839 532.89999999999997726, 3336868.77000000001862645 5577723.25 532.70000000000004547, 3336858.4599999999627471 5577721.67999999970197678 532.39999999999997726, 3336848.10000000009313226 5577718.54999999981373549 532, 3336834.16000000014901161 5577712.74000000022351742 531.60000000000002274, 3336805.22999999998137355 5577696.82000000029802322 530.79999999999995453, 3336776.35999999986961484 5577680.7900000000372529 530.39999999999997726, 3336751.16000000014901161 5577665.87000000011175871 529.89999999999997726, 3336740.97999999998137355 5577661.62000000011175871 529.60000000000002274, 3336718.66000000014901161 5577649.83999999985098839 529.5, 3336696.33999999985098839 5577638.06000000052154064 529.29999999999995453, 3336658.81000000005587935 5577611.37999999988824129 528.29999999999995453, 3336637.93000000016763806 5577597.78000000026077032 527.70000000000004547, 3336627.7099999999627471 5577589.96999999973922968 527.20000000000004547, 3336620.9599999999627471 5577586.06000000052154064 526.89999999999997726, 3336614.82000000029802322 5577581.12000000011175871 526.79999999999995453, 3336603.93000000016763806 5577574.88999999966472387 526.5, 3336593.30000000027939677 5577567.75999999977648258 526.20000000000004547, 3336583.91000000014901161 5577558.91999999992549419 526, 3336552.62999999988824129 5577536.2900000000372529 524.89999999999997726, 3336521.33999999985098839 5577513.65000000037252903 523.39999999999997726, 3336495.5 5577491.74000000022351742 521.89999999999997726, 3336470.58999999985098839 5577467.57000000029802322 520.20000000000004547, 3336462.5400000000372529 5577460.91999999992549419 519.70000000000004547, 3336446.43999999994412065 5577444.94000000040978193 518.60000000000002274, 3336435.85000000009313226 5577434.46999999973922968 517.89999999999997726, 3336428.47000000020489097 5577426.23000000044703484 517.39999999999997726, 3336424.35000000009313226 5577412.88999999966472387 517.29999999999995453, 3336419.75 5577388.65000000037252903 517.20000000000004547, 3336413.91000000014901161 5577349.75999999977648258 514.89999999999997726, 3336405.95000000018626451 5577338.98000000044703484 513.70000000000004547, 3336391.60999999986961484 5577324.73000000044703484 512.39999999999997726, 3336379.16000000014901161 5577313.87000000011175871 511.69999999999998863, 3336388.25 5577310.46999999973922968 511, 3336401.57000000029802322 5577303.37999999988824129 509.90000000000003411, 3336406.75 5577297.77000000048428774 509.30000000000001137, 3336426.43000000016763806 5577261.08999999985098839 506.40000000000003411, 3336430.89000000013038516 5577250.7099999999627471 505.90000000000003411, 3336448.58999999985098839 5577214.5400000000372529 502.60000000000002274, 3336459.14999999990686774 5577191.50999999977648258 500.30000000000001137, 3336469.62999999988824129 5577168.46999999973922968 498.19999999999998863, 3336483.31000000005587935 5577138.21999999973922968 495.90000000000003411, 3336496.91999999992549419 5577107.9599999999627471 493.5, 3336485.72000000020489097 5577100.74000000022351742 493.69999999999998863, 3336460.83999999985098839 5577084.58000000007450581 494, 3336424.68000000016763806 5577061.2099999999627471 494.40000000000003411, 3336409.10999999986961484 5577050.88999999966472387 494.69999999999998863, 3336372.41999999992549419 5577026.53000000026077032 494.60000000000002274, 3336340.64000000013038516 5577001.4599999999627471 494, 3336308.78000000026077032 5576976.50999999977648258 493.19999999999998863, 3336302.85999999986961484 5576971.90000000037252903 493.10000000000002274, 3336278.39999999990686774 5576953.16000000014901161 492.40000000000003411, 3336253.97999999998137355 5576934.31000000052154064 492.10000000000002274, 3336242.87999999988824129 5576925.00999999977648258 491.80000000000001137, 3336239.66999999992549419 5576922.74000000022351742 491.80000000000001137, 3336231.31000000005587935 5576918.95000000018626451 491.80000000000001137, 3336207.10000000009313226 5576899.11000000033527613 491.40000000000003411, 3336182.97999999998137355 5576879.28000000026077032 491, 3336158.81000000005587935 5576859.4599999999627471 490.5, 3336147.72000000020489097 5576849.10000000055879354 490.30000000000001137, 3336145.72000000020489097 5576847.67999999970197678 490.30000000000001137, 3336087.18999999994412065 5576793.20000000018626451 488.80000000000001137, 3336028.64999999990686774 5576738.71999999973922968 487.60000000000002274, 3336020.89999999990686774 5576732.32000000029802322 487.40000000000003411, 3335964.66000000014901161 5576679.60000000055879354 485.69999999999998863, 3335908.43999999994412065 5576626.78000000026077032 483.30000000000001137, 3335875.64000000013038516 5576598.03000000026077032 481.80000000000001137, 3335842.85000000009313226 5576569.27000000048428774 480.30000000000001137, 3335811.89000000013038516 5576540.67999999970197678 478.69999999999998863, 3335780.85000000009313226 5576512.08000000007450581 477, 3335749.83000000007450581 5576483.36000000033527613 475.19999999999998863, 3335718.81000000005587935 5576454.75 473.19999999999998863, 3335706.14000000013038516 5576443.75999999977648258 472.19999999999998863, 3335708.12000000011175871 5576446.70000000018626451 472.40000000000003411, 3335706.14000000013038516 5576443.75999999977648258 472.19999999999998863, 3335687.87999999988824129 5576426.04999999981373549 471.10000000000002274, 3335656.93999999994412065 5576397.2900000000372529 469.10000000000002274, 3335626.07000000029802322 5576368.5400000000372529 466.80000000000001137, 3335595.12000000011175871 5576339.88999999966472387 464.69999999999998863, 3335564.16999999992549419 5576311.12000000011175871 462.80000000000001137, 3335538.91000000014901161 5576287.16000000014901161 461.5, 3335519.85999999986961484 5576267.87000000011175871 460.69999999999998863, 3335497.93000000016763806 5576244.04999999981373549 459.90000000000003411, 3335475.93000000016763806 5576220.24000000022351742 459.69999999999998863, 3335466.27000000001862645 5576210.86000000033527613 459.90000000000003411, 3335455.82000000029802322 5576201.78000000026077032 460.10000000000002274, 3335432.95000000018626451 5576177.38999999966472387 460.19999999999998863, 3335425.58000000007450581 5576168.36000000033527613 460.60000000000002274, 3335421.10999999986961484 5576162.24000000022351742 460.80000000000001137, 3335414.08999999985098839 5576149.83000000007450581 461.30000000000001137, 3335408.64000000013038516 5576137.75999999977648258 462.10000000000002274, 3335404.62999999988824129 5576131.37999999988824129 462.10000000000002274, 3335402.9599999999627471 5576127.37000000011175871 462.10000000000002274, 3335398.9599999999627471 5576119.58999999985098839 461.80000000000001137, 3335383.24000000022351742 5576099.66999999992549419 460.60000000000002274, 3335368.35999999986961484 5576080.92999999970197678 458.69999999999998863, 3335366.43999999994412065 5576078.37999999988824129 458.40000000000003411, 3335345.5 5576052.17999999970197678 456.19999999999998863, 3335329.89999999990686774 5576031.4599999999627471 454.60000000000002274, 3335314.20000000018626451 5576010.74000000022351742 452.80000000000001137, 3335295.60999999986961484 5575988.12999999988824129 451.10000000000002274, 3335277.02000000001862645 5575965.41000000014901161 449.30000000000001137, 3335258.43999999994412065 5575942.70000000018626451 447.19999999999998863, 3335239.79999999981373549 5575920 444.60000000000002274, 3335221.08000000007450581 5575897.2099999999627471 442, 3335202.4599999999627471 5575874.52000000048428774 439.80000000000001137, 3335186.07000000029802322 5575853.58000000007450581 437.90000000000003411, 3335175.08999999985098839 5575838.9599999999627471 436.90000000000003411, 3335169.43999999994412065 5575831.75 436.60000000000002274, 3335161.33999999985098839 5575821.62999999988824129 436.30000000000001137, 3335149.41000000014901161 5575805.66000000014901161 436.10000000000002274, 3335136.52000000001862645 5575788.66000000014901161 436.40000000000003411, 3335113.07000000029802322 5575757.61000000033527613 436.69999999999998863, 3335089.62999999988824129 5575726.4599999999627471 436.30000000000001137, 3335066.12999999988824129 5575695.33000000007450581 436.10000000000002274, 3335042.64999999990686774 5575664.20000000018626451 436.19999999999998863, 3335035.77000000001862645 5575654.83000000007450581 436.40000000000003411, 3335024.91999999992549419 5575640.25999999977648258 437.10000000000002274, 3335014.07000000029802322 5575625.70000000018626451 437.60000000000002274, 3335003.24000000022351742 5575611.12000000011175871 437.90000000000003411, 3334992.39999999990686774 5575596.4599999999627471 437.90000000000003411, 3334981.48000000044703484 5575581.91000000014901161 437.60000000000002274, 3334970.66000000014901161 5575567.35000000055879354 437.30000000000001137, 3334959.83999999985098839 5575552.78000000026077032 437.10000000000002274, 3334948.92999999970197678 5575538.2099999999627471 436.90000000000003411, 3334938.12000000011175871 5575523.66000000014901161 436.80000000000001137, 3334927.2900000000372529 5575509.08000000007450581 436.80000000000001137, 3334916.39000000013038516 5575494.4599999999627471 436.80000000000001137, 3334905.58999999985098839 5575479.91000000014901161 436.60000000000002274, 3334894.77000000048428774 5575465.33000000007450581 436.30000000000001137, 3334883.87000000011175871 5575450.70000000018626451 436.10000000000002274, 3334873.06000000052154064 5575436.12999999988824129 436.10000000000002274, 3334862.16999999992549419 5575421.4599999999627471 436.30000000000001137, 3334851.27000000048428774 5575406.78000000026077032 436.5, 3334840.39000000013038516 5575392.20000000018626451 436.60000000000002274, 3334828.03000000026077032 5575378.04999999981373549 436.80000000000001137, 3334815.52000000048428774 5575363.87999999988824129 437.10000000000002274, 3334803.10000000055879354 5575349.66999999992549419 437.30000000000001137, 3334790.76000000024214387 5575335.53000000026077032 437.30000000000001137, 3334778.33000000007450581 5575321.33000000007450581 437.30000000000001137, 3334765.91000000014901161 5575307.12000000011175871 437.40000000000003411, 3334753.58000000007450581 5575292.99000000022351742 437.5, 3334741.16000000014901161 5575278.79999999981373549 437.5, 3334728.7400000002235174 5575264.58999999985098839 437.5, 3334716.23000000044703484 5575250.38999999966472387 437.5, 3334703.72000000020489097 5575236.20000000018626451 437.5, 3334691.39000000013038516 5575222.08000000007450581 437.60000000000002274, 3334685.81000000052154064 5575215.86000000033527613 437.80000000000001137, 3334677.75 5575206.58999999985098839 438.30000000000001137, 3334665.16999999992549419 5575192.2099999999627471 438.5, 3334637.02000000048428774 5575161.03000000026077032 439.40000000000003411, 3334602.43999999994412065 5575121.95000000018626451 440.19999999999998863, 3334567.86000000033527613 5575082.86000000033527613 440.60000000000002274, 3334533.31000000005587935 5575043.83000000007450581 441.10000000000002274, 3334498.70000000018626451 5575004.75 441.60000000000002274, 3334471.43000000016763806 5574973.86000000033527613 441.80000000000001137, 3334444.16000000014901161 5574942.97000000020489097 442.10000000000002274))", + "LINESTRING (3334177.9 5577952.3, 3337619.7 5578406.6)", + 3.36203807) +} + +func TestDiscreteFrechetDistance_Short(t *testing.T) { + algorithmDistance_discreteFrechetDistance_runTest(t, "LINESTRING (1 1, 2 2)", + "LINESTRING (1 4, 2 3)", 3.0) +} + +func TestDiscreteFrechetDistance_AHasMoreThanTwiceVerticesOfB(t *testing.T) { + algorithmDistance_discreteFrechetDistance_runTest(t, "LINESTRING (80 260, 170 180, 190 290, 310 350, 330 270, 360 280)", + "LINESTRING (120 90, 380 130)", 230.8679276123039) +} + +func TestDiscreteFrechetDistance_A_11_B_3(t *testing.T) { + algorithmDistance_discreteFrechetDistance_runTest(t, "LINESTRING (0 0, 100 10, 0 20, 100 30, 0 40, 100 50, 0 60, 100 70, 0 80, 100 90, 0 100)", + "LINESTRING (0 0, 50 100, 100 0)", 141.4213562373095) +} + +const algorithmDistance_discreteFrechetDistance_tolerance = 0.00001 + +func algorithmDistance_discreteFrechetDistance_runTest(t *testing.T, wkt1, wkt2 string, expectedDistance float64) { + t.Helper() + g1 := algorithmDistance_discreteFrechetDistance_readWKT(t, wkt1) + g2 := algorithmDistance_discreteFrechetDistance_readWKT(t, wkt2) + + AlgorithmDistance_DiscreteFrechetDistance_Distance(g1, g2) + distance1 := AlgorithmDistance_DiscreteFrechetDistance_Distance(g1, g2) + junit.AssertEqualsFloat64(t, expectedDistance, distance1, algorithmDistance_discreteFrechetDistance_tolerance) +} + +func algorithmDistance_discreteFrechetDistance_readWKT(t *testing.T, wkt string) *Geom_Geometry { + t.Helper() + reader := Io_NewWKTReader() + geom, err := reader.Read(wkt) + if err != nil { + t.Fatalf("Failed to parse WKT: %v", err) + } + return geom +} diff --git a/internal/jtsport/jts/algorithm_distance_discrete_hausdorff_distance.go b/internal/jtsport/jts/algorithm_distance_discrete_hausdorff_distance.go new file mode 100644 index 00000000..026ab114 --- /dev/null +++ b/internal/jtsport/jts/algorithm_distance_discrete_hausdorff_distance.go @@ -0,0 +1,198 @@ +package jts + +import "math" + +// AlgorithmDistance_DiscreteHausdorffDistance is an algorithm for computing a distance metric +// which is an approximation to the Hausdorff Distance +// based on a discretization of the input Geometry. +// The algorithm computes the Hausdorff distance restricted to discrete points +// for one of the geometries. +// The points can be either the vertices of the geometries (the default), +// or the geometries with line segments densified by a given fraction. +// Also determines two points of the Geometries which are separated by the computed distance. +// +// This algorithm is an approximation to the standard Hausdorff distance. +// Specifically, +// +// for all geometries a, b: DHD(a, b) <= HD(a, b) +// +// The approximation can be made as close as needed by densifying the input geometries. +// In the limit, this value will approach the true Hausdorff distance: +// +// DHD(A, B, densifyFactor) -> HD(A, B) as densifyFactor -> 0.0 +// +// The default approximation is exact or close enough for a large subset of useful cases. +// Examples of these are: +// - computing distance between Linestrings that are roughly parallel to each other, +// and roughly equal in length. This occurs in matching linear networks. +// - Testing similarity of geometries. +// +// An example where the default approximation is not close is: +// +// A = LINESTRING (0 0, 100 0, 10 100, 10 100) +// B = LINESTRING (0 100, 0 10, 80 10) +// +// DHD(A, B) = 22.360679774997898 +// HD(A, B) ~= 47.8 +type AlgorithmDistance_DiscreteHausdorffDistance struct { + g0 *Geom_Geometry + g1 *Geom_Geometry + ptDist *AlgorithmDistance_PointPairDistance + // Value of 0.0 indicates that no densification should take place + densifyFrac float64 +} + +// AlgorithmDistance_DiscreteHausdorffDistance_Distance computes the Discrete Hausdorff Distance +// of two Geometries. +func AlgorithmDistance_DiscreteHausdorffDistance_Distance(g0, g1 *Geom_Geometry) float64 { + dist := AlgorithmDistance_NewDiscreteHausdorffDistance(g0, g1) + return dist.Distance() +} + +// AlgorithmDistance_DiscreteHausdorffDistance_DistanceWithDensifyFrac computes the Discrete Hausdorff Distance +// of two Geometries with a densify fraction. +func AlgorithmDistance_DiscreteHausdorffDistance_DistanceWithDensifyFrac(g0, g1 *Geom_Geometry, densifyFrac float64) float64 { + dist := AlgorithmDistance_NewDiscreteHausdorffDistance(g0, g1) + dist.SetDensifyFraction(densifyFrac) + return dist.Distance() +} + +// AlgorithmDistance_NewDiscreteHausdorffDistance creates a new DiscreteHausdorffDistance. +func AlgorithmDistance_NewDiscreteHausdorffDistance(g0, g1 *Geom_Geometry) *AlgorithmDistance_DiscreteHausdorffDistance { + return &AlgorithmDistance_DiscreteHausdorffDistance{ + g0: g0, + g1: g1, + ptDist: AlgorithmDistance_NewPointPairDistance(), + } +} + +// SetDensifyFraction sets the fraction by which to densify each segment. +// Each segment will be (virtually) split into a number of equal-length +// subsegments, whose fraction of the total length is closest +// to the given fraction. +func (dhd *AlgorithmDistance_DiscreteHausdorffDistance) SetDensifyFraction(densifyFrac float64) { + if densifyFrac > 1.0 || densifyFrac <= 0.0 { + panic("Fraction is not in range (0.0 - 1.0]") + } + dhd.densifyFrac = densifyFrac +} + +// Distance computes the Discrete Hausdorff Distance. +func (dhd *AlgorithmDistance_DiscreteHausdorffDistance) Distance() float64 { + dhd.compute(dhd.g0, dhd.g1) + return dhd.ptDist.GetDistance() +} + +// OrientedDistance computes the oriented Discrete Hausdorff Distance. +func (dhd *AlgorithmDistance_DiscreteHausdorffDistance) OrientedDistance() float64 { + dhd.computeOrientedDistance(dhd.g0, dhd.g1, dhd.ptDist) + return dhd.ptDist.GetDistance() +} + +// GetCoordinates returns the coordinates of the points that are the computed distance apart. +func (dhd *AlgorithmDistance_DiscreteHausdorffDistance) GetCoordinates() []*Geom_Coordinate { + return dhd.ptDist.GetCoordinates() +} + +func (dhd *AlgorithmDistance_DiscreteHausdorffDistance) compute(g0, g1 *Geom_Geometry) { + dhd.computeOrientedDistance(g0, g1, dhd.ptDist) + dhd.computeOrientedDistance(g1, g0, dhd.ptDist) +} + +func (dhd *AlgorithmDistance_DiscreteHausdorffDistance) computeOrientedDistance(discreteGeom, geom *Geom_Geometry, ptDist *AlgorithmDistance_PointPairDistance) { + distFilter := algorithmDistance_newMaxPointDistanceFilter(geom) + discreteGeom.ApplyCoordinateFilter(distFilter) + ptDist.SetMaximumFromPointPairDistance(distFilter.getMaxPointDistance()) + + if dhd.densifyFrac > 0 { + fracFilter := algorithmDistance_newMaxDensifiedByFractionDistanceFilter(geom, dhd.densifyFrac) + discreteGeom.ApplyCoordinateSequenceFilter(fracFilter) + ptDist.SetMaximumFromPointPairDistance(fracFilter.getMaxPointDistance()) + } +} + +// algorithmDistance_maxPointDistanceFilter is a filter to compute the maximum distance +// from all coordinates to a geometry. +type algorithmDistance_maxPointDistanceFilter struct { + maxPtDist *AlgorithmDistance_PointPairDistance + minPtDist *AlgorithmDistance_PointPairDistance + geom *Geom_Geometry +} + +var _ Geom_CoordinateFilter = (*algorithmDistance_maxPointDistanceFilter)(nil) + +func algorithmDistance_newMaxPointDistanceFilter(geom *Geom_Geometry) *algorithmDistance_maxPointDistanceFilter { + return &algorithmDistance_maxPointDistanceFilter{ + maxPtDist: AlgorithmDistance_NewPointPairDistance(), + minPtDist: AlgorithmDistance_NewPointPairDistance(), + geom: geom, + } +} + +func (f *algorithmDistance_maxPointDistanceFilter) IsGeom_CoordinateFilter() {} + +func (f *algorithmDistance_maxPointDistanceFilter) Filter(pt *Geom_Coordinate) { + f.minPtDist.Initialize() + AlgorithmDistance_DistanceToPoint_ComputeDistanceGeometry(f.geom, pt, f.minPtDist) + f.maxPtDist.SetMaximumFromPointPairDistance(f.minPtDist) +} + +func (f *algorithmDistance_maxPointDistanceFilter) getMaxPointDistance() *AlgorithmDistance_PointPairDistance { + return f.maxPtDist +} + +// algorithmDistance_maxDensifiedByFractionDistanceFilter is a filter to compute the maximum distance +// from densified segments to a geometry. +type algorithmDistance_maxDensifiedByFractionDistanceFilter struct { + maxPtDist *AlgorithmDistance_PointPairDistance + minPtDist *AlgorithmDistance_PointPairDistance + geom *Geom_Geometry + numSubSegs int +} + +var _ Geom_CoordinateSequenceFilter = (*algorithmDistance_maxDensifiedByFractionDistanceFilter)(nil) + +func algorithmDistance_newMaxDensifiedByFractionDistanceFilter(geom *Geom_Geometry, fraction float64) *algorithmDistance_maxDensifiedByFractionDistanceFilter { + return &algorithmDistance_maxDensifiedByFractionDistanceFilter{ + maxPtDist: AlgorithmDistance_NewPointPairDistance(), + minPtDist: AlgorithmDistance_NewPointPairDistance(), + geom: geom, + numSubSegs: int(math.Round(1.0 / fraction)), + } +} + +func (f *algorithmDistance_maxDensifiedByFractionDistanceFilter) IsGeom_CoordinateSequenceFilter() {} + +func (f *algorithmDistance_maxDensifiedByFractionDistanceFilter) Filter(seq Geom_CoordinateSequence, index int) { + // This logic also handles skipping Point geometries + if index == 0 { + return + } + + p0 := seq.GetCoordinate(index - 1) + p1 := seq.GetCoordinate(index) + + delx := (p1.X - p0.X) / float64(f.numSubSegs) + dely := (p1.Y - p0.Y) / float64(f.numSubSegs) + + for i := 0; i < f.numSubSegs; i++ { + x := p0.X + float64(i)*delx + y := p0.Y + float64(i)*dely + pt := Geom_NewCoordinateWithXY(x, y) + f.minPtDist.Initialize() + AlgorithmDistance_DistanceToPoint_ComputeDistanceGeometry(f.geom, pt, f.minPtDist) + f.maxPtDist.SetMaximumFromPointPairDistance(f.minPtDist) + } +} + +func (f *algorithmDistance_maxDensifiedByFractionDistanceFilter) IsGeometryChanged() bool { + return false +} + +func (f *algorithmDistance_maxDensifiedByFractionDistanceFilter) IsDone() bool { + return false +} + +func (f *algorithmDistance_maxDensifiedByFractionDistanceFilter) getMaxPointDistance() *AlgorithmDistance_PointPairDistance { + return f.maxPtDist +} diff --git a/internal/jtsport/jts/algorithm_distance_discrete_hausdorff_distance_test.go b/internal/jtsport/jts/algorithm_distance_discrete_hausdorff_distance_test.go new file mode 100644 index 00000000..075d4257 --- /dev/null +++ b/internal/jtsport/jts/algorithm_distance_discrete_hausdorff_distance_test.go @@ -0,0 +1,57 @@ +package jts + +import ( + "testing" + + "github.com/peterstace/simplefeatures/internal/jtsport/junit" +) + +func TestDiscreteHausdorffDistance_LineSegments(t *testing.T) { + algorithmDistance_discreteHausdorffDistance_runTest(t, "LINESTRING (0 0, 2 1)", "LINESTRING (0 0, 2 0)", 1.0) +} + +func TestDiscreteHausdorffDistance_LineSegments2(t *testing.T) { + algorithmDistance_discreteHausdorffDistance_runTest(t, "LINESTRING (0 0, 2 0)", "LINESTRING (0 1, 1 2, 2 1)", 2.0) +} + +func TestDiscreteHausdorffDistance_LinePoints(t *testing.T) { + algorithmDistance_discreteHausdorffDistance_runTest(t, "LINESTRING (0 0, 2 0)", "MULTIPOINT (0 1, 1 0, 2 1)", 1.0) +} + +// TestDiscreteHausdorffDistance_LinesShowingDiscretenessEffect shows effects of limiting HD to vertices. +// Answer is not true Hausdorff distance. +func TestDiscreteHausdorffDistance_LinesShowingDiscretenessEffect(t *testing.T) { + algorithmDistance_discreteHausdorffDistance_runTest(t, "LINESTRING (130 0, 0 0, 0 150)", "LINESTRING (10 10, 10 150, 130 10)", 14.142135623730951) + // densifying provides accurate HD + algorithmDistance_discreteHausdorffDistance_runTestWithDensifyFrac(t, "LINESTRING (130 0, 0 0, 0 150)", "LINESTRING (10 10, 10 150, 130 10)", 0.5, 70.0) +} + +const algorithmDistance_discreteHausdorffDistance_tolerance = 0.00001 + +func algorithmDistance_discreteHausdorffDistance_runTest(t *testing.T, wkt1, wkt2 string, expectedDistance float64) { + t.Helper() + g1 := algorithmDistance_discreteHausdorffDistance_readWKT(t, wkt1) + g2 := algorithmDistance_discreteHausdorffDistance_readWKT(t, wkt2) + + distance := AlgorithmDistance_DiscreteHausdorffDistance_Distance(g1, g2) + junit.AssertEqualsFloat64(t, expectedDistance, distance, algorithmDistance_discreteHausdorffDistance_tolerance) +} + +func algorithmDistance_discreteHausdorffDistance_runTestWithDensifyFrac(t *testing.T, wkt1, wkt2 string, densifyFrac, expectedDistance float64) { + t.Helper() + g1 := algorithmDistance_discreteHausdorffDistance_readWKT(t, wkt1) + g2 := algorithmDistance_discreteHausdorffDistance_readWKT(t, wkt2) + + distance := AlgorithmDistance_DiscreteHausdorffDistance_DistanceWithDensifyFrac(g1, g2, densifyFrac) + junit.AssertEqualsFloat64(t, expectedDistance, distance, algorithmDistance_discreteHausdorffDistance_tolerance) +} + +func algorithmDistance_discreteHausdorffDistance_readWKT(t *testing.T, wkt string) *Geom_Geometry { + t.Helper() + reader := Io_NewWKTReader() + geom, err := reader.Read(wkt) + if err != nil { + t.Fatalf("Failed to parse WKT: %v", err) + } + return geom +} diff --git a/internal/jtsport/jts/algorithm_distance_distance_to_point.go b/internal/jtsport/jts/algorithm_distance_distance_to_point.go new file mode 100644 index 00000000..d75e0e45 --- /dev/null +++ b/internal/jtsport/jts/algorithm_distance_distance_to_point.go @@ -0,0 +1,60 @@ +package jts + +import "github.com/peterstace/simplefeatures/internal/jtsport/java" + +// AlgorithmDistance_DistanceToPoint computes the Euclidean distance (L2 metric) +// from a Coordinate to a Geometry. +// Also computes two points on the geometry which are separated by the distance found. +type AlgorithmDistance_DistanceToPoint struct{} + +// AlgorithmDistance_NewDistanceToPoint creates a new DistanceToPoint. +func AlgorithmDistance_NewDistanceToPoint() *AlgorithmDistance_DistanceToPoint { + return &AlgorithmDistance_DistanceToPoint{} +} + +// AlgorithmDistance_DistanceToPoint_ComputeDistanceGeometry computes the distance +// from a Coordinate to a Geometry. +func AlgorithmDistance_DistanceToPoint_ComputeDistanceGeometry(geom *Geom_Geometry, pt *Geom_Coordinate, ptDist *AlgorithmDistance_PointPairDistance) { + if java.InstanceOf[*Geom_LineString](geom) { + AlgorithmDistance_DistanceToPoint_ComputeDistanceLineString(java.Cast[*Geom_LineString](geom), pt, ptDist) + } else if java.InstanceOf[*Geom_Polygon](geom) { + AlgorithmDistance_DistanceToPoint_ComputeDistancePolygon(java.Cast[*Geom_Polygon](geom), pt, ptDist) + } else if java.InstanceOf[*Geom_GeometryCollection](geom) { + gc := java.Cast[*Geom_GeometryCollection](geom) + for i := 0; i < gc.GetNumGeometries(); i++ { + g := gc.GetGeometryN(i) + AlgorithmDistance_DistanceToPoint_ComputeDistanceGeometry(g, pt, ptDist) + } + } else { // assume geom is Point + ptDist.SetMinimum(geom.GetCoordinate(), pt) + } +} + +// AlgorithmDistance_DistanceToPoint_ComputeDistanceLineString computes the distance +// from a Coordinate to a LineString. +func AlgorithmDistance_DistanceToPoint_ComputeDistanceLineString(line *Geom_LineString, pt *Geom_Coordinate, ptDist *AlgorithmDistance_PointPairDistance) { + tempSegment := Geom_NewLineSegment() + coords := line.GetCoordinates() + for i := 0; i < len(coords)-1; i++ { + tempSegment.SetCoordinates(coords[i], coords[i+1]) + // this is somewhat inefficient - could do better + closestPt := tempSegment.ClosestPoint(pt) + ptDist.SetMinimum(closestPt, pt) + } +} + +// AlgorithmDistance_DistanceToPoint_ComputeDistanceLineSegment computes the distance +// from a Coordinate to a LineSegment. +func AlgorithmDistance_DistanceToPoint_ComputeDistanceLineSegment(segment *Geom_LineSegment, pt *Geom_Coordinate, ptDist *AlgorithmDistance_PointPairDistance) { + closestPt := segment.ClosestPoint(pt) + ptDist.SetMinimum(closestPt, pt) +} + +// AlgorithmDistance_DistanceToPoint_ComputeDistancePolygon computes the distance +// from a Coordinate to a Polygon. +func AlgorithmDistance_DistanceToPoint_ComputeDistancePolygon(poly *Geom_Polygon, pt *Geom_Coordinate, ptDist *AlgorithmDistance_PointPairDistance) { + AlgorithmDistance_DistanceToPoint_ComputeDistanceLineString(poly.GetExteriorRing().Geom_LineString, pt, ptDist) + for i := 0; i < poly.GetNumInteriorRing(); i++ { + AlgorithmDistance_DistanceToPoint_ComputeDistanceLineString(poly.GetInteriorRingN(i).Geom_LineString, pt, ptDist) + } +} diff --git a/internal/jtsport/jts/algorithm_distance_point_pair_distance.go b/internal/jtsport/jts/algorithm_distance_point_pair_distance.go new file mode 100644 index 00000000..2761ba20 --- /dev/null +++ b/internal/jtsport/jts/algorithm_distance_point_pair_distance.go @@ -0,0 +1,93 @@ +package jts + +import "math" + +// AlgorithmDistance_PointPairDistance contains a pair of points and the distance between them. +// Provides methods to update with a new point pair with +// either maximum or minimum distance. +type AlgorithmDistance_PointPairDistance struct { + pt [2]*Geom_Coordinate + distance float64 + isNull bool +} + +// AlgorithmDistance_NewPointPairDistance creates an instance of this class. +func AlgorithmDistance_NewPointPairDistance() *AlgorithmDistance_PointPairDistance { + return &AlgorithmDistance_PointPairDistance{ + pt: [2]*Geom_Coordinate{Geom_NewCoordinate(), Geom_NewCoordinate()}, + distance: math.NaN(), + isNull: true, + } +} + +// Initialize initializes this instance. +func (ppd *AlgorithmDistance_PointPairDistance) Initialize() { + ppd.isNull = true +} + +// InitializeWithCoordinates initializes the points, computing the distance between them. +func (ppd *AlgorithmDistance_PointPairDistance) InitializeWithCoordinates(p0, p1 *Geom_Coordinate) { + ppd.InitializeWithCoordinatesAndDistance(p0, p1, p0.Distance(p1)) +} + +// InitializeWithCoordinatesAndDistance initializes the points, avoiding recomputing the distance. +func (ppd *AlgorithmDistance_PointPairDistance) InitializeWithCoordinatesAndDistance(p0, p1 *Geom_Coordinate, distance float64) { + ppd.pt[0].SetCoordinate(p0) + ppd.pt[1].SetCoordinate(p1) + ppd.distance = distance + ppd.isNull = false +} + +// GetDistance gets the distance between the paired points. +func (ppd *AlgorithmDistance_PointPairDistance) GetDistance() float64 { + return ppd.distance +} + +// GetCoordinates gets the paired points. +func (ppd *AlgorithmDistance_PointPairDistance) GetCoordinates() []*Geom_Coordinate { + return ppd.pt[:] +} + +// GetCoordinate gets one of the paired points. +func (ppd *AlgorithmDistance_PointPairDistance) GetCoordinate(i int) *Geom_Coordinate { + return ppd.pt[i] +} + +// SetMaximumFromPointPairDistance sets this to the maximum distance found. +func (ppd *AlgorithmDistance_PointPairDistance) SetMaximumFromPointPairDistance(ptDist *AlgorithmDistance_PointPairDistance) { + ppd.SetMaximum(ptDist.pt[0], ptDist.pt[1]) +} + +// SetMaximum sets this to the maximum distance found. +func (ppd *AlgorithmDistance_PointPairDistance) SetMaximum(p0, p1 *Geom_Coordinate) { + if ppd.isNull { + ppd.InitializeWithCoordinates(p0, p1) + return + } + dist := p0.Distance(p1) + if dist > ppd.distance { + ppd.InitializeWithCoordinatesAndDistance(p0, p1, dist) + } +} + +// SetMinimumFromPointPairDistance sets this to the minimum distance found. +func (ppd *AlgorithmDistance_PointPairDistance) SetMinimumFromPointPairDistance(ptDist *AlgorithmDistance_PointPairDistance) { + ppd.SetMinimum(ptDist.pt[0], ptDist.pt[1]) +} + +// SetMinimum sets this to the minimum distance found. +func (ppd *AlgorithmDistance_PointPairDistance) SetMinimum(p0, p1 *Geom_Coordinate) { + if ppd.isNull { + ppd.InitializeWithCoordinates(p0, p1) + return + } + dist := p0.Distance(p1) + if dist < ppd.distance { + ppd.InitializeWithCoordinatesAndDistance(p0, p1, dist) + } +} + +// String returns a string representation of this PointPairDistance. +func (ppd *AlgorithmDistance_PointPairDistance) String() string { + return Io_WKTWriter_ToLineStringFromTwoCoords(ppd.pt[0], ppd.pt[1]) +} diff --git a/internal/jtsport/jts/geom_geometry_overlay_test.go b/internal/jtsport/jts/geom_geometry_overlay_test.go index 7b55e7f3..4f924a18 100644 --- a/internal/jtsport/jts/geom_geometry_overlay_test.go +++ b/internal/jtsport/jts/geom_geometry_overlay_test.go @@ -80,11 +80,10 @@ func TestGeometryOverlayOld(t *testing.T) { // Must set overlay method explicitly since order of tests is not deterministic. jts.Geom_GeometryOverlay_SetOverlayImpl(jts.Geom_GeometryOverlay_PropertyValueOld) - // Note: The original Java test expected this to fail with a TopologyException, - // but the Go SnapOverlayOp implementation handles this case successfully. - // This is actually better behavior - our "old" overlay is more robust. - // We test that the intersection completes without error. - checkIntersectionSucceeds(t) + // The original Java test expected this to fail with a TopologyException. + // Now that FastNodingValidator is properly ported (instead of being a stub), + // the Go implementation correctly throws a TopologyException as expected. + checkIntersectionFails(t) } func TestGeometryOverlayNG(t *testing.T) { @@ -107,6 +106,21 @@ func checkIntersectionSucceeds(t *testing.T) { tryIntersection(t) } +func checkIntersectionFails(t *testing.T) { + t.Helper() + defer func() { + if r := recover(); r != nil { + if _, ok := r.(*jts.Geom_TopologyException); ok { + // Expected - intersection should fail with TopologyException + return + } + panic(r) + } + }() + tryIntersection(t) + t.Fatal("intersection operation should have failed with TopologyException") +} + func tryIntersection(t *testing.T) { t.Helper() a := readGeom(t, "POLYGON ((-1120500.000000126 850931.058865365, -1120500.0000001257 851343.3885007716, -1120500.0000001257 851342.2386007707, -1120399.762684411 851199.4941312922, -1120500.000000126 850931.058865365))") diff --git a/internal/jtsport/jts/geom_point.go b/internal/jtsport/jts/geom_point.go index 32c32007..2f844df9 100644 --- a/internal/jtsport/jts/geom_point.go +++ b/internal/jtsport/jts/geom_point.go @@ -110,8 +110,8 @@ func (p *Geom_Point) GetY() float64 { return p.GetCoordinate().Y } -// GetCoordinate returns the Coordinate or nil if this Point is empty. -func (p *Geom_Point) GetCoordinate() *Geom_Coordinate { +// GetCoordinate_BODY returns the Coordinate or nil if this Point is empty. +func (p *Geom_Point) GetCoordinate_BODY() *Geom_Coordinate { if p.coordinates.Size() != 0 { return p.coordinates.GetCoordinate(0) } diff --git a/internal/jtsport/jts/geomgraph_edge_noding_validator.go b/internal/jtsport/jts/geomgraph_edge_noding_validator.go index b71b58dc..3597c0ae 100644 --- a/internal/jtsport/jts/geomgraph_edge_noding_validator.go +++ b/internal/jtsport/jts/geomgraph_edge_noding_validator.go @@ -40,7 +40,11 @@ func Geomgraph_EdgeNodingValidator_ToSegmentStrings(edges []*Geomgraph_Edge) []* // Geomgraph_NewEdgeNodingValidator creates a new validator for the given // collection of Edges. func Geomgraph_NewEdgeNodingValidator(edges []*Geomgraph_Edge) *Geomgraph_EdgeNodingValidator { - segStrings := Geomgraph_EdgeNodingValidator_ToSegmentStrings(edges) + bssSlice := Geomgraph_EdgeNodingValidator_ToSegmentStrings(edges) + segStrings := make([]Noding_SegmentString, len(bssSlice)) + for i, bss := range bssSlice { + segStrings[i] = bss + } return &Geomgraph_EdgeNodingValidator{ nv: Noding_NewFastNodingValidator(segStrings), } diff --git a/internal/jtsport/jts/geomgraph_node_map.go b/internal/jtsport/jts/geomgraph_node_map.go index 9e2fe015..eee2c098 100644 --- a/internal/jtsport/jts/geomgraph_node_map.go +++ b/internal/jtsport/jts/geomgraph_node_map.go @@ -1,6 +1,10 @@ package jts -import "github.com/peterstace/simplefeatures/internal/jtsport/java" +import ( + "sort" + + "github.com/peterstace/simplefeatures/internal/jtsport/java" +) // Geomgraph_NodeMap is a map of nodes, indexed by the coordinate of the node. type Geomgraph_NodeMap struct { @@ -81,6 +85,13 @@ func (nm *Geomgraph_NodeMap) Values() []*Geomgraph_Node { for _, node := range nm.nodeMap { result = append(result, node) } + // TRANSLITERATION NOTE: Sort by coordinate for deterministic ordering. + // Go's map iteration order is randomized, while Java's HashMap iteration + // order is consistent within a single JVM run. This sorting ensures the + // Go code produces deterministic results. + sort.Slice(result, func(i, j int) bool { + return result[i].GetCoordinate().CompareTo(result[j].GetCoordinate()) < 0 + }) return result } @@ -88,7 +99,8 @@ func (nm *Geomgraph_NodeMap) Values() []*Geomgraph_Node { // geometry index. func (nm *Geomgraph_NodeMap) GetBoundaryNodes(geomIndex int) []*Geomgraph_Node { var bdyNodes []*Geomgraph_Node - for _, node := range nm.nodeMap { + // Use sorted iteration for deterministic results. + for _, node := range nm.Values() { if node.GetLabel().GetLocationOn(geomIndex) == Geom_Location_Boundary { bdyNodes = append(bdyNodes, node) } diff --git a/internal/jtsport/jts/index_strtree_strtree.go b/internal/jtsport/jts/index_strtree_strtree.go index 0cb580ac..c10fb7cf 100644 --- a/internal/jtsport/jts/index_strtree_strtree.go +++ b/internal/jtsport/jts/index_strtree_strtree.go @@ -122,6 +122,12 @@ func (t *IndexStrtree_STRtree) GetParent() java.Polymorphic { return t.IndexStrtree_AbstractSTRtree } +// Compile-time check that IndexStrtree_STRtree implements Index_SpatialIndex. +var _ Index_SpatialIndex = (*IndexStrtree_STRtree)(nil) + +// IsIndex_SpatialIndex is a marker method for interface identification. +func (t *IndexStrtree_STRtree) IsIndex_SpatialIndex() {} + // IndexStrtree_NewSTRtree constructs an STRtree with the default node capacity. func IndexStrtree_NewSTRtree() *IndexStrtree_STRtree { return IndexStrtree_NewSTRtreeWithCapacity(IndexStrtree_STRtree_DEFAULT_NODE_CAPACITY) diff --git a/internal/jtsport/jts/jtstest_geomop_geometry_method_operation.go b/internal/jtsport/jts/jtstest_geomop_geometry_method_operation.go index e0c6c0b1..57941635 100644 --- a/internal/jtsport/jts/jtstest_geomop_geometry_method_operation.go +++ b/internal/jtsport/jts/jtstest_geomop_geometry_method_operation.go @@ -340,8 +340,56 @@ func (op *JtstestGeomop_GeometryMethodOperation) invokeTestCaseGeometryFunction( ) JtstestTestrunner_Result { opLower := strings.ToLower(opName) - // Handle two-geometry NG operations. + // Handle zero-argument operations. + if len(args) == 0 { + switch opLower { + case "minclearance": + return JtstestTestrunner_NewDoubleResult( + JtstestGeomop_TestCaseGeometryFunctions_MinClearance(geometry)) + case "minclearanceline": + return JtstestTestrunner_NewGeometryResult( + JtstestGeomop_TestCaseGeometryFunctions_MinClearanceLine(geometry)) + case "polygonize": + return JtstestTestrunner_NewGeometryResult( + JtstestGeomop_TestCaseGeometryFunctions_Polygonize(geometry)) + } + } + + // Handle single-argument operations (geometry + distance/parameter). if len(args) == 1 { + // Check for operations that take a distance, not a geometry. + switch opLower { + case "buffermitredjoin": + distance, ok := op.parseScale(args[0]) + if !ok { + return nil + } + return JtstestTestrunner_NewGeometryResult( + JtstestGeomop_TestCaseGeometryFunctions_BufferMitredJoin(geometry, distance)) + case "densify": + distance, ok := op.parseScale(args[0]) + if !ok { + return nil + } + return JtstestTestrunner_NewGeometryResult( + JtstestGeomop_TestCaseGeometryFunctions_Densify(geometry, distance)) + case "simplifydp": + distance, ok := op.parseScale(args[0]) + if !ok { + return nil + } + return JtstestTestrunner_NewGeometryResult( + JtstestGeomop_TestCaseGeometryFunctions_SimplifyDP(geometry, distance)) + case "simplifytp": + distance, ok := op.parseScale(args[0]) + if !ok { + return nil + } + return JtstestTestrunner_NewGeometryResult( + JtstestGeomop_TestCaseGeometryFunctions_SimplifyTP(geometry, distance)) + } + + // Handle two-geometry NG operations. geom1, ok := args[0].(*Geom_Geometry) if !ok { return nil diff --git a/internal/jtsport/jts/noding_fast_noding_validator.go b/internal/jtsport/jts/noding_fast_noding_validator.go new file mode 100644 index 00000000..1f2aee2e --- /dev/null +++ b/internal/jtsport/jts/noding_fast_noding_validator.go @@ -0,0 +1,111 @@ +package jts + +// Noding_FastNodingValidator validates that a collection of SegmentStrings is +// correctly noded. Indexing is used to improve performance. By default +// validation stops after a single non-noded intersection is detected. +// Alternatively, it can be requested to detect all intersections by using +// SetFindAllIntersections. +// +// The validator does not check for topology collapse situations (e.g. where two +// segment strings are fully co-incident). +// +// The validator checks for the following situations which indicate incorrect +// noding: +// - Proper intersections between segments (i.e. the intersection is interior +// to both segments) +// - Intersections at an interior vertex (i.e. with an endpoint or another +// interior vertex) +// +// The client may either test the IsValid() condition, or request that a +// suitable TopologyException be thrown. +type Noding_FastNodingValidator struct { + li *Algorithm_LineIntersector + + segStrings []Noding_SegmentString + findAllIntersections bool + segInt *Noding_NodingIntersectionFinder + isValid bool +} + +// Noding_FastNodingValidator_ComputeIntersections gets a list of all +// intersections found. Intersections are represented as Coordinates. List is +// empty if none were found. +func Noding_FastNodingValidator_ComputeIntersections(segStrings []Noding_SegmentString) []*Geom_Coordinate { + nv := Noding_NewFastNodingValidator(segStrings) + nv.SetFindAllIntersections(true) + nv.IsValid() + return nv.GetIntersections() +} + +// Noding_NewFastNodingValidator creates a new noding validator for a given set +// of linework. +func Noding_NewFastNodingValidator(segStrings []Noding_SegmentString) *Noding_FastNodingValidator { + return &Noding_FastNodingValidator{ + li: Algorithm_NewRobustLineIntersector().Algorithm_LineIntersector, + segStrings: segStrings, + isValid: true, + } +} + +// SetFindAllIntersections sets whether all intersections should be found. +func (fnv *Noding_FastNodingValidator) SetFindAllIntersections(findAllIntersections bool) { + fnv.findAllIntersections = findAllIntersections +} + +// GetIntersections gets a list of all intersections found. Intersections are +// represented as Coordinates. List is empty if none were found. +func (fnv *Noding_FastNodingValidator) GetIntersections() []*Geom_Coordinate { + return fnv.segInt.GetIntersections() +} + +// IsValid checks for an intersection and reports if one is found. +func (fnv *Noding_FastNodingValidator) IsValid() bool { + fnv.execute() + return fnv.isValid +} + +// GetErrorMessage returns an error message indicating the segments containing +// the intersection. +func (fnv *Noding_FastNodingValidator) GetErrorMessage() string { + if fnv.isValid { + return "no intersections found" + } + + intSegs := fnv.segInt.GetIntersectionSegments() + return "found non-noded intersection between " + + Io_WKTWriter_ToLineStringFromTwoCoords(intSegs[0], intSegs[1]) + + " and " + + Io_WKTWriter_ToLineStringFromTwoCoords(intSegs[2], intSegs[3]) +} + +// CheckValid checks for an intersection and panics with a TopologyException if +// one is found. +func (fnv *Noding_FastNodingValidator) CheckValid() { + fnv.execute() + if !fnv.isValid { + panic(Geom_NewTopologyExceptionWithCoordinate(fnv.GetErrorMessage(), fnv.segInt.GetIntersection())) + } +} + +func (fnv *Noding_FastNodingValidator) execute() { + if fnv.segInt != nil { + return + } + fnv.checkInteriorIntersections() +} + +func (fnv *Noding_FastNodingValidator) checkInteriorIntersections() { + // MD - It may even be reliable to simply check whether + // end segments (of SegmentStrings) have an interior intersection, + // since noding should have split any true interior intersections already. + fnv.isValid = true + fnv.segInt = Noding_NewNodingIntersectionFinder(fnv.li) + fnv.segInt.SetFindAllIntersections(fnv.findAllIntersections) + noder := Noding_NewMCIndexNoder() + noder.SetSegmentIntersector(fnv.segInt) + noder.ComputeNodes(fnv.segStrings) + if fnv.segInt.HasIntersection() { + fnv.isValid = false + return + } +} diff --git a/internal/jtsport/jts/noding_fast_noding_validator_test.go b/internal/jtsport/jts/noding_fast_noding_validator_test.go new file mode 100644 index 00000000..07ea78bf --- /dev/null +++ b/internal/jtsport/jts/noding_fast_noding_validator_test.go @@ -0,0 +1,130 @@ +package jts + +import ( + "testing" + + "github.com/peterstace/simplefeatures/internal/jtsport/junit" +) + +var noding_FastNodingValidatorTest_VERTEX_INT = []string{ + "LINESTRING (100 100, 200 200, 300 300)", + "LINESTRING (100 300, 200 200)", +} + +var noding_FastNodingValidatorTest_INTERIOR_INT = []string{ + "LINESTRING (100 100, 300 300)", + "LINESTRING (100 300, 300 100)", +} + +var noding_FastNodingValidatorTest_NO_INT = []string{ + "LINESTRING (100 100, 200 200)", + "LINESTRING (200 200, 300 300)", + "LINESTRING (100 300, 200 200)", +} + +var noding_FastNodingValidatorTest_SELF_INTERIOR_INT = []string{ + "LINESTRING (100 100, 300 300, 300 100, 100 300)", +} + +var noding_FastNodingValidatorTest_SELF_VERTEX_INT = []string{ + "LINESTRING (100 100, 200 200, 300 300, 400 200, 200 200)", +} + +func TestFastNodingValidator_InteriorIntersection(t *testing.T) { + noding_FastNodingValidatorTest_checkValid(t, noding_FastNodingValidatorTest_INTERIOR_INT, false) + noding_FastNodingValidatorTest_checkIntersection(t, noding_FastNodingValidatorTest_INTERIOR_INT, "POINT(200 200)") +} + +func TestFastNodingValidator_VertexIntersection(t *testing.T) { + noding_FastNodingValidatorTest_checkValid(t, noding_FastNodingValidatorTest_VERTEX_INT, false) + // checkIntersection(VERTEX_INT, "POINT(200 200)"); +} + +func TestFastNodingValidator_NoIntersection(t *testing.T) { + noding_FastNodingValidatorTest_checkValid(t, noding_FastNodingValidatorTest_NO_INT, true) +} + +func TestFastNodingValidator_SelfInteriorIntersection(t *testing.T) { + noding_FastNodingValidatorTest_checkValid(t, noding_FastNodingValidatorTest_SELF_INTERIOR_INT, false) +} + +func TestFastNodingValidator_SelfVertexIntersection(t *testing.T) { + noding_FastNodingValidatorTest_checkValid(t, noding_FastNodingValidatorTest_SELF_VERTEX_INT, false) +} + +func noding_FastNodingValidatorTest_checkValid(t *testing.T, inputWKT []string, isValidExpected bool) { + t.Helper() + input := noding_FastNodingValidatorTest_readList(t, inputWKT) + segStrings := noding_FastNodingValidatorTest_toSegmentStrings(input) + fnv := Noding_NewFastNodingValidator(segStrings) + isValid := fnv.IsValid() + + junit.AssertTrue(t, isValidExpected == isValid) +} + +func noding_FastNodingValidatorTest_checkIntersection(t *testing.T, inputWKT []string, expectedWKT string) { + t.Helper() + input := noding_FastNodingValidatorTest_readList(t, inputWKT) + expected := noding_FastNodingValidatorTest_read(t, expectedWKT) + pts := expected.GetCoordinates() + intPtsExpected := Geom_NewCoordinateListFromCoordinates(pts) + + segStrings := noding_FastNodingValidatorTest_toSegmentStrings(input) + intPtsActual := Noding_FastNodingValidator_ComputeIntersections(segStrings) + + isSameNumberOfIntersections := intPtsExpected.Size() == len(intPtsActual) + junit.AssertTrue(t, isSameNumberOfIntersections) + + noding_FastNodingValidatorTest_checkIntersections(t, intPtsActual, intPtsExpected) +} + +func noding_FastNodingValidatorTest_checkIntersections(t *testing.T, intPtsActual []*Geom_Coordinate, intPtsExpected *Geom_CoordinateList) { + t.Helper() + // TODO: sort intersections so they can be compared + for i := 0; i < len(intPtsActual); i++ { + ptActual := intPtsActual[i] + ptExpected := intPtsExpected.Get(i) + + isEqual := ptActual.Equals2D(ptExpected) + junit.AssertTrue(t, isEqual) + } +} + +func noding_FastNodingValidatorTest_toSegmentStrings(geoms []*Geom_Geometry) []Noding_SegmentString { + var segStrings []Noding_SegmentString + for _, geom := range geoms { + extracted := noding_FastNodingValidatorTest_extractSegmentStrings(geom) + segStrings = append(segStrings, extracted...) + } + return segStrings +} + +func noding_FastNodingValidatorTest_extractSegmentStrings(geom *Geom_Geometry) []Noding_SegmentString { + var segStr []Noding_SegmentString + lines := GeomUtil_LinearComponentExtracter_GetLines(geom) + for _, line := range lines { + pts := line.GetCoordinates() + segStr = append(segStr, Noding_NewNodedSegmentString(pts, geom)) + } + return segStr +} + +func noding_FastNodingValidatorTest_readList(t *testing.T, wktList []string) []*Geom_Geometry { + t.Helper() + var result []*Geom_Geometry + for _, wkt := range wktList { + geom := noding_FastNodingValidatorTest_read(t, wkt) + result = append(result, geom) + } + return result +} + +func noding_FastNodingValidatorTest_read(t *testing.T, wkt string) *Geom_Geometry { + t.Helper() + reader := Io_NewWKTReader() + geom, err := reader.Read(wkt) + if err != nil { + t.Fatalf("failed to read WKT: %v", err) + } + return geom +} diff --git a/internal/jtsport/jts/noding_noding_intersection_finder.go b/internal/jtsport/jts/noding_noding_intersection_finder.go new file mode 100644 index 00000000..729545fe --- /dev/null +++ b/internal/jtsport/jts/noding_noding_intersection_finder.go @@ -0,0 +1,284 @@ +package jts + +import "github.com/peterstace/simplefeatures/internal/jtsport/java" + +// Noding_NodingIntersectionFinder finds non-noded intersections in a set of +// SegmentStrings, if any exist. +// +// Non-noded intersections include: +// - Interior intersections which lie in the interior of a segment +// (with another segment interior or with a vertex or endpoint) +// - Vertex intersections which occur at vertices in the interior of +// SegmentStrings (with a segment string endpoint or with another interior +// vertex) +// +// The finder can be limited to finding only interior intersections by setting +// SetInteriorIntersectionsOnly. +// +// By default only the first intersection is found, but all can be found by +// setting SetFindAllIntersections. +type Noding_NodingIntersectionFinder struct { + findAllIntersections bool + isCheckEndSegmentsOnly bool + keepIntersections bool + isInteriorIntersectionsOnly bool + + li *Algorithm_LineIntersector + interiorIntersection *Geom_Coordinate + intSegments []*Geom_Coordinate + intersections []*Geom_Coordinate + intersectionCount int +} + +var _ Noding_SegmentIntersector = (*Noding_NodingIntersectionFinder)(nil) + +func (nif *Noding_NodingIntersectionFinder) IsNoding_SegmentIntersector() {} + +// Noding_NodingIntersectionFinder_CreateAnyIntersectionFinder creates a finder +// which tests if there is at least one intersection. Uses short-circuiting for +// efficient performance. The intersection found is recorded. +func Noding_NodingIntersectionFinder_CreateAnyIntersectionFinder(li *Algorithm_LineIntersector) *Noding_NodingIntersectionFinder { + return Noding_NewNodingIntersectionFinder(li) +} + +// Noding_NodingIntersectionFinder_CreateAllIntersectionsFinder creates a finder +// which finds all intersections. The intersections are recorded for later +// inspection. +func Noding_NodingIntersectionFinder_CreateAllIntersectionsFinder(li *Algorithm_LineIntersector) *Noding_NodingIntersectionFinder { + finder := Noding_NewNodingIntersectionFinder(li) + finder.SetFindAllIntersections(true) + return finder +} + +// Noding_NodingIntersectionFinder_CreateInteriorIntersectionsFinder creates a +// finder which finds all interior intersections. The intersections are recorded +// for later inspection. +func Noding_NodingIntersectionFinder_CreateInteriorIntersectionsFinder(li *Algorithm_LineIntersector) *Noding_NodingIntersectionFinder { + finder := Noding_NewNodingIntersectionFinder(li) + finder.SetFindAllIntersections(true) + finder.SetInteriorIntersectionsOnly(true) + return finder +} + +// Noding_NodingIntersectionFinder_CreateIntersectionCounter creates a finder +// which counts all intersections. The intersections are not recorded to reduce +// memory usage. +func Noding_NodingIntersectionFinder_CreateIntersectionCounter(li *Algorithm_LineIntersector) *Noding_NodingIntersectionFinder { + finder := Noding_NewNodingIntersectionFinder(li) + finder.SetFindAllIntersections(true) + finder.SetKeepIntersections(false) + return finder +} + +// Noding_NodingIntersectionFinder_CreateInteriorIntersectionCounter creates a +// finder which counts all interior intersections. The intersections are not +// recorded to reduce memory usage. +func Noding_NodingIntersectionFinder_CreateInteriorIntersectionCounter(li *Algorithm_LineIntersector) *Noding_NodingIntersectionFinder { + finder := Noding_NewNodingIntersectionFinder(li) + finder.SetInteriorIntersectionsOnly(true) + finder.SetFindAllIntersections(true) + finder.SetKeepIntersections(false) + return finder +} + +// Noding_NewNodingIntersectionFinder creates an intersection finder which finds +// an intersection if one exists. +func Noding_NewNodingIntersectionFinder(li *Algorithm_LineIntersector) *Noding_NodingIntersectionFinder { + return &Noding_NodingIntersectionFinder{ + li: li, + interiorIntersection: nil, + keepIntersections: true, + intersections: make([]*Geom_Coordinate, 0), + } +} + +// SetFindAllIntersections sets whether all intersections should be computed. +// When this is false (the default value) the value of IsDone() is true after +// the first intersection is found. +// +// Default is false. +func (nif *Noding_NodingIntersectionFinder) SetFindAllIntersections(findAllIntersections bool) { + nif.findAllIntersections = findAllIntersections +} + +// SetInteriorIntersectionsOnly sets whether only interior (proper) +// intersections will be found. +func (nif *Noding_NodingIntersectionFinder) SetInteriorIntersectionsOnly(isInteriorIntersectionsOnly bool) { + nif.isInteriorIntersectionsOnly = isInteriorIntersectionsOnly +} + +// SetCheckEndSegmentsOnly sets whether only end segments should be tested for +// intersection. This is a performance optimization that may be used if the +// segments have been previously noded by an appropriate algorithm. It may be +// known that any potential noding failures will occur only in end segments. +func (nif *Noding_NodingIntersectionFinder) SetCheckEndSegmentsOnly(isCheckEndSegmentsOnly bool) { + nif.isCheckEndSegmentsOnly = isCheckEndSegmentsOnly +} + +// SetKeepIntersections sets whether intersection points are recorded. If the +// only need is to count intersection points, this can be set to false. +// +// Default is true. +func (nif *Noding_NodingIntersectionFinder) SetKeepIntersections(keepIntersections bool) { + nif.keepIntersections = keepIntersections +} + +// GetIntersections gets the intersections found. +func (nif *Noding_NodingIntersectionFinder) GetIntersections() []*Geom_Coordinate { + return nif.intersections +} + +// Count gets the count of intersections found. +func (nif *Noding_NodingIntersectionFinder) Count() int { + return nif.intersectionCount +} + +// HasIntersection tests whether an intersection was found. +func (nif *Noding_NodingIntersectionFinder) HasIntersection() bool { + return nif.interiorIntersection != nil +} + +// GetIntersection gets the computed location of the intersection. Due to +// round-off, the location may not be exact. +func (nif *Noding_NodingIntersectionFinder) GetIntersection() *Geom_Coordinate { + return nif.interiorIntersection +} + +// GetIntersectionSegments gets the endpoints of the intersecting segments. +func (nif *Noding_NodingIntersectionFinder) GetIntersectionSegments() []*Geom_Coordinate { + return nif.intSegments +} + +// ProcessIntersections is called by clients of the SegmentIntersector class to +// process intersections for two segments of the SegmentStrings being +// intersected. Note that some clients (such as MonotoneChains) may optimize +// away this call for segment pairs which they have determined do not intersect +// (e.g. by a disjoint envelope test). +func (nif *Noding_NodingIntersectionFinder) ProcessIntersections( + e0 Noding_SegmentString, segIndex0 int, + e1 Noding_SegmentString, segIndex1 int, +) { + // short-circuit if intersection already found + if !nif.findAllIntersections && nif.HasIntersection() { + return + } + + // don't bother intersecting a segment with itself + isSameSegString := e0 == e1 + isSameSegment := isSameSegString && segIndex0 == segIndex1 + if isSameSegment { + return + } + + // If enabled, only test end segments (on either segString). + if nif.isCheckEndSegmentsOnly { + isEndSegPresent := noding_NodingIntersectionFinder_isEndSegment(e0, segIndex0) || + noding_NodingIntersectionFinder_isEndSegment(e1, segIndex1) + if !isEndSegPresent { + return + } + } + + p00 := e0.GetCoordinate(segIndex0) + p01 := e0.GetCoordinate(segIndex0 + 1) + p10 := e1.GetCoordinate(segIndex1) + p11 := e1.GetCoordinate(segIndex1 + 1) + isEnd00 := segIndex0 == 0 + isEnd01 := segIndex0+2 == e0.Size() + isEnd10 := segIndex1 == 0 + isEnd11 := segIndex1+2 == e1.Size() + + nif.li.ComputeIntersection(p00, p01, p10, p11) + // if (li.hasIntersection() && li.isProper()) Debug.println(li); + + // Check for an intersection in the interior of a segment + isInteriorInt := nif.li.HasIntersection() && nif.li.IsInteriorIntersection() + + // Check for an intersection between two vertices which are not both endpoints. + isInteriorVertexInt := false + if !nif.isInteriorIntersectionsOnly { + isAdjacentSegment := isSameSegString && java.AbsInt(segIndex1-segIndex0) <= 1 + isInteriorVertexInt = (!isAdjacentSegment) && noding_NodingIntersectionFinder_isInteriorVertexIntersection4( + p00, p01, p10, p11, + isEnd00, isEnd01, isEnd10, isEnd11) + } + + if isInteriorInt || isInteriorVertexInt { + // found an intersection! + nif.intSegments = make([]*Geom_Coordinate, 4) + nif.intSegments[0] = p00 + nif.intSegments[1] = p01 + nif.intSegments[2] = p10 + nif.intSegments[3] = p11 + + // TODO: record endpoint intersection(s) + nif.interiorIntersection = nif.li.GetIntersection(0) + if nif.keepIntersections { + nif.intersections = append(nif.intersections, nif.interiorIntersection) + } + nif.intersectionCount++ + } +} + +// noding_NodingIntersectionFinder_isInteriorVertexIntersection4 tests if an +// intersection occurs between a segmentString interior vertex and another +// vertex. Note that intersections between two endpoint vertices are valid +// noding, and are not flagged. +func noding_NodingIntersectionFinder_isInteriorVertexIntersection4( + p00, p01 *Geom_Coordinate, + p10, p11 *Geom_Coordinate, + isEnd00, isEnd01 bool, + isEnd10, isEnd11 bool, +) bool { + if noding_NodingIntersectionFinder_isInteriorVertexIntersection(p00, p10, isEnd00, isEnd10) { + return true + } + if noding_NodingIntersectionFinder_isInteriorVertexIntersection(p00, p11, isEnd00, isEnd11) { + return true + } + if noding_NodingIntersectionFinder_isInteriorVertexIntersection(p01, p10, isEnd01, isEnd10) { + return true + } + if noding_NodingIntersectionFinder_isInteriorVertexIntersection(p01, p11, isEnd01, isEnd11) { + return true + } + return false +} + +// noding_NodingIntersectionFinder_isInteriorVertexIntersection tests if two +// vertices with at least one in a segmentString interior are equal. +func noding_NodingIntersectionFinder_isInteriorVertexIntersection( + p0, p1 *Geom_Coordinate, + isEnd0, isEnd1 bool, +) bool { + // Intersections between endpoints are valid nodes, so not reported + if isEnd0 && isEnd1 { + return false + } + + if p0.Equals2D(p1) { + return true + } + return false +} + +// noding_NodingIntersectionFinder_isEndSegment tests whether a segment in a +// SegmentString is an end segment (either the first or last). +func noding_NodingIntersectionFinder_isEndSegment(segStr Noding_SegmentString, index int) bool { + if index == 0 { + return true + } + if index >= segStr.Size()-2 { + return true + } + return false +} + +// IsDone reports whether the client of this class needs to continue testing all +// intersections in an arrangement. +func (nif *Noding_NodingIntersectionFinder) IsDone() bool { + if nif.findAllIntersections { + return false + } + return nif.interiorIntersection != nil +} diff --git a/internal/jtsport/jts/noding_scaled_noder.go b/internal/jtsport/jts/noding_scaled_noder.go new file mode 100644 index 00000000..113df608 --- /dev/null +++ b/internal/jtsport/jts/noding_scaled_noder.go @@ -0,0 +1,109 @@ +package jts + +import "github.com/peterstace/simplefeatures/internal/jtsport/java" + +// Noding_ScaledNoder wraps a Noder and transforms its input into the integer +// domain. This is intended for use with Snap-Rounding noders, which typically +// are only intended to work in the integer domain. Offsets can be provided to +// increase the number of digits of available precision. +// +// Clients should be aware that rescaling can involve loss of precision, which +// can cause zero-length line segments to be created. These in turn can cause +// problems when used to build a planar graph. This situation should be checked +// for and collapsed segments removed if necessary. +type Noding_ScaledNoder struct { + noder Noding_Noder + scaleFactor float64 + offsetX float64 + offsetY float64 + isScaled bool +} + +var _ Noding_Noder = (*Noding_ScaledNoder)(nil) + +func (sn *Noding_ScaledNoder) IsNoding_Noder() {} + +// Noding_NewScaledNoder creates a new ScaledNoder with the given noder and +// scale factor. +func Noding_NewScaledNoder(noder Noding_Noder, scaleFactor float64) *Noding_ScaledNoder { + return Noding_NewScaledNoderWithOffsets(noder, scaleFactor, 0, 0) +} + +// Noding_NewScaledNoderWithOffsets creates a new ScaledNoder with the given +// noder, scale factor, and offsets. +func Noding_NewScaledNoderWithOffsets(noder Noding_Noder, scaleFactor, offsetX, offsetY float64) *Noding_ScaledNoder { + sn := &Noding_ScaledNoder{ + noder: noder, + scaleFactor: scaleFactor, + offsetX: offsetX, + offsetY: offsetY, + // no need to scale if input precision is already integral + } + sn.isScaled = !sn.IsIntegerPrecision() + return sn +} + +// IsIntegerPrecision returns true if the scale factor is 1.0. +func (sn *Noding_ScaledNoder) IsIntegerPrecision() bool { + return sn.scaleFactor == 1.0 +} + +// GetNodedSubstrings returns a collection of fully noded SegmentStrings. +func (sn *Noding_ScaledNoder) GetNodedSubstrings() []Noding_SegmentString { + splitSS := sn.noder.GetNodedSubstrings() + if sn.isScaled { + sn.rescaleSegmentStrings(splitSS) + } + return splitSS +} + +// ComputeNodes computes the noding for a collection of SegmentStrings. +func (sn *Noding_ScaledNoder) ComputeNodes(inputSegStrings []Noding_SegmentString) { + intSegStrings := inputSegStrings + if sn.isScaled { + intSegStrings = sn.scale(inputSegStrings) + } + sn.noder.ComputeNodes(intSegStrings) +} + +func (sn *Noding_ScaledNoder) scale(segStrings []Noding_SegmentString) []Noding_SegmentString { + nodedSegmentStrings := make([]Noding_SegmentString, 0, len(segStrings)) + for _, ss := range segStrings { + scaledCoords := sn.scaleCoords(ss.GetCoordinates()) + nodedSegmentStrings = append(nodedSegmentStrings, Noding_NewNodedSegmentString(scaledCoords, ss.GetData())) + } + return nodedSegmentStrings +} + +func (sn *Noding_ScaledNoder) scaleCoords(pts []*Geom_Coordinate) []*Geom_Coordinate { + roundPts := make([]*Geom_Coordinate, len(pts)) + for i := 0; i < len(pts); i++ { + roundPts[i] = Geom_NewCoordinateWithXYZ( + java.Round(float64(float64(pts[i].GetX()-sn.offsetX)*sn.scaleFactor)), + java.Round(float64(float64(pts[i].GetY()-sn.offsetY)*sn.scaleFactor)), + pts[i].GetZ(), + ) + } + roundPtsNoDup := Geom_CoordinateArrays_RemoveRepeatedPoints(roundPts) + return roundPtsNoDup +} + +// private double scale(double val) { return (double) Math.round(val * scaleFactor); } + +func (sn *Noding_ScaledNoder) rescaleSegmentStrings(segStrings []Noding_SegmentString) { + for _, ss := range segStrings { + sn.rescale(ss.GetCoordinates()) + } +} + +func (sn *Noding_ScaledNoder) rescale(pts []*Geom_Coordinate) { + for i := 0; i < len(pts); i++ { + pts[i].SetX(pts[i].GetX()/sn.scaleFactor + sn.offsetX) + pts[i].SetY(pts[i].GetY()/sn.scaleFactor + sn.offsetY) + } + // if (pts.length == 2 && pts[0].equals2D(pts[1])) { + // System.out.println(pts); + // } +} + +// private double rescale(double val) { return val / scaleFactor; } diff --git a/internal/jtsport/jts/noding_validating_noder.go b/internal/jtsport/jts/noding_validating_noder.go index af8e07b3..862e0faa 100644 --- a/internal/jtsport/jts/noding_validating_noder.go +++ b/internal/jtsport/jts/noding_validating_noder.go @@ -31,12 +31,12 @@ func (vn *Noding_ValidatingNoder) ComputeNodes(segStrings []Noding_SegmentString } func (vn *Noding_ValidatingNoder) validate() { - // Convert to BasicSegmentString slice for FastNodingValidator. - bssSlice := make([]*Noding_BasicSegmentString, len(vn.nodedSS)) + // Convert to SegmentString slice for FastNodingValidator. + segStrings := make([]Noding_SegmentString, len(vn.nodedSS)) for i, ss := range vn.nodedSS { - bssSlice[i] = Noding_NewBasicSegmentString(ss.GetCoordinates(), ss.GetData()) + segStrings[i] = Noding_NewBasicSegmentString(ss.GetCoordinates(), ss.GetData()) } - nv := Noding_NewFastNodingValidator(bssSlice) + nv := Noding_NewFastNodingValidator(segStrings) nv.CheckValid() } diff --git a/internal/jtsport/jts/operation_buffer_buffer_builder.go b/internal/jtsport/jts/operation_buffer_buffer_builder.go new file mode 100644 index 00000000..ad4dcf92 --- /dev/null +++ b/internal/jtsport/jts/operation_buffer_buffer_builder.go @@ -0,0 +1,246 @@ +package jts + +import "sort" + +// operationBuffer_BufferBuilder builds the buffer geometry for a given input geometry and precision model. +// Allows setting the level of approximation for circular arcs, +// and the precision model in which to carry out the computation. +// +// When computing buffers in floating point double-precision +// it can happen that the process of iterated noding can fail to converge (terminate). +// In this case a TopologyException will be thrown. +// Retrying the computation in a fixed precision +// can produce more robust results. +type operationBuffer_BufferBuilder struct { + bufParams *OperationBuffer_BufferParameters + + workingPrecisionModel *Geom_PrecisionModel + workingNoder Noding_Noder + geomFact *Geom_GeometryFactory + graph *Geomgraph_PlanarGraph + edgeList *Geomgraph_EdgeList + + isInvertOrientation bool +} + +// operationBuffer_newBufferBuilder creates a new BufferBuilder using the given parameters. +func operationBuffer_newBufferBuilder(bufParams *OperationBuffer_BufferParameters) *operationBuffer_BufferBuilder { + return &operationBuffer_BufferBuilder{ + bufParams: bufParams, + edgeList: Geomgraph_NewEdgeList(), + } +} + +// operationBuffer_bufferBuilder_depthDelta computes the change in depth as an edge is crossed from R to L. +func operationBuffer_bufferBuilder_depthDelta(label *Geomgraph_Label) int { + lLoc := label.GetLocation(0, Geom_Position_Left) + rLoc := label.GetLocation(0, Geom_Position_Right) + if lLoc == Geom_Location_Interior && rLoc == Geom_Location_Exterior { + return 1 + } else if lLoc == Geom_Location_Exterior && rLoc == Geom_Location_Interior { + return -1 + } + return 0 +} + +// SetWorkingPrecisionModel sets the precision model to use during the curve computation and noding, +// if it is different to the precision model of the Geometry. +// If the precision model is less than the precision of the Geometry precision model, +// the Geometry must have previously been rounded to that precision. +func (bb *operationBuffer_BufferBuilder) SetWorkingPrecisionModel(pm *Geom_PrecisionModel) { + bb.workingPrecisionModel = pm +} + +// SetNoder sets the Noder to use during noding. +// This allows choosing fast but non-robust noding, or slower but robust noding. +func (bb *operationBuffer_BufferBuilder) SetNoder(noder Noding_Noder) { + bb.workingNoder = noder +} + +// SetInvertOrientation sets whether the offset curve is generated +// using the inverted orientation of input rings. +// This allows generating a buffer(0) polygon from the smaller lobes +// of self-crossing rings. +func (bb *operationBuffer_BufferBuilder) SetInvertOrientation(isInvertOrientation bool) { + bb.isInvertOrientation = isInvertOrientation +} + +// Buffer computes the buffer for a geometry. +func (bb *operationBuffer_BufferBuilder) Buffer(g *Geom_Geometry, distance float64) *Geom_Geometry { + precisionModel := bb.workingPrecisionModel + if precisionModel == nil { + precisionModel = g.GetPrecisionModel() + } + + // factory must be the same as the one used by the input + bb.geomFact = g.GetFactory() + + curveSetBuilder := OperationBuffer_NewBufferCurveSetBuilder(g, distance, precisionModel, bb.bufParams) + curveSetBuilder.SetInvertOrientation(bb.isInvertOrientation) + + bufferSegStrList := curveSetBuilder.GetCurves() + + // short-circuit test + if len(bufferSegStrList) <= 0 { + return bb.createEmptyResultGeometry() + } + + // Currently only zero-distance buffers are validated, + // to avoid reducing performance for other buffers. + // This fixes some noding failure cases found via GeometryFixer + // (see JTS-852). + isNodingValidated := distance == 0.0 + bb.computeNodedEdges(bufferSegStrList, precisionModel, isNodingValidated) + + bb.graph = Geomgraph_NewPlanarGraph(OperationOverlay_NewOverlayNodeFactory().Geomgraph_NodeFactory) + bb.graph.AddEdges(bb.edgeList.GetEdges()) + + subgraphList := bb.createSubgraphs(bb.graph) + polyBuilder := OperationOverlay_NewPolygonBuilder(bb.geomFact) + bb.buildSubgraphs(subgraphList, polyBuilder) + resultPolyList := polyBuilder.GetPolygons() + + // just in case... + if len(resultPolyList) <= 0 { + return bb.createEmptyResultGeometry() + } + + // Convert polygons to geometries for BuildGeometry + geomList := make([]*Geom_Geometry, len(resultPolyList)) + for i, poly := range resultPolyList { + geomList[i] = poly.Geom_Geometry + } + resultGeom := bb.geomFact.BuildGeometry(geomList) + return resultGeom +} + +func (bb *operationBuffer_BufferBuilder) getNoder(precisionModel *Geom_PrecisionModel) Noding_Noder { + if bb.workingNoder != nil { + return bb.workingNoder + } + + // otherwise use a fast (but non-robust) noder + noder := Noding_NewMCIndexNoder() + li := Algorithm_NewRobustLineIntersector() + li.SetPrecisionModel(precisionModel) + noder.SetSegmentIntersector(Noding_NewIntersectionAdder(li.Algorithm_LineIntersector)) + return noder +} + +func (bb *operationBuffer_BufferBuilder) computeNodedEdges(bufferSegStrList []Noding_SegmentString, precisionModel *Geom_PrecisionModel, isNodingValidated bool) { + noder := bb.getNoder(precisionModel) + noder.ComputeNodes(bufferSegStrList) + nodedSegStrings := noder.GetNodedSubstrings() + + if isNodingValidated { + nv := Noding_NewFastNodingValidator(nodedSegStrings) + nv.CheckValid() + } + + for _, segStr := range nodedSegStrings { + // Discard edges which have zero length, + // since they carry no information and cause problems with topology building + pts := segStr.GetCoordinates() + if len(pts) == 2 && pts[0].Equals2D(pts[1]) { + continue + } + + oldLabel := segStr.GetData().(*Geomgraph_Label) + edge := Geomgraph_NewEdge(segStr.GetCoordinates(), Geomgraph_NewLabelFromLabel(oldLabel)) + bb.insertUniqueEdge(edge) + } +} + +// insertUniqueEdge inserts edges, checking to see if an identical edge already exists. +// If so, the edge is not inserted, but its label is merged +// with the existing edge. +func (bb *operationBuffer_BufferBuilder) insertUniqueEdge(e *Geomgraph_Edge) { + // fast lookup + existingEdge := bb.edgeList.FindEqualEdge(e) + + // If an identical edge already exists, simply update its label + if existingEdge != nil { + existingLabel := existingEdge.GetLabel() + + labelToMerge := e.GetLabel() + // check if new edge is in reverse direction to existing edge + // if so, must flip the label before merging it + if !existingEdge.IsPointwiseEqual(e) { + labelToMerge = Geomgraph_NewLabelFromLabel(e.GetLabel()) + labelToMerge.Flip() + } + existingLabel.Merge(labelToMerge) + + // compute new depth delta of sum of edges + mergeDelta := operationBuffer_bufferBuilder_depthDelta(labelToMerge) + existingDelta := existingEdge.GetDepthDelta() + newDelta := existingDelta + mergeDelta + existingEdge.SetDepthDelta(newDelta) + } else { // no matching existing edge was found + // add this new edge to the list of edges in this graph + bb.edgeList.Add(e) + e.SetDepthDelta(operationBuffer_bufferBuilder_depthDelta(e.GetLabel())) + } +} + +func (bb *operationBuffer_BufferBuilder) createSubgraphs(graph *Geomgraph_PlanarGraph) []*OperationBuffer_BufferSubgraph { + subgraphList := make([]*OperationBuffer_BufferSubgraph, 0) + for _, node := range graph.GetNodes() { + if !node.IsVisited() { + subgraph := OperationBuffer_NewBufferSubgraph() + subgraph.Create(node) + subgraphList = append(subgraphList, subgraph) + } + } + // Sort the subgraphs in descending order of their rightmost coordinate. + // This ensures that when the Polygons for the subgraphs are built, + // subgraphs for shells will have been built before the subgraphs for + // any holes they contain. + sort.Slice(subgraphList, func(i, j int) bool { + return subgraphList[i].CompareTo(subgraphList[j]) > 0 + }) + return subgraphList +} + +// buildSubgraphs completes the building of the input subgraphs by depth-labelling them, +// and adds them to the PolygonBuilder. +// The subgraph list must be sorted in rightmost-coordinate order. +func (bb *operationBuffer_BufferBuilder) buildSubgraphs(subgraphList []*OperationBuffer_BufferSubgraph, polyBuilder *OperationOverlay_PolygonBuilder) { + processedGraphs := make([]*OperationBuffer_BufferSubgraph, 0) + for _, subgraph := range subgraphList { + p := subgraph.GetRightmostCoordinate() + locater := operationBuffer_newSubgraphDepthLocater(processedGraphs) + outsideDepth := locater.GetDepth(p) + subgraph.ComputeDepth(outsideDepth) + subgraph.FindResultEdges() + processedGraphs = append(processedGraphs, subgraph) + // Convert DirectedEdges to EdgeEnds for PolygonBuilder.Add + dirEdges := subgraph.GetDirectedEdges() + edgeEnds := make([]*Geomgraph_EdgeEnd, len(dirEdges)) + for i, de := range dirEdges { + edgeEnds[i] = de.Geomgraph_EdgeEnd + } + polyBuilder.Add(edgeEnds, subgraph.GetNodes()) + } +} + +// TRANSLITERATION NOTE: This method is dead code in Java but included for 1-1 correspondence. +// The Java version takes an Iterator; we use a slice here for simplicity +// since this is dead code. If this were live code, we'd need to model the Iterator properly. +func operationBuffer_bufferBuilder_convertSegStrings(segStrings []Noding_SegmentString) *Geom_Geometry { + fact := Geom_NewGeometryFactoryDefault() + lines := make([]*Geom_Geometry, 0) + for _, ss := range segStrings { + line := fact.CreateLineStringFromCoordinates(ss.GetCoordinates()) + lines = append(lines, line.Geom_Geometry) + } + return fact.BuildGeometry(lines) +} + +// createEmptyResultGeometry gets the standard result for an empty buffer. +// Since buffer always returns a polygonal result, +// this is chosen to be an empty polygon. +func (bb *operationBuffer_BufferBuilder) createEmptyResultGeometry() *Geom_Geometry { + emptyGeom := bb.geomFact.CreatePolygon() + return emptyGeom.Geom_Geometry +} diff --git a/internal/jtsport/jts/operation_buffer_buffer_curve_set_builder.go b/internal/jtsport/jts/operation_buffer_buffer_curve_set_builder.go new file mode 100644 index 00000000..ba68ce30 --- /dev/null +++ b/internal/jtsport/jts/operation_buffer_buffer_curve_set_builder.go @@ -0,0 +1,371 @@ +package jts + +import ( + "math" + + "github.com/peterstace/simplefeatures/internal/jtsport/java" +) + +// OperationBuffer_BufferCurveSetBuilder creates all the raw offset curves for a buffer of a Geometry. +// Raw curves need to be noded together and polygonized to form the final buffer area. +type OperationBuffer_BufferCurveSetBuilder struct { + inputGeom *Geom_Geometry + distance float64 + curveBuilder *OperationBuffer_OffsetCurveBuilder + + curveList []Noding_SegmentString + + isInvertOrientation bool +} + +// OperationBuffer_NewBufferCurveSetBuilder creates a new BufferCurveSetBuilder. +func OperationBuffer_NewBufferCurveSetBuilder(inputGeom *Geom_Geometry, distance float64, precisionModel *Geom_PrecisionModel, bufParams *OperationBuffer_BufferParameters) *OperationBuffer_BufferCurveSetBuilder { + return &OperationBuffer_BufferCurveSetBuilder{ + inputGeom: inputGeom, + distance: distance, + curveBuilder: OperationBuffer_NewOffsetCurveBuilder(precisionModel, bufParams), + curveList: make([]Noding_SegmentString, 0), + } +} + +// SetInvertOrientation sets whether the offset curve is generated +// using the inverted orientation of input rings. +// This allows generating a buffer(0) polygon from the smaller lobes +// of self-crossing rings. +func (bcsb *OperationBuffer_BufferCurveSetBuilder) SetInvertOrientation(isInvertOrientation bool) { + bcsb.isInvertOrientation = isInvertOrientation +} + +// isRingCCW computes orientation of a ring using a signed-area orientation test. +// For invalid (self-crossing) rings this ensures the largest enclosed area +// is taken to be the interior of the ring. +// This produces a more sensible result when +// used for repairing polygonal geometry via buffer-by-zero. +// For buffer use the lower robustness of orientation-by-area +// doesn't matter, since narrow or flat rings +// produce an acceptable offset curve for either orientation. +func (bcsb *OperationBuffer_BufferCurveSetBuilder) isRingCCW(coord []*Geom_Coordinate) bool { + isCCW := Algorithm_Orientation_IsCCWArea(coord) + // invert orientation if required + if bcsb.isInvertOrientation { + return !isCCW + } + return isCCW +} + +// GetCurves computes the set of raw offset curves for the buffer. +// Each offset curve has an attached Label indicating +// its left and right location. +// +// Returns a Collection of SegmentStrings representing the raw buffer curves. +func (bcsb *OperationBuffer_BufferCurveSetBuilder) GetCurves() []Noding_SegmentString { + bcsb.add(bcsb.inputGeom) + return bcsb.curveList +} + +// addCurve creates a SegmentString for a coordinate list which is a raw offset curve, +// and adds it to the list of buffer curves. +// The SegmentString is tagged with a Label giving the topology of the curve. +// The curve may be oriented in either direction. +// If the curve is oriented CW, the locations will be: +// +// Left: Location.EXTERIOR +// Right: Location.INTERIOR +func (bcsb *OperationBuffer_BufferCurveSetBuilder) addCurve(coord []*Geom_Coordinate, leftLoc int, rightLoc int) { + // don't add null or trivial curves + if coord == nil || len(coord) < 2 { + return + } + // add the edge for a coordinate list which is a raw offset curve + e := Noding_NewNodedSegmentString(coord, Geomgraph_NewLabelGeomOnLeftRight(0, Geom_Location_Boundary, leftLoc, rightLoc)) + bcsb.curveList = append(bcsb.curveList, e) +} + +func (bcsb *OperationBuffer_BufferCurveSetBuilder) add(g *Geom_Geometry) { + if g.IsEmpty() { + return + } + + if java.InstanceOf[*Geom_Polygon](g) { + bcsb.addPolygon(java.Cast[*Geom_Polygon](g)) + } else if java.InstanceOf[*Geom_LineString](g) { + // LineString also handles LinearRings + bcsb.addLineString(java.Cast[*Geom_LineString](g)) + } else if java.InstanceOf[*Geom_Point](g) { + bcsb.addPoint(java.Cast[*Geom_Point](g)) + } else if java.InstanceOf[*Geom_MultiPoint](g) { + bcsb.addCollection(java.Cast[*Geom_MultiPoint](g).Geom_GeometryCollection) + } else if java.InstanceOf[*Geom_MultiLineString](g) { + bcsb.addCollection(java.Cast[*Geom_MultiLineString](g).Geom_GeometryCollection) + } else if java.InstanceOf[*Geom_MultiPolygon](g) { + bcsb.addCollection(java.Cast[*Geom_MultiPolygon](g).Geom_GeometryCollection) + } else if java.InstanceOf[*Geom_GeometryCollection](g) { + bcsb.addCollection(java.Cast[*Geom_GeometryCollection](g)) + } else { + panic("unsupported geometry type") + } +} + +func (bcsb *OperationBuffer_BufferCurveSetBuilder) addCollection(gc *Geom_GeometryCollection) { + for i := 0; i < gc.GetNumGeometries(); i++ { + g := gc.GetGeometryN(i) + bcsb.add(g) + } +} + +// addPoint adds a Point to the graph. +func (bcsb *OperationBuffer_BufferCurveSetBuilder) addPoint(p *Geom_Point) { + // a zero or negative width buffer of a point is empty + if bcsb.distance <= 0.0 { + return + } + coord := p.GetCoordinates() + // skip if coordinate is invalid + if len(coord) >= 1 && !coord[0].IsValid() { + return + } + curve := bcsb.curveBuilder.GetLineCurve(coord, bcsb.distance) + bcsb.addCurve(curve, Geom_Location_Exterior, Geom_Location_Interior) +} + +func (bcsb *OperationBuffer_BufferCurveSetBuilder) addLineString(line *Geom_LineString) { + if bcsb.curveBuilder.IsLineOffsetEmpty(bcsb.distance) { + return + } + + coord := operationBuffer_bufferCurveSetBuilder_clean(line.GetCoordinates()) + + // Rings (closed lines) are generated with a continuous curve, + // with no end arcs. This produces better quality linework, + // and avoids noding issues with arcs around almost-parallel end segments. + // See JTS #523 and #518. + // + // Singled-sided buffers currently treat rings as if they are lines. + if Geom_CoordinateArrays_IsRing(coord) && !bcsb.curveBuilder.GetBufferParameters().IsSingleSided() { + bcsb.addRingBothSides(coord, bcsb.distance) + } else { + curve := bcsb.curveBuilder.GetLineCurve(coord, bcsb.distance) + bcsb.addCurve(curve, Geom_Location_Exterior, Geom_Location_Interior) + } +} + +// operationBuffer_bufferCurveSetBuilder_clean keeps only valid coordinates, and removes repeated points. +func operationBuffer_bufferCurveSetBuilder_clean(coords []*Geom_Coordinate) []*Geom_Coordinate { + return Geom_CoordinateArrays_RemoveRepeatedOrInvalidPoints(coords) +} + +func (bcsb *OperationBuffer_BufferCurveSetBuilder) addPolygon(p *Geom_Polygon) { + offsetDistance := bcsb.distance + offsetSide := Geom_Position_Left + if bcsb.distance < 0.0 { + offsetDistance = -bcsb.distance + offsetSide = Geom_Position_Right + } + + shell := p.GetExteriorRing() + shellCoord := operationBuffer_bufferCurveSetBuilder_clean(shell.GetCoordinates()) + // optimization - don't bother computing buffer + // if the polygon would be completely eroded + if bcsb.distance < 0.0 && operationBuffer_bufferCurveSetBuilder_isErodedCompletely(shell, bcsb.distance) { + return + } + // don't attempt to buffer a polygon with too few distinct vertices + if bcsb.distance <= 0.0 && len(shellCoord) < 3 { + return + } + + bcsb.addRingSide( + shellCoord, + offsetDistance, + offsetSide, + Geom_Location_Exterior, + Geom_Location_Interior) + + for i := 0; i < p.GetNumInteriorRing(); i++ { + hole := p.GetInteriorRingN(i) + holeCoord := operationBuffer_bufferCurveSetBuilder_clean(hole.GetCoordinates()) + + // optimization - don't bother computing buffer for this hole + // if the hole would be completely covered + if bcsb.distance > 0.0 && operationBuffer_bufferCurveSetBuilder_isErodedCompletely(hole, -bcsb.distance) { + continue + } + + // Holes are topologically labelled opposite to the shell, since + // the interior of the polygon lies on their opposite side + // (on the left, if the hole is oriented CCW) + bcsb.addRingSide( + holeCoord, + offsetDistance, + Geom_Position_Opposite(offsetSide), + Geom_Location_Interior, + Geom_Location_Exterior) + } +} + +func (bcsb *OperationBuffer_BufferCurveSetBuilder) addRingBothSides(coord []*Geom_Coordinate, distance float64) { + bcsb.addRingSide(coord, distance, + Geom_Position_Left, + Geom_Location_Exterior, Geom_Location_Interior) + // Add the opposite side of the ring + bcsb.addRingSide(coord, distance, + Geom_Position_Right, + Geom_Location_Interior, Geom_Location_Exterior) +} + +// addRingSide adds an offset curve for one side of a ring. +// The side and left and right topological location arguments +// are provided as if the ring is oriented CW. +// (If the ring is in the opposite orientation, +// this is detected and the left and right locations are interchanged and the side is flipped.) +func (bcsb *OperationBuffer_BufferCurveSetBuilder) addRingSide(coord []*Geom_Coordinate, offsetDistance float64, side int, cwLeftLoc int, cwRightLoc int) { + // don't bother adding ring if it is "flat" and will disappear in the output + if offsetDistance == 0.0 && len(coord) < Geom_LinearRing_MinimumValidSize { + return + } + + leftLoc := cwLeftLoc + rightLoc := cwRightLoc + isCCW := bcsb.isRingCCW(coord) + if len(coord) >= Geom_LinearRing_MinimumValidSize && isCCW { + leftLoc = cwRightLoc + rightLoc = cwLeftLoc + side = Geom_Position_Opposite(side) + } + curve := bcsb.curveBuilder.GetRingCurve(coord, side, offsetDistance) + + // If the offset curve has inverted completely it will produce + // an unwanted artifact in the result, so skip it. + if operationBuffer_bufferCurveSetBuilder_isRingCurveInverted(coord, offsetDistance, curve) { + return + } + + bcsb.addCurve(curve, leftLoc, rightLoc) +} + +const operationBuffer_bufferCurveSetBuilder_MAX_INVERTED_RING_SIZE = 9 +const operationBuffer_bufferCurveSetBuilder_INVERTED_CURVE_VERTEX_FACTOR = 4 +const operationBuffer_bufferCurveSetBuilder_NEARNESS_FACTOR = 0.99 + +// operationBuffer_bufferCurveSetBuilder_isRingCurveInverted tests whether the offset curve for a ring is fully inverted. +// An inverted ("inside-out") curve occurs in some specific situations +// involving a buffer distance which should result in a fully-eroded (empty) buffer. +// It can happen that the sides of a small, convex polygon +// produce offset segments which all cross one another to form +// a curve with inverted orientation. +// This happens at buffer distances slightly greater than the distance at +// which the buffer should disappear. +// The inverted curve will produce an incorrect non-empty buffer (for a shell) +// or an incorrect hole (for a hole). +// It must be discarded from the set of offset curves used in the buffer. +// Heuristics are used to reduce the number of cases which area checked, +// for efficiency and correctness. +func operationBuffer_bufferCurveSetBuilder_isRingCurveInverted(inputRing []*Geom_Coordinate, distance float64, curveRing []*Geom_Coordinate) bool { + if distance == 0.0 { + return false + } + // Only proper rings can invert. + if len(inputRing) <= 3 { + return false + } + // Heuristic based on low chance that a ring with many vertices will invert. + // This low limit ensures this test is fairly efficient. + if len(inputRing) >= operationBuffer_bufferCurveSetBuilder_MAX_INVERTED_RING_SIZE { + return false + } + + // Don't check curves which are much larger than the input. + // This improves performance by avoiding checking some concave inputs + // (which can produce fillet arcs with many more vertices) + if len(curveRing) > operationBuffer_bufferCurveSetBuilder_INVERTED_CURVE_VERTEX_FACTOR*len(inputRing) { + return false + } + + // If curve contains points which are on the buffer, + // it is not inverted and can be included in the raw curves. + if operationBuffer_bufferCurveSetBuilder_hasPointOnBuffer(inputRing, distance, curveRing) { + return false + } + + // curve is inverted, so discard it + return true +} + +// operationBuffer_bufferCurveSetBuilder_hasPointOnBuffer tests if there are points on the raw offset curve which may +// lie on the final buffer curve +// (i.e. they are (approximately) at the buffer distance from the input ring). +// For efficiency this only tests a limited set of points on the curve. +func operationBuffer_bufferCurveSetBuilder_hasPointOnBuffer(inputRing []*Geom_Coordinate, distance float64, curveRing []*Geom_Coordinate) bool { + distTol := operationBuffer_bufferCurveSetBuilder_NEARNESS_FACTOR * math.Abs(distance) + + for i := 0; i < len(curveRing)-1; i++ { + v := curveRing[i] + + // check curve vertices + dist := Algorithm_Distance_PointToSegmentString(v, inputRing) + if dist > distTol { + return true + } + + // check curve segment midpoints + iNext := i + 1 + if i >= len(curveRing)-1 { + iNext = 0 + } + vnext := curveRing[iNext] + midPt := Geom_LineSegment_MidPoint(v, vnext) + + distMid := Algorithm_Distance_PointToSegmentString(midPt, inputRing) + if distMid > distTol { + return true + } + } + return false +} + +// operationBuffer_bufferCurveSetBuilder_isErodedCompletely tests whether a ring buffer is eroded completely (is empty) +// based on simple heuristics. +// +// The ringCoord is assumed to contain no repeated points. +// It may be degenerate (i.e. contain only 1, 2, or 3 points). +// In this case it has no area, and hence has a minimum diameter of 0. +func operationBuffer_bufferCurveSetBuilder_isErodedCompletely(ring *Geom_LinearRing, bufferDistance float64) bool { + ringCoord := ring.GetCoordinates() + // degenerate ring has no area + if len(ringCoord) < 4 { + return bufferDistance < 0 + } + + // important test to eliminate inverted triangle bug + // also optimizes erosion test for triangles + if len(ringCoord) == 4 { + return operationBuffer_bufferCurveSetBuilder_isTriangleErodedCompletely(ringCoord, bufferDistance) + } + + // if envelope is narrower than twice the buffer distance, ring is eroded + env := ring.GetEnvelopeInternal() + envMinDimension := math.Min(env.GetHeight(), env.GetWidth()) + if bufferDistance < 0.0 && 2*math.Abs(bufferDistance) > envMinDimension { + return true + } + + return false +} + +// operationBuffer_bufferCurveSetBuilder_isTriangleErodedCompletely tests whether a triangular ring would be eroded completely by the given +// buffer distance. +// This is a precise test. It uses the fact that the inner buffer of a +// triangle converges on the inCentre of the triangle (the point +// equidistant from all sides). If the buffer distance is greater than the +// distance of the inCentre from a side, the triangle will be eroded completely. +// +// This test is important, since it removes a problematic case where +// the buffer distance is slightly larger than the inCentre distance. +// In this case the triangle buffer curve "inverts" with incorrect topology, +// producing an incorrect hole in the buffer. +func operationBuffer_bufferCurveSetBuilder_isTriangleErodedCompletely(triangleCoord []*Geom_Coordinate, bufferDistance float64) bool { + tri := Geom_NewTriangle(triangleCoord[0], triangleCoord[1], triangleCoord[2]) + inCentre := tri.InCentre() + distToCentre := Algorithm_Distance_PointToSegment(inCentre, tri.P0, tri.P1) + return distToCentre < math.Abs(bufferDistance) +} diff --git a/internal/jtsport/jts/operation_buffer_buffer_input_line_simplifier.go b/internal/jtsport/jts/operation_buffer_buffer_input_line_simplifier.go new file mode 100644 index 00000000..af496ec5 --- /dev/null +++ b/internal/jtsport/jts/operation_buffer_buffer_input_line_simplifier.go @@ -0,0 +1,188 @@ +package jts + +import "math" + +// OperationBuffer_BufferInputLineSimplifier_Simplify simplifies the input coordinate list. +// If the distance tolerance is positive, +// concavities on the LEFT side of the line are simplified. +// If the supplied distance tolerance is negative, +// concavities on the RIGHT side of the line are simplified. +func OperationBuffer_BufferInputLineSimplifier_Simplify(inputLine []*Geom_Coordinate, distanceTol float64) []*Geom_Coordinate { + simp := operationBuffer_newBufferInputLineSimplifier(inputLine) + return simp.simplify(distanceTol) +} + +const operationBuffer_bufferInputLineSimplifier_delete = 1 + +// operationBuffer_BufferInputLineSimplifier simplifies a buffer input line to +// remove concavities with shallow depth. +// +// The major benefit of doing this is to reduce the number of points and the complexity of +// shape which will be buffered. +// This improve performance and robustness. +// It also reduces the risk of gores created by +// the quantized fillet arcs (although this issue +// should be eliminated by the +// offset curve generation logic). +// +// A key aspect of the simplification is that it +// affects inside (concave or inward) corners only. +// Convex (outward) corners are preserved, since they +// are required to ensure that the generated buffer curve +// lies at the correct distance from the input geometry. +// +// Another important heuristic used is that the end segments +// of linear inputs are never simplified. This ensures that +// the client buffer code is able to generate end caps faithfully. +// Ring inputs can have end segments removed by simplification. +// +// No attempt is made to avoid self-intersections in the output. +// This is acceptable for use for generating a buffer offset curve, +// since the buffer algorithm is insensitive to invalid polygonal +// geometry. However, this means that this algorithm +// cannot be used as a general-purpose polygon simplification technique. +type operationBuffer_BufferInputLineSimplifier struct { + inputLine []*Geom_Coordinate + distanceTol float64 + isRing bool + isDeleted []bool + angleOrientation int +} + +// operationBuffer_newBufferInputLineSimplifier creates a new BufferInputLineSimplifier. +func operationBuffer_newBufferInputLineSimplifier(inputLine []*Geom_Coordinate) *operationBuffer_BufferInputLineSimplifier { + return &operationBuffer_BufferInputLineSimplifier{ + inputLine: inputLine, + isRing: Geom_CoordinateArrays_IsRing(inputLine), + angleOrientation: Algorithm_Orientation_Counterclockwise, + } +} + +// simplify simplifies the input coordinate list. +// If the distance tolerance is positive, +// concavities on the LEFT side of the line are simplified. +// If the supplied distance tolerance is negative, +// concavities on the RIGHT side of the line are simplified. +func (bils *operationBuffer_BufferInputLineSimplifier) simplify(distanceTol float64) []*Geom_Coordinate { + bils.distanceTol = math.Abs(distanceTol) + bils.angleOrientation = Algorithm_Orientation_Counterclockwise + if distanceTol < 0 { + bils.angleOrientation = Algorithm_Orientation_Clockwise + } + + // rely on fact that boolean array is filled with false values + bils.isDeleted = make([]bool, len(bils.inputLine)) + + isChanged := false + for { + isChanged = bils.deleteShallowConcavities() + if !isChanged { + break + } + } + + return bils.collapseLine() +} + +// deleteShallowConcavities uses a sliding window containing 3 vertices to detect shallow angles +// in which the middle vertex can be deleted, since it does not +// affect the shape of the resulting buffer in a significant way. +// +// Returns true if any vertices were deleted. +func (bils *operationBuffer_BufferInputLineSimplifier) deleteShallowConcavities() bool { + // Do not simplify end line segments of lines. + // This ensures that end caps are generated consistently. + index := 0 + if !bils.isRing { + index = 1 + } + + midIndex := bils.nextIndex(index) + lastIndex := bils.nextIndex(midIndex) + + isChanged := false + for lastIndex < len(bils.inputLine) { + // test triple for shallow concavity + isMiddleVertexDeleted := false + if bils.isDeletable(index, midIndex, lastIndex, bils.distanceTol) { + bils.isDeleted[midIndex] = true + isMiddleVertexDeleted = true + isChanged = true + } + // move simplification window forward + if isMiddleVertexDeleted { + index = lastIndex + } else { + index = midIndex + } + + midIndex = bils.nextIndex(index) + lastIndex = bils.nextIndex(midIndex) + } + return isChanged +} + +// nextIndex finds the next non-deleted index, or the end of the point array if none. +func (bils *operationBuffer_BufferInputLineSimplifier) nextIndex(index int) int { + next := index + 1 + for next < len(bils.inputLine) && bils.isDeleted[next] { + next++ + } + return next +} + +func (bils *operationBuffer_BufferInputLineSimplifier) collapseLine() []*Geom_Coordinate { + coordList := Geom_NewCoordinateList() + for i := 0; i < len(bils.inputLine); i++ { + if !bils.isDeleted[i] { + coordList.AddCoordinate(bils.inputLine[i], true) + } + } + return coordList.ToCoordinateArray() +} + +func (bils *operationBuffer_BufferInputLineSimplifier) isDeletable(i0, i1, i2 int, distanceTol float64) bool { + p0 := bils.inputLine[i0] + p1 := bils.inputLine[i1] + p2 := bils.inputLine[i2] + + if !bils.isConcave(p0, p1, p2) { + return false + } + if !operationBuffer_bufferInputLineSimplifier_isShallow(p0, p1, p2, distanceTol) { + return false + } + + return bils.isShallowSampled(p0, p2, i0, i2, distanceTol) +} + +const operationBuffer_bufferInputLineSimplifier_num_pts_to_check = 10 + +// isShallowSampled checks for shallowness over a sample of points in the given section. +// This helps prevents the simplification from incrementally +// "skipping" over points which are in fact non-shallow. +func (bils *operationBuffer_BufferInputLineSimplifier) isShallowSampled(p0, p2 *Geom_Coordinate, i0, i2 int, distanceTol float64) bool { + // check every n'th point to see if it is within tolerance + inc := (i2 - i0) / operationBuffer_bufferInputLineSimplifier_num_pts_to_check + if inc <= 0 { + inc = 1 + } + + for i := i0; i < i2; i += inc { + if !operationBuffer_bufferInputLineSimplifier_isShallow(p0, bils.inputLine[i], p2, distanceTol) { + return false + } + } + return true +} + +func operationBuffer_bufferInputLineSimplifier_isShallow(p0, p1, p2 *Geom_Coordinate, distanceTol float64) bool { + dist := Algorithm_Distance_PointToSegment(p1, p0, p2) + return dist < distanceTol +} + +func (bils *operationBuffer_BufferInputLineSimplifier) isConcave(p0, p1, p2 *Geom_Coordinate) bool { + orientation := Algorithm_Orientation_Index(p0, p1, p2) + isConcave := orientation == bils.angleOrientation + return isConcave +} diff --git a/internal/jtsport/jts/operation_buffer_buffer_op.go b/internal/jtsport/jts/operation_buffer_buffer_op.go new file mode 100644 index 00000000..fec98fa5 --- /dev/null +++ b/internal/jtsport/jts/operation_buffer_buffer_op.go @@ -0,0 +1,300 @@ +package jts + +import ( + "math" + + "github.com/peterstace/simplefeatures/internal/jtsport/java" +) + +// OperationBuffer_BufferOp_CAP_ROUND specifies a round line buffer end cap style. +// Deprecated: use OperationBuffer_BufferParameters +const OperationBuffer_BufferOp_CAP_ROUND = OperationBuffer_BufferParameters_CAP_ROUND + +// OperationBuffer_BufferOp_CAP_BUTT specifies a butt (or flat) line buffer end cap style. +// Deprecated: use OperationBuffer_BufferParameters +const OperationBuffer_BufferOp_CAP_BUTT = OperationBuffer_BufferParameters_CAP_FLAT + +// OperationBuffer_BufferOp_CAP_FLAT specifies a butt (or flat) line buffer end cap style. +// Deprecated: use OperationBuffer_BufferParameters +const OperationBuffer_BufferOp_CAP_FLAT = OperationBuffer_BufferParameters_CAP_FLAT + +// OperationBuffer_BufferOp_CAP_SQUARE specifies a square line buffer end cap style. +// Deprecated: use OperationBuffer_BufferParameters +const OperationBuffer_BufferOp_CAP_SQUARE = OperationBuffer_BufferParameters_CAP_SQUARE + +// operationBuffer_bufferOp_MAX_PRECISION_DIGITS is a number of digits of precision which leaves some computational "headroom" +// for floating point operations. +// +// This value should be less than the decimal precision of double-precision values (16). +const operationBuffer_bufferOp_MAX_PRECISION_DIGITS = 12 + +// OperationBuffer_BufferOp computes the buffer of a geometry, for both positive and negative buffer distances. +// +// In GIS, the positive (or negative) buffer of a geometry is defined as +// the Minkowski sum (or difference) of the geometry +// with a circle of radius equal to the absolute value of the buffer distance. +// In the CAD/CAM world buffers are known as offset curves. +// In morphological analysis the operation of positive and negative buffering +// is referred to as erosion and dilation. +// +// The buffer operation always returns a polygonal result. +// The negative or zero-distance buffer of lines and points is always an empty Polygon. +// +// Since true buffer curves may contain circular arcs, +// computed buffer polygons are only approximations to the true geometry. +// The user can control the accuracy of the approximation by specifying +// the number of linear segments used to approximate arcs. +// +// Buffer results are always valid geometry. +// Given this, computing a zero-width buffer of an invalid polygonal geometry is +// an effective way to "validify" the geometry. +// Note however that in the case of self-intersecting "bow-tie" geometries, +// only the largest enclosed area will be retained. +type OperationBuffer_BufferOp struct { + argGeom *Geom_Geometry + distance float64 + bufParams *OperationBuffer_BufferParameters + resultGeom *Geom_Geometry + saveException error + + isInvertOrientation bool +} + +// operationBuffer_bufferOp_precisionScaleFactor computes a scale factor to limit the precision of +// a given combination of Geometry and buffer distance. +// The scale factor is determined by +// the number of digits of precision in the (geometry + buffer distance), +// limited by the supplied maxPrecisionDigits value. +// +// The scale factor is based on the absolute magnitude of the (geometry + buffer distance). +// since this determines the number of digits of precision which must be handled. +func operationBuffer_bufferOp_precisionScaleFactor(g *Geom_Geometry, distance float64, maxPrecisionDigits int) float64 { + env := g.GetEnvelopeInternal() + envMax := Math_MathUtil_Max4( + math.Abs(env.GetMaxX()), + math.Abs(env.GetMaxY()), + math.Abs(env.GetMinX()), + math.Abs(env.GetMinY())) + + expandByDistance := 0.0 + if distance > 0.0 { + expandByDistance = distance + } + bufEnvMax := envMax + 2*expandByDistance + + // the smallest power of 10 greater than the buffer envelope + bufEnvPrecisionDigits := int(math.Log10(bufEnvMax) + 1.0) + minUnitLog10 := maxPrecisionDigits - bufEnvPrecisionDigits + + scaleFactor := math.Pow(10.0, float64(minUnitLog10)) + return scaleFactor +} + +// OperationBuffer_BufferOp_BufferOp computes the buffer of a geometry for a given buffer distance. +func OperationBuffer_BufferOp_BufferOp(g *Geom_Geometry, distance float64) *Geom_Geometry { + gBuf := OperationBuffer_NewBufferOp(g) + geomBuf := gBuf.GetResultGeometry(distance) + return geomBuf +} + +// OperationBuffer_BufferOp_BufferOpWithParams computes the buffer for a geometry for a given buffer distance +// and accuracy of approximation. +func OperationBuffer_BufferOp_BufferOpWithParams(g *Geom_Geometry, distance float64, params *OperationBuffer_BufferParameters) *Geom_Geometry { + bufOp := OperationBuffer_NewBufferOpWithParams(g, params) + geomBuf := bufOp.GetResultGeometry(distance) + return geomBuf +} + +// OperationBuffer_BufferOp_BufferOpWithQuadrantSegments computes the buffer for a geometry for a given buffer distance +// and accuracy of approximation. +func OperationBuffer_BufferOp_BufferOpWithQuadrantSegments(g *Geom_Geometry, distance float64, quadrantSegments int) *Geom_Geometry { + bufOp := OperationBuffer_NewBufferOp(g) + bufOp.SetQuadrantSegments(quadrantSegments) + geomBuf := bufOp.GetResultGeometry(distance) + return geomBuf +} + +// OperationBuffer_BufferOp_BufferOpWithQuadrantSegmentsAndEndCapStyle computes the buffer for a geometry for a given buffer distance +// and accuracy of approximation. +func OperationBuffer_BufferOp_BufferOpWithQuadrantSegmentsAndEndCapStyle(g *Geom_Geometry, distance float64, quadrantSegments int, endCapStyle int) *Geom_Geometry { + bufOp := OperationBuffer_NewBufferOp(g) + bufOp.SetQuadrantSegments(quadrantSegments) + bufOp.SetEndCapStyle(endCapStyle) + geomBuf := bufOp.GetResultGeometry(distance) + return geomBuf +} + +// OperationBuffer_BufferOp_BufferByZero buffers a geometry with distance zero. +// The result can be computed using the maximum-signed-area orientation, +// or by combining both orientations. +// +// This can be used to fix an invalid polygonal geometry to be valid +// (i.e. with no self-intersections). +// For some uses (e.g. fixing the result of a simplification) +// a better result is produced by using only the max-area orientation. +// Other uses (e.g. fixing geometry) require both orientations to be used. +// +// This function is for INTERNAL use only. +func OperationBuffer_BufferOp_BufferByZero(geom *Geom_Geometry, isBothOrientations bool) *Geom_Geometry { + // compute buffer using maximum signed-area orientation + buf0 := geom.Buffer(0) + if !isBothOrientations { + return buf0 + } + + // compute buffer using minimum signed-area orientation + op := OperationBuffer_NewBufferOp(geom) + op.isInvertOrientation = true + buf0Inv := op.GetResultGeometry(0) + + // the buffer results should be non-adjacent, so combining is safe + return operationBuffer_bufferOp_combine(buf0, buf0Inv) +} + +// operationBuffer_bufferOp_combine combines the elements of two polygonal geometries together. +// The input geometries must be non-adjacent, to avoid creating an invalid result. +func operationBuffer_bufferOp_combine(poly0, poly1 *Geom_Geometry) *Geom_Geometry { + // short-circuit - handles case where geometry is valid + if poly1.IsEmpty() { + return poly0 + } + if poly0.IsEmpty() { + return poly1 + } + + polys := make([]*Geom_Polygon, 0) + operationBuffer_bufferOp_extractPolygons(poly0, &polys) + operationBuffer_bufferOp_extractPolygons(poly1, &polys) + if len(polys) == 1 { + return polys[0].Geom_Geometry + } + return poly0.GetFactory().CreateMultiPolygonFromPolygons(polys).Geom_Geometry +} + +func operationBuffer_bufferOp_extractPolygons(poly0 *Geom_Geometry, polys *[]*Geom_Polygon) { + for i := 0; i < poly0.GetNumGeometries(); i++ { + p := poly0.GetGeometryN(i) + *polys = append(*polys, java.Cast[*Geom_Polygon](p)) + } +} + +// OperationBuffer_NewBufferOp initializes a buffer computation for the given geometry. +func OperationBuffer_NewBufferOp(g *Geom_Geometry) *OperationBuffer_BufferOp { + return &OperationBuffer_BufferOp{ + argGeom: g, + bufParams: OperationBuffer_NewBufferParameters(), + } +} + +// OperationBuffer_NewBufferOpWithParams initializes a buffer computation for the given geometry +// with the given set of parameters. +func OperationBuffer_NewBufferOpWithParams(g *Geom_Geometry, bufParams *OperationBuffer_BufferParameters) *OperationBuffer_BufferOp { + return &OperationBuffer_BufferOp{ + argGeom: g, + bufParams: bufParams, + } +} + +// SetEndCapStyle specifies the end cap style of the generated buffer. +// The styles supported are CAP_ROUND, CAP_FLAT, and CAP_SQUARE. +// The default is CAP_ROUND. +func (bo *OperationBuffer_BufferOp) SetEndCapStyle(endCapStyle int) { + bo.bufParams.SetEndCapStyle(endCapStyle) +} + +// SetQuadrantSegments sets the number of line segments in a quarter-circle +// used to approximate angle fillets for round end caps and joins. +func (bo *OperationBuffer_BufferOp) SetQuadrantSegments(quadrantSegments int) { + bo.bufParams.SetQuadrantSegments(quadrantSegments) +} + +// GetResultGeometry returns the buffer computed for a geometry for a given buffer distance. +func (bo *OperationBuffer_BufferOp) GetResultGeometry(distance float64) *Geom_Geometry { + bo.distance = distance + bo.computeGeometry() + return bo.resultGeom +} + +func (bo *OperationBuffer_BufferOp) computeGeometry() { + bo.bufferOriginalPrecision() + if bo.resultGeom != nil { + return + } + + argPM := bo.argGeom.GetFactory().GetPrecisionModel() + if argPM.GetType() == Geom_PrecisionModel_Fixed { + bo.bufferFixedPrecision(argPM) + } else { + bo.bufferReducedPrecision() + } +} + +func (bo *OperationBuffer_BufferOp) bufferReducedPrecision() { + // try and compute with decreasing precision + for precDigits := operationBuffer_bufferOp_MAX_PRECISION_DIGITS; precDigits >= 0; precDigits-- { + func() { + defer func() { + if r := recover(); r != nil { + // update the saved exception to reflect the new input geometry + if err, ok := r.(error); ok { + bo.saveException = err + } + // don't propagate the exception - it will be detected by fact that resultGeometry is null + } + }() + bo.bufferReducedPrecisionDigits(precDigits) + }() + if bo.resultGeom != nil { + return + } + } + + // tried everything - have to bail + panic(bo.saveException) +} + +func (bo *OperationBuffer_BufferOp) bufferReducedPrecisionDigits(precisionDigits int) { + sizeBasedScaleFactor := operationBuffer_bufferOp_precisionScaleFactor(bo.argGeom, bo.distance, precisionDigits) + + fixedPM := Geom_NewPrecisionModelWithScale(sizeBasedScaleFactor) + bo.bufferFixedPrecision(fixedPM) +} + +func (bo *OperationBuffer_BufferOp) bufferOriginalPrecision() { + defer func() { + if r := recover(); r != nil { + if err, ok := r.(error); ok { + bo.saveException = err + } + // don't propagate the exception - it will be detected by fact that resultGeometry is null + } + }() + // use fast noding by default + bufBuilder := bo.createBufferBuilder() + bo.resultGeom = bufBuilder.Buffer(bo.argGeom, bo.distance) +} + +func (bo *OperationBuffer_BufferOp) createBufferBuilder() *operationBuffer_BufferBuilder { + bufBuilder := operationBuffer_newBufferBuilder(bo.bufParams) + bufBuilder.SetInvertOrientation(bo.isInvertOrientation) + return bufBuilder +} + +func (bo *OperationBuffer_BufferOp) bufferFixedPrecision(fixedPM *Geom_PrecisionModel) { + // Snap-Rounding provides both robustness and a fixed output precision. + // + // SnapRoundingNoder does not require rounded input, + // so could be used by itself. + // But using ScaledNoder may be faster, since it avoids + // rounding within SnapRoundingNoder. + // (Note this only works for buffering, because + // ScaledNoder may invalidate topology.) + snapNoder := NodingSnapround_NewSnapRoundingNoder(Geom_NewPrecisionModelWithScale(1.0)) + noder := Noding_NewScaledNoder(snapNoder, fixedPM.GetScale()) + + bufBuilder := bo.createBufferBuilder() + bufBuilder.SetWorkingPrecisionModel(fixedPM) + bufBuilder.SetNoder(noder) + // this may throw an exception, if robustness errors are encountered + bo.resultGeom = bufBuilder.Buffer(bo.argGeom, bo.distance) +} diff --git a/internal/jtsport/jts/operation_buffer_buffer_parameter_test.go b/internal/jtsport/jts/operation_buffer_buffer_parameter_test.go new file mode 100644 index 00000000..4a249d7b --- /dev/null +++ b/internal/jtsport/jts/operation_buffer_buffer_parameter_test.go @@ -0,0 +1,170 @@ +package jts + +import ( + "testing" +) + +// Tests for the effect of buffer parameter values. + +func TestBufferParameterQuadSegsNeg(t *testing.T) { + checkBufferParam(t, "LINESTRING (20 20, 80 20, 80 80)", + 10.0, -99, + "POLYGON ((70 30, 70 80, 80 90, 90 80, 90 20, 80 10, 20 10, 10 20, 20 30, 70 30))") +} + +func TestBufferParameterQuadSegs0(t *testing.T) { + checkBufferParam(t, "LINESTRING (20 20, 80 20, 80 80)", + 10.0, 0, + "POLYGON ((70 30, 70 80, 80 90, 90 80, 90 20, 80 10, 20 10, 10 20, 20 30, 70 30))") +} + +func TestBufferParameterQuadSegs1(t *testing.T) { + checkBufferParam(t, "LINESTRING (20 20, 80 20, 80 80)", + 10.0, 1, + "POLYGON ((70 30, 70 80, 80 90, 90 80, 90 20, 80 10, 20 10, 10 20, 20 30, 70 30))") +} + +func TestBufferParameterQuadSegs2(t *testing.T) { + checkBufferParam(t, "LINESTRING (20 20, 80 20, 80 80)", + 10.0, 2, + "POLYGON ((70 30, 70 80, 72.92893218813452 87.07106781186548, 80 90, 87.07106781186548 87.07106781186548, 90 80, 90 20, 87.07106781186548 12.928932188134524, 80 10, 20 10, 12.928932188134523 12.928932188134524, 10 20, 12.928932188134524 27.071067811865476, 20 30, 70 30))") +} + +func TestBufferParameterQuadSegs2Bevel(t *testing.T) { + checkBufferParamWithJoinStyle(t, "LINESTRING (20 20, 80 20, 80 80)", + 10.0, 2, OperationBuffer_BufferParameters_JOIN_BEVEL, + "POLYGON ((70 30, 70 80, 72.92893218813452 87.07106781186548, 80 90, 87.07106781186548 87.07106781186548, 90 80, 90 20, 80 10, 20 10, 12.928932188134523 12.928932188134524, 10 20, 12.928932188134524 27.071067811865476, 20 30, 70 30))") +} + +//---------------------------------------------------- + +func TestBufferParameterMitreRight0(t *testing.T) { + checkBufferParamFlatMitre(t, "LINESTRING (20 20, 20 80, 80 80)", + 10.0, 0, + "POLYGON ((10 80, 20 90, 80 90, 80 70, 30 70, 30 20, 10 20, 10 80))") +} + +func TestBufferParameterMitreRight1(t *testing.T) { + checkBufferParamFlatMitre(t, "LINESTRING (20 20, 20 80, 80 80)", + 10.0, 1, + "POLYGON ((10 20, 10 84.14213562373095, 15.857864376269049 90, 80 90, 80 70, 30 70, 30 20, 10 20))") +} + +func TestBufferParameterMitreRight2(t *testing.T) { + checkBufferParamFlatMitre(t, "LINESTRING (20 20, 20 80, 80 80)", + 10.0, 2, + "POLYGON ((10 20, 10 90, 80 90, 80 70, 30 70, 30 20, 10 20))") +} + +func TestBufferParameterMitreNarrow0(t *testing.T) { + checkBufferParamFlatMitre(t, "LINESTRING (10 20, 20 80, 30 20)", + 10.0, 0, + "POLYGON ((10.136060761678563 81.64398987305357, 29.863939238321436 81.64398987305357, 39.863939238321436 21.643989873053574, 20.136060761678564 18.356010126946426, 20 19.172374697017812, 19.863939238321436 18.356010126946426, 0.1360607616785625 21.643989873053574, 10.136060761678563 81.64398987305357))") +} + +func TestBufferParameterMitreNarrow1(t *testing.T) { + checkBufferParamFlatMitre(t, "LINESTRING (10 20, 20 80, 30 20)", + 10.0, 1, + "POLYGON ((11.528729116169634 90, 28.47127088383036 90, 39.863939238321436 21.643989873053574, 20.136060761678564 18.356010126946426, 20 19.172374697017812, 19.863939238321436 18.356010126946426, 0.1360607616785625 21.643989873053574, 11.528729116169634 90))") +} + +func TestBufferParameterMitreNarrow5(t *testing.T) { + checkBufferParamFlatMitre(t, "LINESTRING (10 20, 20 80, 30 20)", + 10.0, 5, + "POLYGON ((18.1953957828363 130, 21.804604217163696 130, 39.863939238321436 21.643989873053574, 20.136060761678564 18.356010126946426, 20 19.172374697017812, 19.863939238321436 18.356010126946426, 0.1360607616785625 21.643989873053574, 18.1953957828363 130))") +} + +func TestBufferParameterMitreNarrow10(t *testing.T) { + checkBufferParamFlatMitre(t, "LINESTRING (10 20, 20 80, 30 20)", + 10.0, 10, + "POLYGON ((20 140.82762530298217, 39.863939238321436 21.643989873053574, 20.136060761678564 18.356010126946426, 20 19.172374697017812, 19.863939238321436 18.356010126946426, 0.1360607616785625 21.643989873053574, 20 140.82762530298217))") +} + +func TestBufferParameterMitreObtuse0(t *testing.T) { + checkBufferParamFlatMitre(t, "LINESTRING (10 10, 50 20, 90 10)", + 1.0, 0, + "POLYGON ((49.75746437496367 20.970142500145332, 50.24253562503633 20.970142500145332, 90.24253562503634 10.970142500145332, 89.75746437496366 9.029857499854668, 50 18.969223593595583, 10.242535625036332 9.029857499854668, 9.757464374963668 10.970142500145332, 49.75746437496367 20.970142500145332))") +} + +func TestBufferParameterMitreObtuse1(t *testing.T) { + checkBufferParamFlatMitre(t, "LINESTRING (10 10, 50 20, 90 10)", + 1.0, 1, + "POLYGON ((9.757464374963668 10.970142500145332, 49.876894374382324 21, 50.12310562561766 20.999999999999996, 90.24253562503634 10.970142500145332, 89.75746437496366 9.029857499854668, 50 18.969223593595583, 10.242535625036332 9.029857499854668, 9.757464374963668 10.970142500145332))") +} + +func TestBufferParameterMitreObtuse2(t *testing.T) { + checkBufferParamFlatMitre(t, "LINESTRING (10 10, 50 20, 90 10)", + 1.0, 2, + "POLYGON ((50 21.030776406404417, 90.24253562503634 10.970142500145332, 89.75746437496366 9.029857499854668, 50 18.969223593595583, 10.242535625036332 9.029857499854668, 9.757464374963668 10.970142500145332, 50 21.030776406404417))") +} + +//---------------------------------------------------- + +func TestBufferParameterMitreSquareCCW1(t *testing.T) { + checkBufferParamFlatMitre(t, "POLYGON((0 0, 100 0, 100 100, 0 100, 0 0))", + 10.0, 1, + "POLYGON ((-10 -4.142135623730949, -10 104.14213562373095, -4.142135623730949 110, 104.14213562373095 110, 110 104.14213562373095, 110 -4.142135623730949, 104.14213562373095 -10, -4.142135623730949 -10, -10 -4.142135623730949))") +} + +func TestBufferParameterMitreSquare1(t *testing.T) { + checkBufferParamFlatMitre(t, "POLYGON ((0 0, 0 100, 100 100, 100 0, 0 0))", + 10.0, 1, + "POLYGON ((-4.14213562373095 -10, -10 -4.14213562373095, -10 104.14213562373095, -4.14213562373095 110, 104.14213562373095 110, 110 104.14213562373095, 110 -4.142135623730951, 104.14213562373095 -10, -4.14213562373095 -10))") +} + +//---------------------------------------------------- + +func checkBufferParam(t *testing.T, wkt string, dist float64, quadSegs int, wktExpected string) { + t.Helper() + checkBufferParamWithJoinStyle(t, wkt, dist, quadSegs, OperationBuffer_BufferParameters_JOIN_ROUND, wktExpected) +} + +func checkBufferParamWithJoinStyle(t *testing.T, wkt string, dist float64, quadSegs int, joinStyle int, wktExpected string) { + t.Helper() + param := OperationBuffer_NewBufferParameters() + param.SetQuadrantSegments(quadSegs) + param.SetJoinStyle(joinStyle) + checkBufferWithParams(t, wkt, dist, param, wktExpected) +} + +func checkBufferWithParams(t *testing.T, wkt string, dist float64, param *OperationBuffer_BufferParameters, wktExpected string) { + t.Helper() + reader := Io_NewWKTReader() + geom, err := reader.Read(wkt) + if err != nil { + t.Fatalf("failed to read input WKT: %v", err) + } + result := OperationBuffer_BufferOp_BufferOpWithParams(geom, dist, param) + expected, err := reader.Read(wktExpected) + if err != nil { + t.Fatalf("failed to read expected WKT: %v", err) + } + checkGeomEqualWithTolerance(t, expected, result, 0.00001) +} + +func checkBufferParamFlatMitre(t *testing.T, wkt string, dist float64, mitreLimit float64, wktExpected string) { + t.Helper() + param := bufParamFlatMitre(mitreLimit) + checkBufferWithParams(t, wkt, dist, param, wktExpected) +} + +func bufParamFlatMitre(mitreLimit float64) *OperationBuffer_BufferParameters { + param := OperationBuffer_NewBufferParameters() + param.SetJoinStyle(OperationBuffer_BufferParameters_JOIN_MITRE) + param.SetMitreLimit(mitreLimit) + param.SetEndCapStyle(OperationBuffer_BufferParameters_CAP_FLAT) + return param +} + +// TRANSLITERATION NOTE: This helper replaces GeometryTestCase.checkEqual() which +// is inherited by Java test classes. Go doesn't have test class inheritance, so +// the helper is defined locally. +func checkGeomEqualWithTolerance(t *testing.T, expected, actual *Geom_Geometry, tolerance float64) { + t.Helper() + actualNorm := actual.Norm() + expectedNorm := expected.Norm() + equal := actualNorm.EqualsExactWithTolerance(expectedNorm, tolerance) + if !equal { + t.Errorf("geometries not equal\nexpected: %v\nactual: %v", expectedNorm, actualNorm) + } +} diff --git a/internal/jtsport/jts/operation_buffer_buffer_parameters.go b/internal/jtsport/jts/operation_buffer_buffer_parameters.go new file mode 100644 index 00000000..d8e9d2fc --- /dev/null +++ b/internal/jtsport/jts/operation_buffer_buffer_parameters.go @@ -0,0 +1,213 @@ +package jts + +import "math" + +// OperationBuffer_BufferParameters_CAP_ROUND specifies a round line buffer end cap style. +const OperationBuffer_BufferParameters_CAP_ROUND = 1 + +// OperationBuffer_BufferParameters_CAP_FLAT specifies a flat line buffer end cap style. +const OperationBuffer_BufferParameters_CAP_FLAT = 2 + +// OperationBuffer_BufferParameters_CAP_SQUARE specifies a square line buffer end cap style. +const OperationBuffer_BufferParameters_CAP_SQUARE = 3 + +// OperationBuffer_BufferParameters_JOIN_ROUND specifies a round join style. +const OperationBuffer_BufferParameters_JOIN_ROUND = 1 + +// OperationBuffer_BufferParameters_JOIN_MITRE specifies a mitre join style. +const OperationBuffer_BufferParameters_JOIN_MITRE = 2 + +// OperationBuffer_BufferParameters_JOIN_BEVEL specifies a bevel join style. +const OperationBuffer_BufferParameters_JOIN_BEVEL = 3 + +// OperationBuffer_BufferParameters_DEFAULT_QUADRANT_SEGMENTS is the default number of facets +// into which to divide a fillet of 90 degrees. +// A value of 8 gives less than 2% max error in the buffer distance. +// For a max error of < 1%, use QS = 12. +// For a max error of < 0.1%, use QS = 18. +const OperationBuffer_BufferParameters_DEFAULT_QUADRANT_SEGMENTS = 8 + +// OperationBuffer_BufferParameters_DEFAULT_MITRE_LIMIT is the default mitre limit. +// Allows fairly pointy mitres. +const OperationBuffer_BufferParameters_DEFAULT_MITRE_LIMIT = 5.0 + +// OperationBuffer_BufferParameters_DEFAULT_SIMPLIFY_FACTOR is the default simplify factor. +// Provides an accuracy of about 1%, which matches the accuracy of the default Quadrant Segments parameter. +const OperationBuffer_BufferParameters_DEFAULT_SIMPLIFY_FACTOR = 0.01 + +// OperationBuffer_BufferParameters is a value class containing the parameters which +// specify how a buffer should be constructed. +// +// The parameters allow control over: +// - Quadrant segments (accuracy of approximation for circular arcs) +// - End Cap style +// - Join style +// - Mitre limit +// - whether the buffer is single-sided +type OperationBuffer_BufferParameters struct { + quadrantSegments int + endCapStyle int + joinStyle int + mitreLimit float64 + isSingleSided bool + simplifyFactor float64 +} + +// OperationBuffer_NewBufferParameters creates a default set of parameters. +func OperationBuffer_NewBufferParameters() *OperationBuffer_BufferParameters { + return &OperationBuffer_BufferParameters{ + quadrantSegments: OperationBuffer_BufferParameters_DEFAULT_QUADRANT_SEGMENTS, + endCapStyle: OperationBuffer_BufferParameters_CAP_ROUND, + joinStyle: OperationBuffer_BufferParameters_JOIN_ROUND, + mitreLimit: OperationBuffer_BufferParameters_DEFAULT_MITRE_LIMIT, + isSingleSided: false, + simplifyFactor: OperationBuffer_BufferParameters_DEFAULT_SIMPLIFY_FACTOR, + } +} + +// OperationBuffer_NewBufferParametersWithQuadrantSegments creates a set of parameters with the +// given quadrantSegments value. +func OperationBuffer_NewBufferParametersWithQuadrantSegments(quadrantSegments int) *OperationBuffer_BufferParameters { + bp := OperationBuffer_NewBufferParameters() + bp.SetQuadrantSegments(quadrantSegments) + return bp +} + +// OperationBuffer_NewBufferParametersWithQuadrantSegmentsAndEndCapStyle creates a set of parameters with the +// given quadrantSegments and endCapStyle values. +func OperationBuffer_NewBufferParametersWithQuadrantSegmentsAndEndCapStyle(quadrantSegments int, endCapStyle int) *OperationBuffer_BufferParameters { + bp := OperationBuffer_NewBufferParameters() + bp.SetQuadrantSegments(quadrantSegments) + bp.SetEndCapStyle(endCapStyle) + return bp +} + +// OperationBuffer_NewBufferParametersWithQuadrantSegmentsEndCapStyleJoinStyleAndMitreLimit creates a set of parameters with the +// given parameter values. +func OperationBuffer_NewBufferParametersWithQuadrantSegmentsEndCapStyleJoinStyleAndMitreLimit(quadrantSegments int, endCapStyle int, joinStyle int, mitreLimit float64) *OperationBuffer_BufferParameters { + bp := OperationBuffer_NewBufferParameters() + bp.SetQuadrantSegments(quadrantSegments) + bp.SetEndCapStyle(endCapStyle) + bp.SetJoinStyle(joinStyle) + bp.SetMitreLimit(mitreLimit) + return bp +} + +// GetQuadrantSegments gets the number of quadrant segments which will be used +// to approximate angle fillets in round endcaps and joins. +func (bp *OperationBuffer_BufferParameters) GetQuadrantSegments() int { + return bp.quadrantSegments +} + +// SetQuadrantSegments sets the number of line segments in a quarter-circle +// used to approximate angle fillets in round endcaps and joins. +// The value should be at least 1. +// +// This determines the error in the approximation to the true buffer curve. +// The default value of 8 gives less than 2% error in the buffer distance. +// For a error of < 1%, use QS = 12. +// For a error of < 0.1%, use QS = 18. +// The error is always less than the buffer distance +// (in other words, the computed buffer curve is always inside the true curve). +func (bp *OperationBuffer_BufferParameters) SetQuadrantSegments(quadSegs int) { + bp.quadrantSegments = quadSegs +} + +// OperationBuffer_BufferParameters_BufferDistanceError computes the maximum distance error due to a given level +// of approximation to a true arc. +func OperationBuffer_BufferParameters_BufferDistanceError(quadSegs int) float64 { + alpha := Algorithm_Angle_PiOver2 / float64(quadSegs) + return 1 - math.Cos(alpha/2.0) +} + +// GetEndCapStyle gets the end cap style. +func (bp *OperationBuffer_BufferParameters) GetEndCapStyle() int { + return bp.endCapStyle +} + +// SetEndCapStyle specifies the end cap style of the generated buffer. +// The styles supported are CAP_ROUND, CAP_FLAT, and CAP_SQUARE. +// The default is CAP_ROUND. +func (bp *OperationBuffer_BufferParameters) SetEndCapStyle(endCapStyle int) { + bp.endCapStyle = endCapStyle +} + +// GetJoinStyle gets the join style. +func (bp *OperationBuffer_BufferParameters) GetJoinStyle() int { + return bp.joinStyle +} + +// SetJoinStyle sets the join style for outside (reflex) corners between line segments. +// The styles supported are JOIN_ROUND, JOIN_MITRE and JOIN_BEVEL. +// The default is JOIN_ROUND. +func (bp *OperationBuffer_BufferParameters) SetJoinStyle(joinStyle int) { + bp.joinStyle = joinStyle +} + +// GetMitreLimit gets the mitre ratio limit. +func (bp *OperationBuffer_BufferParameters) GetMitreLimit() float64 { + return bp.mitreLimit +} + +// SetMitreLimit sets the limit on the mitre ratio used for very sharp corners. +// The mitre ratio is the ratio of the distance from the corner +// to the end of the mitred offset corner. +// When two line segments meet at a sharp angle, +// a miter join will extend far beyond the original geometry. +// (and in the extreme case will be infinitely far.) +// To prevent unreasonable geometry, the mitre limit +// allows controlling the maximum length of the join corner. +// Corners with a ratio which exceed the limit will be beveled. +func (bp *OperationBuffer_BufferParameters) SetMitreLimit(mitreLimit float64) { + bp.mitreLimit = mitreLimit +} + +// SetSingleSided sets whether the computed buffer should be single-sided. +// A single-sided buffer is constructed on only one side of each input line. +// +// The side used is determined by the sign of the buffer distance: +// - a positive distance indicates the left-hand side +// - a negative distance indicates the right-hand side +// +// The single-sided buffer of point geometries is +// the same as the regular buffer. +// +// The End Cap Style for single-sided buffers is +// always ignored, and forced to the equivalent of CAP_FLAT. +func (bp *OperationBuffer_BufferParameters) SetSingleSided(isSingleSided bool) { + bp.isSingleSided = isSingleSided +} + +// IsSingleSided tests whether the buffer is to be generated on a single side only. +func (bp *OperationBuffer_BufferParameters) IsSingleSided() bool { + return bp.isSingleSided +} + +// GetSimplifyFactor gets the simplify factor. +func (bp *OperationBuffer_BufferParameters) GetSimplifyFactor() float64 { + return bp.simplifyFactor +} + +// SetSimplifyFactor sets the factor used to determine the simplify distance tolerance +// for input simplification. +// Simplifying can increase the performance of computing buffers. +// Generally the simplify factor should be greater than 0. +// Values between 0.01 and .1 produce relatively good accuracy for the generate buffer. +// Larger values sacrifice accuracy in return for performance. +func (bp *OperationBuffer_BufferParameters) SetSimplifyFactor(simplifyFactor float64) { + if simplifyFactor < 0 { + bp.simplifyFactor = 0 + } else { + bp.simplifyFactor = simplifyFactor + } +} + +// Copy creates a copy of this BufferParameters. +func (bp *OperationBuffer_BufferParameters) Copy() *OperationBuffer_BufferParameters { + newBp := OperationBuffer_NewBufferParameters() + newBp.quadrantSegments = bp.quadrantSegments + newBp.endCapStyle = bp.endCapStyle + newBp.joinStyle = bp.joinStyle + newBp.mitreLimit = bp.mitreLimit + return newBp +} diff --git a/internal/jtsport/jts/operation_buffer_buffer_result_validator_test.go b/internal/jtsport/jts/operation_buffer_buffer_result_validator_test.go new file mode 100644 index 00000000..93a1ec44 --- /dev/null +++ b/internal/jtsport/jts/operation_buffer_buffer_result_validator_test.go @@ -0,0 +1,27 @@ +package jts + +import "testing" + +func TestBufferResultValidator_randomLineString_envDistanceFailure(t *testing.T) { + wkt := "LINESTRING (0 0, 0.0050121406740709 -0.0178116943078762, 0.023360713164988 -0.0154225740096356, 0.0242435510469024 -0.0324899203339147, 0.0085030847458749 -0.0391466037351922, -0.0049763942180625 -0.037881923003369, -0.0178369891369056 -0.0336509811666022, -0.0177592362824081 -0.0478222381025471, -0.0261518089363225 -0.059241313936419, -0.0341770463763633 -0.0645869491810857, -0.0427699646025609 -0.0602117910701216, -0.0465729748988988 -0.0703610553736549, -0.0572399695179476 -0.0684412431395525, -0.064840373708758 -0.0597916835657802, -0.075933665026951 -0.0567062480351575, -0.0906286002341713 -0.059530806708663, -0.0891053530465528 -0.0744170073807583, -0.0746455187887388 -0.0582637398359729, -0.0876322125310863 -0.0409039977647514, -0.1069996284174552 -0.0514712920048503, -0.1204569088827982 -0.0339879825884526, -0.1354060760983749 -0.0341983165162511, -0.1424144995136747 -0.0474045211955802, -0.1561943830808726 -0.0516357586378472, -0.1609639311781655 -0.0652386984806159, -0.172820609900011 -0.0479563759837032, -0.1931585264285574 -0.0530188509197635, -0.1876254420927919 -0.0738312584480705, -0.2060740501060544 -0.0849407225109319, -0.1954015340280759 -0.0931083780890523, -0.1913365994785588 -0.1059181206385984, -0.1879772905502195 -0.1185977617930016, -0.1901791119746468 -0.1315287415971536, -0.2055975201959379 -0.1276390095109663, -0.2180744798435294 -0.1374971434993573, -0.2261858140167908 -0.1504179698941716, -0.2236828711990071 -0.1654671151870497, -0.225571090488597 -0.1471690572150352, -0.2434396962271624 -0.1427990198476331, -0.2503470559042911 -0.1628488501608332, -0.2704331618796968 -0.1560476987846242, -0.2550448722209311 -0.1440053393069962, -0.2635574811871452 -0.1264168951177332, -0.2758386441327004 -0.116818079927041, -0.2828404821964569 -0.1028919002610568, -0.2928940810123721 -0.0879293917136913, -0.3108998104218049 -0.0870661306780576, -0.3294214224615548 -0.0886284960279035, -0.3373659412990392 -0.1054325347828655, -0.3503843166187969 -0.1008471110331078, -0.3620893100993321 -0.0935329238225187, -0.3752347454586858 -0.0807205639798017, -0.364050120731684 -0.0661650425105326, -0.3780981811728654 -0.0583863178624236, -0.3740658987852153 -0.0428429186828223, -0.3864807698899518 -0.0394112742622611, -0.397014675967126 -0.0319989850926683, -0.3792640781001624 -0.0475790727438729, -0.3635724248095485 -0.0299270226402436, -0.3531693783166142 -0.0125463838807832, -0.3655005683918717 0.0035238286675586, -0.3493015470929176 0.0029497655991704, -0.3378627422211032 0.0144341716593467, -0.3379405126929051 0.0293510688455208, -0.3406883764612563 0.0440128939235851, -0.3324332748392658 0.0227821327097087, -0.3462851476872376 0.0046985063279884, -0.3661593446838672 0.0075718173506031, -0.3859950986841109 0.0044441207963732, -0.4027319978512339 0.0004805553317992, -0.4150094528052938 0.012526207857646, -0.4311725983161582 0.0071653266858711, -0.4447171238919916 0.0174867677998271, -0.465291792103443 0.0104740624365847, -0.4615219171240586 -0.0109334841571465, -0.4526590891130391 -0.025063534859475, -0.4401615889615084 -0.0361098257829378, -0.451476583234073 -0.0503332357093948, -0.43876845696841 -0.0633270039134291, -0.4220363317446629 -0.0713764311308523, -0.4287707591717834 -0.0886797481614276, -0.4163583595694239 -0.0805826015178939, -0.4017174055456447 -0.0782861013700334, -0.3864498922291822 -0.087086838473576, -0.3893076893621629 -0.1044759998099015, -0.3782293922748502 -0.1056035091475074, -0.3670938749838836 -0.1055894264015696, -0.3613890574287955 -0.1164741204510282, -0.3493671904520086 -0.1190228892282226, -0.3511433302216462 -0.1319276046935007, -0.3402777116820132 -0.139112636876606, -0.3469922590359794 -0.1509770179277757, -0.3377737639033578 -0.1610203257188295, -0.3368889950817616 -0.1732679389871961, -0.3269102987026388 -0.1804242274953837, -0.3177137792270426 -0.1867051970797883, -0.3092836077007141 -0.1939824738659162, -0.3116643890978169 -0.2050741572602805, -0.3229956647241386 -0.2045303252439961, -0.3221430239883363 -0.2150674360329148, -0.331767345466129 -0.2194412371365768, -0.3315329076661971 -0.2298055225437402, -0.3362944034438575 -0.2390142972202375, -0.3470221693455817 -0.2369980102710796, -0.3559158724222612 -0.2433267083905513, -0.3644604760007383 -0.2436755181520328, -0.372881872491775 -0.2421882388562105, -0.3814256019023612 -0.2406668899680489, -0.3898804777999258 -0.2426226221167863, -0.3970018876959567 -0.2364253071078368, -0.4060835430038888 -0.2338475699033603, -0.412594493640082 -0.2439820360262829, -0.4236884218893332 -0.2392889946047656, -0.425726993166183 -0.2499686502941685, -0.4362210512383605 -0.2528121628530928, -0.4454415965196014 -0.2541298962589096, -0.4546585745696004 -0.2527874387221312, -0.4687370955646141 -0.2482192707581446, -0.4645693181446278 -0.2340170686988974, -0.4747109779380611 -0.2373166792512392, -0.4853026776378588 -0.2385643132649516, -0.4823430301358789 -0.2261580832778798, -0.4921708793894377 -0.2180286654095807, -0.5014310346680746 -0.2104343904289632, -0.5131767467491777 -0.2127714261525942, -0.524758242265578 -0.2138677953865533, -0.5281216797546923 -0.225004237913249, -0.5402456559428697 -0.2190784275354333, -0.5464620363415711 -0.2310560166911842, -0.542512721959894 -0.2190727838184703, -0.5299158885511648 -0.2183553284263113, -0.5323313826508866 -0.2081056749441625, -0.5374613194449342 -0.1989092758260843, -0.5308959758078244 -0.192158102294992, -0.5252832259614064 -0.18459641463404, -0.5221681982846436 -0.1741043062332059, -0.5299291504240683 -0.1663870944302677, -0.538075291830999 -0.1606196846947658, -0.5460958408390417 -0.1546788574385282, -0.5571976352029643 -0.1590683728220992, -0.5656419053954334 -0.15062965346914, -0.5626710205200925 -0.1391545786037842, -0.5716905851300862 -0.131463612841658, -0.5620517316800521 -0.1229727179031278, -0.5681678748101774 -0.1116768962906813, -0.5771327800896917 -0.1056434422681761, -0.5826417573967029 -0.0963470309636358, -0.5787652458443208 -0.0819577497852585, -0.5931515506438964 -0.07807020700609, -0.6000930810759233 -0.0897522881719957, -0.613090596340292 -0.0857873843827383, -0.6265660218475042 -0.08732871676454, -0.6283374626939596 -0.1007758278088917, -0.6376657588583103 -0.103841922781183, -0.6474053207345623 -0.1025933307070406, -0.6548736564285454 -0.0953067735503082, -0.654680552705842 -0.0848744840514193, -0.6673818543076171 -0.0797269153099037, -0.6732514461208984 -0.0921111156584666, -0.680340410670068 -0.0848045371912123, -0.6839141811082304 -0.0752720835651596, -0.6952357315689285 -0.0780839448648954, -0.7043638998905275 -0.0708202948254428, -0.7089478156886628 -0.0611570039128909, -0.7170491012862124 -0.0541741214871344, -0.7299366109544918 -0.0489860575040238, -0.743822620267513 -0.0494134433970277, -0.7525108381809805 -0.0614072740889566, -0.7497387870295431 -0.0759555673769118, -0.7639646144394061 -0.0777167567822421, -0.7647857704051172 -0.0920276497137342, -0.7665925966102568 -0.1068906296980722, -0.751925224535934 -0.1098974538338984, -0.7553757071995333 -0.1178721548416768, -0.7582065416490503 -0.1260872667152114, -0.7646431127084641 -0.1319348983754947, -0.7703918589555896 -0.1384599393426758, -0.7782472054819142 -0.148123733387558, -0.7905826527045907 -0.1498360786235145, -0.805019711373555 -0.1498484410770697, -0.8091953806227662 -0.1360284337629413, -0.8239770665663367 -0.138740014308915, -0.8358364299184061 -0.1295092825245303, -0.8317679166580235 -0.1163824870318194, -0.8294437111734797 -0.102837612395154, -0.8347428924366753 -0.1129319903177962, -0.82988311192825 -0.1232451043535887, -0.8422385495990062 -0.1216458574069483, -0.8479693210652968 -0.1327080764082801, -0.8494086824352087 -0.143259925911373, -0.8594354099962246 -0.1468485222255715, -0.8710700404835304 -0.1418156960697787, -0.8764637086499377 -0.1532875010455122, -0.871877966331779 -0.1412974383981378, -0.8823312633464051 -0.1338463165751822, -0.8927539427986722 -0.1352439397632697, -0.9024673537790425 -0.1312146414785791, -0.9169666300372805 -0.1335633933170455, -0.9209834585760139 -0.1194350269676821, -0.9100334022350977 -0.1118099759715185, -0.9133093778973502 -0.0988750186842421, -0.904147237248668 -0.0981354871802091, -0.867761668652526 -0.0926437616303994, -0.8913826744373572 -0.0840621883819444, -0.8812480806037308 -0.0842143981575342, -0.8685379344019649 -0.083568724024858, -0.8675160071300994 -0.0708832844894344, -0.8580205523689814 -0.0613140308561704, -0.8675158498660372 -0.0517446211742423, -0.872409166809526 -0.0609547213497025, -0.8814761088393026 -0.0661084635027518, -0.8953889326977116 -0.0653897157193776, -0.8965747408556268 -0.0792705343228981, -0.8971756623331555 -0.0639311912103026, -0.9092402266247249 -0.0544389395173335, -0.9210951910761328 -0.0619763164030262, -0.9344515553812949 -0.0663305976306128, -0.9293822574409418 -0.0516430853363158, -0.9432685864947324 -0.044672384911463, -0.9334103096655941 -0.0379224973468535, -0.9271045458452278 -0.0277743966961627, -0.9402339665028446 -0.031168284369269, -0.9452756930518931 -0.0185793545598317, -0.945522913806585 -0.0067054298128354, -0.9561187700225338 -0.001340820036736, -0.953028314165927 0.0069471451590414, -0.9539875490127478 0.0157403898955819, -0.9454055349138165 0.0164413901291298, -0.9367970827899018 0.0166335342207932, -0.9348612526701263 0.0262474007677379, -0.9444648321049989 0.0282336359686109, -0.9416892723465026 0.0350367191376586, -0.9436581770452196 0.0421154963530633, -0.9392419545310323 0.0479752065653825, -0.9429529439318587 0.0543051183131185, -0.9498274711576367 0.0567906035358281, -0.9502592194732825 0.0640878876033568, -0.9520104655644192 0.0697913324971662, -0.9506223265991526 0.075593850635143, -0.9522815769508243 0.0816306792021164, -0.949060273844204 0.0869990697590549, -0.9422910357785079 0.092524671810989, -0.939968942049302 0.1009486104823701, -0.9324013321982897 0.1049244389670934, -0.9273714883420019 0.11183650101676, -0.9288062097408671 0.1235145389288624, -0.9183235713304518 0.1288576922043494, -0.9306750764088837 0.1320777269837099, -0.9292202577363909 0.1447588864067668, -0.9370186524242582 0.1524827493617562, -0.9433294881114153 0.1614630818491934, -0.9465907937786249 0.1720205011056855, -0.9466837543675501 0.1830697804641427, -0.961723424524043 0.1824460721954856, -0.9729999178465795 0.1924170996634692, -0.9712379977563 0.1757762035680768, -0.9858512041816034 0.1676228003992367)" + bufferResultValidatorTest_runTest(t, wkt, 1.0) +} + +func bufferResultValidatorTest_runTest(t *testing.T, wkt string, dist float64) { + rdr := Io_NewWKTReader() + g, err := rdr.Read(wkt) + if err != nil { + t.Fatalf("Failed to read WKT: %v", err) + } + buf := g.Buffer(dist) + validator := OperationBufferValidate_NewBufferResultValidator(g, dist, buf) + + if !validator.IsValid() { + // msg := validator.GetErrorMessage() + // System.out.println(msg); + // System.out.println(WKTWriter.toPoint(validator.getErrorLocation())); + } + if !validator.IsValid() { + t.Errorf("Buffer result validation failed: %s", validator.GetErrorMessage()) + } +} diff --git a/internal/jtsport/jts/operation_buffer_buffer_subgraph.go b/internal/jtsport/jts/operation_buffer_buffer_subgraph.go new file mode 100644 index 00000000..675d6208 --- /dev/null +++ b/internal/jtsport/jts/operation_buffer_buffer_subgraph.go @@ -0,0 +1,223 @@ +package jts + +import "github.com/peterstace/simplefeatures/internal/jtsport/java" + +// OperationBuffer_BufferSubgraph is a connected subset of the graph of +// DirectedEdges and Nodes. +// Its edges will generate either +// - a single polygon in the complete buffer, with zero or more holes, or +// - one or more connected holes +type OperationBuffer_BufferSubgraph struct { + finder *operationBuffer_RightmostEdgeFinder + dirEdgeList []*Geomgraph_DirectedEdge + nodes []*Geomgraph_Node + rightMostCoord *Geom_Coordinate + env *Geom_Envelope +} + +// OperationBuffer_NewBufferSubgraph creates a new BufferSubgraph. +func OperationBuffer_NewBufferSubgraph() *OperationBuffer_BufferSubgraph { + return &OperationBuffer_BufferSubgraph{ + finder: operationBuffer_newRightmostEdgeFinder(), + dirEdgeList: make([]*Geomgraph_DirectedEdge, 0), + nodes: make([]*Geomgraph_Node, 0), + } +} + +// GetDirectedEdges returns the list of DirectedEdges. +func (bs *OperationBuffer_BufferSubgraph) GetDirectedEdges() []*Geomgraph_DirectedEdge { + return bs.dirEdgeList +} + +// GetNodes returns the list of nodes. +func (bs *OperationBuffer_BufferSubgraph) GetNodes() []*Geomgraph_Node { + return bs.nodes +} + +// GetEnvelope computes the envelope of the edges in the subgraph. +// The envelope is cached after being computed. +func (bs *OperationBuffer_BufferSubgraph) GetEnvelope() *Geom_Envelope { + if bs.env == nil { + edgeEnv := Geom_NewEnvelope() + for _, dirEdge := range bs.dirEdgeList { + pts := dirEdge.GetEdge().GetCoordinates() + for i := 0; i < len(pts)-1; i++ { + edgeEnv.ExpandToIncludeCoordinate(pts[i]) + } + } + bs.env = edgeEnv + } + return bs.env +} + +// GetRightmostCoordinate gets the rightmost coordinate in the edges of the subgraph. +func (bs *OperationBuffer_BufferSubgraph) GetRightmostCoordinate() *Geom_Coordinate { + return bs.rightMostCoord +} + +// Create creates the subgraph consisting of all edges reachable from this node. +// Finds the edges in the graph and the rightmost coordinate. +func (bs *OperationBuffer_BufferSubgraph) Create(node *Geomgraph_Node) { + bs.addReachable(node) + bs.finder.FindEdge(bs.dirEdgeList) + bs.rightMostCoord = bs.finder.GetCoordinate() +} + +// addReachable adds all nodes and edges reachable from this node to the subgraph. +// Uses an explicit stack to avoid a large depth of recursion. +func (bs *OperationBuffer_BufferSubgraph) addReachable(startNode *Geomgraph_Node) { + nodeStack := make([]*Geomgraph_Node, 0) + nodeStack = append(nodeStack, startNode) + for len(nodeStack) > 0 { + // pop from stack + node := nodeStack[len(nodeStack)-1] + nodeStack = nodeStack[:len(nodeStack)-1] + bs.add(node, &nodeStack) + } +} + +// add adds the argument node and all its out edges to the subgraph. +func (bs *OperationBuffer_BufferSubgraph) add(node *Geomgraph_Node, nodeStack *[]*Geomgraph_Node) { + node.SetVisited(true) + bs.nodes = append(bs.nodes, node) + star := java.Cast[*Geomgraph_DirectedEdgeStar](node.GetEdges()) + for _, ee := range star.GetEdges() { + de := java.Cast[*Geomgraph_DirectedEdge](ee) + bs.dirEdgeList = append(bs.dirEdgeList, de) + sym := de.GetSym() + symNode := sym.GetNode() + // NOTE: this is a depth-first traversal of the graph. + // This will cause a large depth of recursion. + // It might be better to do a breadth-first traversal. + if !symNode.IsVisited() { + *nodeStack = append(*nodeStack, symNode) + } + } +} + +func (bs *OperationBuffer_BufferSubgraph) clearVisitedEdges() { + for _, de := range bs.dirEdgeList { + de.SetVisited(false) + } +} + +// ComputeDepth computes the depth for all edges in the subgraph. +func (bs *OperationBuffer_BufferSubgraph) ComputeDepth(outsideDepth int) { + bs.clearVisitedEdges() + // find an outside edge to assign depth to + de := bs.finder.GetEdge() + // right side of line returned by finder is on the outside + de.SetEdgeDepths(Geom_Position_Right, outsideDepth) + bs.copySymDepths(de) + + bs.computeDepths(de) +} + +// computeDepths computes depths for all dirEdges via breadth-first traversal of nodes in graph. +func (bs *OperationBuffer_BufferSubgraph) computeDepths(startEdge *Geomgraph_DirectedEdge) { + nodesVisited := make(map[*Geomgraph_Node]bool) + nodeQueue := make([]*Geomgraph_Node, 0) + + startNode := startEdge.GetNode() + nodeQueue = append(nodeQueue, startNode) + nodesVisited[startNode] = true + startEdge.SetVisited(true) + + for len(nodeQueue) > 0 { + // remove from front of queue + n := nodeQueue[0] + nodeQueue = nodeQueue[1:] + nodesVisited[n] = true + // compute depths around node, starting at this edge since it has depths assigned + bs.computeNodeDepth(n) + + // add all adjacent nodes to process queue, + // unless the node has been visited already + star := java.Cast[*Geomgraph_DirectedEdgeStar](n.GetEdges()) + for _, ee := range star.GetEdges() { + de := java.Cast[*Geomgraph_DirectedEdge](ee) + sym := de.GetSym() + if sym.IsVisited() { + continue + } + adjNode := sym.GetNode() + if !nodesVisited[adjNode] { + nodeQueue = append(nodeQueue, adjNode) + nodesVisited[adjNode] = true + } + } + } +} + +func (bs *OperationBuffer_BufferSubgraph) computeNodeDepth(n *Geomgraph_Node) { + // find a visited dirEdge to start at + var startEdge *Geomgraph_DirectedEdge + star := java.Cast[*Geomgraph_DirectedEdgeStar](n.GetEdges()) + for _, ee := range star.GetEdges() { + de := java.Cast[*Geomgraph_DirectedEdge](ee) + if de.IsVisited() || de.GetSym().IsVisited() { + startEdge = de + break + } + } + + // only compute string append if assertion would fail + if startEdge == nil { + panic("unable to find edge to compute depths at " + n.GetCoordinate().String()) + } + + star.ComputeDepths(startEdge) + + // copy depths to sym edges + for _, ee := range star.GetEdges() { + de := java.Cast[*Geomgraph_DirectedEdge](ee) + de.SetVisited(true) + bs.copySymDepths(de) + } +} + +func (bs *OperationBuffer_BufferSubgraph) copySymDepths(de *Geomgraph_DirectedEdge) { + sym := de.GetSym() + sym.SetDepth(Geom_Position_Left, de.GetDepth(Geom_Position_Right)) + sym.SetDepth(Geom_Position_Right, de.GetDepth(Geom_Position_Left)) +} + +// FindResultEdges finds all edges whose depths indicates that they are in the result area(s). +// Since we want polygon shells to be oriented CW, choose dirEdges with the interior of the result on the RHS. +// Mark them as being in the result. +// Interior Area edges are the result of dimensional collapses. +// They do not form part of the result area boundary. +func (bs *OperationBuffer_BufferSubgraph) FindResultEdges() { + for _, de := range bs.dirEdgeList { + // Select edges which have an interior depth on the RHS + // and an exterior depth on the LHS. + // Note that because of weird rounding effects there may be + // edges which have negative depths! Negative depths + // count as "outside". + // - handle negative depths + if de.GetDepth(Geom_Position_Right) >= 1 && + de.GetDepth(Geom_Position_Left) <= 0 && + !de.IsInteriorAreaEdge() { + de.SetInResult(true) + } + } +} + +// CompareTo compares BufferSubgraphs on the x-value of their rightmost Coordinate. +// This defines a partial ordering on the graphs such that: +// +// g1 >= g2 <==> Ring(g2) does not contain Ring(g1) +// +// where Polygon(g) is the buffer polygon that is built from g. +// +// This relationship is used to sort the BufferSubgraphs so that shells are guaranteed to +// be built before holes. +func (bs *OperationBuffer_BufferSubgraph) CompareTo(other *OperationBuffer_BufferSubgraph) int { + if bs.rightMostCoord.X < other.rightMostCoord.X { + return -1 + } + if bs.rightMostCoord.X > other.rightMostCoord.X { + return 1 + } + return 0 +} diff --git a/internal/jtsport/jts/operation_buffer_buffer_test.go b/internal/jtsport/jts/operation_buffer_buffer_test.go new file mode 100644 index 00000000..7834f472 --- /dev/null +++ b/internal/jtsport/jts/operation_buffer_buffer_test.go @@ -0,0 +1,689 @@ +package jts + +import ( + "testing" + + "github.com/peterstace/simplefeatures/internal/jtsport/java" + "github.com/peterstace/simplefeatures/internal/jtsport/junit" +) + +func TestBuffer_First(t *testing.T) { + TestBufferMultiLineString_separateBuffers_floatingSingle(t) +} + +func TestBufferPointBufferSegmentCount(t *testing.T) { + g := bufferTestRead("POINT ( 100 100 )") + checkPointBufferSegmentCount(t, g, 80, 53) + checkPointBufferSegmentCount(t, g, 80, 129) +} + +func checkPointBufferSegmentCount(t *testing.T, g *Geom_Geometry, dist float64, quadSegs int) { + t.Helper() + buf := g.BufferWithQuadrantSegments(dist, quadSegs) + segsExpected := 4*quadSegs + 1 + junit.AssertEquals(t, segsExpected, buf.GetNumPoints()) +} + +func TestBufferMultiLineString_depthFailure(t *testing.T) { + operationBuffer_NewBufferValidator( + 15, + "MULTILINESTRING ((1335558.59524 631743.01449, 1335572.28215 631775.89056, 1335573.2578018496 631782.1915185435), (1335573.2578018496 631782.1915185435, 1335576.62035 631803.90754), (1335558.59524 631743.01449, 1335573.2578018496 631782.1915185435), (1335573.2578018496 631782.1915185435, 1335580.70187 631802.08139))"). + SetEmptyBufferExpected(false). + Test(t) +} + +func TestBufferMultiLineString_separateBuffers_floating(t *testing.T) { + operationBuffer_NewBufferValidator( + 0.01, + "MULTILINESTRING (( 635074.5418406526 6184832.4888257105, 635074.5681951842 6184832.571842485, 635074.6472587794 6184832.575795664 ), ( 635074.6657069515 6184832.53889932, 635074.6933792098 6184832.451929366, 635074.5642420045 6184832.474330718 ))"). + SetBufferHolesExpected(false). + SetEmptyBufferExpected(false). + Test(t) +} + +func TestBufferMultiLineString2_buffersTouchToMakeHole_floating(t *testing.T) { + operationBuffer_NewBufferValidator( + 0.037, + "MULTILINESTRING (( 635074.5418406526 6184832.4888257105, 635074.5681951842 6184832.571842485, 635074.6472587794 6184832.575795664 ), ( 635074.6657069515 6184832.53889932, 635074.6933792098 6184832.451929366, 635074.5642420045 6184832.474330718 ))"). + SetBufferHolesExpected(true). + SetEmptyBufferExpected(false). + Test(t) +} + +func TestBufferMultiLineString3_holeVanishes_floating(t *testing.T) { + operationBuffer_NewBufferValidator( + 0.16, + "MULTILINESTRING (( 635074.5418406526 6184832.4888257105, 635074.5681951842 6184832.571842485, 635074.6472587794 6184832.575795664 ), ( 635074.6657069515 6184832.53889932, 635074.6933792098 6184832.451929366, 635074.5642420045 6184832.474330718 ))"). + SetBufferHolesExpected(false). + SetEmptyBufferExpected(false). + Test(t) +} + +func TestBufferMultiLineString4_reallyBigDistance_floating(t *testing.T) { + operationBuffer_NewBufferValidator( + 1e10, + "MULTILINESTRING (( 635074.5418406526 6184832.4888257105, 635074.5681951842 6184832.571842485, 635074.6472587794 6184832.575795664 ), ( 635074.6657069515 6184832.53889932, 635074.6933792098 6184832.451929366, 635074.5642420045 6184832.474330718 ))"). + SetBufferHolesExpected(false). + SetEmptyBufferExpected(false). + Test(t) +} + +func TestBufferMultiLineString_separateBuffers_floatingSingle(t *testing.T) { + bv := operationBuffer_NewBufferValidatorWithContainsTest( + 0.01, + "MULTILINESTRING (( 635074.5418406526 6184832.4888257105, 635074.5681951842 6184832.571842485, 635074.6472587794 6184832.575795664 ), ( 635074.6657069515 6184832.53889932, 635074.6933792098 6184832.451929366, 635074.5642420045 6184832.474330718 ))", + false) + bv.SetBufferHolesExpected(false) + bv.SetEmptyBufferExpected(true) + bv.SetPrecisionModel(Geom_NewPrecisionModelWithType(Geom_PrecisionModel_FloatingSingle)) + bv.Test(t) +} + +func TestBufferMultiLineString2_buffersTouchToMakeHole_floatingSingle(t *testing.T) { + operationBuffer_NewBufferValidatorWithContainsTest( + 0.037, + "MULTILINESTRING (( 635074.5418406526 6184832.4888257105, 635074.5681951842 6184832.571842485, 635074.6472587794 6184832.575795664 ), ( 635074.6657069515 6184832.53889932, 635074.6933792098 6184832.451929366, 635074.5642420045 6184832.474330718 ))", + false). + SetBufferHolesExpected(false). + SetEmptyBufferExpected(true). + SetPrecisionModel(Geom_NewPrecisionModelWithType(Geom_PrecisionModel_FloatingSingle)). + Test(t) +} + +func TestBufferMultiLineString3_holeVanishes_floatingSingle(t *testing.T) { + operationBuffer_NewBufferValidatorWithContainsTest( + 0.16, + "MULTILINESTRING (( 635074.5418406526 6184832.4888257105, 635074.5681951842 6184832.571842485, 635074.6472587794 6184832.575795664 ), ( 635074.6657069515 6184832.53889932, 635074.6933792098 6184832.451929366, 635074.5642420045 6184832.474330718 ))", + false). + SetBufferHolesExpected(false). + SetEmptyBufferExpected(true). + SetPrecisionModel(Geom_NewPrecisionModelWithType(Geom_PrecisionModel_FloatingSingle)). + Test(t) +} + +func TestBufferMultiLineString4_reallyBigDistance_floatingSingle(t *testing.T) { + operationBuffer_NewBufferValidator( + 1e10, + "MULTILINESTRING (( 635074.5418406526 6184832.4888257105, 635074.5681951842 6184832.571842485, 635074.6472587794 6184832.575795664 ), ( 635074.6657069515 6184832.53889932, 635074.6933792098 6184832.451929366, 635074.5642420045 6184832.474330718 ))"). + SetBufferHolesExpected(false). + SetEmptyBufferExpected(false). + SetPrecisionModel(Geom_NewPrecisionModelWithType(Geom_PrecisionModel_FloatingSingle)). + Test(t) +} + +func TestBufferPolygon_MultipleHoles(t *testing.T) { + operationBuffer_NewBufferValidator( + 10.0, + "POLYGON (( 78 82, 78 282, 312 282, 312 82, 78 82 ), ( 117 242, 122 242, 122 248, 117 248, 117 242 ), ( 156 104, 288 104, 288 210, 156 210, 156 104 ))"). + SetBufferHolesExpected(true). + SetEmptyBufferExpected(false). + SetPrecisionModel(Geom_NewPrecisionModel()). + Test(t) +} + +func TestBuffer1(t *testing.T) { + operationBuffer_NewBufferValidator( + 0, + "POINT (100 100)"). + SetEmptyBufferExpected(true). + Test(t) +} + +func TestBuffer2(t *testing.T) { + operationBuffer_NewBufferValidator( + 0, + "LINESTRING (10 10, 100 100)"). + SetEmptyBufferExpected(true). + Test(t) +} + +func TestBuffer1a(t *testing.T) { + operationBuffer_NewBufferValidator( + -1, + "POINT (100 100)"). + SetEmptyBufferExpected(true). + Test(t) +} + +func TestBuffer2a(t *testing.T) { + operationBuffer_NewBufferValidator( + -1, + "LINESTRING (10 10, 100 100)"). + SetEmptyBufferExpected(true). + Test(t) +} + +func TestBuffer3(t *testing.T) { + operationBuffer_NewBufferValidator( + 10, + "LINESTRING (100 100, 200 100, 200 200, 100 200, 100 100)"). + Test(t) +} + +func TestBuffer4(t *testing.T) { + operationBuffer_NewBufferValidator( + 50, + "LINESTRING (40 40, 160 40, 100 180, 40 80)"). + Test(t) +} + +func TestBuffer5(t *testing.T) { + operationBuffer_NewBufferValidator( + 0, + "POLYGON ((80 300, 280 300, 280 300, 280 300, 280 80, 80 80, 80 300))"). + Test(t) +} + +func TestBuffer6(t *testing.T) { + operationBuffer_NewBufferValidator( + 10, + "POLYGON ((60 300, 60 160, 240 160, 240 300, 60 300))"). + Test(t) +} + +func TestBuffer7(t *testing.T) { + operationBuffer_NewBufferValidator( + 10, + "POLYGON ((80 300, 280 300, 280 80, 80 80, 80 300), (260 280, 180 200, 100 280, 100 100, 260 100, 260 280))"). + Test(t) +} + +func TestBuffer8(t *testing.T) { + operationBuffer_NewBufferValidator( + 200, + "POLYGON ((80 300, 280 300, 280 80, 80 80, 80 300), (260 280, 180 200, 100 280, 100 100, 260 100, 260 280))"). + Test(t) +} + +func TestBuffer9(t *testing.T) { + operationBuffer_NewBufferValidator( + -10, + "POLYGON ((80 300, 280 300, 280 80, 80 80, 80 300))"). + Test(t) +} + +func TestBuffer10(t *testing.T) { + operationBuffer_NewBufferValidator( + 10, + "POLYGON ((100 300, 300 300, 300 100, 100 100, 100 300), (220 220, 180 220, 180 180, 220 180, 220 220))"). + Test(t) +} + +func TestBuffer11(t *testing.T) { + operationBuffer_NewBufferValidator( + 5, + "POLYGON ((260 400, 220 300, 80 300, 180 220, 40 200, 180 160, 60 20, 200 80, 280 20, 260 140, 440 20, 340 180, 520 160, 280 220, 460 340, 300 300, 260 400), (260 320, 240 260, 220 220, 160 180, 220 160, 200 100, 260 160, 300 140, 320 180, 260 200, 260 320))"). + Test(t) +} + +func TestBuffer12(t *testing.T) { + operationBuffer_NewBufferValidator( + -17, + "POLYGON ((260 320, 240 260, 220 220, 160 180, 220 160, 260 160, 260 200, 260 320))"). + Test(t) +} + +func TestBuffer13(t *testing.T) { + operationBuffer_NewBufferValidator( + -17, + "POLYGON ((260 320, 240 260, 220 220, 260 160, 260 320))"). + Test(t) +} + +func TestBuffer14(t *testing.T) { + operationBuffer_NewBufferValidator( + -14, + "POLYGON ((260 320, 240 260, 220 220, 260 160, 260 320))"). + Test(t) +} + +func TestBuffer15(t *testing.T) { + operationBuffer_NewBufferValidator( + 26, + "LINESTRING (260 160, 260 200, 260 320, 240 260, 220 220)"). + Test(t) +} + +func TestBuffer16(t *testing.T) { + operationBuffer_NewBufferValidator( + -7, + "POLYGON ((260 400, 220 300, 80 300, 180 220, 40 200, 180 160, 60 20, 200 80, 280 20, 260 140, 440 20, 340 180, 520 160, 280 220, 460 340, 300 300, 260 400), (260 320, 240 260, 220 220, 160 180, 220 160, 200 100, 260 160, 300 140, 320 180, 260 200, 260 320))"). + Test(t) +} + +func TestBuffer17(t *testing.T) { + operationBuffer_NewBufferValidator( + -183, + "POLYGON ((32 136, 27 163, 30 236, 34 252, 49 291, 72 326, 83 339, 116 369, 155 391, 176 400, 219 414, 264 417, 279 416, 339 401, 353 395, 380 381, 394 372, 441 328, 458 303, 463 294, 480 251, 486 205, 486 183, 473 115, 469 105, 460 85, 454 74, 423 33, 382 2, 373 -3, 336 -19, 319 -24, 275 -31, 252 -32, 203 -27, 190 -24, 149 -10, 139 -5, 84 37, 76 46, 52 81, 36 121, 32 136))"). + Test(t) +} + +func TestBuffer18(t *testing.T) { + operationBuffer_NewBufferValidator( + 20, + "POLYGON((-4 225, -17 221, -16 223, -15 224, -13 227, -4 225))"). + Test(t) +} + +func TestBuffer19(t *testing.T) { + operationBuffer_NewBufferValidator( + 21, + "POLYGON ((184 369, 181 368, 180 368, 179 367, 176 366, 185 357, 184 369 ))"). + Test(t) +} + +func TestBuffer20(t *testing.T) { + operationBuffer_NewBufferValidator( + 1000, + "POLYGON ((13841 1031, 13851 903, 13853 885, 13853 875, 13856 862, 13859 831, 13670 900, 13841 1031))"). + Test(t) +} + +func TestBuffer21(t *testing.T) { + operationBuffer_NewBufferValidator( + 18, + "POLYGON ((164 84, 185 91, 190 75, 187 76, 182 77, 179 79, 178 79, 174 81, 173 81, 172 82, 169 83, 164 84 ))"). + Test(t) +} + +func TestBuffer22(t *testing.T) { + operationBuffer_NewBufferValidator( + 15, + "POLYGON ((224 271, 225 261, 214 258, 210 266, 212 267, 214 267, 217 268, 218 268, 219 268, 221 269, 222 270, 224 271 ))"). + Test(t) +} + +func TestBuffer23(t *testing.T) { + operationBuffer_NewBufferValidator( + 25, + "POLYGON ((484 76, 474 79, 492 122, 502 119, 501 117, 500 112, 499 111, 498 107, 497 104, 496 103, 494 98, 493 96, 491 92, 490 90, 489 86, 487 81, 486 79, 485 77, 484 76 ))"). + Test(t) +} + +func TestBuffer24(t *testing.T) { + operationBuffer_NewBufferValidator( + 160, + "POLYGON ((20 60, 20 20, 240 20, 40 21, 240 22, 40 22, 240 23, 240 60, 20 60))"). + Test(t) +} + +func TestBuffer25(t *testing.T) { + operationBuffer_NewBufferValidator( + -3, + "POLYGON ((233 195, 232 195, 231 194, 222 188, 226 187, 227 187, 229 187, 230 186, 232 186, 234 185, 236 184, 237 183, 238 182, 237 184, 236 185, 236 186, 235 187, 235 188, 234 189, 234 191, 234 192, 233 193, 233 195 ))"). + Test(t) +} + +func TestBuffer26(t *testing.T) { + operationBuffer_NewBufferValidator( + 6, + "LINESTRING (233 195, 232 195, 231 194, 222 188, 226 187, 227 187, 229 187, 230 186, 232 186, 234 185, 236 184, 237 183, 238 182, 237 184, 236 185, 236 186, 235 187, 235 188, 234 189, 234 191, 234 192, 233 193, 233 195 )"). + Test(t) +} + +func TestBuffer27(t *testing.T) { + operationBuffer_NewBufferValidator( + -30, + "POLYGON ((2330 1950, 2320 1950, 2310 1940, 2220 1880, 2260 1870, 2270 1870, 2290 1870, 2300 1860, 2320 1860, 2340 1850, 2360 1840, 2370 1830, 2380 1820, 2370 1840, 2360 1850, 2360 1860, 2350 1870, 2350 1880, 2340 1890, 2340 1910, 2340 1920, 2330 1930, 2330 1950 ))"). + Test(t) +} + +func TestBuffer28(t *testing.T) { + operationBuffer_NewBufferValidator( + 30, + "LINESTRING (2330 1950, 2320 1950, 2310 1940, 2220 1880, 2260 1870, 2270 1870, 2290 1870, 2300 1860, 2320 1860, 2340 1850, 2360 1840, 2370 1830, 2380 1820, 2370 1840, 2360 1850, 2360 1860, 2350 1870, 2350 1880, 2340 1890, 2340 1910, 2340 1920, 2330 1930, 2330 1950 )"). + Test(t) +} + +func TestBuffer29(t *testing.T) { + operationBuffer_NewBufferValidator( + 26, + "POLYGON ((440 -93, 440 -67, 475 -67, 471 -71, 469 -72, 468 -73, 467 -74, 463 -78, 459 -81, 458 -82, 454 -84, 453 -85, 452 -86, 450 -86, 449 -87, 448 -88, 444 -90, 443 -91, 441 -92, 440 -93 ))"). + Test(t) +} + +func TestBuffer30(t *testing.T) { + operationBuffer_NewBufferValidator( + 260, + "POLYGON ((4400 -930, 4400 -670, 4750 -670, 4710 -710, 4690 -720, 4680 -730, 4670 -740, 4630 -780, 4590 -810, 4580 -820, 4540 -840, 4530 -850, 4520 -860, 4500 -860, 4490 -870, 4480 -880, 4440 -900, 4430 -910, 4410 -920, 4400 -930 ))"). + Test(t) +} + +func TestBuffer31(t *testing.T) { + operationBuffer_NewBufferValidator( + 0.1, + "POLYGON ((635074.6769928858 6184832.427381967, 635075.6723193424 6184799.950949265, 634717.5983159657 6184655.107092909, 634701.0176852546 6184648.498845058, 634697.7188197445 6184647.20632975, 634694.416887708 6184645.922033237, 634691.1138635761 6184644.642692243, 634687.8077729489 6184643.371570057, 634684.498667351 6184642.107006015, 634681.1875340013 6184640.847368483, 634677.8742698929 6184639.595978798, 634674.5570551592 6184638.351118257, 634671.2386969016 6184637.112873929, 634667.9173237421 6184635.881187774, 634664.5938713895 6184634.656088823, 634661.2674041622 6184633.437548058, 634657.9388577675 6184632.2255945075, 634654.6082322216 6184631.02022817, 634651.2745403448 6184629.823080709, 634647.9388208436 6184628.630859804, 634644.6000865338 6184627.4451971175, 634641.2592216335 6184626.267782336, 634637.9163291481 6184625.095294129, 634634.5713061031 6184623.931053837, 634631.2232683088 6184622.773371783, 634636.1918816608 6184608.365992378, 634633.2495506873 6184607.353869728, 634630.3051410569 6184606.348333739, 634627.3587557608 6184605.346063082, 634624.4102918282 6184604.3503790945, 634621.4607364619 6184603.359650123, 634618.5091539674 6184602.37384716, 634615.5564800596 6184601.392999219, 634612.6017790422 6184600.417077295, 634609.6450509242 6184599.446081388, 634606.6862442375 6184598.481672177, 634603.7263976521 6184597.52055733, 634600.7654082242 6184596.566058185, 634597.80145603 6184595.61645607, 634594.8364124894 6184594.671808995, 634591.8702261405 6184593.733777636, 634588.9020642313 6184592.799011653, 634585.9318238292 6184591.870832384, 634582.960543591 6184590.945947501, 634579.9871848791 6184590.027649342, 634577.0127348808 6184589.11430624, 634574.0362578988 6184588.205889201, 634571.0586381858 6184587.304087893, 634568.0790429743 6184586.405551985, 634565.0983050519 6184585.513631817, 634562.115540186 6184584.626637723, 634559.1316840936 6184583.744598703, 634556.1458010782 6184582.867485766, 634553.1587753976 6184581.996988578, 634550.1697742692 6184581.12975681, 634547.1796305005 6184580.269140797, 634544.1874598458 6184579.41345088, 634541.194198027 6184578.562716054, 634538.1998450506 6184577.716936321, 634535.2034137691 6184576.877743362, 634532.2059428038 6184576.041844833, 634529.2063935531 6184575.212533085, 634526.2057531879 6184574.388176442, 634523.2040217179 6184573.568774906, 634520.2002119992 6184572.755960159, 634517.1953626422 6184571.946439856, 634514.1893707667 6184571.143535337, 634510.267712847 6184585.871039091, 634281.9449709259 6184525.076957544, 633860.4859191478 6184412.861324424, 633664.3557212166 6184360.639468017, 633645.5884675509 6184355.641948889, 633486.222 6184313.208, 633485.7474265156 6184328.852301474, 633485.2749953512 6184344.496113185, 633650.4562371405 6184388.478170839, 633669.5206846121 6184393.553017912, 633852.6461183216 6184442.312440121, 634280.9949861752 6184556.364455, 634502.4254528129 6184615.324425217, 634505.716566367 6184616.204307566, 634509.0065372197 6184617.090806118, 634512.2953653594 6184617.983920872, 634515.5812308139 6184618.88193318, 634518.8659020835 6184619.788222348, 634522.1484948951 6184620.7010987215, 634525.4299963829 6184621.61893061, 634528.7093679372 6184622.545010364, 634531.9867124417 6184623.476016638, 634535.2619784358 6184624.413610098, 634538.5360501477 6184625.3594804, 634541.807159074 6184626.310248219, 634545.0771251463 6184627.267632202, 634548.3459483465 6184628.231632348, 634551.611808724 6184629.200529998, 634554.8764747474 6184630.177704472, 634558.138126462 6184631.161437107, 634561.3986867118 6184632.150125222, 634564.6571168633 6184633.147061154, 634567.9135198268 6184634.14892356, 634571.1678441261 6184635.157373106, 634574.421025444 6184636.172438785, 634577.6711923747 6184637.194062597, 634580.9192805979 6184638.222273537, 634584.1662257991 6184639.257100599, 634587.410208043 6184640.296825108, 634590.6529957657 6184641.3448264, 634593.8928205046 6184642.397725132, 634597.131502168 6184643.457239971, 634600.3671178726 6184644.524973575, 634603.6016934284 6184645.596001944, 634605.6958877691 6184646.2958191885, 634606.946276627 6184646.713661825, 634608.6177147877 6184647.275847967, 634610.2887808911 6184647.8379089665, 634613.6292576884 6184648.967082693, 634616.9666683461 6184650.104475331, 634620.3029357173 6184651.248484221, 634623.6361369188 6184652.4007120095, 634626.9663749042 6184653.557837355, 634630.2954695637 6184654.721578941, 634633.6214980055 6184655.893539405, 634636.9454988878 6184657.070426424, 634640.2664335228 6184658.255532312, 634643.5852890809 6184659.44722541, 634646.9020655481 6184660.6455057105, 634650.2167629072 6184661.850373212, 634653.5284454564 6184663.061798892, 634656.8370616816 6184664.2814434115, 634660.1436502574 6184665.506014449, 634663.4481081718 6184666.7388333315, 634666.7496027217 6184667.976549702, 634670.0489665787 6184669.222513908, 634673.3453155413 6184670.475036258, 634676.6396367943 6184671.732485097, 634679.9308916207 6184672.9981527375, 634683.2200672035 6184674.270407524, 634686.5062278372 6184675.54922043, 634689.789322 6184676.8362521175, 634693.0703883899 6184678.128210268, 634696.3493754807 6184679.426755545, 634699.6253475712 6184680.731858918, 634702.8982531315 6184682.04518105, 634706.1681951834 6184683.363400595, 635074.6769928858 6184832.427381967))"). + Test(t) +} + +func TestBuffer32(t *testing.T) { + operationBuffer_NewBufferValidator( + 30, + "MULTILINESTRING ((80 285, 85.5939933259177 234.65406006674084 ), (85.5939933259177 234.65406006674084, 98 123, 294 92, 344.3694502052736 126.0884157954882 ), (344.3694502052736 126.0884157954882, 393 159 ), (51 235, 85.5939933259177 234.65406006674084 ), (85.5939933259177 234.65406006674084, 251 233, 344.3694502052736 126.0884157954882 ), (344.3694502052736 126.0884157954882, 382 83 ))"). + Test(t) +} + +// test33 is commented out in Java due to invalid geometry + +func TestBuffer34(t *testing.T) { + operationBuffer_NewBufferValidator( + 1, + "GEOMETRYCOLLECTION (POLYGON ((0 10, 10 0, 10 10, 0 10), (4 8, 8 4, 8 8, 4 8)), LINESTRING (6 6, 20 20))"). + Test(t) +} + +func TestBuffer35(t *testing.T) { + operationBuffer_NewBufferValidator( + 20, + "GEOMETRYCOLLECTION (POINT (100 100), POLYGON ((400 260, 280 380, 240 220, 120 300, 120 100, 260 40, 200 160, 400 260)), LINESTRING (260 400, 220 280, 120 400, 20 280, 160 160, 60 40, 160 20, 360 140))"). + Test(t) +} + +func TestBuffer36(t *testing.T) { + operationBuffer_NewBufferValidator( + 20, + "GEOMETRYCOLLECTION (POINT (100 100), POLYGON ((400 260, 120 300, 120 100, 400 260)), LINESTRING (20 280, 160 160, 60 40))"). + Test(t) +} + +func TestBuffer37(t *testing.T) { + operationBuffer_NewBufferValidator( + 300, + "POLYGON ((-140 700, 880 1120, 1280 -120, 300 -600, -480 -480, -140 700), (0 360, 780 500, 240 -220, 0 360))"). + Test(t) +} + +func TestBuffer38(t *testing.T) { + operationBuffer_NewBufferValidator( + 300, + "POLYGON ((-140 700, 880 1120, 1280 -120, 300 -600, -480 -480, -140 700), (0 360, 240 -220, 780 500, 0 360))"). + Test(t) +} + +func TestBuffer39(t *testing.T) { + operationBuffer_NewBufferValidator( + 30, + "MULTIPOLYGON (((0 400, 440 400, 440 0, 0 0, 0 400),(380 360, 160 120, 260 80, 380 360)), ((360 320, 200 120, 240 100, 360 320)))"). + Test(t) +} + +func TestBufferFloatingPrecision1(t *testing.T) { + operationBuffer_NewBufferValidator( + 100, + "LINESTRING (331771 5530174, 331776 5530175, 331782 5530177, 331787 5530177, 331791 5530178, 331796 5530178, 331800 5530178, 331805 5530177, 331811 5530176, 331817 5530175, 331823 5530173, 331828 5530171, 331832 5530169, 331835 5530167, 331839 5530163, 331843 5530160, 331846 5530157, 331849 5530154, 331853 5530150, 331855 5530145, 331857 5530141)"). + Test(t) +} + +func TestBufferFloatingPrecision2(t *testing.T) { + operationBuffer_NewBufferValidator( + 100, + "LINESTRING (317091 5557033, 317079 5557042, 317067 5557053, 317055 5557065, 317045 5557078, 317037 5557091, 317029 5557098, 317016 5557108, 317002 5557118, 316990 5557129, 316986 5557131, 316978 5557133, 316968 5557133, 316965 5557131, 316954 5557120, 316952 5557115, 316951 5557108, 316949 5557092, 316948 5557076, 316946 5557063, 316944 5557057, 316937 5557042, 316924 5557029, 316911 5557019, 316896 5557009, 316881 5557001, 316865 5556997, 316849 5556992, 316834 5556988, 316817 5556985, 316801 5556983, 316766 5556983, 316751 5556982, 316733 5556980, 316716 5556976, 316702 5556968, 316699 5556964, 316691 5556951, 316680 5556934, 316670 5556922, 316660 5556911, 316642 5556885, 316637 5556881)"). + Test(t) +} + +func TestBufferFloatingPrecision3(t *testing.T) { + operationBuffer_NewBufferValidator( + 100, + "LINESTRING (300181 5547255, 300183 5547252, 300203 5547253, 300209 5547261, 300237 5547277, 300262 5547286, 300280 5547292, 300288 5547297, 300293 5547303, 300297 5547311, 300299 5547319, 300299 5547334, 300306 5547349, 300320 5547367)"). + Test(t) +} + +func TestBufferFloatingPrecision4(t *testing.T) { + operationBuffer_NewBufferValidator( + 100, + "LINESTRING (301546 5537924, 301547 5537922, 301551 5537919, 301555 5537919, 301559 5537918, 301565 5537918, 301569 5537917, 301573 5537915, 301580 5537912, 301583 5537909, 301587 5537906, 301594 5537900, 301598 5537897, 301601 5537893, 301605 5537889, 301608 5537885, 301609 5537880, 301612 5537876, 301614 5537873, 301616 5537869, 301620 5537865, 301624 5537860, 301632 5537852, 301640 5537842, 301643 5537836, 301644 5537829, 301644 5537822, 301646 5537815, 301647 5537808, 301650 5537802, 301650 5537796, 301651 5537791, 301653 5537786, 301654 5537780, 301656 5537773, 301658 5537767, 301662 5537761)"). + Test(t) +} + +func TestBufferFloatingPrecision5(t *testing.T) { + operationBuffer_NewBufferValidator( + 100, + "LINESTRING (334797 5560136, 334781 5560129, 334777 5560128, 334762 5560122, 334760 5560121, 334752 5560116, 334745 5560109, 334742 5560103, 334741 5560098, 334736 5560087, 334731 5560082, 334726 5560081, 334708 5560072, 334691 5560063, 334674 5560052, 334660 5560048, 334655 5560048, 334633 5560049, 334621 5560046, 334616 5560043, 334596 5560034, 334586 5560025, 334573 5560009, 334562 5559982, 334549 5559943, 334543 5559923, 334538 5559905, 334535 5559887, 334530 5559869, 334536 5559853)"). + Test(t) +} + +func TestBufferFloatingPrecision6(t *testing.T) { + operationBuffer_NewBufferValidator( + 100, + "LINESTRING (316640 5563099, 316639 5563114, 316642 5563132, 316644 5563137, 316650 5563144, 316653 5563147, 316663 5563159, 316665 5563164, 316667 5563172, 316667 5563193, 316668 5563209, 316672 5563214, 316678 5563228, 316679 5563230, 316679 5563236, 316678 5563252, 316676 5563256, 316671 5563270, 316669 5563289, 316667 5563304, 316666 5563308, 316656 5563323, 316646 5563336, 316639 5563347)"). + Test(t) +} + +func TestBufferFloatingPrecision7(t *testing.T) { + operationBuffer_NewBufferValidator( + 100, + "LINESTRING (301178 5534835, 301189 5534837, 301218 5534837, 301229 5534836, 301237 5534836, 301245 5534838, 301268 5534838, 301273 5534837, 301279 5534838, 301286 5534838, 301289 5534839, 301296 5534842, 301302 5534844, 301306 5534846, 301309 5534850, 301313 5534853, 301316 5534856, 301319 5534868, 301320 5534873, 301323 5534877, 301326 5534882, 301334 5534896, 301340 5534902, 301344 5534908, 301348 5534913, 301352 5534919, 301357 5534925, 301363 5534932, 301369 5534937, 301375 5534941, 301382 5534949, 301386 5534955, 301397 5534964, 301402 5534967, 301407 5534972, 301411 5534975, 301414 5534980, 301418 5534986, 301419 5534989, 301422 5534994, 301426 5535000, 301438 5535012, 301444 5535017, 301456 5535030, 301457 5535033)"). + Test(t) +} + +func TestBufferFloatingPrecision8(t *testing.T) { + operationBuffer_NewBufferValidator( + 100, + "LINESTRING (303722 5533544, 303713 5533542, 303706 5533539, 303697 5533537, 303694 5533534, 303677 5533527, 303673 5533525, 303670 5533524, 303669 5533523, 303664 5533519, 303654 5533513, 303647 5533507, 303644 5533506, 303634 5533504, 303633 5533504, 303627 5533502)"). + Test(t) +} + +func TestBufferFloatingPrecision9(t *testing.T) { + operationBuffer_NewBufferValidator( + 100, + "LINESTRING (309969 5563538, 309955 5563542, 309941 5563548, 309913 5563562, 309896 5563569, 309883 5563576, 309868 5563586, 309855 5563594, 309841 5563603, 309830 5563614, 309818 5563624, 309805 5563635, 309791 5563645, 309778 5563654, 309766 5563663, 309752 5563672, 309722 5563692, 309709 5563699, 309681 5563713, 309667 5563721, 309651 5563728, 309631 5563734, 309615 5563739, 309602 5563747, 309589 5563756, 309578 5563766, 309566 5563775, 309554 5563785, 309542 5563796, 309538 5563801, 309535 5563810, 309532 5563828, 309533 5563833, 309540 5563855, 309546 5563868, 309552 5563884, 309556 5563900, 309559 5563916, 309561 5563933, 309561 5563970, 309559 5563988, 309554 5564003, 309550 5564018, 309546 5564032, 309542 5564047, 309538 5564061, 309531 5564074, 309528 5564077, 309518 5564090, 309507 5564101, 309493 5564110, 309492 5564111, 309480 5564119, 309474 5564121, 309458 5564123, 309443 5564125, 309426 5564125, 309408 5564123, 309393 5564123, 309377 5564126, 309373 5564129, 309364 5564139, 309360 5564147, 309359 5564151, 309359 5564155, 309362 5564159)"). + Test(t) +} + +func TestBufferFloatingPrecision10(t *testing.T) { + operationBuffer_NewBufferValidator( + 100, + "LINESTRING (299331 5536963, 299335 5536956, 299335 5536953, 299336 5536949, 299338 5536942, 299345 5536933, 299349 5536927, 299352 5536924, 299358 5536922, 299364 5536919, 299369 5536916, 299375 5536912, 299380 5536908, 299387 5536905, 299391 5536904, 299395 5536902, 299399 5536899, 299402 5536896, 299405 5536892, 299415 5536886, 299425 5536882, 299435 5536880, 299449 5536874, 299455 5536869, 299461 5536865, 299468 5536862, 299474 5536859, 299480 5536855, 299491 5536846, 299497 5536842, 299502 5536838, 299508 5536835, 299513 5536834, 299523 5536830, 299527 5536826, 299532 5536824, 299542 5536821, 299548 5536818, 299552 5536814, 299556 5536812, 299566 5536808, 299574 5536804, 299588 5536798, 299594 5536796, 299602 5536792, 299611 5536789, 299625 5536784, 299632 5536782, 299648 5536780, 299657 5536785, 299672 5536798, 299678 5536801, 299688 5536801, 299696 5536802, 299711 5536807, 299729 5536807, 299739 5536808, 299748 5536807, 299766 5536810, 299777 5536813, 299801 5536816, 299825 5536811, 299850 5536810, 299867 5536809, 299875 5536811, 299887 5536811, 299895 5536815, 299913 5536822)"). + Test(t) +} + +func TestBufferFloatingPrecision11(t *testing.T) { + operationBuffer_NewBufferValidator( + 100, + "LINESTRING (325089 5534737, 325089 5534733, 325093 5534723, 325099 5534718, 325118 5534712, 325129 5534709, 325147 5534709, 325164 5534732, 325164 5534740, 325162 5534746, 325159 5534749, 325144 5534760, 325143 5534763, 325145 5534782, 325162 5534800, 325184 5534812, 325187 5534815, 325190 5534831, 325196 5534852, 325205 5534867, 325214 5534876, 325219 5534878, 325239 5534880, 325251 5534890, 325259 5534892, 325282 5534887, 325294 5534883, 325314 5534864)"). + Test(t) +} + +func TestBufferFloatingPrecision12(t *testing.T) { + operationBuffer_NewBufferValidator( + 100, + "LINESTRING (307468 5557827, 307467 5557842, 307466 5557854, 307463 5557874, 307459 5557889, 307454 5557902, 307447 5557922, 307440 5557944, 307428 5557965, 307417 5557986, 307411 5557996, 307404 5558020, 307398 5558031, 307390 5558056, 307387 5558066, 307384 5558084, 307383 5558093, 307385 5558102, 307389 5558110, 307394 5558116, 307404 5558121, 307421 5558122, 307443 5558121, 307464 5558127, 307486 5558133, 307502 5558142, 307508 5558150)"). + Test(t) +} + +func TestBufferFloatingPrecision13(t *testing.T) { + operationBuffer_NewBufferValidator( + 100, + "LINESTRING (301395 5535820, 301412 5535803, 301416 5535798, 301420 5535786, 301423 5535782, 301427 5535778, 301432 5535771, 301437 5535768, 301444 5535763, 301447 5535760, 301452 5535757, 301459 5535754, 301462 5535753, 301468 5535750, 301473 5535747, 301481 5535742, 301487 5535739, 301494 5535734, 301499 5535730, 301508 5535725, 301514 5535721, 301521 5535716, 301527 5535714, 301533 5535711, 301538 5535707, 301542 5535703, 301554 5535693, 301559 5535688, 301563 5535681)"). + Test(t) +} + +func TestBufferFloatingPrecision14(t *testing.T) { + operationBuffer_NewBufferValidator( + 100, + "LINESTRING (331384 5535032, 331397 5535031, 331415 5535025, 331426 5535022, 331436 5535016, 331451 5534999, 331460 5534994, 331468 5534993, 331474 5534996, 331479 5535001, 331482 5535015, 331486 5535019, 331493 5535018, 331504 5535011, 331508 5535011, 331519 5535022, 331526 5535023, 331542 5535019, 331547 5535016, 331549 5534994, 331558 5534975, 331562 5534968, 331565 5534966, 331571 5534967, 331575 5534970, 331576 5534978, 331575 5534987, 331568 5535005, 331564 5535022, 331565 5535030, 331570 5535038, 331578 5535044, 331582 5535046, 331592 5535046, 331602 5535043, 331613 5535038, 331631 5535027, 331640 5535021, 331645 5535020, 331654 5535020, 331662 5535022, 331669 5535028, 331676 5535040, 331674 5535065, 331668 5535089, 331659 5535108, 331655 5535118, 331655 5535123, 331662 5535143, 331662 5535147, 331651 5535167, 331646 5535181, 331642 5535198, 331640 5535210, 331641 5535234, 331642 5535245, 331645 5535255, 331648 5535276, 331651 5535287, 331663 5535309, 331665 5535316, 331666 5535324, 331671 5535337, 331677 5535344)"). + Test(t) +} + +func TestBufferQuickPolygonUnion(t *testing.T) { + a := bufferTestRead("POLYGON((0 0, 100 0, 100 100, 0 100, 0 0))") + b := bufferTestRead("POLYGON((50 50, 150 50, 150 150, 50 150, 50 50))") + polygons := []*Geom_Geometry{a, b} + polygonCollection := Geom_NewGeometryFactoryDefault().CreateGeometryCollectionFromGeometries(polygons) + union := polygonCollection.Geom_Geometry.Buffer(0) + junit.AssertEquals(t, "POLYGON ((0 0, 0 100, 50 100, 50 150, 150 150, 150 50, 100 50, 100 0, 0 0))", union.String()) +} + +func TestBufferBowtiePolygonLargestAreaRetained(t *testing.T) { + a := bufferTestRead("POLYGON ((10 10, 50 10, 25 35, 35 35, 10 10))") + result := a.Buffer(0) + expected := bufferTestRead("POLYGON ((10 10, 30 30, 50 10, 10 10))") + bufferTestCheckEqual(t, expected, result) +} + +func TestBufferBowtiePolygonHoleLargestAreaRetained(t *testing.T) { + a := bufferTestRead("POLYGON ((0 40, 60 40, 60 0, 0 0, 0 40), (10 10, 50 10, 25 35, 35 35, 10 10))") + result := a.Buffer(0) + expected := bufferTestRead("POLYGON ((0 40, 60 40, 60 0, 0 0, 0 40), (10 10, 50 10, 30 30, 10 10))") + bufferTestCheckEqual(t, expected, result) +} + +func TestBufferPolygon4NegBufferEmpty(t *testing.T) { + wkt := "POLYGON ((666360.09 429614.71, 666344.4 429597.12, 666358.47 429584.52, 666374.5 429602.33, 666360.09 429614.71))" + checkBufferTestEmpty(t, wkt, -9, false) + checkBufferTestEmpty(t, wkt, -10, true) + checkBufferTestEmpty(t, wkt, -15, true) + checkBufferTestEmpty(t, wkt, -18, true) +} + +func TestBufferPolygon5NegBufferEmpty(t *testing.T) { + wkt := "POLYGON ((6 20, 16 20, 21 9, 9 0, 0 10, 6 20))" + checkBufferTestEmpty(t, wkt, -8, false) + checkBufferTestEmpty(t, wkt, -8.6, true) + checkBufferTestEmpty(t, wkt, -9.6, true) + checkBufferTestEmpty(t, wkt, -11, true) +} + +func TestBufferPolygonHole5BufferNoHole(t *testing.T) { + wkt := "POLYGON ((-6 26, 29 26, 29 -5, -6 -5, -6 26), (6 20, 16 20, 21 9, 9 0, 0 10, 6 20))" + checkBufferTestHasHole(t, wkt, 8, true) + checkBufferTestHasHole(t, wkt, 8.6, false) + checkBufferTestHasHole(t, wkt, 9.6, false) + checkBufferTestHasHole(t, wkt, 11, false) +} + +func TestBufferMultiPolygonElementRemoved(t *testing.T) { + wkt := "MULTIPOLYGON (((30 18, 14 0, 0 13, 16 30, 30 18)), ((180 210, 60 50, 154 6, 270 40, 290 130, 250 190, 180 210)))" + checkBufferTestNumGeometries(t, wkt, -9, 2) + checkBufferTestNumGeometries(t, wkt, -10, 1) + checkBufferTestNumGeometries(t, wkt, -15, 1) + checkBufferTestNumGeometries(t, wkt, -18, 1) +} + +func TestBufferLineClosedNoHole(t *testing.T) { + wkt := "LINESTRING (-20 0, 0 20, 20 0, 0 -20, -20 0)" + checkBufferTestHasHole(t, wkt, 70, false) +} + +func TestBufferSmallPolygonNegativeBuffer_1(t *testing.T) { + wkt := "MULTIPOLYGON (((833454.7163917861 6312507.405413097, 833455.3726665961 6312510.208920742, 833456.301153878 6312514.207390314, 833492.2432584754 6312537.770332065, 833493.0901320165 6312536.098774815, 833502.6580673696 6312517.561360772, 833503.9404352929 6312515.0542803425, 833454.7163917861 6312507.405413097)))" + checkBufferTestWithExpected(t, wkt, -3.8, + "POLYGON ((833459.9671068499 6312512.066918822, 833490.7876785189 6312532.272283619, 833498.1465258132 6312517.999574621, 833459.9671068499 6312512.066918822))") + checkBufferTestWithExpected(t, wkt, -7, + "POLYGON ((833474.0912127121 6312517.50004999, 833489.5713439264 6312527.648521655, 833493.2674441456 6312520.479822435, 833474.0912127121 6312517.50004999))") +} + +func TestBufferSmallPolygonNegativeBuffer_2(t *testing.T) { + wkt := "POLYGON ((182719.04521570954238996 224897.14115349075291306, 182807.02887436276068911 224880.64421749324537814, 182808.47314301913138479 224877.25002362736267969, 182718.38701137207681313 224740.00115247094072402, 182711.82697281913715415 224742.08599378637154587, 182717.1393717635946814 224895.61432328051887453, 182719.04521570954238996 224897.14115349075291306))" + checkBufferTestWithExpected(t, wkt, -5, + "POLYGON ((182717 224746.99999999997, 182722.00000000003 224891.5, 182801.99999999997 224876.49999999997, 182717 224746.99999999997))") + checkBufferTestWithExpected(t, wkt, -30, + "POLYGON ((182745.07127364463 224835.32741176756, 182745.97926048582 224861.56823147752, 182760.5070499446 224858.844270954, 182745.07127364463 224835.32741176756))") +} + +func TestBufferDefaultBuffer(t *testing.T) { + g := bufferTestRead("POINT (0 0)").Buffer(1.0) + b := g.GetBoundary() + coords := b.GetCoordinates() + junit.AssertEquals(t, 33, len(coords)) + junit.AssertEquals(t, 1.0, coords[0].GetX()) + junit.AssertEquals(t, 0.0, coords[0].GetY()) + junit.AssertEquals(t, 0.0, coords[8].GetX()) + junit.AssertEquals(t, -1.0, coords[8].GetY()) + junit.AssertEquals(t, -1.0, coords[16].GetX()) + junit.AssertEquals(t, 0.0, coords[16].GetY()) + junit.AssertEquals(t, 0.0, coords[24].GetX()) + junit.AssertEquals(t, 1.0, coords[24].GetY()) +} + +func TestBufferRingStartSimplified(t *testing.T) { + checkBufferTestWithParams(t, "POLYGON ((200 300, 200 299.9999, 350 100, 30 40, 200 300))", + 20, bufParamRoundMitre(5), + "POLYGON ((198.88 334.83, 385.3 86.27, -12.4 11.7, 198.88 334.83))") +} + +func TestBufferRingEndSimplified(t *testing.T) { + checkBufferTestWithParams(t, "POLYGON ((200 300, 350 100, 30 40, 200 299.9999, 200 300))", + 20, bufParamRoundMitre(5), + "POLYGON ((198.88 334.83, 385.3 86.27, -12.4 11.7, 198.88 334.83))") +} + +//=================================================== + +func bufParamRoundMitre(mitreLimit float64) *OperationBuffer_BufferParameters { + param := OperationBuffer_NewBufferParameters() + param.SetJoinStyle(OperationBuffer_BufferParameters_JOIN_MITRE) + param.SetMitreLimit(mitreLimit) + return param +} + +func checkBufferTestWithParams(t *testing.T, wkt string, dist float64, param *OperationBuffer_BufferParameters, wktExpected string) { + t.Helper() + geom := bufferTestRead(wkt) + result := OperationBuffer_BufferOp_BufferOpWithParams(geom, dist, param) + expected := bufferTestRead(wktExpected) + bufferTestCheckEqualWithTolerance(t, expected, result, 0.01) +} + +func checkBufferTestWithExpected(t *testing.T, wkt string, dist float64, wktExpected string) { + t.Helper() + geom := bufferTestRead(wkt) + result := OperationBuffer_BufferOp_BufferOp(geom, dist) + expected := bufferTestRead(wktExpected) + bufferTestCheckEqualWithTolerance(t, expected, result, 0.01) +} + +func checkBufferTestEmpty(t *testing.T, wkt string, dist float64, isEmptyExpected bool) { + t.Helper() + a := bufferTestRead(wkt) + result := a.Buffer(dist) + junit.AssertTrue(t, isEmptyExpected == result.IsEmpty()) +} + +func checkBufferTestHasHole(t *testing.T, wkt string, dist float64, isHoleExpected bool) { + t.Helper() + a := bufferTestRead(wkt) + result := a.Buffer(dist) + junit.AssertTrue(t, isHoleExpected == bufferTestHasHole(result)) +} + +func checkBufferTestNumGeometries(t *testing.T, wkt string, dist float64, numExpected int) { + t.Helper() + a := bufferTestRead(wkt) + result := a.Buffer(dist) + junit.AssertTrue(t, numExpected == result.GetNumGeometries()) +} + +func bufferTestHasHole(geom *Geom_Geometry) bool { + for i := 0; i < geom.GetNumGeometries(); i++ { + poly := java.Cast[*Geom_Polygon](geom.GetGeometryN(i)) + if poly.GetNumInteriorRing() > 0 { + return true + } + } + return false +} + +func bufferTestRead(wkt string) *Geom_Geometry { + reader := Io_NewWKTReader() + geom, err := reader.Read(wkt) + if err != nil { + panic("failed to read WKT: " + err.Error()) + } + return geom +} + +func bufferTestCheckEqual(t *testing.T, expected, actual *Geom_Geometry) { + t.Helper() + bufferTestCheckEqualWithTolerance(t, expected, actual, 0) +} + +func bufferTestCheckEqualWithTolerance(t *testing.T, expected, actual *Geom_Geometry, tolerance float64) { + t.Helper() + actualNorm := actual.Norm() + expectedNorm := expected.Norm() + equal := actualNorm.EqualsExactWithTolerance(expectedNorm, tolerance) + if !equal { + t.Errorf("geometries not equal\nexpected: %v\nactual: %v", expectedNorm, actualNorm) + } +} diff --git a/internal/jtsport/jts/operation_buffer_buffer_validator_test.go b/internal/jtsport/jts/operation_buffer_buffer_validator_test.go new file mode 100644 index 00000000..6ef133d9 --- /dev/null +++ b/internal/jtsport/jts/operation_buffer_buffer_validator_test.go @@ -0,0 +1,263 @@ +package jts + +import ( + "fmt" + "testing" + + "github.com/peterstace/simplefeatures/internal/jtsport/java" +) + +// BufferValidator is a test helper for validating buffer operations. + +type operationBuffer_BufferValidator_Test interface { + GetName() string + Test(t *testing.T, bv *operationBuffer_BufferValidator) error + GetPriority() int +} + +type operationBuffer_BufferValidator_baseTest struct { + name string + priority int +} + +func (bt *operationBuffer_BufferValidator_baseTest) GetName() string { + return bt.name +} + +func (bt *operationBuffer_BufferValidator_baseTest) GetPriority() int { + return bt.priority +} + +type operationBuffer_BufferValidator struct { + original *Geom_Geometry + bufferDistance float64 + nameToTestMap map[string]operationBuffer_BufferValidator_Test + buffer *Geom_Geometry + wkt string + geomFact *Geom_GeometryFactory + wktWriter *Io_WKTWriter + wktReader *Io_WKTReader + t *testing.T +} + +const operationBuffer_BufferValidator_QUADRANT_SEGMENTS_1 = 100 +const operationBuffer_BufferValidator_QUADRANT_SEGMENTS_2 = 50 + +func operationBuffer_NewBufferValidator(bufferDistance float64, wkt string) *operationBuffer_BufferValidator { + return operationBuffer_NewBufferValidatorWithContainsTest(bufferDistance, wkt, true) +} + +func operationBuffer_NewBufferValidatorWithContainsTest(bufferDistance float64, wkt string, addContainsTest bool) *operationBuffer_BufferValidator { + bv := &operationBuffer_BufferValidator{ + bufferDistance: bufferDistance, + wkt: wkt, + nameToTestMap: make(map[string]operationBuffer_BufferValidator_Test), + geomFact: Geom_NewGeometryFactoryDefault(), + wktWriter: Io_NewWKTWriter(), + } + // SRID = 888 is to test that SRID is preserved in computed buffers + bv.SetFactory(Geom_NewPrecisionModel(), 888) + if addContainsTest { + bv.addContainsTest() + } + //bv.addBufferResultValidatorTest() + return bv +} + +func (bv *operationBuffer_BufferValidator) Test(t *testing.T) { + bv.t = t + for _, test := range bv.nameToTestMap { + err := test.Test(t, bv) + if err != nil { + t.Errorf("%s", bv.supplement(err.Error())) + } + } +} + +func (bv *operationBuffer_BufferValidator) supplement(message string) string { + newMessage := "\n" + message + "\n" + original := bv.getOriginal() + newMessage += "Original: " + bv.wktWriter.WriteFormatted(original) + "\n" + newMessage += fmt.Sprintf("Buffer Distance: %v\n", bv.bufferDistance) + buffer := bv.getBuffer() + newMessage += "Buffer: " + bv.wktWriter.WriteFormatted(buffer) + "\n" + return newMessage[:len(newMessage)-1] +} + +func (bv *operationBuffer_BufferValidator) addTest(test operationBuffer_BufferValidator_Test) *operationBuffer_BufferValidator { + bv.nameToTestMap[test.GetName()] = test + return bv +} + +func (bv *operationBuffer_BufferValidator) SetExpectedArea(expectedArea float64) *operationBuffer_BufferValidator { + return bv.addTest(&operationBuffer_BufferValidator_areaTest{ + operationBuffer_BufferValidator_baseTest: operationBuffer_BufferValidator_baseTest{name: "Area Test", priority: 2}, + expectedArea: expectedArea, + }) +} + +type operationBuffer_BufferValidator_areaTest struct { + operationBuffer_BufferValidator_baseTest + expectedArea float64 +} + +func (at *operationBuffer_BufferValidator_areaTest) Test(t *testing.T, bv *operationBuffer_BufferValidator) error { + tolerance := bv.getBuffer().GetArea() - bv.getOriginal().BufferWithQuadrantSegments( + bv.bufferDistance, + operationBuffer_BufferValidator_QUADRANT_SEGMENTS_1-operationBuffer_BufferValidator_QUADRANT_SEGMENTS_2, + ).GetArea() + if tolerance < 0 { + tolerance = -tolerance + } + actual := bv.getBuffer().GetArea() + if actual < at.expectedArea-tolerance || actual > at.expectedArea+tolerance { + return fmt.Errorf("%s: expected %v, got %v (tolerance %v)", at.GetName(), at.expectedArea, actual, tolerance) + } + return nil +} + +func (bv *operationBuffer_BufferValidator) SetEmptyBufferExpected(emptyBufferExpected bool) *operationBuffer_BufferValidator { + return bv.addTest(&operationBuffer_BufferValidator_emptyTest{ + operationBuffer_BufferValidator_baseTest: operationBuffer_BufferValidator_baseTest{name: "Empty Buffer Test", priority: 1}, + emptyBufferExpected: emptyBufferExpected, + }) +} + +type operationBuffer_BufferValidator_emptyTest struct { + operationBuffer_BufferValidator_baseTest + emptyBufferExpected bool +} + +func (et *operationBuffer_BufferValidator_emptyTest) Test(t *testing.T, bv *operationBuffer_BufferValidator) error { + buffer := bv.getBuffer() + isEmpty := buffer.IsEmpty() + if isEmpty != et.emptyBufferExpected { + expectedStr := "" + if !et.emptyBufferExpected { + expectedStr = "not " + } + return fmt.Errorf("Expected buffer %sto be empty", expectedStr) + } + return nil +} + +func (bv *operationBuffer_BufferValidator) SetBufferHolesExpected(bufferHolesExpected bool) *operationBuffer_BufferValidator { + return bv.addTest(&operationBuffer_BufferValidator_holesTest{ + operationBuffer_BufferValidator_baseTest: operationBuffer_BufferValidator_baseTest{name: "Buffer Holes Test", priority: 2}, + bufferHolesExpected: bufferHolesExpected, + }) +} + +type operationBuffer_BufferValidator_holesTest struct { + operationBuffer_BufferValidator_baseTest + bufferHolesExpected bool +} + +func (ht *operationBuffer_BufferValidator_holesTest) Test(t *testing.T, bv *operationBuffer_BufferValidator) error { + buffer := bv.getBuffer() + hasHoles := ht.hasHoles(buffer) + if hasHoles != ht.bufferHolesExpected { + expectedStr := "" + if !ht.bufferHolesExpected { + expectedStr = "not " + } + return fmt.Errorf("Expected buffer %sto have holes", expectedStr) + } + return nil +} + +func (ht *operationBuffer_BufferValidator_holesTest) hasHoles(buffer *Geom_Geometry) bool { + if buffer.IsEmpty() { + return false + } + if java.InstanceOf[*Geom_Polygon](buffer) { + return java.Cast[*Geom_Polygon](buffer).GetNumInteriorRing() > 0 + } + multiPolygon := java.Cast[*Geom_MultiPolygon](buffer) + for i := 0; i < multiPolygon.GetNumGeometries(); i++ { + if ht.hasHoles(multiPolygon.GetGeometryN(i)) { + return true + } + } + return false +} + +func (bv *operationBuffer_BufferValidator) getOriginal() *Geom_Geometry { + if bv.original == nil { + geom, err := bv.wktReader.Read(bv.wkt) + if err != nil { + panic(fmt.Sprintf("failed to read WKT: %v", err)) + } + bv.original = geom + } + return bv.original +} + +func (bv *operationBuffer_BufferValidator) SetPrecisionModel(precisionModel *Geom_PrecisionModel) *operationBuffer_BufferValidator { + bv.wktReader = Io_NewWKTReaderWithFactory(Geom_NewGeometryFactoryWithPrecisionModel(precisionModel)) + return bv +} + +func (bv *operationBuffer_BufferValidator) SetFactory(precisionModel *Geom_PrecisionModel, srid int) *operationBuffer_BufferValidator { + bv.wktReader = Io_NewWKTReaderWithFactory(Geom_NewGeometryFactoryWithPrecisionModelAndSRID(precisionModel, srid)) + return bv +} + +func (bv *operationBuffer_BufferValidator) getBuffer() *Geom_Geometry { + if bv.buffer == nil { + bv.buffer = bv.getOriginal().BufferWithQuadrantSegments(bv.bufferDistance, operationBuffer_BufferValidator_QUADRANT_SEGMENTS_1) + _, isGeomCollection := bv.buffer.GetChild().(*Geom_GeometryCollection) + if isGeomCollection && bv.buffer.IsEmpty() { + // #contains doesn't work with GeometryCollections [Jon Aquino 10/29/2003] + geom, err := bv.wktReader.Read("POINT EMPTY") + if err != nil { + Util_Assert_ShouldNeverReachHere() + } + bv.buffer = geom + } + } + return bv.buffer +} + +func (bv *operationBuffer_BufferValidator) addContainsTest() { + bv.addTest(&operationBuffer_BufferValidator_containsTest{ + operationBuffer_BufferValidator_baseTest: operationBuffer_BufferValidator_baseTest{name: "Contains Test", priority: 2}, + }) +} + +type operationBuffer_BufferValidator_containsTest struct { + operationBuffer_BufferValidator_baseTest +} + +func (ct *operationBuffer_BufferValidator_containsTest) Test(t *testing.T, bv *operationBuffer_BufferValidator) error { + original := bv.getOriginal() + // Skip for GeometryCollection + if _, isGeomCollection := original.GetChild().(*Geom_GeometryCollection); isGeomCollection { + return nil + } + if !original.IsValid() { + return fmt.Errorf("original geometry is not valid") + } + buffer := bv.getBuffer() + if bv.bufferDistance > 0 { + if !ct.contains(buffer, original) { + return fmt.Errorf("Expected buffer to contain original") + } + } else { + if !ct.contains(original, buffer) { + return fmt.Errorf("Expected original to contain buffer") + } + } + return nil +} + +func (ct *operationBuffer_BufferValidator_containsTest) contains(a, b *Geom_Geometry) bool { + // JTS doesn't currently handle empty geometries correctly [Jon Aquino 10/29/2003] + if b.IsEmpty() { + return true + } + return a.Contains(b) +} + +// addBufferResultValidatorTest is commented out in Java +// func (bv *operationBuffer_BufferValidator) addBufferResultValidatorTest() { ... } diff --git a/internal/jtsport/jts/operation_buffer_depth_segment_test.go b/internal/jtsport/jts/operation_buffer_depth_segment_test.go new file mode 100644 index 00000000..41d49b4d --- /dev/null +++ b/internal/jtsport/jts/operation_buffer_depth_segment_test.go @@ -0,0 +1,52 @@ +package jts + +import ( + "testing" + + "github.com/peterstace/simplefeatures/internal/jtsport/junit" +) + +func TestDepthSegmentCompareTipToTail(t *testing.T) { + ds0 := depthSeg(0.7, 0.2, 1.4, 0.9) + ds1 := depthSeg(0.7, 0.2, 0.3, 1.1) + checkDepthSegmentCompare(t, ds0, ds1, 1) +} + +func TestDepthSegmentCompare2(t *testing.T) { + ds0 := depthSeg(0.5, 1.0, 0.1, 1.9) + ds1 := depthSeg(1.0, 0.9, 1.9, 1.4) + checkDepthSegmentCompare(t, ds0, ds1, -1) +} + +func TestDepthSegmentCompareVertical(t *testing.T) { + ds0 := depthSeg(1, 1, 1, 2) + ds1 := depthSeg(1, 0, 1, 1) + checkDepthSegmentCompare(t, ds0, ds1, 1) +} + +func TestDepthSegmentCompareOrientBug(t *testing.T) { + ds0 := depthSeg(146.268, -8.42361, 146.263, -8.3875) + ds1 := depthSeg(146.269, -8.42889, 146.268, -8.42361) + checkDepthSegmentCompare(t, ds0, ds1, -1) +} + +func TestDepthSegmentCompareEqual(t *testing.T) { + ds0 := depthSeg(1, 1, 2, 2) + checkDepthSegmentCompare(t, ds0, ds0, 0) +} + +func checkDepthSegmentCompare(t *testing.T, ds0, ds1 *operationBuffer_DepthSegment, expectedComp int) { + t.Helper() + junit.AssertTrue(t, ds0.isUpward()) + junit.AssertTrue(t, ds1.isUpward()) + + // check compareTo contract - should never have ds1 < ds2 && ds2 < ds1 + comp0 := ds0.compareTo(ds1) + comp1 := ds1.compareTo(ds0) + junit.AssertEquals(t, expectedComp, comp0) + junit.AssertTrue(t, comp0 == -comp1) +} + +func depthSeg(x0, y0, x1, y1 float64) *operationBuffer_DepthSegment { + return operationBuffer_newDepthSegment(Geom_NewLineSegmentFromXY(x0, y0, x1, y1), 0) +} diff --git a/internal/jtsport/jts/operation_buffer_offset_curve.go b/internal/jtsport/jts/operation_buffer_offset_curve.go new file mode 100644 index 00000000..b2248bc0 --- /dev/null +++ b/internal/jtsport/jts/operation_buffer_offset_curve.go @@ -0,0 +1,522 @@ +package jts + +import ( + "math" + + "github.com/peterstace/simplefeatures/internal/jtsport/java" +) + +// operationBuffer_offsetCurve_MATCH_DISTANCE_FACTOR is the nearness tolerance +// for matching the raw offset linework and the buffer curve. +const operationBuffer_offsetCurve_MATCH_DISTANCE_FACTOR = 10000 + +// operationBuffer_offsetCurve_MIN_QUADRANT_SEGMENTS is a QuadSegs minimum value +// that will prevent generating unwanted offset curve artifacts near end caps. +const operationBuffer_offsetCurve_MIN_QUADRANT_SEGMENTS = 8 + +// OperationBuffer_OffsetCurve computes an offset curve from a geometry. +// An offset curve is a linear geometry which is offset a given distance +// from the input. +// If the offset distance is positive the curve lies on the left side of the input; +// if it is negative the curve is on the right side. +// The curve(s) have the same direction as the input line(s). +// The result for a zero offset distance is a copy of the input linework. +// +// The offset curve is based on the boundary of the buffer for the geometry +// at the offset distance (see BufferOp). +// The normal mode of operation is to return the sections of the buffer boundary +// which lie on the raw offset curve +// (obtained via RawOffset). +// The offset curve will contain multiple sections +// if the input self-intersects or has close approaches. +// The computed sections are ordered along the raw offset curve. +// Sections are disjoint. They never self-intersect, but may be rings. +// +// For a LineString the offset curve is a linear geometry +// (LineString or MultiLineString). +// For a Point or MultiPoint the offset curve is an empty LineString. +// For a Polygon the offset curve is the boundary of the polygon buffer (which +// may be a MultiLineString). +// For a collection the output is a MultiLineString containing the offset curves of the elements. +// +// In "joined" mode (see SetJoined) +// the sections computed for each input line are joined into a single offset curve line. +// The joined curve may self-intersect. +// At larger offset distances the curve may contain "flat-line" artifacts +// in places where the input self-intersects. +// +// Offset curves support setting the number of quadrant segments, +// the join style, and the mitre limit (if applicable) via +// the BufferParameters. +type OperationBuffer_OffsetCurve struct { + inputGeom *Geom_Geometry + distance float64 + isJoined bool + bufferParams *OperationBuffer_BufferParameters + matchDistance float64 + geomFactory *Geom_GeometryFactory +} + +// OperationBuffer_OffsetCurve_GetCurve computes the offset curve of a geometry at a given distance. +func OperationBuffer_OffsetCurve_GetCurve(geom *Geom_Geometry, distance float64) *Geom_Geometry { + oc := OperationBuffer_NewOffsetCurve(geom, distance) + return oc.GetCurve() +} + +// OperationBuffer_OffsetCurve_GetCurveWithParams computes the offset curve of a geometry at a given distance, +// with specified quadrant segments, join style and mitre limit. +func OperationBuffer_OffsetCurve_GetCurveWithParams(geom *Geom_Geometry, distance float64, quadSegs, joinStyle int, mitreLimit float64) *Geom_Geometry { + bufferParams := OperationBuffer_NewBufferParameters() + if quadSegs >= 0 { + bufferParams.SetQuadrantSegments(quadSegs) + } + if joinStyle >= 0 { + bufferParams.SetJoinStyle(joinStyle) + } + if mitreLimit >= 0 { + bufferParams.SetMitreLimit(mitreLimit) + } + oc := OperationBuffer_NewOffsetCurveWithParams(geom, distance, bufferParams) + return oc.GetCurve() +} + +// OperationBuffer_OffsetCurve_GetCurveJoined computes the offset curve of a geometry at a given distance, +// joining curve sections into a single line for each input line. +func OperationBuffer_OffsetCurve_GetCurveJoined(geom *Geom_Geometry, distance float64) *Geom_Geometry { + oc := OperationBuffer_NewOffsetCurve(geom, distance) + oc.SetJoined(true) + return oc.GetCurve() +} + +// OperationBuffer_NewOffsetCurve creates a new instance for computing an offset curve +// for a geometry at a given distance with default quadrant segments +// (BufferParameters.DEFAULT_QUADRANT_SEGMENTS) and join style (BufferParameters.JOIN_STYLE). +func OperationBuffer_NewOffsetCurve(geom *Geom_Geometry, distance float64) *OperationBuffer_OffsetCurve { + return OperationBuffer_NewOffsetCurveWithParams(geom, distance, nil) +} + +// OperationBuffer_NewOffsetCurveWithParams creates a new instance for computing an offset curve +// for a geometry at a given distance, setting the quadrant segments and join style +// and mitre limit via BufferParameters. +func OperationBuffer_NewOffsetCurveWithParams(geom *Geom_Geometry, distance float64, bufParams *OperationBuffer_BufferParameters) *OperationBuffer_OffsetCurve { + oc := &OperationBuffer_OffsetCurve{ + inputGeom: geom, + distance: distance, + matchDistance: math.Abs(distance) / operationBuffer_offsetCurve_MATCH_DISTANCE_FACTOR, + geomFactory: geom.GetFactory(), + } + + //-- make new buffer params since the end cap style must be the default + oc.bufferParams = OperationBuffer_NewBufferParameters() + if bufParams != nil { + // Prevent using a very small QuadSegs value, to avoid + // offset curve artifacts near the end caps. + quadSegs := bufParams.GetQuadrantSegments() + if quadSegs < operationBuffer_offsetCurve_MIN_QUADRANT_SEGMENTS { + quadSegs = operationBuffer_offsetCurve_MIN_QUADRANT_SEGMENTS + } + oc.bufferParams.SetQuadrantSegments(quadSegs) + oc.bufferParams.SetJoinStyle(bufParams.GetJoinStyle()) + oc.bufferParams.SetMitreLimit(bufParams.GetMitreLimit()) + } + + return oc +} + +// SetJoined computes a single curve line for each input linear component, +// by joining curve sections in order along the raw offset curve. +// The default mode is to compute separate curve sections. +func (oc *OperationBuffer_OffsetCurve) SetJoined(isJoined bool) { + oc.isJoined = isJoined +} + +// GetCurve gets the computed offset curve lines. +func (oc *OperationBuffer_OffsetCurve) GetCurve() *Geom_Geometry { + return GeomUtil_GeometryMapper_FlatMap(oc.inputGeom, 1, &operationBuffer_offsetCurveMapOp{oc: oc}) +} + +type operationBuffer_offsetCurveMapOp struct { + oc *OperationBuffer_OffsetCurve +} + +func (op *operationBuffer_offsetCurveMapOp) Map(geom *Geom_Geometry) *Geom_Geometry { + if java.InstanceOf[*Geom_Point](geom) { + return nil + } + if java.InstanceOf[*Geom_Polygon](geom) { + return op.oc.toLineString(geom.Buffer(op.oc.distance).GetBoundary()) + } + return op.oc.computeCurve(java.Cast[*Geom_LineString](geom), op.oc.distance) +} + +// toLineString forces LinearRings to be LineStrings. +func (oc *OperationBuffer_OffsetCurve) toLineString(geom *Geom_Geometry) *Geom_Geometry { + if java.InstanceOf[*Geom_LinearRing](geom) { + ring := java.Cast[*Geom_LinearRing](geom) + return geom.GetFactory().CreateLineStringFromCoordinateSequence(ring.GetCoordinateSequence()).Geom_Geometry + } + return geom +} + +// OperationBuffer_OffsetCurve_RawOffsetWithParams gets the raw offset curve for a line at a given distance. +// The quadrant segments, join style and mitre limit can be specified +// via BufferParameters. +// +// The raw offset line may contain loops and other artifacts which are +// not present in the true offset curve. +func OperationBuffer_OffsetCurve_RawOffsetWithParams(line *Geom_LineString, distance float64, bufParams *OperationBuffer_BufferParameters) []*Geom_Coordinate { + pts := line.GetCoordinates() + cleanPts := Geom_CoordinateArrays_RemoveRepeatedOrInvalidPoints(pts) + ocb := OperationBuffer_NewOffsetCurveBuilder( + line.GetFactory().GetPrecisionModel(), bufParams, + ) + rawPts := ocb.GetOffsetCurve(cleanPts, distance) + return rawPts +} + +// OperationBuffer_OffsetCurve_RawOffset gets the raw offset curve for a line at a given distance, +// with default buffer parameters. +func OperationBuffer_OffsetCurve_RawOffset(line *Geom_LineString, distance float64) []*Geom_Coordinate { + return OperationBuffer_OffsetCurve_RawOffsetWithParams(line, distance, OperationBuffer_NewBufferParameters()) +} + +func (oc *OperationBuffer_OffsetCurve) computeCurve(lineGeom *Geom_LineString, distance float64) *Geom_Geometry { + //-- first handle simple cases + //-- empty or single-point line + if lineGeom.GetNumPoints() < 2 || lineGeom.GetLength() == 0.0 { + return oc.geomFactory.CreateLineString().Geom_Geometry + } + //-- zero offset distance + if distance == 0 { + return lineGeom.Copy() + } + //-- two-point line + if lineGeom.GetNumPoints() == 2 { + return oc.offsetSegment(lineGeom.GetCoordinates(), distance) + } + + sections := oc.computeSections(lineGeom, distance) + + var offsetCurve *Geom_Geometry + if oc.isJoined { + offsetCurve = OperationBuffer_OffsetCurveSection_ToLine(sections, oc.geomFactory) + } else { + offsetCurve = OperationBuffer_OffsetCurveSection_ToGeometry(sections, oc.geomFactory) + } + return offsetCurve +} + +func (oc *OperationBuffer_OffsetCurve) computeSections(lineGeom *Geom_LineString, distance float64) []*OperationBuffer_OffsetCurveSection { + rawCurve := OperationBuffer_OffsetCurve_RawOffsetWithParams(lineGeom, distance, oc.bufferParams) + sections := make([]*OperationBuffer_OffsetCurveSection, 0) + if len(rawCurve) == 0 { + return sections + } + + // Note: If the raw offset curve has no + // narrow concave angles or self-intersections it could be returned as is. + // However, this is likely to be a less frequent situation, + // and testing indicates little performance advantage, + // so not doing this. + + bufferPoly := operationBuffer_offsetCurve_getBufferOriented(lineGeom, distance, oc.bufferParams) + + //-- first extract offset curve sections from shell + shell := bufferPoly.GetExteriorRing().GetCoordinates() + oc.computeCurveSections(shell, rawCurve, §ions) + + //-- extract offset curve sections from holes + for i := 0; i < bufferPoly.GetNumInteriorRing(); i++ { + hole := bufferPoly.GetInteriorRingN(i).GetCoordinates() + oc.computeCurveSections(hole, rawCurve, §ions) + } + return sections +} + +func (oc *OperationBuffer_OffsetCurve) offsetSegment(pts []*Geom_Coordinate, distance float64) *Geom_Geometry { + offsetSeg := Geom_NewLineSegmentFromCoordinates(pts[0], pts[1]).Offset(distance) + return oc.geomFactory.CreateLineStringFromCoordinates([]*Geom_Coordinate{offsetSeg.P0, offsetSeg.P1}).Geom_Geometry +} + +func operationBuffer_offsetCurve_getBufferOriented(geom *Geom_LineString, distance float64, bufParams *OperationBuffer_BufferParameters) *Geom_Polygon { + buffer := OperationBuffer_BufferOp_BufferOpWithParams(geom.Geom_Geometry, math.Abs(distance), bufParams) + bufferPoly := operationBuffer_offsetCurve_extractMaxAreaPolygon(buffer) + //-- for negative distances (Right of input) reverse buffer direction to match offset curve + if distance < 0 { + bufferPoly = bufferPoly.ReverseInternal() + } + return bufferPoly +} + +// extractMaxAreaPolygon extracts the largest polygon by area from a geometry. +// Used here to avoid issues with non-robust buffer results +// which have spurious extra polygons. +func operationBuffer_offsetCurve_extractMaxAreaPolygon(geom *Geom_Geometry) *Geom_Polygon { + if geom.GetNumGeometries() == 1 { + return java.Cast[*Geom_Polygon](geom) + } + + maxArea := 0.0 + var maxPoly *Geom_Polygon + for i := 0; i < geom.GetNumGeometries(); i++ { + poly := java.Cast[*Geom_Polygon](geom.GetGeometryN(i)) + area := poly.GetArea() + if maxPoly == nil || area > maxArea { + maxPoly = poly + maxArea = area + } + } + return maxPoly +} + +const operationBuffer_offsetCurve_NOT_IN_CURVE = -1.0 + +func (oc *OperationBuffer_OffsetCurve) computeCurveSections(bufferRingPts []*Geom_Coordinate, + rawCurve []*Geom_Coordinate, sections *[]*OperationBuffer_OffsetCurveSection) { + rawPosition := make([]float64, len(bufferRingPts)-1) + for i := 0; i < len(rawPosition); i++ { + rawPosition[i] = operationBuffer_offsetCurve_NOT_IN_CURVE + } + bufferSegIndex := operationBuffer_newSegmentMCIndex(bufferRingPts) + bufferFirstIndex := -1 + minRawPosition := -1.0 + for i := 0; i < len(rawCurve)-1; i++ { + minBufferIndexForSeg := oc.matchSegments( + rawCurve[i], rawCurve[i+1], i, bufferSegIndex, bufferRingPts, rawPosition) + if minBufferIndexForSeg >= 0 { + pos := rawPosition[minBufferIndexForSeg] + if bufferFirstIndex < 0 || pos < minRawPosition { + minRawPosition = pos + bufferFirstIndex = minBufferIndexForSeg + } + } + } + //-- no matching sections found in this buffer ring + if bufferFirstIndex < 0 { + return + } + oc.extractSections(bufferRingPts, rawPosition, bufferFirstIndex, sections) +} + +// matchSegments matches the segments in a buffer ring to the raw offset curve +// to obtain their match positions (if any). +func (oc *OperationBuffer_OffsetCurve) matchSegments(raw0, raw1 *Geom_Coordinate, rawCurveIndex int, + bufferSegIndex *operationBuffer_SegmentMCIndex, bufferPts []*Geom_Coordinate, + rawCurvePos []float64) int { + matchEnv := Geom_NewEnvelopeFromCoordinates(raw0, raw1) + matchEnv.ExpandBy(oc.matchDistance) + matchAction := operationBuffer_newMatchCurveSegmentAction(raw0, raw1, rawCurveIndex, oc.matchDistance, bufferPts, rawCurvePos) + bufferSegIndex.Query(matchEnv, matchAction.IndexChain_MonotoneChainSelectAction) + return matchAction.GetBufferMinIndex() +} + +// operationBuffer_MatchCurveSegmentAction is an action to match a raw offset curve segment +// to segments in a buffer ring and record the matched segment locations(s) along the raw curve. +type operationBuffer_MatchCurveSegmentAction struct { + *IndexChain_MonotoneChainSelectAction + child java.Polymorphic + raw0 *Geom_Coordinate + raw1 *Geom_Coordinate + rawLen float64 + rawCurveIndex int + bufferRingPts []*Geom_Coordinate + matchDistance float64 + rawCurveLoc []float64 + minRawLocation float64 + bufferRingMinIdx int +} + +func (ma *operationBuffer_MatchCurveSegmentAction) GetChild() java.Polymorphic { + return ma.child +} + +func (ma *operationBuffer_MatchCurveSegmentAction) GetParent() java.Polymorphic { + return ma.IndexChain_MonotoneChainSelectAction +} + +func operationBuffer_newMatchCurveSegmentAction(raw0, raw1 *Geom_Coordinate, rawCurveIndex int, + matchDistance float64, bufferRingPts []*Geom_Coordinate, rawCurveLoc []float64) *operationBuffer_MatchCurveSegmentAction { + base := IndexChain_NewMonotoneChainSelectAction() + action := &operationBuffer_MatchCurveSegmentAction{ + IndexChain_MonotoneChainSelectAction: base, + raw0: raw0, + raw1: raw1, + rawLen: raw0.Distance(raw1), + rawCurveIndex: rawCurveIndex, + bufferRingPts: bufferRingPts, + matchDistance: matchDistance, + rawCurveLoc: rawCurveLoc, + minRawLocation: -1, + bufferRingMinIdx: -1, + } + base.child = action + return action +} + +func (ma *operationBuffer_MatchCurveSegmentAction) GetBufferMinIndex() int { + return ma.bufferRingMinIdx +} + +func (ma *operationBuffer_MatchCurveSegmentAction) Select_BODY(mc *IndexChain_MonotoneChain, segIndex int) { + // Generally buffer segments are no longer than raw curve segments, + // since the final buffer line likely has node points added. + // So a buffer segment may match all or only a portion of a single raw segment. + // There may be multiple buffer ring segs that match along the raw segment. + // + // HOWEVER, in some cases the buffer construction may contain + // a matching buffer segment which is slightly longer than a raw curve segment. + // Specifically, at the endpoint of a closed line with nearly parallel end segments + // - the closing fillet line is very short so is heuristically removed in the buffer. + // In this case, the buffer segment must still be matched. + // This produces closed offset curves, which is technically + // an anomaly, but only happens in rare cases. + frac := ma.segmentMatchFrac(ma.bufferRingPts[segIndex], ma.bufferRingPts[segIndex+1], + ma.raw0, ma.raw1, ma.matchDistance) + //-- no match + if frac < 0 { + return + } + + //-- location is used to sort segments along raw curve + location := float64(ma.rawCurveIndex) + frac + ma.rawCurveLoc[segIndex] = location + //-- buffer seg index at lowest raw location is the curve start + if ma.minRawLocation < 0 || location < ma.minRawLocation { + ma.minRawLocation = location + ma.bufferRingMinIdx = segIndex + } +} + +func (ma *operationBuffer_MatchCurveSegmentAction) segmentMatchFrac(buf0, buf1, raw0, raw1 *Geom_Coordinate, matchDistance float64) float64 { + if !ma.isMatch(buf0, buf1, raw0, raw1, matchDistance) { + return -1 + } + + //-- matched - determine location as fraction along raw segment + seg := Geom_NewLineSegmentFromCoordinates(raw0, raw1) + return seg.SegmentFraction(buf0) +} + +func (ma *operationBuffer_MatchCurveSegmentAction) isMatch(buf0, buf1, raw0, raw1 *Geom_Coordinate, matchDistance float64) bool { + bufSegLen := buf0.Distance(buf1) + if ma.rawLen <= bufSegLen { + if matchDistance < Algorithm_Distance_PointToSegment(raw0, buf0, buf1) { + return false + } + if matchDistance < Algorithm_Distance_PointToSegment(raw1, buf0, buf1) { + return false + } + } else { + //TODO: only match longer buf segs at raw curve end segs? + if matchDistance < Algorithm_Distance_PointToSegment(buf0, raw0, raw1) { + return false + } + if matchDistance < Algorithm_Distance_PointToSegment(buf1, raw0, raw1) { + return false + } + } + return true +} + +// extractSections is only called when there is at least one ring segment matched +// (so rawCurvePos has at least one entry != NOT_IN_CURVE). +// The start index of the first section must be provided. +// This is intended to be the section with lowest position +// along the raw curve. +func (oc *OperationBuffer_OffsetCurve) extractSections(ringPts []*Geom_Coordinate, rawCurveLoc []float64, + startIndex int, sections *[]*OperationBuffer_OffsetCurveSection) { + sectionStart := startIndex + sectionCount := 0 + var sectionEnd int + for { + sectionEnd = oc.findSectionEnd(rawCurveLoc, sectionStart, startIndex) + location := rawCurveLoc[sectionStart] + lastIndex := operationBuffer_offsetCurve_prev(sectionEnd, len(rawCurveLoc)) + lastLoc := rawCurveLoc[lastIndex] + section := OperationBuffer_OffsetCurveSection_Create(ringPts, sectionStart, sectionEnd, location, lastLoc) + *sections = append(*sections, section) + sectionStart = oc.findSectionStart(rawCurveLoc, sectionEnd) + + //-- check for an abnormal state + sectionCount++ + if sectionCount > len(ringPts) { + Util_Assert_ShouldNeverReachHereWithMessage("Too many sections for ring - probable bug") + } + if !(sectionStart != startIndex && sectionEnd != startIndex) { + break + } + } +} + +func (oc *OperationBuffer_OffsetCurve) findSectionStart(loc []float64, end int) int { + start := end + for { + next := operationBuffer_offsetCurve_next(start, len(loc)) + //-- skip ahead if segment is not in raw curve + if loc[start] == operationBuffer_offsetCurve_NOT_IN_CURVE { + start = next + continue + } + prev := operationBuffer_offsetCurve_prev(start, len(loc)) + //-- if prev segment is not in raw curve then have found a start + if loc[prev] == operationBuffer_offsetCurve_NOT_IN_CURVE { + return start + } + if oc.isJoined { + // Start section at next gap in raw curve. + // Only needed for joined curve, since otherwise + // contiguous buffer segments can be in same curve section. + locDelta := math.Abs(loc[start] - loc[prev]) + if locDelta > 1 { + return start + } + } + start = next + if start == end { + break + } + } + return start +} + +func (oc *OperationBuffer_OffsetCurve) findSectionEnd(loc []float64, start, firstStartIndex int) int { + // assert: pos[start] is IN CURVE + end := start + var next int + for { + next = operationBuffer_offsetCurve_next(end, len(loc)) + if loc[next] == operationBuffer_offsetCurve_NOT_IN_CURVE { + return next + } + if oc.isJoined { + // End section at gap in raw curve. + // Only needed for joined curve, since otherwise + // contiguous buffer segments can be in same section + locDelta := math.Abs(loc[next] - loc[end]) + if locDelta > 1 { + return next + } + } + end = next + if !(end != start && end != firstStartIndex) { + break + } + } + return end +} + +func operationBuffer_offsetCurve_next(i, size int) int { + i += 1 + if i < size { + return i + } + return 0 +} + +func operationBuffer_offsetCurve_prev(i, size int) int { + i -= 1 + if i < 0 { + return size - 1 + } + return i +} diff --git a/internal/jtsport/jts/operation_buffer_offset_curve_builder.go b/internal/jtsport/jts/operation_buffer_offset_curve_builder.go new file mode 100644 index 00000000..fef5eb59 --- /dev/null +++ b/internal/jtsport/jts/operation_buffer_offset_curve_builder.go @@ -0,0 +1,274 @@ +package jts + +import "math" + +// OperationBuffer_OffsetCurveBuilder computes the raw offset curve for a +// single Geometry component (ring, line or point). +// A raw offset curve line is not noded - +// it may contain self-intersections (and usually will). +// The final buffer polygon is computed by forming a topological graph +// of all the noded raw curves and tracing outside contours. +// The points in the raw curve are rounded +// to a given PrecisionModel. +// +// Note: this may not produce correct results if the input +// contains repeated or invalid points. +// Repeated points should be removed before calling. +// See CoordinateArrays.removeRepeatedOrInvalidPoints. +type OperationBuffer_OffsetCurveBuilder struct { + distance float64 + precisionModel *Geom_PrecisionModel + bufParams *OperationBuffer_BufferParameters +} + +// OperationBuffer_NewOffsetCurveBuilder creates a new OffsetCurveBuilder. +func OperationBuffer_NewOffsetCurveBuilder(precisionModel *Geom_PrecisionModel, bufParams *OperationBuffer_BufferParameters) *OperationBuffer_OffsetCurveBuilder { + return &OperationBuffer_OffsetCurveBuilder{ + precisionModel: precisionModel, + bufParams: bufParams, + } +} + +// GetBufferParameters gets the buffer parameters being used to generate the curve. +func (ocb *OperationBuffer_OffsetCurveBuilder) GetBufferParameters() *OperationBuffer_BufferParameters { + return ocb.bufParams +} + +// GetLineCurve computes the offset curve for a line. +// This method handles single points as well as LineStrings. +// LineStrings are assumed NOT to be closed (the function will not +// fail for closed lines, but will generate superfluous line caps). +// +// Returns a Coordinate array representing the curve or nil if the curve is empty. +func (ocb *OperationBuffer_OffsetCurveBuilder) GetLineCurve(inputPts []*Geom_Coordinate, distance float64) []*Geom_Coordinate { + ocb.distance = distance + + if ocb.IsLineOffsetEmpty(distance) { + return nil + } + + posDistance := math.Abs(distance) + segGen := ocb.getSegGen(posDistance) + if len(inputPts) <= 1 { + ocb.computePointCurve(inputPts[0], segGen) + } else { + if ocb.bufParams.IsSingleSided() { + isRightSide := distance < 0.0 + ocb.computeSingleSidedBufferCurve(inputPts, isRightSide, segGen) + } else { + ocb.computeLineBufferCurve(inputPts, segGen) + } + } + + lineCoord := segGen.GetCoordinates() + return lineCoord +} + +// IsLineOffsetEmpty tests whether the offset curve for line or point geometries +// at the given offset distance is empty (does not exist). +// This is the case if: +// - the distance is zero, +// - the distance is negative, except for the case of singled-sided buffers +func (ocb *OperationBuffer_OffsetCurveBuilder) IsLineOffsetEmpty(distance float64) bool { + // a zero width buffer of a line or point is empty + if distance == 0.0 { + return true + } + // a negative width buffer of a line or point is empty, + // except for single-sided buffers, where the sign indicates the side + if distance < 0.0 && !ocb.bufParams.IsSingleSided() { + return true + } + return false +} + +// GetRingCurve computes the offset curve for a ring. +// This method handles the degenerate cases of single points and lines, +// as well as valid rings. +// +// Returns a Coordinate array representing the curve, or nil if the curve is empty. +func (ocb *OperationBuffer_OffsetCurveBuilder) GetRingCurve(inputPts []*Geom_Coordinate, side int, distance float64) []*Geom_Coordinate { + ocb.distance = distance + if len(inputPts) <= 2 { + return ocb.GetLineCurve(inputPts, distance) + } + + // optimize creating ring for for zero distance + if distance == 0.0 { + return operationBuffer_offsetCurveBuilder_copyCoordinates(inputPts) + } + segGen := ocb.getSegGen(distance) + ocb.computeRingBufferCurve(inputPts, side, segGen) + return segGen.GetCoordinates() +} + +// GetOffsetCurve computes the offset curve for a coordinate sequence. +func (ocb *OperationBuffer_OffsetCurveBuilder) GetOffsetCurve(inputPts []*Geom_Coordinate, distance float64) []*Geom_Coordinate { + ocb.distance = distance + + // a zero width offset curve is empty + if distance == 0.0 { + return nil + } + + isRightSide := distance < 0.0 + posDistance := math.Abs(distance) + segGen := ocb.getSegGen(posDistance) + if len(inputPts) <= 1 { + ocb.computePointCurve(inputPts[0], segGen) + } else { + ocb.computeOffsetCurve(inputPts, isRightSide, segGen) + } + curvePts := segGen.GetCoordinates() + // for right side line is traversed in reverse direction, so have to reverse generated line + if isRightSide { + Geom_CoordinateArrays_Reverse(curvePts) + } + return curvePts +} + +func operationBuffer_offsetCurveBuilder_copyCoordinates(pts []*Geom_Coordinate) []*Geom_Coordinate { + copyArr := make([]*Geom_Coordinate, len(pts)) + for i := 0; i < len(copyArr); i++ { + copyArr[i] = pts[i].Copy() + } + return copyArr +} + +func (ocb *OperationBuffer_OffsetCurveBuilder) getSegGen(distance float64) *operationBuffer_OffsetSegmentGenerator { + return operationBuffer_newOffsetSegmentGenerator(ocb.precisionModel, ocb.bufParams, distance) +} + +// simplifyTolerance computes the distance tolerance to use during input +// line simplification. +func (ocb *OperationBuffer_OffsetCurveBuilder) simplifyTolerance(bufDistance float64) float64 { + return bufDistance * ocb.bufParams.GetSimplifyFactor() +} + +func (ocb *OperationBuffer_OffsetCurveBuilder) computePointCurve(pt *Geom_Coordinate, segGen *operationBuffer_OffsetSegmentGenerator) { + switch ocb.bufParams.GetEndCapStyle() { + case OperationBuffer_BufferParameters_CAP_ROUND: + segGen.CreateCircle(pt) + case OperationBuffer_BufferParameters_CAP_SQUARE: + segGen.CreateSquare(pt) + // otherwise curve is empty (e.g. for a butt cap); + } +} + +func (ocb *OperationBuffer_OffsetCurveBuilder) computeLineBufferCurve(inputPts []*Geom_Coordinate, segGen *operationBuffer_OffsetSegmentGenerator) { + distTol := ocb.simplifyTolerance(ocb.distance) + + //--------- compute points for left side of line + // Simplify the appropriate side of the line before generating + simp1 := OperationBuffer_BufferInputLineSimplifier_Simplify(inputPts, distTol) + + n1 := len(simp1) - 1 + segGen.InitSideSegments(simp1[0], simp1[1], Geom_Position_Left) + for i := 2; i <= n1; i++ { + segGen.AddNextSegment(simp1[i], true) + } + segGen.AddLastSegment() + // add line cap for end of line + segGen.AddLineEndCap(simp1[n1-1], simp1[n1]) + + //---------- compute points for right side of line + // Simplify the appropriate side of the line before generating + simp2 := OperationBuffer_BufferInputLineSimplifier_Simplify(inputPts, -distTol) + n2 := len(simp2) - 1 + + // since we are traversing line in opposite order, offset position is still LEFT + segGen.InitSideSegments(simp2[n2], simp2[n2-1], Geom_Position_Left) + for i := n2 - 2; i >= 0; i-- { + segGen.AddNextSegment(simp2[i], true) + } + segGen.AddLastSegment() + // add line cap for start of line + segGen.AddLineEndCap(simp2[1], simp2[0]) + + segGen.CloseRing() +} + +func (ocb *OperationBuffer_OffsetCurveBuilder) computeSingleSidedBufferCurve(inputPts []*Geom_Coordinate, isRightSide bool, segGen *operationBuffer_OffsetSegmentGenerator) { + distTol := ocb.simplifyTolerance(ocb.distance) + + if isRightSide { + // add original line + segGen.AddSegments(inputPts, true) + + //---------- compute points for right side of line + // Simplify the appropriate side of the line before generating + simp2 := OperationBuffer_BufferInputLineSimplifier_Simplify(inputPts, -distTol) + n2 := len(simp2) - 1 + + // since we are traversing line in opposite order, offset position is still LEFT + segGen.InitSideSegments(simp2[n2], simp2[n2-1], Geom_Position_Left) + segGen.AddFirstSegment() + for i := n2 - 2; i >= 0; i-- { + segGen.AddNextSegment(simp2[i], true) + } + } else { + // add original line + segGen.AddSegments(inputPts, false) + + //--------- compute points for left side of line + // Simplify the appropriate side of the line before generating + simp1 := OperationBuffer_BufferInputLineSimplifier_Simplify(inputPts, distTol) + + n1 := len(simp1) - 1 + segGen.InitSideSegments(simp1[0], simp1[1], Geom_Position_Left) + segGen.AddFirstSegment() + for i := 2; i <= n1; i++ { + segGen.AddNextSegment(simp1[i], true) + } + } + segGen.AddLastSegment() + segGen.CloseRing() +} + +func (ocb *OperationBuffer_OffsetCurveBuilder) computeOffsetCurve(inputPts []*Geom_Coordinate, isRightSide bool, segGen *operationBuffer_OffsetSegmentGenerator) { + distTol := ocb.simplifyTolerance(math.Abs(ocb.distance)) + + if isRightSide { + //---------- compute points for right side of line + // Simplify the appropriate side of the line before generating + simp2 := OperationBuffer_BufferInputLineSimplifier_Simplify(inputPts, -distTol) + n2 := len(simp2) - 1 + + // since we are traversing line in opposite order, offset position is still LEFT + segGen.InitSideSegments(simp2[n2], simp2[n2-1], Geom_Position_Left) + segGen.AddFirstSegment() + for i := n2 - 2; i >= 0; i-- { + segGen.AddNextSegment(simp2[i], true) + } + } else { + //--------- compute points for left side of line + // Simplify the appropriate side of the line before generating + simp1 := OperationBuffer_BufferInputLineSimplifier_Simplify(inputPts, distTol) + + n1 := len(simp1) - 1 + segGen.InitSideSegments(simp1[0], simp1[1], Geom_Position_Left) + segGen.AddFirstSegment() + for i := 2; i <= n1; i++ { + segGen.AddNextSegment(simp1[i], true) + } + } + segGen.AddLastSegment() +} + +func (ocb *OperationBuffer_OffsetCurveBuilder) computeRingBufferCurve(inputPts []*Geom_Coordinate, side int, segGen *operationBuffer_OffsetSegmentGenerator) { + // simplify input line to improve performance + distTol := ocb.simplifyTolerance(ocb.distance) + // ensure that correct side is simplified + if side == Geom_Position_Right { + distTol = -distTol + } + simp := OperationBuffer_BufferInputLineSimplifier_Simplify(inputPts, distTol) + + n := len(simp) - 1 + segGen.InitSideSegments(simp[n-1], simp[0], side) + for i := 1; i <= n; i++ { + addStartPoint := i != 1 + segGen.AddNextSegment(simp[i], addStartPoint) + } + segGen.CloseRing() +} diff --git a/internal/jtsport/jts/operation_buffer_offset_curve_section.go b/internal/jtsport/jts/operation_buffer_offset_curve_section.go new file mode 100644 index 00000000..87e214e6 --- /dev/null +++ b/internal/jtsport/jts/operation_buffer_offset_curve_section.go @@ -0,0 +1,119 @@ +package jts + +import "sort" + +// OperationBuffer_OffsetCurveSection models a section of a raw offset curve, +// starting at a given location along the raw curve. +// The location is a decimal number, with the integer part +// containing the segment index and the fractional part +// giving the fractional distance along the segment. +// The location of the last section segment +// is also kept, to allow optimizing joining sections together. +type OperationBuffer_OffsetCurveSection struct { + sectionPts []*Geom_Coordinate + location float64 + locLast float64 +} + +// OperationBuffer_OffsetCurveSection_ToGeometry converts a list of offset curve sections to a geometry. +func OperationBuffer_OffsetCurveSection_ToGeometry(sections []*OperationBuffer_OffsetCurveSection, geomFactory *Geom_GeometryFactory) *Geom_Geometry { + if len(sections) == 0 { + return geomFactory.CreateLineString().Geom_Geometry + } + if len(sections) == 1 { + return geomFactory.CreateLineStringFromCoordinates(sections[0].getCoordinates()).Geom_Geometry + } + + //-- sort sections in order along the offset curve + sort.Slice(sections, func(i, j int) bool { + return sections[i].CompareTo(sections[j]) < 0 + }) + lines := make([]*Geom_LineString, len(sections)) + + for i := 0; i < len(sections); i++ { + lines[i] = geomFactory.CreateLineStringFromCoordinates(sections[i].getCoordinates()) + } + return geomFactory.CreateMultiLineStringFromLineStrings(lines).Geom_Geometry +} + +// OperationBuffer_OffsetCurveSection_ToLine joins section coordinates into a LineString. +// Join vertices which lie in the same raw curve segment +// are removed, to simplify the result linework. +func OperationBuffer_OffsetCurveSection_ToLine(sections []*OperationBuffer_OffsetCurveSection, geomFactory *Geom_GeometryFactory) *Geom_Geometry { + if len(sections) == 0 { + return geomFactory.CreateLineString().Geom_Geometry + } + if len(sections) == 1 { + return geomFactory.CreateLineStringFromCoordinates(sections[0].getCoordinates()).Geom_Geometry + } + + //-- sort sections in order along the offset curve + sort.Slice(sections, func(i, j int) bool { + return sections[i].CompareTo(sections[j]) < 0 + }) + pts := Geom_NewCoordinateList() + + removeStartPt := false + for i := 0; i < len(sections); i++ { + section := sections[i] + + removeEndPt := false + if i < len(sections)-1 { + nextStartLoc := sections[i+1].location + removeEndPt = section.isEndInSameSegment(nextStartLoc) + } + sectionPts := section.getCoordinates() + for j := 0; j < len(sectionPts); j++ { + if (removeStartPt && j == 0) || (removeEndPt && j == len(sectionPts)-1) { + continue + } + pts.AddCoordinate(sectionPts[j], false) + } + removeStartPt = removeEndPt + } + return geomFactory.CreateLineStringFromCoordinates(pts.ToCoordinateArray()).Geom_Geometry +} + +// OperationBuffer_OffsetCurveSection_Create creates a new offset curve section. +func OperationBuffer_OffsetCurveSection_Create(srcPts []*Geom_Coordinate, start, end int, loc, locLast float64) *OperationBuffer_OffsetCurveSection { + length := end - start + 1 + if end <= start { + length = len(srcPts) - start + end + } + + sectionPts := make([]*Geom_Coordinate, length) + for i := 0; i < length; i++ { + index := (start + i) % (len(srcPts) - 1) + sectionPts[i] = srcPts[index].Copy() + } + return operationBuffer_newOffsetCurveSection(sectionPts, loc, locLast) +} + +func operationBuffer_newOffsetCurveSection(pts []*Geom_Coordinate, loc, locLast float64) *OperationBuffer_OffsetCurveSection { + return &OperationBuffer_OffsetCurveSection{ + sectionPts: pts, + location: loc, + locLast: locLast, + } +} + +func (ocs *OperationBuffer_OffsetCurveSection) getCoordinates() []*Geom_Coordinate { + return ocs.sectionPts +} + +func (ocs *OperationBuffer_OffsetCurveSection) isEndInSameSegment(nextLoc float64) bool { + segIndex := int(ocs.locLast) + nextIndex := int(nextLoc) + return segIndex == nextIndex +} + +// CompareTo orders sections by their location along the raw offset curve. +func (ocs *OperationBuffer_OffsetCurveSection) CompareTo(section *OperationBuffer_OffsetCurveSection) int { + if ocs.location < section.location { + return -1 + } + if ocs.location > section.location { + return 1 + } + return 0 +} diff --git a/internal/jtsport/jts/operation_buffer_offset_curve_test.go b/internal/jtsport/jts/operation_buffer_offset_curve_test.go new file mode 100644 index 00000000..1d44a55d --- /dev/null +++ b/internal/jtsport/jts/operation_buffer_offset_curve_test.go @@ -0,0 +1,390 @@ +package jts + +import ( + "testing" +) + +func TestOffsetCurve_Point(t *testing.T) { + checkOffsetCurve(t, + "POINT (0 0)", 1, + "LINESTRING EMPTY") +} + +func TestOffsetCurve_Empty(t *testing.T) { + checkOffsetCurve(t, + "LINESTRING EMPTY", 1, + "LINESTRING EMPTY") +} + +func TestOffsetCurve_ZeroLenLine(t *testing.T) { + checkOffsetCurve(t, + "LINESTRING (1 1, 1 1)", 1, + "LINESTRING EMPTY") +} + +func TestOffsetCurve_ZeroOffsetLine(t *testing.T) { + checkOffsetCurve(t, + "LINESTRING (0 0, 1 0, 1 1)", 0, + "LINESTRING (0 0, 1 0, 1 1)") +} + +func TestOffsetCurve_ZeroOffsetPolygon(t *testing.T) { + checkOffsetCurve(t, + "POLYGON ((1 9, 9 1, 1 1, 1 9))", 0, + "LINESTRING (1 9, 1 1, 9 1, 1 9)") +} + +func TestOffsetCurve_RepeatedPoint(t *testing.T) { + checkOffsetCurve(t, + "LINESTRING (4 9, 1 2, 7 5, 7 5, 4 9)", 1, + "LINESTRING (4.24 7.02, 2.99 4.12, 5.48 5.36, 4.24 7.02)") +} + +func TestOffsetCurve_Segment1Short(t *testing.T) { + checkOffsetCurveWithTolerance(t, + "LINESTRING (2 2, 2 2.0000001)", 1, + "LINESTRING (1 2, 1 2.0000001)", + 0.00000001) +} + +func TestOffsetCurve_Segment1(t *testing.T) { + checkOffsetCurve(t, + "LINESTRING (0 0, 9 9)", 1, + "LINESTRING (-0.71 0.71, 8.29 9.71)") +} + +func TestOffsetCurve_Segment1Neg(t *testing.T) { + checkOffsetCurve(t, + "LINESTRING (0 0, 9 9)", -1, + "LINESTRING (0.71 -0.71, 9.71 8.29)") +} + +func TestOffsetCurve_Segments2(t *testing.T) { + checkOffsetCurve(t, + "LINESTRING (0 0, 9 9, 25 0)", 1, + "LINESTRING (-0.707 0.707, 8.293 9.707, 8.435 9.825, 8.597 9.915, 8.773 9.974, 8.956 9.999, 9.141 9.99, 9.321 9.947, 9.49 9.872, 25.49 0.872)") +} + +func TestOffsetCurve_Segments3(t *testing.T) { + checkOffsetCurve(t, + "LINESTRING (0 0, 9 9, 25 0, 30 15)", 1, + "LINESTRING (-0.71 0.71, 8.29 9.71, 8.44 9.83, 8.6 9.92, 8.77 9.97, 8.96 10, 9.14 9.99, 9.32 9.95, 9.49 9.87, 24.43 1.47, 29.05 15.32)") +} + +func TestOffsetCurve_RightAngle(t *testing.T) { + checkOffsetCurve(t, + "LINESTRING (2 8, 8 8, 8 1)", 1, + "LINESTRING (2 9, 8 9, 8.2 8.98, 8.38 8.92, 8.56 8.83, 8.71 8.71, 8.83 8.56, 8.92 8.38, 8.98 8.2, 9 8, 9 1)") +} + +func TestOffsetCurve_ZigzagOneEndCurved4(t *testing.T) { + checkOffsetCurve(t, + "LINESTRING (1 3, 6 3, 4 5, 9 5)", 4, + "LINESTRING (0.53 6.95, 0.67 7.22, 1.17 7.83, 1.78 8.33, 2.47 8.7, 3.22 8.92, 4 9, 9 9)") +} + +func TestOffsetCurve_ZigzagOneEndCurved1(t *testing.T) { + checkOffsetCurve(t, + "LINESTRING (1 3, 6 3, 4 5, 9 5)", 1, + "LINESTRING (1 4, 3.59 4, 3.29 4.29, 3.17 4.44, 3.08 4.62, 3.02 4.8, 3 5, 3.02 5.2, 3.08 5.38, 3.17 5.56, 3.29 5.71, 3.44 5.83, 3.62 5.92, 3.8 5.98, 4 6, 9 6)") +} + +func TestOffsetCurve_AsymmetricU(t *testing.T) { + wkt := "LINESTRING (1 1, 9 1, 9 2, 5 2)" + checkOffsetCurve(t, + wkt, 1, + "LINESTRING (1 2, 4 2)") + checkOffsetCurve(t, + wkt, -1, + "LINESTRING (1 0, 9 0, 9.2 0.02, 9.38 0.08, 9.56 0.17, 9.71 0.29, 9.83 0.44, 9.92 0.62, 9.98 0.8, 10 1, 10 2, 9.98 2.2, 9.92 2.38, 9.83 2.56, 9.71 2.71, 9.56 2.83, 9.38 2.92, 9.2 2.98, 9 3, 5 3)") +} + +func TestOffsetCurve_SymmetricU(t *testing.T) { + wkt := "LINESTRING (1 1, 9 1, 9 2, 1 2)" + checkOffsetCurve(t, + wkt, 1, + "LINESTRING EMPTY") + checkOffsetCurve(t, + wkt, -1, + "LINESTRING (1 0, 9 0, 9.2 0.02, 9.38 0.08, 9.56 0.17, 9.71 0.29, 9.83 0.44, 9.92 0.62, 9.98 0.8, 10 1, 10 2, 9.98 2.2, 9.92 2.38, 9.83 2.56, 9.71 2.71, 9.56 2.83, 9.38 2.92, 9.2 2.98, 9 3, 1 3)") +} + +func TestOffsetCurve_EmptyResult(t *testing.T) { + checkOffsetCurve(t, + "LINESTRING (3 5, 5 7, 7 5)", -4, + "LINESTRING EMPTY") +} + +func TestOffsetCurve_SelfCross(t *testing.T) { + checkOffsetCurve(t, + "LINESTRING (50 90, 50 10, 90 50, 10 50)", 10, + "MULTILINESTRING ((60 90, 60 60), (60 40, 60 34.14, 65.85 40, 60 40), (40 40, 10 40))") +} + +func TestOffsetCurve_SelfCrossNeg(t *testing.T) { + checkOffsetCurve(t, + "LINESTRING (50 90, 50 10, 90 50, 10 50)", -10, + "MULTILINESTRING ((40 90, 40 60, 10 60), (40 40, 40 10, 40.19 8.05, 40.76 6.17, 41.69 4.44, 42.93 2.93, 44.44 1.69, 46.17 0.76, 48.05 0.19, 50 0, 51.95 0.19, 53.83 0.76, 55.56 1.69, 57.07 2.93, 97.07 42.93, 98.31 44.44, 99.24 46.17, 99.81 48.05, 100 50, 99.81 51.95, 99.24 53.83, 98.31 55.56, 97.07 57.07, 95.56 58.31, 93.83 59.24, 91.95 59.81, 90 60, 60 60))") +} + +func TestOffsetCurve_SelfCrossCWNeg(t *testing.T) { + checkOffsetCurve(t, + "LINESTRING (0 70, 100 70, 40 0, 40 100)", -10, + "MULTILINESTRING ((0 60, 30 60), (50 60, 50 27.03, 78.25 60, 50 60), (50 80, 50 100))") +} + +func TestOffsetCurve_SelfCrossDartInside(t *testing.T) { + checkOffsetCurve(t, + "LINESTRING (60 50, 10 80, 50 10, 90 80, 40 50)", 10, + "MULTILINESTRING ((54.86 41.43, 50 44.34, 45.14 41.43), (43.9 40.83, 50 30.16, 56.1 40.83))") +} + +func TestOffsetCurve_SelfCrossDartOutside(t *testing.T) { + checkOffsetCurve(t, + "LINESTRING (60 50, 10 80, 50 10, 90 80, 40 50)", -10, + "LINESTRING (50 67.66, 15.14 88.57, 13.32 89.43, 11.35 89.91, 9.33 89.98, 7.34 89.64, 5.46 88.91, 3.76 87.82, 2.32 86.4, 1.19 84.73, 0.42 82.86, 0.04 80.88, 0.07 78.86, 0.5 76.88, 1.32 75.04, 41.32 5.04, 42.42 3.48, 43.8 2.16, 45.4 1.12, 47.17 0.41, 49.05 0.05, 50.95 0.05, 52.83 0.41, 54.6 1.12, 56.2 2.16, 57.58 3.48, 58.68 5.04, 98.68 75.04, 99.5 76.88, 99.93 78.86, 99.96 80.88, 99.58 82.86, 98.81 84.73, 97.68 86.4, 96.24 87.82, 94.54 88.91, 92.66 89.64, 90.67 89.98, 88.65 89.91, 86.68 89.43, 84.86 88.57, 50 67.66)") +} + +func TestOffsetCurve_SelfCrossDart2Inside(t *testing.T) { + checkOffsetCurve(t, + "LINESTRING (64 45, 10 80, 50 10, 90 80, 35 45)", 10, + "LINESTRING (55.00 38.91, 49.58 42.42, 44.74 39.34, 50 30.15, 55.00 38.91)") +} + +func TestOffsetCurve_Ring(t *testing.T) { + checkOffsetCurve(t, + "LINESTRING (10 10, 50 90, 90 10, 10 10)", -10, + "LINESTRING (26.18 20, 50 67.63, 73.81 20, 26.18 20)") +} + +func TestOffsetCurve_ClosedCurve(t *testing.T) { + checkOffsetCurve(t, + "LINESTRING (30 70, 80 80, 50 10, 10 80, 60 70)", 10, + "LINESTRING (45 83.2, 78.04 89.81, 80 90, 81.96 89.81, 83.85 89.23, 85.59 88.29, 87.11 87.04, 88.35 85.5, 89.27 83.76, 89.82 81.87, 90 79.9, 89.79 77.94, 89.19 76.06, 59.19 6.06, 58.22 4.3, 56.91 2.77, 55.32 1.53, 53.52 0.64, 51.57 0.12, 49.56 0.01, 47.57 0.3, 45.68 0.98, 43.96 2.03, 42.49 3.4, 41.32 5.04, 1.32 75.04, 0.53 76.77, 0.09 78.63, 0.01 80.53, 0.29 82.41, 0.93 84.2, 1.89 85.85, 3.14 87.28, 4.65 88.45, 6.34 89.31, 8.17 89.83, 10.07 90, 11.96 89.81, 45 83.2)") +} + +func TestOffsetCurve_OverlapTriangleInside(t *testing.T) { + checkOffsetCurve(t, + "LINESTRING (70 80, 10 80, 50 10, 90 80, 40 80)", 10, + "LINESTRING (70 70, 40 70, 27.23 70, 50 30.15, 72.76 70, 70 70)") +} + +func TestOffsetCurve_OverlapTriangleOutside(t *testing.T) { + checkOffsetCurve(t, + "LINESTRING (70 80, 10 80, 50 10, 90 80, 40 80)", -10, + "LINESTRING (70 90, 40 90, 10 90, 8.11 89.82, 6.29 89.29, 4.6 88.42, 3.11 87.25, 1.87 85.82, 0.91 84.18, 0.29 82.39, 0.01 80.51, 0.1 78.61, 0.54 76.77, 1.32 75.04, 41.32 5.04, 42.42 3.48, 43.8 2.16, 45.4 1.12, 47.17 0.41, 49.05 0.05, 50.95 0.05, 52.83 0.41, 54.6 1.12, 56.2 2.16, 57.58 3.48, 58.68 5.04, 98.68 75.04, 99.46 76.77, 99.9 78.61, 99.99 80.51, 99.71 82.39, 99.09 84.18, 98.13 85.82, 96.89 87.25, 95.4 88.42, 93.71 89.29, 91.89 89.82, 90 90, 70 90)") +} + +//-------------------------------------------------------- + +func TestOffsetCurve_MultiPoint(t *testing.T) { + checkOffsetCurve(t, + "MULTIPOINT ((0 0), (1 1))", 1, + "LINESTRING EMPTY") +} + +func TestOffsetCurve_MultiLine(t *testing.T) { + checkOffsetCurve(t, + "MULTILINESTRING ((20 30, 60 10, 80 60), (40 50, 80 30))", 10, + "MULTILINESTRING ((24.47 38.94, 54.75 23.8, 70.72 63.71), (44.47 58.94, 84.47 38.94))") +} + +func TestOffsetCurve_MixedWithPoint(t *testing.T) { + checkOffsetCurve(t, + "GEOMETRYCOLLECTION (LINESTRING (20 30, 60 10, 80 60), POINT (0 0))", 10, + "LINESTRING (24.47 38.94, 54.75 23.8, 70.72 63.71)") +} + +func TestOffsetCurve_Polygon(t *testing.T) { + checkOffsetCurve(t, + "POLYGON ((100 200, 200 100, 100 100, 100 200))", 10, + "LINESTRING (90 200, 90.19 201.95, 90.76 203.83, 91.69 205.56, 92.93 207.07, 94.44 208.31, 96.17 209.24, 98.05 209.81, 100 210, 101.95 209.81, 103.83 209.24, 105.56 208.31, 107.07 207.07, 207.07 107.07, 208.31 105.56, 209.24 103.83, 209.81 101.95, 210 100, 209.81 98.05, 209.24 96.17, 208.31 94.44, 207.07 92.93, 205.56 91.69, 203.83 90.76, 201.95 90.19, 200 90, 100 90, 98.05 90.19, 96.17 90.76, 94.44 91.69, 92.93 92.93, 91.69 94.44, 90.76 96.17, 90.19 98.05, 90 100, 90 200)") + checkOffsetCurve(t, + "POLYGON ((100 200, 200 100, 100 100, 100 200))", -10, + "LINESTRING (110 175.86, 175.86 110, 110 110, 110 175.86)") +} + +func TestOffsetCurve_PolygonWithHole(t *testing.T) { + checkOffsetCurve(t, + "POLYGON ((20 80, 80 80, 80 20, 20 20, 20 80), (30 70, 70 70, 70 30, 30 30, 30 70))", 10, + "MULTILINESTRING ((10 80, 10.19 81.95, 10.76 83.83, 11.69 85.56, 12.93 87.07, 14.44 88.31, 16.17 89.24, 18.05 89.81, 20 90, 80 90, 81.95 89.81, 83.83 89.24, 85.56 88.31, 87.07 87.07, 88.31 85.56, 89.24 83.83, 89.81 81.95, 90 80, 90 20, 89.81 18.05, 89.24 16.17, 88.31 14.44, 87.07 12.93, 85.56 11.69, 83.83 10.76, 81.95 10.19, 80 10, 20 10, 18.05 10.19, 16.17 10.76, 14.44 11.69, 12.93 12.93, 11.69 14.44, 10.76 16.17, 10.19 18.05, 10 20, 10 80), (40 60, 40 40, 60 40, 60 60, 40 60))") + checkOffsetCurve(t, + "POLYGON ((20 80, 80 80, 80 20, 20 20, 20 80), (30 70, 70 70, 70 30, 30 30, 30 70))", -10, + "LINESTRING EMPTY") +} + +//------------------------------------------------- + +func TestOffsetCurve_Joined(t *testing.T) { + input := "LINESTRING (0 50, 100 50, 50 100, 50 0)" + checkOffsetCurveJoined(t, input, 10, + "LINESTRING (0 60, 75.85 60, 60 75.85, 60 0)") + checkOffsetCurveJoined(t, input, -10, + "LINESTRING (0 40, 100 40, 101.95 40.19, 103.83 40.76, 105.56 41.69, 107.07 42.93, 108.31 44.44, 109.24 46.17, 109.81 48.05, 110 50, 109.81 51.95, 109.24 53.83, 108.31 55.56, 107.07 57.07, 57.07 107.07, 55.56 108.31, 53.83 109.24, 51.95 109.81, 50 110, 48.05 109.81, 46.17 109.24, 44.44 108.31, 42.93 107.07, 41.69 105.56, 40.76 103.83, 40.19 101.95, 40 100, 40 0)") +} + +//------------------------------------------------- + +func TestOffsetCurve_InfiniteLoop(t *testing.T) { + checkOffsetCurveNoExpected(t, + "LINESTRING (21 101, -1 78, 12 43, 50 112, 73 -5, 19 2, 87 85, -7 38, 105 40)", 4) +} + +func TestOffsetCurve_OffsetError(t *testing.T) { + checkOffsetCurve(t, + "LINESTRING (12 20, 60 68, 111 114, 151 159, 210 218)", + 3, + "LINESTRING (9.878679656440358 22.121320343559642, 57.878679656440355 70.12132034355965, 57.99069368916718 70.22770917070595, 108.86775926900314 116.11682714467565, 148.75777204394902 160.99309151648976, 148.87867965644037 161.12132034355963, 207.87867965644037 220.12132034355963)") +} + +//--------------------------------------- + +func TestOffsetCurve_QuadSegs(t *testing.T) { + checkOffsetCurveWithParams(t, + "LINESTRING (20 20, 50 50, 80 20)", + 10, 10, -1, -1, + "LINESTRING (12.93 27.07, 42.93 57.07, 44.12 58.09, 45.46 58.91, 46.91 59.51, 48.44 59.88, 50 60, 51.56 59.88, 53.09 59.51, 54.54 58.91, 55.88 58.09, 57.07 57.07, 87.07 27.07)") +} + +func TestOffsetCurve_JoinBevel(t *testing.T) { + checkOffsetCurveWithParams(t, + "LINESTRING (20 20, 50 50, 80 20)", + 10, -1, OperationBuffer_BufferParameters_JOIN_BEVEL, -1, + "LINESTRING (12.93 27.07, 42.93 57.07, 57.07 57.07, 87.07 27.07)") +} + +func TestOffsetCurve_JoinMitre(t *testing.T) { + checkOffsetCurveWithParams(t, + "LINESTRING (20 20, 50 50, 80 20)", + 10, -1, OperationBuffer_BufferParameters_JOIN_MITRE, -1, + "LINESTRING (12.93 27.07, 50 64.14, 87.07 27.07)") +} + +func TestOffsetCurve_MinQuadrantSegments(t *testing.T) { + checkOffsetCurveWithParams(t, + "LINESTRING (553772.0645892698 177770.05079236583, 553780.9235869241 177768.99614978794, 553781.8325485934 177768.41771963477)", + -11, 0, OperationBuffer_BufferParameters_JOIN_MITRE, -1, + "LINESTRING (553770.76 177759.13, 553777.54 177758.32)") +} + +func TestOffsetCurve_MinQuadrantSegments_QGIS(t *testing.T) { + checkOffsetCurveWithParams(t, + "LINESTRING (421 622, 446 625, 449 627)", + 133, 0, OperationBuffer_BufferParameters_JOIN_MITRE, -1, + "LINESTRING (405.15 754.05, 416.3 755.39)") +} + +func TestOffsetCurve_MitreJoinError(t *testing.T) { + checkOffsetCurveWithParams(t, + "LINESTRING(362194.505 5649993.044,362197.451 5649994.125,362194.624 5650001.876,362189.684 5650000.114,362192.542 5649992.324,362194.505 5649993.044)", + -0.045, 0, OperationBuffer_BufferParameters_JOIN_MITRE, -1, + "LINESTRING (362194.52050157124 5649993.001754275, 362197.5086649931 5649994.098225646, 362194.65096611937 5650001.933395073, 362189.626113625 5650000.141129872, 362192.51525161567 5649992.266257602, 362194.5204958858 5649993.001752188)") +} + +func TestOffsetCurve_MitreJoinErrorSimple(t *testing.T) { + checkOffsetCurveWithParams(t, + "LINESTRING (4.821 0.72, 7.767 1.801, 4.94 9.552, 0 7.79, 2.858 0, 4.821 0.72)", + -0.045, 0, OperationBuffer_BufferParameters_JOIN_MITRE, -1, + "LINESTRING (4.83650157122754 0.6777542748970088, 7.824664993161384 1.7742256459460533, 4.966966119329371 9.6093950732796, -0.057886375241824 7.817129871774653, 2.8312516154153906 -0.0577423980712891, 4.836495885800319 0.6777521891305186)") +} + +func TestOffsetCurve_MitreJoinSingleLine(t *testing.T) { + checkOffsetCurveWithParams(t, + "LINESTRING (0.39 -0.02, 0.4650008997915482 -0.02, 0.4667128891457749 -0.0202500016082272, 0.4683515425280024 -0.0210000000000019, 0.4699159706879993 -0.0222499999999996, 0.4714061701120011 -0.0240000000000018, 0.4929087886040002 -0.0535958153351002, 0.4968358395870001 -0.0507426457862002, 0.4774061701119963 -0.0239999999999952, 0.476353470688 -0.0222500000000011, 0.4761015425280001 -0.0210000000000007, 0.4766503813740676 -0.0202500058185111, 0.4779990890331232 -0.02, 0.6189999999999996 -0.02, 0.619 -0.0700000000000002, 0.634 -0.0700000000000002, 0.6339999999999998 -0.02, 0.65 -0.02)", + -0.002, 0, OperationBuffer_BufferParameters_JOIN_MITRE, -1, + "LINESTRING (0.39 -0.022, 0.4648556402268155 -0.022, 0.4661407414895839 -0.0221876631893964, 0.4672953866748729 -0.022716134946407, 0.4685176359449585 -0.0236927292232623, 0.4698334593862525 -0.0252379526243584, 0.4924663251198579 -0.0563894198284619, 0.499629444080312 -0.0511851092703384, 0.479075235654203 -0.022894668402962, 0.4785370545613636 -0.022, 0.6169999999999995 -0.022, 0.617 -0.0720000000000002, 0.636 -0.0720000000000002, 0.6359999999999998 -0.022, 0.65 -0.022)") +} + +// ======================================= + +const offsetCurve_equalsTol = 0.05 + +var offsetCurve_testFactory = Geom_NewGeometryFactoryDefault() + +func checkOffsetCurve(t *testing.T, wkt string, distance float64, wktExpected string) { + t.Helper() + checkOffsetCurveWithTolerance(t, wkt, distance, wktExpected, 0.05) +} + +func checkOffsetCurveWithParams(t *testing.T, wkt string, distance float64, quadSegs, joinStyle int, mitreLimit float64, wktExpected string) { + t.Helper() + checkOffsetCurveWithParamsAndTolerance(t, wkt, distance, quadSegs, joinStyle, mitreLimit, wktExpected, offsetCurve_equalsTol) +} + +func checkOffsetCurveJoined(t *testing.T, wkt string, distance float64, wktExpected string) { + t.Helper() + geom := offsetCurveRead(wkt) + result := OperationBuffer_OffsetCurve_GetCurveJoined(geom, distance) + //System.out.println(result); + + if wktExpected == "" { + return + } + + expected := offsetCurveRead(wktExpected) + offsetCurveCheckEqual(t, expected, result, offsetCurve_equalsTol) +} + +func checkOffsetCurveWithTolerance(t *testing.T, wkt string, distance float64, wktExpected string, tolerance float64) { + t.Helper() + geom := offsetCurveRead(wkt) + result := OperationBuffer_OffsetCurve_GetCurve(geom, distance) + //System.out.println(result); + + if wktExpected == "" { + return + } + + expected := offsetCurveRead(wktExpected) + offsetCurveCheckEqual(t, expected, result, tolerance) +} + +func checkOffsetCurveWithParamsAndTolerance(t *testing.T, wkt string, distance float64, quadSegs, joinStyle int, mitreLimit float64, wktExpected string, tolerance float64) { + t.Helper() + geom := offsetCurveRead(wkt) + result := OperationBuffer_OffsetCurve_GetCurveWithParams(geom, distance, quadSegs, joinStyle, mitreLimit) + //System.out.println(result); + + if wktExpected == "" { + return + } + + expected := offsetCurveRead(wktExpected) + offsetCurveCheckEqual(t, expected, result, tolerance) +} + +func checkOffsetCurveNoExpected(t *testing.T, wkt string, distance float64) { + t.Helper() + geom := offsetCurveRead(wkt) + OperationBuffer_OffsetCurve_GetCurve(geom, distance) + // Just verifies no infinite loop or panic occurs +} + +func offsetCurveRead(wkt string) *Geom_Geometry { + reader := Io_NewWKTReaderWithFactory(offsetCurve_testFactory) + g, err := reader.Read(wkt) + if err != nil { + panic(err) + } + return g +} + +func offsetCurveCheckEqual(t *testing.T, expected, actual *Geom_Geometry, tolerance float64) { + t.Helper() + if expected.IsEmpty() && actual.IsEmpty() { + return + } + if expected.IsEmpty() != actual.IsEmpty() { + t.Fatalf("expected empty=%v, got empty=%v\nexpected: %s\ngot: %s", + expected.IsEmpty(), actual.IsEmpty(), + expected.String(), actual.String()) + } + if !expected.EqualsNorm(actual) { + hd := AlgorithmDistance_DiscreteHausdorffDistance_Distance(expected, actual) + if hd > tolerance { + t.Fatalf("Hausdorff distance %v exceeds tolerance %v\nexpected: %s\ngot: %s", + hd, tolerance, + expected.String(), actual.String()) + } + } +} diff --git a/internal/jtsport/jts/operation_buffer_offset_segment_generator.go b/internal/jtsport/jts/operation_buffer_offset_segment_generator.go new file mode 100644 index 00000000..b1feb243 --- /dev/null +++ b/internal/jtsport/jts/operation_buffer_offset_segment_generator.go @@ -0,0 +1,515 @@ +package jts + +import "math" + +// operationBuffer_OffsetSegmentGenerator_OFFSET_SEGMENT_SEPARATION_FACTOR is the factor controlling how close offset segments can be to +// skip adding a fillet or mitre. +// This eliminates very short fillet segments, +// reduces the number of offset curve vertices. +// and improves the robustness of mitre construction. +const operationBuffer_OffsetSegmentGenerator_OFFSET_SEGMENT_SEPARATION_FACTOR = 0.05 + +// operationBuffer_OffsetSegmentGenerator_INSIDE_TURN_VERTEX_SNAP_DISTANCE_FACTOR is the factor controlling how close curve vertices on inside turns can be to be snapped. +const operationBuffer_OffsetSegmentGenerator_INSIDE_TURN_VERTEX_SNAP_DISTANCE_FACTOR = 1.0e-3 + +// operationBuffer_OffsetSegmentGenerator_CURVE_VERTEX_SNAP_DISTANCE_FACTOR is the factor which controls how close curve vertices can be to be snapped. +const operationBuffer_OffsetSegmentGenerator_CURVE_VERTEX_SNAP_DISTANCE_FACTOR = 1.0e-6 + +// operationBuffer_OffsetSegmentGenerator_MAX_CLOSING_SEG_LEN_FACTOR is the factor which determines how short closing segs can be for round buffers. +const operationBuffer_OffsetSegmentGenerator_MAX_CLOSING_SEG_LEN_FACTOR = 80 + +// operationBuffer_OffsetSegmentGenerator generates segments which form an offset curve. +// Supports all end cap and join options provided for buffering. +// This algorithm implements various heuristics to produce smoother, simpler curves which are +// still within a reasonable tolerance of the true curve. +type operationBuffer_OffsetSegmentGenerator struct { + // maxCurveSegmentError is the max error of approximation (distance) between a quad segment and the true fillet curve. + maxCurveSegmentError float64 + + // filletAngleQuantum is the angle quantum with which to approximate a fillet curve + // (based on the input # of quadrant segments). + filletAngleQuantum float64 + + // closingSegLengthFactor controls how long "closing segments" are. + // Closing segments are added at the middle of inside corners to ensure a smoother + // boundary for the buffer offset curve. + // In some cases (particularly for round joins with default-or-better + // quantization) the closing segments can be made quite short. + // This substantially improves performance (due to fewer intersections being created). + // + // A closingSegFactor of 0 results in lines to the corner vertex + // A closingSegFactor of 1 results in lines halfway to the corner vertex + // A closingSegFactor of 80 results in lines 1/81 of the way to the corner vertex + // (this option is reasonable for the very common default situation of round joins + // and quadrantSegs >= 8) + closingSegLengthFactor int + + segList *operationBuffer_OffsetSegmentString + distance float64 + precisionModel *Geom_PrecisionModel + bufParams *OperationBuffer_BufferParameters + li *Algorithm_LineIntersector + + s0 *Geom_Coordinate + s1 *Geom_Coordinate + s2 *Geom_Coordinate + seg0 *Geom_LineSegment + seg1 *Geom_LineSegment + offset0 *Geom_LineSegment + offset1 *Geom_LineSegment + side int + + hasNarrowConcaveAngle bool +} + +// operationBuffer_newOffsetSegmentGenerator creates a new OffsetSegmentGenerator. +func operationBuffer_newOffsetSegmentGenerator(precisionModel *Geom_PrecisionModel, bufParams *OperationBuffer_BufferParameters, distance float64) *operationBuffer_OffsetSegmentGenerator { + osg := &operationBuffer_OffsetSegmentGenerator{ + precisionModel: precisionModel, + bufParams: bufParams, + closingSegLengthFactor: 1, + seg0: Geom_NewLineSegment(), + seg1: Geom_NewLineSegment(), + offset0: Geom_NewLineSegment(), + offset1: Geom_NewLineSegment(), + side: 0, + } + + // compute intersections in full precision, to provide accuracy + // the points are rounded as they are inserted into the curve line + osg.li = Algorithm_NewRobustLineIntersector().Algorithm_LineIntersector + + quadSegs := bufParams.GetQuadrantSegments() + if quadSegs < 1 { + quadSegs = 1 + } + osg.filletAngleQuantum = Algorithm_Angle_PiOver2 / float64(quadSegs) + + // Non-round joins cause issues with short closing segments, so don't use + // them. In any case, non-round joins only really make sense for relatively + // small buffer distances. + if bufParams.GetQuadrantSegments() >= 8 && bufParams.GetJoinStyle() == OperationBuffer_BufferParameters_JOIN_ROUND { + osg.closingSegLengthFactor = operationBuffer_OffsetSegmentGenerator_MAX_CLOSING_SEG_LEN_FACTOR + } + osg.init(distance) + return osg +} + +// HasNarrowConcaveAngle tests whether the input has a narrow concave angle +// (relative to the offset distance). +// In this case the generated offset curve will contain self-intersections +// and heuristic closing segments. +// This is expected behaviour in the case of Buffer curves. +// For pure Offset Curves, the output needs to be further treated before it can be used. +func (osg *operationBuffer_OffsetSegmentGenerator) HasNarrowConcaveAngle() bool { + return osg.hasNarrowConcaveAngle +} + +func (osg *operationBuffer_OffsetSegmentGenerator) init(distance float64) { + osg.distance = math.Abs(distance) + osg.maxCurveSegmentError = osg.distance * (1 - math.Cos(osg.filletAngleQuantum/2.0)) + osg.segList = operationBuffer_newOffsetSegmentString() + osg.segList.SetPrecisionModel(osg.precisionModel) + // Choose the min vertex separation as a small fraction of the offset distance. + osg.segList.SetMinimumVertexDistance(osg.distance * operationBuffer_OffsetSegmentGenerator_CURVE_VERTEX_SNAP_DISTANCE_FACTOR) +} + +// InitSideSegments initializes the side segments for offset curve generation. +func (osg *operationBuffer_OffsetSegmentGenerator) InitSideSegments(s1 *Geom_Coordinate, s2 *Geom_Coordinate, side int) { + osg.s1 = s1 + osg.s2 = s2 + osg.side = side + osg.seg1.SetCoordinates(s1, s2) + operationBuffer_OffsetSegmentGenerator_computeOffsetSegment(osg.seg1, side, osg.distance, osg.offset1) +} + +// GetCoordinates returns the coordinates of the offset curve. +func (osg *operationBuffer_OffsetSegmentGenerator) GetCoordinates() []*Geom_Coordinate { + pts := osg.segList.GetCoordinates() + return pts +} + +// CloseRing closes the ring. +func (osg *operationBuffer_OffsetSegmentGenerator) CloseRing() { + osg.segList.CloseRing() +} + +// AddSegments adds an array of segments to the offset curve. +func (osg *operationBuffer_OffsetSegmentGenerator) AddSegments(pt []*Geom_Coordinate, isForward bool) { + osg.segList.AddPts(pt, isForward) +} + +// AddFirstSegment adds the first segment point. +func (osg *operationBuffer_OffsetSegmentGenerator) AddFirstSegment() { + osg.segList.AddPt(osg.offset1.P0) +} + +// AddLastSegment adds the last offset point. +func (osg *operationBuffer_OffsetSegmentGenerator) AddLastSegment() { + osg.segList.AddPt(osg.offset1.P1) +} + +// AddNextSegment adds the next segment to the offset curve. +func (osg *operationBuffer_OffsetSegmentGenerator) AddNextSegment(p *Geom_Coordinate, addStartPoint bool) { + // s0-s1-s2 are the coordinates of the previous segment and the current one + osg.s0 = osg.s1 + osg.s1 = osg.s2 + osg.s2 = p + osg.seg0.SetCoordinates(osg.s0, osg.s1) + operationBuffer_OffsetSegmentGenerator_computeOffsetSegment(osg.seg0, osg.side, osg.distance, osg.offset0) + osg.seg1.SetCoordinates(osg.s1, osg.s2) + operationBuffer_OffsetSegmentGenerator_computeOffsetSegment(osg.seg1, osg.side, osg.distance, osg.offset1) + + // do nothing if points are equal + if osg.s1.Equals(osg.s2) { + return + } + + orientation := Algorithm_Orientation_Index(osg.s0, osg.s1, osg.s2) + outsideTurn := (orientation == Algorithm_Orientation_Clockwise && osg.side == Geom_Position_Left) || + (orientation == Algorithm_Orientation_Counterclockwise && osg.side == Geom_Position_Right) + + if orientation == 0 { // lines are collinear + osg.addCollinear(addStartPoint) + } else if outsideTurn { + osg.addOutsideTurn(orientation, addStartPoint) + } else { // inside turn + osg.addInsideTurn(orientation, addStartPoint) + } +} + +func (osg *operationBuffer_OffsetSegmentGenerator) addCollinear(addStartPoint bool) { + // This test could probably be done more efficiently, + // but the situation of exact collinearity should be fairly rare. + osg.li.ComputeIntersection(osg.s0, osg.s1, osg.s1, osg.s2) + numInt := osg.li.GetIntersectionNum() + // if numInt is < 2, the lines are parallel and in the same direction. In + // this case the point can be ignored, since the offset lines will also be + // parallel. + if numInt >= 2 { + // segments are collinear but reversing. + // Add an "end-cap" fillet all the way around to other direction. + // This case should ONLY happen for LineStrings, so the orientation is always CW. + // (Polygons can never have two consecutive segments which are parallel but reversed, + // because that would be a self intersection.) + if osg.bufParams.GetJoinStyle() == OperationBuffer_BufferParameters_JOIN_BEVEL || + osg.bufParams.GetJoinStyle() == OperationBuffer_BufferParameters_JOIN_MITRE { + if addStartPoint { + osg.segList.AddPt(osg.offset0.P1) + } + osg.segList.AddPt(osg.offset1.P0) + } else { + osg.addCornerFillet(osg.s1, osg.offset0.P1, osg.offset1.P0, Algorithm_Orientation_Clockwise, osg.distance) + } + } +} + +// addOutsideTurn adds the offset points for an outside (convex) turn. +func (osg *operationBuffer_OffsetSegmentGenerator) addOutsideTurn(orientation int, addStartPoint bool) { + // Heuristic: If offset endpoints are very close together, + // (which happens for nearly-parallel segments), + // use an endpoint as the single offset corner vertex. + // This eliminates very short single-segment joins + // and reduces the number of offset curve vertices. + // This also avoids robustness problems with computing mitre corners + // for nearly-parallel segments. + if osg.offset0.P1.Distance(osg.offset1.P0) < osg.distance*operationBuffer_OffsetSegmentGenerator_OFFSET_SEGMENT_SEPARATION_FACTOR { + // use endpoint of longest segment, to reduce change in area + segLen0 := osg.s0.Distance(osg.s1) + segLen1 := osg.s1.Distance(osg.s2) + var offsetPt *Geom_Coordinate + if segLen0 > segLen1 { + offsetPt = osg.offset0.P1 + } else { + offsetPt = osg.offset1.P0 + } + osg.segList.AddPt(offsetPt) + return + } + + if osg.bufParams.GetJoinStyle() == OperationBuffer_BufferParameters_JOIN_MITRE { + osg.addMitreJoin(osg.s1, osg.offset0, osg.offset1, osg.distance) + } else if osg.bufParams.GetJoinStyle() == OperationBuffer_BufferParameters_JOIN_BEVEL { + osg.addBevelJoin(osg.offset0, osg.offset1) + } else { + // add a circular fillet connecting the endpoints of the offset segments + if addStartPoint { + osg.segList.AddPt(osg.offset0.P1) + } + osg.addCornerFillet(osg.s1, osg.offset0.P1, osg.offset1.P0, orientation, osg.distance) + osg.segList.AddPt(osg.offset1.P0) + } +} + +// addInsideTurn adds the offset points for an inside (concave) turn. +func (osg *operationBuffer_OffsetSegmentGenerator) addInsideTurn(orientation int, addStartPoint bool) { + // add intersection point of offset segments (if any) + osg.li.ComputeIntersection(osg.offset0.P0, osg.offset0.P1, osg.offset1.P0, osg.offset1.P1) + if osg.li.HasIntersection() { + osg.segList.AddPt(osg.li.GetIntersection(0)) + } else { + // If no intersection is detected, + // it means the angle is so small and/or the offset so + // large that the offsets segments don't intersect. + // In this case we must add a "closing segment" to make sure the buffer curve is continuous, + // fairly smooth (e.g. no sharp reversals in direction) + // and tracks the buffer correctly around the corner. The curve connects + // the endpoints of the segment offsets to points + // which lie toward the centre point of the corner. + // The joining curve will not appear in the final buffer outline, since it + // is completely internal to the buffer polygon. + // + // In complex buffer cases the closing segment may cut across many other + // segments in the generated offset curve. In order to improve the + // performance of the noding, the closing segment should be kept as short as possible. + // (But not too short, since that would defeat its purpose). + // This is the purpose of the closingSegFactor heuristic value. + + // The intersection test above is vulnerable to robustness errors; i.e. it + // may be that the offsets should intersect very close to their endpoints, + // but aren't reported as such due to rounding. To handle this situation + // appropriately, we use the following test: If the offset points are very + // close, don't add closing segments but simply use one of the offset + // points + osg.hasNarrowConcaveAngle = true + if osg.offset0.P1.Distance(osg.offset1.P0) < osg.distance*operationBuffer_OffsetSegmentGenerator_INSIDE_TURN_VERTEX_SNAP_DISTANCE_FACTOR { + osg.segList.AddPt(osg.offset0.P1) + } else { + // add endpoint of this segment offset + osg.segList.AddPt(osg.offset0.P1) + + // Add "closing segment" of required length. + if osg.closingSegLengthFactor > 0 { + mid0 := Geom_NewCoordinateWithXY( + (float64(osg.closingSegLengthFactor)*osg.offset0.P1.X+osg.s1.X)/float64(osg.closingSegLengthFactor+1), + (float64(osg.closingSegLengthFactor)*osg.offset0.P1.Y+osg.s1.Y)/float64(osg.closingSegLengthFactor+1)) + osg.segList.AddPt(mid0) + mid1 := Geom_NewCoordinateWithXY( + (float64(osg.closingSegLengthFactor)*osg.offset1.P0.X+osg.s1.X)/float64(osg.closingSegLengthFactor+1), + (float64(osg.closingSegLengthFactor)*osg.offset1.P0.Y+osg.s1.Y)/float64(osg.closingSegLengthFactor+1)) + osg.segList.AddPt(mid1) + } else { + // This branch is not expected to be used except for testing purposes. + // It is equivalent to the JTS 1.9 logic for closing segments + // (which results in very poor performance for large buffer distances) + osg.segList.AddPt(osg.s1) + } + + // add start point of next segment offset + osg.segList.AddPt(osg.offset1.P0) + } + } +} + +// operationBuffer_OffsetSegmentGenerator_computeOffsetSegment computes an offset segment for an input segment on a given side and at a given distance. +// The offset points are computed in full double precision, for accuracy. +func operationBuffer_OffsetSegmentGenerator_computeOffsetSegment(seg *Geom_LineSegment, side int, distance float64, offset *Geom_LineSegment) { + sideSign := 1 + if side != Geom_Position_Left { + sideSign = -1 + } + dx := seg.P1.X - seg.P0.X + dy := seg.P1.Y - seg.P0.Y + length := math.Hypot(dx, dy) + // u is the vector that is the length of the offset, in the direction of the segment + ux := float64(sideSign) * distance * dx / length + uy := float64(sideSign) * distance * dy / length + offset.P0.X = seg.P0.X - uy + offset.P0.Y = seg.P0.Y + ux + offset.P1.X = seg.P1.X - uy + offset.P1.Y = seg.P1.Y + ux +} + +// AddLineEndCap adds an end cap around point p1, terminating a line segment coming from p0. +func (osg *operationBuffer_OffsetSegmentGenerator) AddLineEndCap(p0 *Geom_Coordinate, p1 *Geom_Coordinate) { + seg := Geom_NewLineSegmentFromCoordinates(p0, p1) + + offsetL := Geom_NewLineSegment() + operationBuffer_OffsetSegmentGenerator_computeOffsetSegment(seg, Geom_Position_Left, osg.distance, offsetL) + offsetR := Geom_NewLineSegment() + operationBuffer_OffsetSegmentGenerator_computeOffsetSegment(seg, Geom_Position_Right, osg.distance, offsetR) + + dx := p1.X - p0.X + dy := p1.Y - p0.Y + angle := math.Atan2(dy, dx) + + switch osg.bufParams.GetEndCapStyle() { + case OperationBuffer_BufferParameters_CAP_ROUND: + // add offset seg points with a fillet between them + osg.segList.AddPt(offsetL.P1) + osg.addDirectedFillet(p1, angle+Algorithm_Angle_PiOver2, angle-Algorithm_Angle_PiOver2, Algorithm_Orientation_Clockwise, osg.distance) + osg.segList.AddPt(offsetR.P1) + case OperationBuffer_BufferParameters_CAP_FLAT: + // only offset segment points are added + osg.segList.AddPt(offsetL.P1) + osg.segList.AddPt(offsetR.P1) + case OperationBuffer_BufferParameters_CAP_SQUARE: + // add a square defined by extensions of the offset segment endpoints + squareCapSideOffset := Geom_NewCoordinate() + squareCapSideOffset.X = math.Abs(osg.distance) * Algorithm_Angle_CosSnap(angle) + squareCapSideOffset.Y = math.Abs(osg.distance) * Algorithm_Angle_SinSnap(angle) + + squareCapLOffset := Geom_NewCoordinateWithXY( + offsetL.P1.X+squareCapSideOffset.X, + offsetL.P1.Y+squareCapSideOffset.Y) + squareCapROffset := Geom_NewCoordinateWithXY( + offsetR.P1.X+squareCapSideOffset.X, + offsetR.P1.Y+squareCapSideOffset.Y) + osg.segList.AddPt(squareCapLOffset) + osg.segList.AddPt(squareCapROffset) + } +} + +// addMitreJoin adds a mitre join connecting two convex offset segments. +// The mitre is beveled if it exceeds the mitre limit factor. +// The mitre limit is intended to prevent extremely long corners occurring. +// If the mitre limit is very small it can cause unwanted artifacts around fairly flat corners. +// This is prevented by using a simple bevel join in this case. +// In other words, the limit prevents the corner from getting too long, +// but it won't force it to be very short/flat. +func (osg *operationBuffer_OffsetSegmentGenerator) addMitreJoin(cornerPt *Geom_Coordinate, offset0 *Geom_LineSegment, offset1 *Geom_LineSegment, distance float64) { + mitreLimitDistance := osg.bufParams.GetMitreLimit() * distance + // First try a non-beveled join. + // Compute the intersection point of the lines determined by the offsets. + // Parallel or collinear lines will return a null point ==> need to be beveled + // + // Note: This computation is unstable if the offset segments are nearly collinear. + // However, this situation should have been eliminated earlier by the check + // for whether the offset segment endpoints are almost coincident + intPt := Algorithm_Intersection_Intersection(offset0.P0, offset0.P1, offset1.P0, offset1.P1) + if intPt != nil && intPt.Distance(cornerPt) <= mitreLimitDistance { + osg.segList.AddPt(intPt) + return + } + // In case the mitre limit is very small, try a plain bevel. + // Use it if it's further than the limit. + bevelDist := Algorithm_Distance_PointToSegment(cornerPt, offset0.P1, offset1.P0) + if bevelDist >= mitreLimitDistance { + osg.addBevelJoin(offset0, offset1) + return + } + // Have to construct a limited mitre bevel. + osg.addLimitedMitreJoin(offset0, offset1, distance, mitreLimitDistance) +} + +// addLimitedMitreJoin adds a limited mitre join connecting two convex offset segments. +// A limited mitre join is beveled at the distance determined by the mitre limit factor, +// or as a standard bevel join, whichever is further. +func (osg *operationBuffer_OffsetSegmentGenerator) addLimitedMitreJoin(offset0 *Geom_LineSegment, offset1 *Geom_LineSegment, distance float64, mitreLimitDistance float64) { + cornerPt := osg.seg0.P1 + // oriented angle of the corner formed by segments + angInterior := Algorithm_Angle_AngleBetweenOriented(osg.seg0.P0, cornerPt, osg.seg1.P1) + // half of the interior angle + angInterior2 := angInterior / 2 + + // direction of bisector of the interior angle between the segments + dir0 := Algorithm_Angle_AngleBetweenPoints(cornerPt, osg.seg0.P0) + dirBisector := Algorithm_Angle_Normalize(dir0 + angInterior2) + + // midpoint of the bevel segment + bevelMidPt := operationBuffer_OffsetSegmentGenerator_project(cornerPt, -mitreLimitDistance, dirBisector) + + // direction of bevel segment (at right angle to corner bisector) + dirBevel := Algorithm_Angle_Normalize(dirBisector + Algorithm_Angle_PiOver2) + + // compute the candidate bevel segment by projecting both sides of the midpoint + bevel0 := operationBuffer_OffsetSegmentGenerator_project(bevelMidPt, distance, dirBevel) + bevel1 := operationBuffer_OffsetSegmentGenerator_project(bevelMidPt, distance, dirBevel+math.Pi) + + // compute actual bevel segment between the offset lines + bevelInt0 := Algorithm_Intersection_LineSegment(offset0.P0, offset0.P1, bevel0, bevel1) + bevelInt1 := Algorithm_Intersection_LineSegment(offset1.P0, offset1.P1, bevel0, bevel1) + + // add the limited bevel, if it intersects the offsets + if bevelInt0 != nil && bevelInt1 != nil { + osg.segList.AddPt(bevelInt0) + osg.segList.AddPt(bevelInt1) + return + } + // If the corner is very flat or the mitre limit is very small + // the limited bevel segment may not intersect the offsets. + // In this case just bevel the join. + osg.addBevelJoin(offset0, offset1) +} + +// operationBuffer_OffsetSegmentGenerator_project projects a point to a given distance in a given direction angle. +func operationBuffer_OffsetSegmentGenerator_project(pt *Geom_Coordinate, d float64, dir float64) *Geom_Coordinate { + x := pt.X + d*Algorithm_Angle_CosSnap(dir) + y := pt.Y + d*Algorithm_Angle_SinSnap(dir) + return Geom_NewCoordinateWithXY(x, y) +} + +// addBevelJoin adds a bevel join connecting two offset segments around a convex corner. +func (osg *operationBuffer_OffsetSegmentGenerator) addBevelJoin(offset0 *Geom_LineSegment, offset1 *Geom_LineSegment) { + osg.segList.AddPt(offset0.P1) + osg.segList.AddPt(offset1.P0) +} + +// addCornerFillet adds points for a circular fillet around a convex corner. +// Adds the start and end points. +func (osg *operationBuffer_OffsetSegmentGenerator) addCornerFillet(p *Geom_Coordinate, p0 *Geom_Coordinate, p1 *Geom_Coordinate, direction int, radius float64) { + dx0 := p0.X - p.X + dy0 := p0.Y - p.Y + startAngle := math.Atan2(dy0, dx0) + dx1 := p1.X - p.X + dy1 := p1.Y - p.Y + endAngle := math.Atan2(dy1, dx1) + + if direction == Algorithm_Orientation_Clockwise { + if startAngle <= endAngle { + startAngle += Algorithm_Angle_PiTimes2 + } + } else { // direction == COUNTERCLOCKWISE + if startAngle >= endAngle { + startAngle -= Algorithm_Angle_PiTimes2 + } + } + osg.segList.AddPt(p0) + osg.addDirectedFillet(p, startAngle, endAngle, direction, radius) + osg.segList.AddPt(p1) +} + +// addDirectedFillet adds points for a circular fillet arc between two specified angles. +// The start and end point for the fillet are not added - the caller must add them if required. +func (osg *operationBuffer_OffsetSegmentGenerator) addDirectedFillet(p *Geom_Coordinate, startAngle float64, endAngle float64, direction int, radius float64) { + directionFactor := -1 + if direction != Algorithm_Orientation_Clockwise { + directionFactor = 1 + } + + totalAngle := math.Abs(startAngle - endAngle) + nSegs := int(totalAngle/osg.filletAngleQuantum + 0.5) + + if nSegs < 1 { + return // no segments because angle is less than increment - nothing to do! + } + + // choose angle increment so that each segment has equal length + angleInc := totalAngle / float64(nSegs) + + pt := Geom_NewCoordinate() + for i := 0; i < nSegs; i++ { + angle := startAngle + float64(directionFactor)*float64(i)*angleInc + pt.X = p.X + radius*Algorithm_Angle_CosSnap(angle) + pt.Y = p.Y + radius*Algorithm_Angle_SinSnap(angle) + osg.segList.AddPt(pt) + } +} + +// CreateCircle creates a CW circle around a point. +func (osg *operationBuffer_OffsetSegmentGenerator) CreateCircle(p *Geom_Coordinate) { + // add start point + pt := Geom_NewCoordinateWithXY(p.X+osg.distance, p.Y) + osg.segList.AddPt(pt) + osg.addDirectedFillet(p, 0.0, Algorithm_Angle_PiTimes2, -1, osg.distance) + osg.segList.CloseRing() +} + +// CreateSquare creates a CW square around a point. +func (osg *operationBuffer_OffsetSegmentGenerator) CreateSquare(p *Geom_Coordinate) { + osg.segList.AddPt(Geom_NewCoordinateWithXY(p.X+osg.distance, p.Y+osg.distance)) + osg.segList.AddPt(Geom_NewCoordinateWithXY(p.X+osg.distance, p.Y-osg.distance)) + osg.segList.AddPt(Geom_NewCoordinateWithXY(p.X-osg.distance, p.Y-osg.distance)) + osg.segList.AddPt(Geom_NewCoordinateWithXY(p.X-osg.distance, p.Y+osg.distance)) + osg.segList.CloseRing() +} diff --git a/internal/jtsport/jts/operation_buffer_offset_segment_string.go b/internal/jtsport/jts/operation_buffer_offset_segment_string.go new file mode 100644 index 00000000..fba5f09d --- /dev/null +++ b/internal/jtsport/jts/operation_buffer_offset_segment_string.go @@ -0,0 +1,100 @@ +package jts + +// operationBuffer_OffsetSegmentString is a dynamic list of the vertices in a constructed offset curve. +// Automatically removes adjacent vertices which are closer than a given tolerance. +type operationBuffer_OffsetSegmentString struct { + ptList []*Geom_Coordinate + precisionModel *Geom_PrecisionModel + + // minimimVertexDistance is the distance below which two adjacent points on the curve + // are considered to be coincident. + // This is chosen to be a small fraction of the offset distance. + minimimVertexDistance float64 +} + +// operationBuffer_newOffsetSegmentString creates a new OffsetSegmentString. +func operationBuffer_newOffsetSegmentString() *operationBuffer_OffsetSegmentString { + return &operationBuffer_OffsetSegmentString{ + ptList: make([]*Geom_Coordinate, 0), + } +} + +// SetPrecisionModel sets the precision model for this offset segment string. +func (oss *operationBuffer_OffsetSegmentString) SetPrecisionModel(precisionModel *Geom_PrecisionModel) { + oss.precisionModel = precisionModel +} + +// SetMinimumVertexDistance sets the minimum vertex distance. +func (oss *operationBuffer_OffsetSegmentString) SetMinimumVertexDistance(minimimVertexDistance float64) { + oss.minimimVertexDistance = minimimVertexDistance +} + +// AddPt adds a point to the offset segment string. +func (oss *operationBuffer_OffsetSegmentString) AddPt(pt *Geom_Coordinate) { + bufPt := Geom_NewCoordinateFromCoordinate(pt) + oss.precisionModel.MakePreciseCoordinate(bufPt) + // don't add duplicate (or near-duplicate) points + if oss.isRedundant(bufPt) { + return + } + oss.ptList = append(oss.ptList, bufPt) +} + +// AddPts adds an array of points to the offset segment string. +func (oss *operationBuffer_OffsetSegmentString) AddPts(pt []*Geom_Coordinate, isForward bool) { + if isForward { + for i := 0; i < len(pt); i++ { + oss.AddPt(pt[i]) + } + } else { + for i := len(pt) - 1; i >= 0; i-- { + oss.AddPt(pt[i]) + } + } +} + +// isRedundant tests whether the given point is redundant +// relative to the previous point in the list (up to tolerance). +func (oss *operationBuffer_OffsetSegmentString) isRedundant(pt *Geom_Coordinate) bool { + if len(oss.ptList) < 1 { + return false + } + lastPt := oss.ptList[len(oss.ptList)-1] + ptDist := pt.Distance(lastPt) + if ptDist < oss.minimimVertexDistance { + return true + } + return false +} + +// CloseRing closes the ring by adding the first point at the end if needed. +func (oss *operationBuffer_OffsetSegmentString) CloseRing() { + if len(oss.ptList) < 1 { + return + } + startPt := Geom_NewCoordinateFromCoordinate(oss.ptList[0]) + lastPt := oss.ptList[len(oss.ptList)-1] + if startPt.Equals(lastPt) { + return + } + oss.ptList = append(oss.ptList, startPt) +} + +// Reverse reverses the order of points in the list. +func (oss *operationBuffer_OffsetSegmentString) Reverse() { + // The Java implementation is empty, so we keep it empty too +} + +// GetCoordinates returns the coordinates as an array. +func (oss *operationBuffer_OffsetSegmentString) GetCoordinates() []*Geom_Coordinate { + coord := make([]*Geom_Coordinate, len(oss.ptList)) + copy(coord, oss.ptList) + return coord +} + +// String returns a string representation of this offset segment string. +func (oss *operationBuffer_OffsetSegmentString) String() string { + fact := Geom_NewGeometryFactoryDefault() + line := fact.CreateLineStringFromCoordinates(oss.GetCoordinates()) + return Io_WKTWriter_ToLineStringFromCoords(line.GetCoordinates()) +} diff --git a/internal/jtsport/jts/operation_buffer_rightmost_edge_finder.go b/internal/jtsport/jts/operation_buffer_rightmost_edge_finder.go new file mode 100644 index 00000000..8e32306d --- /dev/null +++ b/internal/jtsport/jts/operation_buffer_rightmost_edge_finder.go @@ -0,0 +1,143 @@ +package jts + +import "github.com/peterstace/simplefeatures/internal/jtsport/java" + +// operationBuffer_RightmostEdgeFinder finds the DirectedEdge in a list which has the highest coordinate, +// and which is oriented L to R at that point. (I.e. the right side is on the RHS of the edge.) +type operationBuffer_RightmostEdgeFinder struct { + minIndex int + minCoord *Geom_Coordinate + minDe *Geomgraph_DirectedEdge + orientedDe *Geomgraph_DirectedEdge +} + +// operationBuffer_newRightmostEdgeFinder creates a RightmostEdgeFinder. +// A RightmostEdgeFinder finds the DirectedEdge with the rightmost coordinate. +// The DirectedEdge returned is guaranteed to have the R of the world on its RHS. +func operationBuffer_newRightmostEdgeFinder() *operationBuffer_RightmostEdgeFinder { + return &operationBuffer_RightmostEdgeFinder{ + minIndex: -1, + } +} + +// GetEdge returns the rightmost edge. +func (ref *operationBuffer_RightmostEdgeFinder) GetEdge() *Geomgraph_DirectedEdge { + return ref.orientedDe +} + +// GetCoordinate returns the coordinate of the rightmost edge. +func (ref *operationBuffer_RightmostEdgeFinder) GetCoordinate() *Geom_Coordinate { + return ref.minCoord +} + +// FindEdge finds the rightmost edge in the list. +func (ref *operationBuffer_RightmostEdgeFinder) FindEdge(dirEdgeList []*Geomgraph_DirectedEdge) { + // Check all forward DirectedEdges only. This is still general, + // because each edge has a forward DirectedEdge. + for _, de := range dirEdgeList { + if !de.IsForward() { + continue + } + ref.checkForRightmostCoordinate(de) + } + + // If the rightmost point is a node, we need to identify which of + // the incident edges is rightmost. + Util_Assert_IsTrueWithMessage(ref.minIndex != 0 || ref.minCoord.Equals(ref.minDe.GetCoordinate()), "inconsistency in rightmost processing") + if ref.minIndex == 0 { + ref.findRightmostEdgeAtNode() + } else { + ref.findRightmostEdgeAtVertex() + } + // now check that the extreme side is the R side. + // If not, use the sym instead. + ref.orientedDe = ref.minDe + rightmostSide := ref.getRightmostSide(ref.minDe, ref.minIndex) + if rightmostSide == Geom_Position_Left { + ref.orientedDe = ref.minDe.GetSym() + } +} + +func (ref *operationBuffer_RightmostEdgeFinder) findRightmostEdgeAtNode() { + node := ref.minDe.GetNode() + star := java.Cast[*Geomgraph_DirectedEdgeStar](node.GetEdges()) + ref.minDe = star.GetRightmostEdge() + // the DirectedEdge returned by the previous call is not + // necessarily in the forward direction. Use the sym edge if it isn't. + if !ref.minDe.IsForward() { + ref.minDe = ref.minDe.GetSym() + ref.minIndex = len(ref.minDe.GetEdge().GetCoordinates()) - 1 + } +} + +func (ref *operationBuffer_RightmostEdgeFinder) findRightmostEdgeAtVertex() { + // The rightmost point is an interior vertex, so it has a segment on either side of it. + // If these segments are both above or below the rightmost point, we need to + // determine their relative orientation to decide which is rightmost. + pts := ref.minDe.GetEdge().GetCoordinates() + if ref.minIndex <= 0 || ref.minIndex >= len(pts) { + panic("rightmost point expected to be interior vertex of edge") + } + pPrev := pts[ref.minIndex-1] + pNext := pts[ref.minIndex+1] + orientation := Algorithm_Orientation_Index(ref.minCoord, pNext, pPrev) + usePrev := false + // both segments are below min point + if pPrev.Y < ref.minCoord.Y && pNext.Y < ref.minCoord.Y && + orientation == Algorithm_Orientation_Counterclockwise { + usePrev = true + } else if pPrev.Y > ref.minCoord.Y && pNext.Y > ref.minCoord.Y && + orientation == Algorithm_Orientation_Clockwise { + usePrev = true + } + // if both segments are on the same side, do nothing - either is safe + // to select as a rightmost segment + if usePrev { + ref.minIndex = ref.minIndex - 1 + } +} + +func (ref *operationBuffer_RightmostEdgeFinder) checkForRightmostCoordinate(de *Geomgraph_DirectedEdge) { + coord := de.GetEdge().GetCoordinates() + for i := 0; i < len(coord)-1; i++ { + // only check vertices which are the start or end point of a non-horizontal segment + // MD 19 Sep 03 - NO! we can test all vertices, since the rightmost must have a non-horiz segment adjacent to it + if ref.minCoord == nil || coord[i].X > ref.minCoord.X { + ref.minDe = de + ref.minIndex = i + ref.minCoord = coord[i] + } + } +} + +func (ref *operationBuffer_RightmostEdgeFinder) getRightmostSide(de *Geomgraph_DirectedEdge, index int) int { + side := ref.getRightmostSideOfSegment(de, index) + if side < 0 { + side = ref.getRightmostSideOfSegment(de, index-1) + } + if side < 0 { + // reaching here can indicate that segment is horizontal + // testing only + ref.minCoord = nil + ref.checkForRightmostCoordinate(de) + } + return side +} + +func (ref *operationBuffer_RightmostEdgeFinder) getRightmostSideOfSegment(de *Geomgraph_DirectedEdge, i int) int { + e := de.GetEdge() + coord := e.GetCoordinates() + + if i < 0 || i+1 >= len(coord) { + return -1 + } + if coord[i].Y == coord[i+1].Y { + return -1 // indicates edge is parallel to x-axis + } + + pos := Geom_Position_Left + if coord[i].Y < coord[i+1].Y { + pos = Geom_Position_Right + } + return pos +} diff --git a/internal/jtsport/jts/operation_buffer_segment_mc_index.go b/internal/jtsport/jts/operation_buffer_segment_mc_index.go new file mode 100644 index 00000000..614abc76 --- /dev/null +++ b/internal/jtsport/jts/operation_buffer_segment_mc_index.go @@ -0,0 +1,31 @@ +package jts + +// operationBuffer_SegmentMCIndex is a spatial index over a segment sequence +// using MonotoneChains. +type operationBuffer_SegmentMCIndex struct { + index *IndexStrtree_STRtree +} + +// operationBuffer_newSegmentMCIndex creates a new SegmentMCIndex for the given coordinates. +func operationBuffer_newSegmentMCIndex(segs []*Geom_Coordinate) *operationBuffer_SegmentMCIndex { + smi := &operationBuffer_SegmentMCIndex{} + smi.index = smi.buildIndex(segs) + return smi +} + +func (smi *operationBuffer_SegmentMCIndex) buildIndex(segs []*Geom_Coordinate) *IndexStrtree_STRtree { + index := IndexStrtree_NewSTRtree() + segChains := IndexChain_MonotoneChainBuilder_GetChainsWithContext(segs, segs) + for _, mc := range segChains { + index.Insert(mc.GetEnvelope(), mc) + } + return index +} + +// Query queries the index with an envelope and calls the action for each matching segment. +func (smi *operationBuffer_SegmentMCIndex) Query(env *Geom_Envelope, action *IndexChain_MonotoneChainSelectAction) { + smi.index.QueryWithVisitor(env, Index_NewItemVisitorFunc(func(item any) { + testChain := item.(*IndexChain_MonotoneChain) + testChain.Select(env, action) + })) +} diff --git a/internal/jtsport/jts/operation_buffer_subgraph_depth_locater.go b/internal/jtsport/jts/operation_buffer_subgraph_depth_locater.go new file mode 100644 index 00000000..93a02078 --- /dev/null +++ b/internal/jtsport/jts/operation_buffer_subgraph_depth_locater.go @@ -0,0 +1,205 @@ +package jts + +import "sort" + +// operationBuffer_SubgraphDepthLocater locates a subgraph inside a set of subgraphs, +// in order to determine the outside depth of the subgraph. +// The input subgraphs are assumed to have had depths +// already calculated for their edges. +type operationBuffer_SubgraphDepthLocater struct { + subgraphs []*OperationBuffer_BufferSubgraph + seg *Geom_LineSegment +} + +// operationBuffer_newSubgraphDepthLocater creates a new SubgraphDepthLocater. +func operationBuffer_newSubgraphDepthLocater(subgraphs []*OperationBuffer_BufferSubgraph) *operationBuffer_SubgraphDepthLocater { + return &operationBuffer_SubgraphDepthLocater{ + subgraphs: subgraphs, + seg: Geom_NewLineSegment(), + } +} + +// GetDepth returns the depth at the given coordinate. +func (sdl *operationBuffer_SubgraphDepthLocater) GetDepth(p *Geom_Coordinate) int { + stabbedSegments := sdl.findStabbedSegments(p) + // if no segments on stabbing line subgraph must be outside all others. + if len(stabbedSegments) == 0 { + return 0 + } + sort.Slice(stabbedSegments, func(i, j int) bool { + return stabbedSegments[i].compareTo(stabbedSegments[j]) < 0 + }) + ds := stabbedSegments[0] + return ds.leftDepth +} + +// findStabbedSegments finds all non-horizontal segments intersecting the stabbing line. +// The stabbing line is the ray to the right of stabbingRayLeftPt. +func (sdl *operationBuffer_SubgraphDepthLocater) findStabbedSegments(stabbingRayLeftPt *Geom_Coordinate) []*operationBuffer_DepthSegment { + stabbedSegments := make([]*operationBuffer_DepthSegment, 0) + for _, bsg := range sdl.subgraphs { + // optimization - don't bother checking subgraphs which the ray does not intersect + env := bsg.GetEnvelope() + if stabbingRayLeftPt.Y < env.GetMinY() || stabbingRayLeftPt.Y > env.GetMaxY() { + continue + } + + sdl.findStabbedSegmentsInList(stabbingRayLeftPt, bsg.GetDirectedEdges(), &stabbedSegments) + } + return stabbedSegments +} + +// findStabbedSegmentsInList finds all non-horizontal segments intersecting the stabbing line +// in the list of dirEdges. +// The stabbing line is the ray to the right of stabbingRayLeftPt. +func (sdl *operationBuffer_SubgraphDepthLocater) findStabbedSegmentsInList(stabbingRayLeftPt *Geom_Coordinate, dirEdges []*Geomgraph_DirectedEdge, stabbedSegments *[]*operationBuffer_DepthSegment) { + // Check all forward DirectedEdges only. This is still general, + // because each Edge has a forward DirectedEdge. + for _, de := range dirEdges { + if !de.IsForward() { + continue + } + sdl.findStabbedSegmentsInEdge(stabbingRayLeftPt, de, stabbedSegments) + } +} + +// findStabbedSegmentsInEdge finds all non-horizontal segments intersecting the stabbing line +// in the input dirEdge. +// The stabbing line is the ray to the right of stabbingRayLeftPt. +func (sdl *operationBuffer_SubgraphDepthLocater) findStabbedSegmentsInEdge(stabbingRayLeftPt *Geom_Coordinate, dirEdge *Geomgraph_DirectedEdge, stabbedSegments *[]*operationBuffer_DepthSegment) { + pts := dirEdge.GetEdge().GetCoordinates() + for i := 0; i < len(pts)-1; i++ { + sdl.seg.P0 = pts[i] + sdl.seg.P1 = pts[i+1] + // ensure segment always points upwards + if sdl.seg.P0.Y > sdl.seg.P1.Y { + sdl.seg.Reverse() + } + + // skip segment if it is left of the stabbing line + maxx := sdl.seg.P0.X + if sdl.seg.P1.X > maxx { + maxx = sdl.seg.P1.X + } + if maxx < stabbingRayLeftPt.X { + continue + } + + // skip horizontal segments (there will be a non-horizontal one carrying the same depth info + if sdl.seg.IsHorizontal() { + continue + } + + // skip if segment is above or below stabbing line + if stabbingRayLeftPt.Y < sdl.seg.P0.Y || stabbingRayLeftPt.Y > sdl.seg.P1.Y { + continue + } + + // skip if stabbing ray is right of the segment + if Algorithm_Orientation_Index(sdl.seg.P0, sdl.seg.P1, stabbingRayLeftPt) == Algorithm_Orientation_Right { + continue + } + + // stabbing line cuts this segment, so record it + depth := dirEdge.GetDepth(Geom_Position_Left) + // if segment direction was flipped, use RHS depth instead + if !sdl.seg.P0.Equals(pts[i]) { + depth = dirEdge.GetDepth(Geom_Position_Right) + } + ds := operationBuffer_newDepthSegment(sdl.seg, depth) + *stabbedSegments = append(*stabbedSegments, ds) + } +} + +// operationBuffer_DepthSegment is a segment from a directed edge which has been assigned a depth value +// for its sides. +type operationBuffer_DepthSegment struct { + upwardSeg *Geom_LineSegment + leftDepth int +} + +// operationBuffer_newDepthSegment creates a new DepthSegment. +func operationBuffer_newDepthSegment(seg *Geom_LineSegment, depth int) *operationBuffer_DepthSegment { + // Assert: input seg is upward (p0.y <= p1.y) + return &operationBuffer_DepthSegment{ + upwardSeg: Geom_NewLineSegmentFromLineSegment(seg), + leftDepth: depth, + } +} + +// isUpward tests if the segment points upward. +func (ds *operationBuffer_DepthSegment) isUpward() bool { + return ds.upwardSeg.P0.Y <= ds.upwardSeg.P1.Y +} + +// compareTo is a comparison operation which orders segments left to right. +// +// The definition of the ordering is: +// - -1 : if DS1.seg is left of or below DS2.seg (DS1 < DS2) +// - 1 : if DS1.seg is right of or above DS2.seg (DS1 > DS2) +// - 0 : if the segments are identical +func (ds *operationBuffer_DepthSegment) compareTo(other *operationBuffer_DepthSegment) int { + // If segment envelopes do not overlap, then + // can use standard segment lexicographic ordering. + if ds.upwardSeg.MinX() >= other.upwardSeg.MaxX() || + ds.upwardSeg.MaxX() <= other.upwardSeg.MinX() || + ds.upwardSeg.MinY() >= other.upwardSeg.MaxY() || + ds.upwardSeg.MaxY() <= other.upwardSeg.MinY() { + return ds.upwardSeg.CompareTo(other.upwardSeg) + } + + // Otherwise if envelopes overlap, use relative segment orientation. + // + // Collinear segments should be evaluated by previous logic + orientIndex := ds.upwardSeg.OrientationIndexSegment(other.upwardSeg) + if orientIndex != 0 { + return orientIndex + } + + // If comparison between this and other is indeterminate, + // try the opposite call order. + // The sign of the result needs to be flipped. + orientIndex = -1 * other.upwardSeg.OrientationIndexSegment(ds.upwardSeg) + if orientIndex != 0 { + return orientIndex + } + + // If segment envelopes overlap and they are collinear, + // since segments do not cross they must be equal. + // assert: segments are equal + return 0 +} + +// oldCompareTo is dead code in Java but included for 1-1 correspondence. +func (ds *operationBuffer_DepthSegment) oldCompareTo(other *operationBuffer_DepthSegment) int { + // fast check if segments are trivially ordered along X + if ds.upwardSeg.MinX() > other.upwardSeg.MaxX() { + return 1 + } + if ds.upwardSeg.MaxX() < other.upwardSeg.MinX() { + return -1 + } + + // try and compute a determinate orientation for the segments. + // Test returns 1 if other is left of this (i.e. this > other) + orientIndex := ds.upwardSeg.OrientationIndexSegment(other.upwardSeg) + if orientIndex != 0 { + return orientIndex + } + + // If comparison between this and other is indeterminate, + // try the opposite call order. + // The sign of the result needs to be flipped. + orientIndex = -1 * other.upwardSeg.OrientationIndexSegment(ds.upwardSeg) + if orientIndex != 0 { + return orientIndex + } + + // otherwise, use standard lexicographic segment ordering + return ds.upwardSeg.CompareTo(other.upwardSeg) +} + +// String returns a string representation of the depth segment. +func (ds *operationBuffer_DepthSegment) String() string { + return ds.upwardSeg.String() +} diff --git a/internal/jtsport/jts/operation_buffer_validate_buffer_curve_max_distance_finder.go b/internal/jtsport/jts/operation_buffer_validate_buffer_curve_max_distance_finder.go new file mode 100644 index 00000000..1815b12a --- /dev/null +++ b/internal/jtsport/jts/operation_buffer_validate_buffer_curve_max_distance_finder.go @@ -0,0 +1,126 @@ +package jts + +// OperationBufferValidate_BufferCurveMaximumDistanceFinder finds the approximate maximum distance +// from a buffer curve to the originating geometry. +// This is similar to the Discrete Oriented Hausdorff distance +// from the buffer curve to the input. +// +// The approximate maximum distance is determined by testing +// all vertices in the buffer curve, as well +// as midpoints of the curve segments. +// Due to the way buffer curves are constructed, this +// should be a very close approximation. +type OperationBufferValidate_BufferCurveMaximumDistanceFinder struct { + inputGeom *Geom_Geometry + maxPtDist *OperationBufferValidate_PointPairDistance +} + +// OperationBufferValidate_NewBufferCurveMaximumDistanceFinder creates a new BufferCurveMaximumDistanceFinder. +func OperationBufferValidate_NewBufferCurveMaximumDistanceFinder(inputGeom *Geom_Geometry) *OperationBufferValidate_BufferCurveMaximumDistanceFinder { + return &OperationBufferValidate_BufferCurveMaximumDistanceFinder{ + inputGeom: inputGeom, + maxPtDist: OperationBufferValidate_NewPointPairDistance(), + } +} + +// FindDistance finds the maximum distance from the buffer curve to the input geometry. +func (f *OperationBufferValidate_BufferCurveMaximumDistanceFinder) FindDistance(bufferCurve *Geom_Geometry) float64 { + f.computeMaxVertexDistance(bufferCurve) + f.computeMaxMidpointDistance(bufferCurve) + return f.maxPtDist.GetDistance() +} + +// GetDistancePoints gets the point pair containing the points that have the computed distance. +func (f *OperationBufferValidate_BufferCurveMaximumDistanceFinder) GetDistancePoints() *OperationBufferValidate_PointPairDistance { + return f.maxPtDist +} + +func (f *OperationBufferValidate_BufferCurveMaximumDistanceFinder) computeMaxVertexDistance(curve *Geom_Geometry) { + distFilter := operationBufferValidate_newMaxPointDistanceFilter(f.inputGeom) + curve.ApplyCoordinateFilter(distFilter) + f.maxPtDist.SetMaximumFromPointPairDistance(distFilter.getMaxPointDistance()) +} + +func (f *OperationBufferValidate_BufferCurveMaximumDistanceFinder) computeMaxMidpointDistance(curve *Geom_Geometry) { + distFilter := operationBufferValidate_newMaxMidpointDistanceFilter(f.inputGeom) + curve.ApplyCoordinateSequenceFilter(distFilter) + f.maxPtDist.SetMaximumFromPointPairDistance(distFilter.getMaxPointDistance()) +} + +// operationBufferValidate_MaxPointDistanceFilter is a filter to compute the maximum distance +// from all vertices of a geometry to another geometry. +type operationBufferValidate_MaxPointDistanceFilter struct { + maxPtDist *OperationBufferValidate_PointPairDistance + minPtDist *OperationBufferValidate_PointPairDistance + geom *Geom_Geometry +} + +var _ Geom_CoordinateFilter = (*operationBufferValidate_MaxPointDistanceFilter)(nil) + +func operationBufferValidate_newMaxPointDistanceFilter(geom *Geom_Geometry) *operationBufferValidate_MaxPointDistanceFilter { + return &operationBufferValidate_MaxPointDistanceFilter{ + maxPtDist: OperationBufferValidate_NewPointPairDistance(), + minPtDist: OperationBufferValidate_NewPointPairDistance(), + geom: geom, + } +} + +func (f *operationBufferValidate_MaxPointDistanceFilter) IsGeom_CoordinateFilter() {} + +func (f *operationBufferValidate_MaxPointDistanceFilter) Filter(pt *Geom_Coordinate) { + f.minPtDist.Initialize() + OperationBufferValidate_DistanceToPointFinder_ComputeDistanceGeometry(f.geom, pt, f.minPtDist) + f.maxPtDist.SetMaximumFromPointPairDistance(f.minPtDist) +} + +func (f *operationBufferValidate_MaxPointDistanceFilter) getMaxPointDistance() *OperationBufferValidate_PointPairDistance { + return f.maxPtDist +} + +// operationBufferValidate_MaxMidpointDistanceFilter is a filter to compute the maximum distance +// from segment midpoints of a geometry to another geometry. +type operationBufferValidate_MaxMidpointDistanceFilter struct { + maxPtDist *OperationBufferValidate_PointPairDistance + minPtDist *OperationBufferValidate_PointPairDistance + geom *Geom_Geometry +} + +var _ Geom_CoordinateSequenceFilter = (*operationBufferValidate_MaxMidpointDistanceFilter)(nil) + +func operationBufferValidate_newMaxMidpointDistanceFilter(geom *Geom_Geometry) *operationBufferValidate_MaxMidpointDistanceFilter { + return &operationBufferValidate_MaxMidpointDistanceFilter{ + maxPtDist: OperationBufferValidate_NewPointPairDistance(), + minPtDist: OperationBufferValidate_NewPointPairDistance(), + geom: geom, + } +} + +func (f *operationBufferValidate_MaxMidpointDistanceFilter) IsGeom_CoordinateSequenceFilter() {} + +func (f *operationBufferValidate_MaxMidpointDistanceFilter) Filter(seq Geom_CoordinateSequence, index int) { + if index == 0 { + return + } + + p0 := seq.GetCoordinate(index - 1) + p1 := seq.GetCoordinate(index) + midPt := Geom_NewCoordinateWithXY( + (p0.X+p1.X)/2, + (p0.Y+p1.Y)/2) + + f.minPtDist.Initialize() + OperationBufferValidate_DistanceToPointFinder_ComputeDistanceGeometry(f.geom, midPt, f.minPtDist) + f.maxPtDist.SetMaximumFromPointPairDistance(f.minPtDist) +} + +func (f *operationBufferValidate_MaxMidpointDistanceFilter) IsGeometryChanged() bool { + return false +} + +func (f *operationBufferValidate_MaxMidpointDistanceFilter) IsDone() bool { + return false +} + +func (f *operationBufferValidate_MaxMidpointDistanceFilter) getMaxPointDistance() *OperationBufferValidate_PointPairDistance { + return f.maxPtDist +} diff --git a/internal/jtsport/jts/operation_buffer_validate_buffer_distance_validator.go b/internal/jtsport/jts/operation_buffer_validate_buffer_distance_validator.go new file mode 100644 index 00000000..316a6cf3 --- /dev/null +++ b/internal/jtsport/jts/operation_buffer_validate_buffer_distance_validator.go @@ -0,0 +1,181 @@ +package jts + +import ( + "fmt" + "math" + + "github.com/peterstace/simplefeatures/internal/jtsport/java" +) + +var operationBufferValidate_BufferDistanceValidator_VERBOSE = false + +// operationBufferValidate_BufferDistanceValidator_MAX_DISTANCE_DIFF_FRAC is the +// maximum allowable fraction of buffer distance the actual distance can differ by. +// 1% sometimes causes an error - 1.2% should be safe. +const operationBufferValidate_BufferDistanceValidator_MAX_DISTANCE_DIFF_FRAC = .012 + +// OperationBufferValidate_BufferDistanceValidator validates that a given buffer curve lies +// an appropriate distance from the input generating it. +// Useful only for round buffers (cap and join). +// Can be used for either positive or negative distances. +// +// This is a heuristic test, and may return false positive results +// (I.e. it may fail to detect an invalid result.) +// It should never return a false negative result, however +// (I.e. it should never report a valid result as invalid.) +type OperationBufferValidate_BufferDistanceValidator struct { + input *Geom_Geometry + bufDistance float64 + result *Geom_Geometry + + minValidDistance float64 + maxValidDistance float64 + + minDistanceFound float64 + maxDistanceFound float64 + + isValid bool + errMsg string + errorLocation *Geom_Coordinate + errorIndicator *Geom_Geometry +} + +// OperationBufferValidate_NewBufferDistanceValidator creates a new BufferDistanceValidator. +func OperationBufferValidate_NewBufferDistanceValidator(input *Geom_Geometry, bufDistance float64, result *Geom_Geometry) *OperationBufferValidate_BufferDistanceValidator { + return &OperationBufferValidate_BufferDistanceValidator{ + input: input, + bufDistance: bufDistance, + result: result, + isValid: true, + } +} + +// IsValid validates the buffer distance. +func (v *OperationBufferValidate_BufferDistanceValidator) IsValid() bool { + posDistance := math.Abs(v.bufDistance) + distDelta := operationBufferValidate_BufferDistanceValidator_MAX_DISTANCE_DIFF_FRAC * posDistance + v.minValidDistance = posDistance - distDelta + v.maxValidDistance = posDistance + distDelta + + // can't use this test if either is empty + if v.input.IsEmpty() || v.result.IsEmpty() { + return true + } + + if v.bufDistance > 0.0 { + v.checkPositiveValid() + } else { + v.checkNegativeValid() + } + if operationBufferValidate_BufferDistanceValidator_VERBOSE { + Util_Debug_Println("Min Dist= " + fmt.Sprintf("%v", v.minDistanceFound) + " err= " + + fmt.Sprintf("%v", 1.0-v.minDistanceFound/v.bufDistance) + + " Max Dist= " + fmt.Sprintf("%v", v.maxDistanceFound) + " err= " + + fmt.Sprintf("%v", v.maxDistanceFound/v.bufDistance-1.0)) + } + return v.isValid +} + +// GetErrorMessage returns an appropriate error message if the buffer is invalid. +func (v *OperationBufferValidate_BufferDistanceValidator) GetErrorMessage() string { + return v.errMsg +} + +// GetErrorLocation returns the location of the error. +func (v *OperationBufferValidate_BufferDistanceValidator) GetErrorLocation() *Geom_Coordinate { + return v.errorLocation +} + +// GetErrorIndicator gets a geometry which indicates the location and nature of a validation failure. +// +// The indicator is a line segment showing the location and size +// of the distance discrepancy. +// +// Returns a geometric error indicator or nil if no error was found. +func (v *OperationBufferValidate_BufferDistanceValidator) GetErrorIndicator() *Geom_Geometry { + return v.errorIndicator +} + +func (v *OperationBufferValidate_BufferDistanceValidator) checkPositiveValid() { + bufCurve := v.result.GetBoundary() + v.checkMinimumDistance(v.input, bufCurve, v.minValidDistance) + if !v.isValid { + return + } + + v.checkMaximumDistance(v.input, bufCurve, v.maxValidDistance) +} + +func (v *OperationBufferValidate_BufferDistanceValidator) checkNegativeValid() { + // Assert: only polygonal inputs can be checked for negative buffers + + // MD - could generalize this to handle GCs too + if !java.InstanceOf[*Geom_Polygon](v.input) && + !java.InstanceOf[*Geom_MultiPolygon](v.input) && + !java.InstanceOf[*Geom_GeometryCollection](v.input) { + return + } + inputCurve := v.getPolygonLines(v.input) + v.checkMinimumDistance(inputCurve, v.result, v.minValidDistance) + if !v.isValid { + return + } + + v.checkMaximumDistance(inputCurve, v.result, v.maxValidDistance) +} + +func (v *OperationBufferValidate_BufferDistanceValidator) getPolygonLines(g *Geom_Geometry) *Geom_Geometry { + var lines []*Geom_Geometry + lineExtracter := GeomUtil_NewLinearComponentExtracter(nil) + // Store lines temporarily and convert to []*Geom_Geometry + polys := GeomUtil_PolygonExtracter_GetPolygons(g) + for _, poly := range polys { + poly.Geom_Geometry.Apply(lineExtracter) + } + // Get the extracted lines + extractedLines := lineExtracter.lines + for _, line := range extractedLines { + lines = append(lines, line.Geom_Geometry) + } + return g.GetFactory().BuildGeometry(lines) +} + +// checkMinimumDistance checks that two geometries are at least a minimum distance apart. +func (v *OperationBufferValidate_BufferDistanceValidator) checkMinimumDistance(g1, g2 *Geom_Geometry, minDist float64) { + distOp := OperationDistance_NewDistanceOpWithTerminate(g1, g2, minDist) + v.minDistanceFound = distOp.Distance() + + if v.minDistanceFound < minDist { + v.isValid = false + pts := distOp.NearestPoints() + v.errorLocation = distOp.NearestPoints()[1] + v.errorIndicator = g1.GetFactory().CreateLineStringFromCoordinates(pts).Geom_Geometry + v.errMsg = "Distance between buffer curve and input is too small " + + "(" + fmt.Sprintf("%v", v.minDistanceFound) + + " at " + Io_WKTWriter_ToLineStringFromTwoCoords(pts[0], pts[1]) + " )" + } +} + +// checkMaximumDistance checks that the furthest distance from the buffer curve to the input +// is less than the given maximum distance. +// This uses the Oriented Hausdorff distance metric. +// It corresponds to finding +// the point on the buffer curve which is furthest from some point on the input. +func (v *OperationBufferValidate_BufferDistanceValidator) checkMaximumDistance(input, bufCurve *Geom_Geometry, maxDist float64) { + // BufferCurveMaximumDistanceFinder maxDistFinder = new BufferCurveMaximumDistanceFinder(input); + // maxDistanceFound = maxDistFinder.findDistance(bufCurve); + + haus := AlgorithmDistance_NewDiscreteHausdorffDistance(bufCurve, input) + haus.SetDensifyFraction(0.25) + v.maxDistanceFound = haus.OrientedDistance() + + if v.maxDistanceFound > maxDist { + v.isValid = false + pts := haus.GetCoordinates() + v.errorLocation = pts[1] + v.errorIndicator = input.GetFactory().CreateLineStringFromCoordinates(pts).Geom_Geometry + v.errMsg = "Distance between buffer curve and input is too large " + + "(" + fmt.Sprintf("%v", v.maxDistanceFound) + + " at " + Io_WKTWriter_ToLineStringFromTwoCoords(pts[0], pts[1]) + ")" + } +} diff --git a/internal/jtsport/jts/operation_buffer_validate_buffer_result_validator.go b/internal/jtsport/jts/operation_buffer_validate_buffer_result_validator.go new file mode 100644 index 00000000..a3b505ff --- /dev/null +++ b/internal/jtsport/jts/operation_buffer_validate_buffer_result_validator.go @@ -0,0 +1,196 @@ +package jts + +import "github.com/peterstace/simplefeatures/internal/jtsport/java" + +var operationBufferValidate_BufferResultValidator_VERBOSE = false + +// operationBufferValidate_BufferResultValidator_MAX_ENV_DIFF_FRAC is the +// maximum allowable fraction of buffer distance the actual distance can differ by. +// 1% sometimes causes an error - 1.2% should be safe. +const operationBufferValidate_BufferResultValidator_MAX_ENV_DIFF_FRAC = .012 + +// OperationBufferValidate_BufferResultValidator_IsValid checks whether the geometry buffer is valid. +func OperationBufferValidate_BufferResultValidator_IsValid(g *Geom_Geometry, distance float64, result *Geom_Geometry) bool { + validator := OperationBufferValidate_NewBufferResultValidator(g, distance, result) + if validator.IsValid() { + return true + } + return false +} + +// OperationBufferValidate_BufferResultValidator_IsValidMsg checks whether the geometry buffer is valid, +// and returns an error message if not. +// +// Returns an appropriate error message or empty string if the buffer is valid. +func OperationBufferValidate_BufferResultValidator_IsValidMsg(g *Geom_Geometry, distance float64, result *Geom_Geometry) string { + validator := OperationBufferValidate_NewBufferResultValidator(g, distance, result) + if !validator.IsValid() { + return validator.GetErrorMessage() + } + return "" +} + +// OperationBufferValidate_BufferResultValidator validates that the result of a buffer operation +// is geometrically correct, within a computed tolerance. +// +// This is a heuristic test, and may return false positive results +// (I.e. it may fail to detect an invalid result.) +// It should never return a false negative result, however +// (I.e. it should never report a valid result as invalid.) +// +// This test may be (much) more expensive than the original +// buffer computation. +type OperationBufferValidate_BufferResultValidator struct { + input *Geom_Geometry + distance float64 + result *Geom_Geometry + isValid bool + errorMsg string + errorLocation *Geom_Coordinate + errorIndicator *Geom_Geometry +} + +// OperationBufferValidate_NewBufferResultValidator creates a new BufferResultValidator. +func OperationBufferValidate_NewBufferResultValidator(input *Geom_Geometry, distance float64, result *Geom_Geometry) *OperationBufferValidate_BufferResultValidator { + return &OperationBufferValidate_BufferResultValidator{ + input: input, + distance: distance, + result: result, + isValid: true, + } +} + +// IsValid validates the buffer result. +func (v *OperationBufferValidate_BufferResultValidator) IsValid() bool { + v.checkPolygonal() + if !v.isValid { + return v.isValid + } + v.checkExpectedEmpty() + if !v.isValid { + return v.isValid + } + v.checkEnvelope() + if !v.isValid { + return v.isValid + } + v.checkArea() + if !v.isValid { + return v.isValid + } + v.checkDistance() + return v.isValid +} + +// GetErrorMessage returns the error message. +func (v *OperationBufferValidate_BufferResultValidator) GetErrorMessage() string { + return v.errorMsg +} + +// GetErrorLocation returns the location of the error. +func (v *OperationBufferValidate_BufferResultValidator) GetErrorLocation() *Geom_Coordinate { + return v.errorLocation +} + +// GetErrorIndicator gets a geometry which indicates the location and nature of a validation failure. +// +// If the failure is due to the buffer curve being too far or too close +// to the input, the indicator is a line segment showing the location and size +// of the discrepancy. +// +// Returns a geometric error indicator or nil if no error was found. +func (v *OperationBufferValidate_BufferResultValidator) GetErrorIndicator() *Geom_Geometry { + return v.errorIndicator +} + +func (v *OperationBufferValidate_BufferResultValidator) report(checkName string) { + if !operationBufferValidate_BufferResultValidator_VERBOSE { + return + } + status := "passed" + if !v.isValid { + status = "FAILED" + } + Util_Debug_Println("Check " + checkName + ": " + status) +} + +func (v *OperationBufferValidate_BufferResultValidator) checkPolygonal() { + if !java.InstanceOf[*Geom_Polygon](v.result) && + !java.InstanceOf[*Geom_MultiPolygon](v.result) { + v.isValid = false + } + v.errorMsg = "Result is not polygonal" + v.errorIndicator = v.result + v.report("Polygonal") +} + +func (v *OperationBufferValidate_BufferResultValidator) checkExpectedEmpty() { + // can't check areal features + if v.input.GetDimension() >= 2 { + return + } + // can't check positive distances + if v.distance > 0.0 { + return + } + + // at this point can expect an empty result + if !v.result.IsEmpty() { + v.isValid = false + v.errorMsg = "Result is non-empty" + v.errorIndicator = v.result + } + v.report("ExpectedEmpty") +} + +func (v *OperationBufferValidate_BufferResultValidator) checkEnvelope() { + if v.distance < 0.0 { + return + } + + padding := v.distance * operationBufferValidate_BufferResultValidator_MAX_ENV_DIFF_FRAC + if padding == 0.0 { + padding = 0.001 + } + + expectedEnv := Geom_NewEnvelopeFromEnvelope(v.input.GetEnvelopeInternal()) + expectedEnv.ExpandBy(v.distance) + + bufEnv := Geom_NewEnvelopeFromEnvelope(v.result.GetEnvelopeInternal()) + bufEnv.ExpandBy(padding) + + if !bufEnv.ContainsEnvelope(expectedEnv) { + v.isValid = false + v.errorMsg = "Buffer envelope is incorrect" + v.errorIndicator = v.input.GetFactory().ToGeometry(bufEnv) + } + v.report("Envelope") +} + +func (v *OperationBufferValidate_BufferResultValidator) checkArea() { + inputArea := v.input.GetArea() + resultArea := v.result.GetArea() + + if v.distance > 0.0 && inputArea > resultArea { + v.isValid = false + v.errorMsg = "Area of positive buffer is smaller than input" + v.errorIndicator = v.result + } + if v.distance < 0.0 && inputArea < resultArea { + v.isValid = false + v.errorMsg = "Area of negative buffer is larger than input" + v.errorIndicator = v.result + } + v.report("Area") +} + +func (v *OperationBufferValidate_BufferResultValidator) checkDistance() { + distValid := OperationBufferValidate_NewBufferDistanceValidator(v.input, v.distance, v.result) + if !distValid.IsValid() { + v.isValid = false + v.errorMsg = distValid.GetErrorMessage() + v.errorLocation = distValid.GetErrorLocation() + v.errorIndicator = distValid.GetErrorIndicator() + } + v.report("Distance") +} diff --git a/internal/jtsport/jts/operation_buffer_validate_distance_to_point_finder.go b/internal/jtsport/jts/operation_buffer_validate_distance_to_point_finder.go new file mode 100644 index 00000000..3d68e43d --- /dev/null +++ b/internal/jtsport/jts/operation_buffer_validate_distance_to_point_finder.go @@ -0,0 +1,60 @@ +package jts + +import "github.com/peterstace/simplefeatures/internal/jtsport/java" + +// OperationBufferValidate_DistanceToPointFinder computes the Euclidean distance (L2 metric) +// from a Point to a Geometry. +// Also computes two points which are separated by the distance. +type OperationBufferValidate_DistanceToPointFinder struct{} + +// OperationBufferValidate_NewDistanceToPointFinder creates a new DistanceToPointFinder. +func OperationBufferValidate_NewDistanceToPointFinder() *OperationBufferValidate_DistanceToPointFinder { + return &OperationBufferValidate_DistanceToPointFinder{} +} + +// OperationBufferValidate_DistanceToPointFinder_ComputeDistanceGeometry computes the distance +// from a geometry to a point. +func OperationBufferValidate_DistanceToPointFinder_ComputeDistanceGeometry(geom *Geom_Geometry, pt *Geom_Coordinate, ptDist *OperationBufferValidate_PointPairDistance) { + if java.InstanceOf[*Geom_LineString](geom) { + OperationBufferValidate_DistanceToPointFinder_ComputeDistanceLineString(java.Cast[*Geom_LineString](geom), pt, ptDist) + } else if java.InstanceOf[*Geom_Polygon](geom) { + OperationBufferValidate_DistanceToPointFinder_ComputeDistancePolygon(java.Cast[*Geom_Polygon](geom), pt, ptDist) + } else if java.InstanceOf[*Geom_GeometryCollection](geom) { + gc := java.Cast[*Geom_GeometryCollection](geom) + for i := 0; i < gc.GetNumGeometries(); i++ { + g := gc.GetGeometryN(i) + OperationBufferValidate_DistanceToPointFinder_ComputeDistanceGeometry(g, pt, ptDist) + } + } else { // assume geom is Point + ptDist.SetMinimum(geom.GetCoordinate(), pt) + } +} + +// OperationBufferValidate_DistanceToPointFinder_ComputeDistanceLineString computes the distance +// from a LineString to a point. +func OperationBufferValidate_DistanceToPointFinder_ComputeDistanceLineString(line *Geom_LineString, pt *Geom_Coordinate, ptDist *OperationBufferValidate_PointPairDistance) { + coords := line.GetCoordinates() + tempSegment := Geom_NewLineSegment() + for i := 0; i < len(coords)-1; i++ { + tempSegment.SetCoordinates(coords[i], coords[i+1]) + // this is somewhat inefficient - could do better + closestPt := tempSegment.ClosestPoint(pt) + ptDist.SetMinimum(closestPt, pt) + } +} + +// OperationBufferValidate_DistanceToPointFinder_ComputeDistanceLineSegment computes the distance +// from a LineSegment to a point. +func OperationBufferValidate_DistanceToPointFinder_ComputeDistanceLineSegment(segment *Geom_LineSegment, pt *Geom_Coordinate, ptDist *OperationBufferValidate_PointPairDistance) { + closestPt := segment.ClosestPoint(pt) + ptDist.SetMinimum(closestPt, pt) +} + +// OperationBufferValidate_DistanceToPointFinder_ComputeDistancePolygon computes the distance +// from a Polygon to a point. +func OperationBufferValidate_DistanceToPointFinder_ComputeDistancePolygon(poly *Geom_Polygon, pt *Geom_Coordinate, ptDist *OperationBufferValidate_PointPairDistance) { + OperationBufferValidate_DistanceToPointFinder_ComputeDistanceLineString(poly.GetExteriorRing().Geom_LineString, pt, ptDist) + for i := 0; i < poly.GetNumInteriorRing(); i++ { + OperationBufferValidate_DistanceToPointFinder_ComputeDistanceLineString(poly.GetInteriorRingN(i).Geom_LineString, pt, ptDist) + } +} diff --git a/internal/jtsport/jts/operation_buffer_validate_point_pair_distance.go b/internal/jtsport/jts/operation_buffer_validate_point_pair_distance.go new file mode 100644 index 00000000..9f1672ac --- /dev/null +++ b/internal/jtsport/jts/operation_buffer_validate_point_pair_distance.go @@ -0,0 +1,91 @@ +package jts + +import "math" + +// OperationBufferValidate_PointPairDistance contains a pair of points and the distance between them. +// Provides methods to update with a new point pair with +// either maximum or minimum distance. +type OperationBufferValidate_PointPairDistance struct { + pt [2]*Geom_Coordinate + distance float64 + isNull bool +} + +// OperationBufferValidate_NewPointPairDistance creates a new PointPairDistance. +func OperationBufferValidate_NewPointPairDistance() *OperationBufferValidate_PointPairDistance { + return &OperationBufferValidate_PointPairDistance{ + pt: [2]*Geom_Coordinate{Geom_NewCoordinate(), Geom_NewCoordinate()}, + distance: math.NaN(), + isNull: true, + } +} + +// Initialize initializes this PointPairDistance. +func (ppd *OperationBufferValidate_PointPairDistance) Initialize() { + ppd.isNull = true +} + +// InitializeWithCoordinates initializes the points, computing the distance between them. +func (ppd *OperationBufferValidate_PointPairDistance) InitializeWithCoordinates(p0, p1 *Geom_Coordinate) { + ppd.pt[0].SetCoordinate(p0) + ppd.pt[1].SetCoordinate(p1) + ppd.distance = p0.Distance(p1) + ppd.isNull = false +} + +// initializeWithCoordinatesAndDistance initializes the points, avoiding recomputing the distance. +func (ppd *OperationBufferValidate_PointPairDistance) initializeWithCoordinatesAndDistance(p0, p1 *Geom_Coordinate, distance float64) { + ppd.pt[0].SetCoordinate(p0) + ppd.pt[1].SetCoordinate(p1) + ppd.distance = distance + ppd.isNull = false +} + +// GetDistance gets the distance between the paired points. +func (ppd *OperationBufferValidate_PointPairDistance) GetDistance() float64 { + return ppd.distance +} + +// GetCoordinates gets the paired points. +func (ppd *OperationBufferValidate_PointPairDistance) GetCoordinates() []*Geom_Coordinate { + return ppd.pt[:] +} + +// GetCoordinate gets one of the paired points. +func (ppd *OperationBufferValidate_PointPairDistance) GetCoordinate(i int) *Geom_Coordinate { + return ppd.pt[i] +} + +// SetMaximumFromPointPairDistance sets this to the maximum distance found. +func (ppd *OperationBufferValidate_PointPairDistance) SetMaximumFromPointPairDistance(ptDist *OperationBufferValidate_PointPairDistance) { + ppd.SetMaximum(ptDist.pt[0], ptDist.pt[1]) +} + +// SetMaximum sets this to the maximum distance found. +func (ppd *OperationBufferValidate_PointPairDistance) SetMaximum(p0, p1 *Geom_Coordinate) { + if ppd.isNull { + ppd.InitializeWithCoordinates(p0, p1) + return + } + dist := p0.Distance(p1) + if dist > ppd.distance { + ppd.initializeWithCoordinatesAndDistance(p0, p1, dist) + } +} + +// SetMinimumFromPointPairDistance sets this to the minimum distance found. +func (ppd *OperationBufferValidate_PointPairDistance) SetMinimumFromPointPairDistance(ptDist *OperationBufferValidate_PointPairDistance) { + ppd.SetMinimum(ptDist.pt[0], ptDist.pt[1]) +} + +// SetMinimum sets this to the minimum distance found. +func (ppd *OperationBufferValidate_PointPairDistance) SetMinimum(p0, p1 *Geom_Coordinate) { + if ppd.isNull { + ppd.InitializeWithCoordinates(p0, p1) + return + } + dist := p0.Distance(p1) + if dist < ppd.distance { + ppd.initializeWithCoordinatesAndDistance(p0, p1, dist) + } +} diff --git a/internal/jtsport/jts/operation_buffer_variable_buffer.go b/internal/jtsport/jts/operation_buffer_variable_buffer.go new file mode 100644 index 00000000..7a2693ba --- /dev/null +++ b/internal/jtsport/jts/operation_buffer_variable_buffer.go @@ -0,0 +1,387 @@ +package jts + +import ( + "math" + + "github.com/peterstace/simplefeatures/internal/jtsport/java" +) + +const operationBuffer_variableBuffer_MIN_CAP_SEG_LEN_FACTOR = 4 + +// OperationBuffer_VariableBuffer_Buffer creates a buffer polygon along a line with the buffer distance interpolated +// between a start distance and an end distance. +func OperationBuffer_VariableBuffer_Buffer(line *Geom_Geometry, startDistance, endDistance float64) *Geom_Geometry { + distance := operationBuffer_variableBuffer_interpolate(java.Cast[*Geom_LineString](line), startDistance, endDistance) + vb := operationBuffer_newVariableBuffer(line, distance) + return vb.getResult() +} + +// OperationBuffer_VariableBuffer_BufferWithMidDistance creates a buffer polygon along a line with the buffer distance interpolated +// between a start distance, a middle distance and an end distance. +// The middle distance is attained at +// the vertex at or just past the half-length of the line. +// For smooth buffering of a LinearRing (or the rings of a Polygon) +// the start distance and end distance should be equal. +func OperationBuffer_VariableBuffer_BufferWithMidDistance(line *Geom_Geometry, startDistance, midDistance, endDistance float64) *Geom_Geometry { + distance := operationBuffer_variableBuffer_interpolateMid(java.Cast[*Geom_LineString](line), startDistance, midDistance, endDistance) + vb := operationBuffer_newVariableBuffer(line, distance) + return vb.getResult() +} + +// OperationBuffer_VariableBuffer_BufferWithDistances creates a buffer polygon along a line with the distance specified +// at each vertex. +func OperationBuffer_VariableBuffer_BufferWithDistances(line *Geom_Geometry, distance []float64) *Geom_Geometry { + vb := operationBuffer_newVariableBuffer(line, distance) + return vb.getResult() +} + +// operationBuffer_variableBuffer_interpolate computes a list of values for the points along a line by +// interpolating between values for the start and end point. +// The interpolation is based on the distance of each point along the line +// relative to the total line length. +func operationBuffer_variableBuffer_interpolate(line *Geom_LineString, startValue, endValue float64) []float64 { + startValue = math.Abs(startValue) + endValue = math.Abs(endValue) + values := make([]float64, line.GetNumPoints()) + values[0] = startValue + values[len(values)-1] = endValue + + totalLen := line.GetLength() + pts := line.GetCoordinates() + currLen := 0.0 + for i := 1; i < len(values)-1; i++ { + segLen := pts[i].Distance(pts[i-1]) + currLen += segLen + lenFrac := currLen / totalLen + delta := lenFrac * (endValue - startValue) + values[i] = startValue + delta + } + return values +} + +// operationBuffer_variableBuffer_interpolateMid computes a list of values for the points along a line by +// interpolating between values for the start, middle and end points. +// The interpolation is based on the distance of each point along the line +// relative to the total line length. +// The middle distance is attained at the vertex at or just past the half-length of the line. +func operationBuffer_variableBuffer_interpolateMid(line *Geom_LineString, startValue, midValue, endValue float64) []float64 { + startValue = math.Abs(startValue) + midValue = math.Abs(midValue) + endValue = math.Abs(endValue) + + values := make([]float64, line.GetNumPoints()) + values[0] = startValue + values[len(values)-1] = endValue + + pts := line.GetCoordinates() + lineLen := line.GetLength() + midIndex := operationBuffer_variableBuffer_indexAtLength(pts, lineLen/2) + + delMidStart := midValue - startValue + delEndMid := endValue - midValue + + lenSM := operationBuffer_variableBuffer_length(pts, 0, midIndex) + currLen := 0.0 + for i := 1; i <= midIndex; i++ { + segLen := pts[i].Distance(pts[i-1]) + currLen += segLen + lenFrac := currLen / lenSM + val := startValue + lenFrac*delMidStart + values[i] = val + } + + lenME := operationBuffer_variableBuffer_length(pts, midIndex, len(pts)-1) + currLen = 0 + for i := midIndex + 1; i < len(values)-1; i++ { + segLen := pts[i].Distance(pts[i-1]) + currLen += segLen + lenFrac := currLen / lenME + val := midValue + lenFrac*delEndMid + values[i] = val + } + return values +} + +func operationBuffer_variableBuffer_indexAtLength(pts []*Geom_Coordinate, targetLen float64) int { + length := 0.0 + for i := 1; i < len(pts); i++ { + length += pts[i].Distance(pts[i-1]) + if length > targetLen { + return i + } + } + return len(pts) - 1 +} + +func operationBuffer_variableBuffer_length(pts []*Geom_Coordinate, i1, i2 int) float64 { + length := 0.0 + for i := i1 + 1; i <= i2; i++ { + length += pts[i].Distance(pts[i-1]) + } + return length +} + +// operationBuffer_VariableBuffer creates a buffer polygon with a varying buffer distance +// at each vertex along a line. +type operationBuffer_VariableBuffer struct { + line *Geom_LineString + distance []float64 + geomFactory *Geom_GeometryFactory + quadrantSegs int +} + +// operationBuffer_newVariableBuffer creates a generator for a variable-distance line buffer. +func operationBuffer_newVariableBuffer(line *Geom_Geometry, distance []float64) *operationBuffer_VariableBuffer { + vb := &operationBuffer_VariableBuffer{ + line: java.Cast[*Geom_LineString](line), + distance: distance, + geomFactory: line.GetFactory(), + quadrantSegs: OperationBuffer_BufferParameters_DEFAULT_QUADRANT_SEGMENTS, + } + + if len(distance) != vb.line.GetNumPoints() { + panic("Number of distances is not equal to number of vertices") + } + + return vb +} + +// getResult computes the variable buffer polygon. +func (vb *operationBuffer_VariableBuffer) getResult() *Geom_Geometry { + var parts []*Geom_Geometry + + pts := vb.line.GetCoordinates() + // construct segment buffers + for i := 1; i < len(pts); i++ { + dist0 := vb.distance[i-1] + dist1 := vb.distance[i] + if dist0 > 0 || dist1 > 0 { + poly := vb.segmentBuffer(pts[i-1], pts[i], dist0, dist1) + if poly != nil { + parts = append(parts, poly.Geom_Geometry) + } + } + } + + partsGeom := vb.geomFactory.CreateGeometryCollectionFromGeometries(Geom_GeometryFactory_ToGeometryArray(parts)) + buffer := partsGeom.Geom_Geometry.UnionSelf() + + //-- ensure an empty polygon is returned if needed + if buffer.IsEmpty() { + return vb.geomFactory.CreatePolygon().Geom_Geometry + } + return buffer +} + +// segmentBuffer computes a variable buffer polygon for a single segment, +// with the given endpoints and buffer distances. +// The individual segment buffers are unioned to form the final buffer. +// If one distance is zero, the end cap at that segment end is the endpoint of the segment. +// If both distances are zero, no polygon is returned. +func (vb *operationBuffer_VariableBuffer) segmentBuffer(p0, p1 *Geom_Coordinate, dist0, dist1 float64) *Geom_Polygon { + // Skip buffer polygon if both distances are zero + if dist0 <= 0 && dist1 <= 0 { + return nil + } + + // Generation algorithm requires increasing distance, so flip if needed + if dist0 > dist1 { + return vb.segmentBufferOriented(p1, p0, dist1, dist0) + } + return vb.segmentBufferOriented(p0, p1, dist0, dist1) +} + +func (vb *operationBuffer_VariableBuffer) segmentBufferOriented(p0, p1 *Geom_Coordinate, dist0, dist1 float64) *Geom_Polygon { + //-- Assert: dist0 <= dist1 + + //-- forward tangent line + tangent := operationBuffer_variableBuffer_outerTangent(p0, dist0, p1, dist1) + + //-- if tangent is null then compute a buffer for largest circle + if tangent == nil { + center := p0 + dist := dist0 + if dist1 > dist0 { + center = p1 + dist = dist1 + } + return vb.circle(center, dist) + } + + //-- reverse tangent line on other side of segment + tangentReflect := vb.reflect(tangent, p0, p1, dist0) + + coords := Geom_NewCoordinateList() + //-- end cap + vb.addCap(p1, dist1, tangent.P1, tangentReflect.P1, coords) + //-- start cap + vb.addCap(p0, dist0, tangentReflect.P0, tangent.P0, coords) + + coords.CloseRing() + + pts := coords.ToCoordinateArray() + polygon := vb.geomFactory.CreatePolygonFromCoordinates(pts) + return polygon +} + +func (vb *operationBuffer_VariableBuffer) reflect(seg *Geom_LineSegment, p0, p1 *Geom_Coordinate, dist0 float64) *Geom_LineSegment { + line := Geom_NewLineSegmentFromCoordinates(p0, p1) + r0 := line.Reflect(seg.P0) + r1 := line.Reflect(seg.P1) + //-- avoid numeric jitter if first distance is zero (second dist must be > 0) + if dist0 == 0 { + r0 = p0.Copy() + } + return Geom_NewLineSegmentFromCoordinates(r0, r1) +} + +// circle returns a circular polygon. +func (vb *operationBuffer_VariableBuffer) circle(center *Geom_Coordinate, radius float64) *Geom_Polygon { + if radius <= 0 { + return nil + } + nPts := 4 * vb.quadrantSegs + pts := make([]*Geom_Coordinate, nPts+1) + angInc := math.Pi / 2 / float64(vb.quadrantSegs) + for i := 0; i < nPts; i++ { + pts[i] = operationBuffer_variableBuffer_projectPolar(center, radius, float64(i)*angInc) + } + pts[len(pts)-1] = pts[0].Copy() + return vb.geomFactory.CreatePolygonFromCoordinates(pts) +} + +// addCap adds a semi-circular cap CCW around the point p. +// The vertices in caps are generated at fixed angles around a point. +// This allows caps at the same point to share vertices, +// which reduces artifacts when the segment buffers are merged. +func (vb *operationBuffer_VariableBuffer) addCap(p *Geom_Coordinate, r float64, t1, t2 *Geom_Coordinate, coords *Geom_CoordinateList) { + //-- if radius is zero just copy the vertex + if r == 0 { + coords.AddCoordinate(p.Copy(), false) + return + } + + coords.AddCoordinate(t1, false) + + angStart := Algorithm_Angle_AngleBetweenPoints(p, t1) + angEnd := Algorithm_Angle_AngleBetweenPoints(p, t2) + if angStart < angEnd { + angStart += 2 * math.Pi + } + + indexStart := vb.capAngleIndex(angStart) + indexEnd := vb.capAngleIndex(angEnd) + + capSegLen := r * 2 * math.Sin(math.Pi/4/float64(vb.quadrantSegs)) + minSegLen := capSegLen / operationBuffer_variableBuffer_MIN_CAP_SEG_LEN_FACTOR + + for i := indexStart; i >= indexEnd; i-- { + //-- use negative increment to create points CW + ang := vb.capAngle(i) + capPt := operationBuffer_variableBuffer_projectPolar(p, r, ang) + + isCapPointHighQuality := true + // Due to the fixed locations of the cap points, + // a start or end cap point might create + // a "reversed" segment to the next tangent point. + // This causes an unwanted narrow spike in the buffer curve, + // which can cause holes in the final buffer polygon. + // These checks remove these points. + if i == indexStart && Algorithm_Orientation_Clockwise != Algorithm_Orientation_Index(p, t1, capPt) { + isCapPointHighQuality = false + } else if i == indexEnd && Algorithm_Orientation_Counterclockwise != Algorithm_Orientation_Index(p, t2, capPt) { + isCapPointHighQuality = false + } + + // Remove short segments between the cap and the tangent segments. + if capPt.Distance(t1) < minSegLen { + isCapPointHighQuality = false + } else if capPt.Distance(t2) < minSegLen { + isCapPointHighQuality = false + } + + if isCapPointHighQuality { + coords.AddCoordinate(capPt, false) + } + } + + coords.AddCoordinate(t2, false) +} + +// capAngle computes the actual angle for a cap angle index. +func (vb *operationBuffer_VariableBuffer) capAngle(index int) float64 { + capSegAng := math.Pi / 2 / float64(vb.quadrantSegs) + return float64(index) * capSegAng +} + +// capAngleIndex computes the canonical cap point index for a given angle. +// The angle is rounded down to the next lower index. +// In order to reduce the number of points created by overlapping end caps, +// cap points are generated at the same locations around a circle. +// The index is the index of the points around the circle, +// with 0 being the point at (1,0). +// The total number of points around the circle is 4 * quadrantSegs. +func (vb *operationBuffer_VariableBuffer) capAngleIndex(ang float64) int { + capSegAng := math.Pi / 2 / float64(vb.quadrantSegs) + index := int(ang / capSegAng) + return index +} + +// operationBuffer_variableBuffer_outerTangent computes the two circumference points defining the outer tangent line +// between two circles. +// The tangent line may be null if one circle mostly overlaps the other. +// For the algorithm see https://en.wikipedia.org/wiki/Tangent_lines_to_circles#Outer_tangent. +func operationBuffer_variableBuffer_outerTangent(c1 *Geom_Coordinate, r1 float64, c2 *Geom_Coordinate, r2 float64) *Geom_LineSegment { + // If distances are inverted then flip to compute and flip result back. + if r1 > r2 { + seg := operationBuffer_variableBuffer_outerTangent(c2, r2, c1, r1) + return Geom_NewLineSegmentFromCoordinates(seg.P1, seg.P0) + } + x1 := c1.GetX() + y1 := c1.GetY() + x2 := c2.GetX() + y2 := c2.GetY() + // TODO: handle r1 == r2? + a3 := -math.Atan2(y2-y1, x2-x1) + + dr := r2 - r1 + d := math.Sqrt((x2-x1)*(x2-x1) + (y2-y1)*(y2-y1)) + + a2 := math.Asin(dr / d) + // check if no tangent exists + if math.IsNaN(a2) { + return nil + } + + a1 := a3 - a2 + + aa := math.Pi/2 - a1 + x3 := x1 + r1*math.Cos(aa) + y3 := y1 + r1*math.Sin(aa) + x4 := x2 + r2*math.Cos(aa) + y4 := y2 + r2*math.Sin(aa) + + return Geom_NewLineSegmentFromXY(x3, y3, x4, y4) +} + +func operationBuffer_variableBuffer_projectPolar(p *Geom_Coordinate, r, ang float64) *Geom_Coordinate { + x := p.GetX() + r*operationBuffer_variableBuffer_snapTrig(math.Cos(ang)) + y := p.GetY() + r*operationBuffer_variableBuffer_snapTrig(math.Sin(ang)) + return Geom_NewCoordinateWithXY(x, y) +} + +const operationBuffer_variableBuffer_SNAP_TRIG_TOL = 1e-6 + +// operationBuffer_variableBuffer_snapTrig snaps trig values to integer values for better consistency. +func operationBuffer_variableBuffer_snapTrig(x float64) float64 { + if x > (1 - operationBuffer_variableBuffer_SNAP_TRIG_TOL) { + return 1 + } + if x < (-1 + operationBuffer_variableBuffer_SNAP_TRIG_TOL) { + return -1 + } + if math.Abs(x) < operationBuffer_variableBuffer_SNAP_TRIG_TOL { + return 0 + } + return x +} diff --git a/internal/jtsport/jts/operation_buffer_variable_buffer_test.go b/internal/jtsport/jts/operation_buffer_variable_buffer_test.go new file mode 100644 index 00000000..e51ce2e2 --- /dev/null +++ b/internal/jtsport/jts/operation_buffer_variable_buffer_test.go @@ -0,0 +1,134 @@ +package jts + +import ( + "testing" +) + +// -- low tolerance reduces expected geometry literal size +const variableBuffer_DEFAULT_TOLERANCE = 1.0e-2 + +func TestVariableBuffer_ZeroWidth(t *testing.T) { + checkVariableBuffer(t, "LINESTRING( 0 0, 6 6, 10 10)", + 0, 0, + "POLYGON EMPTY") +} + +func TestVariableBuffer_ZeroLength(t *testing.T) { + checkVariableBuffer(t, "LINESTRING( 10 10, 10 10 )", + 0, 0, + "POLYGON EMPTY") +} + +func TestVariableBuffer_SegmentInverseDist(t *testing.T) { + checkVariableBuffer(t, "LINESTRING (100 100, 200 100)", + 10, 1, + "POLYGON ((100 90, 98.05 90.19, 96.17 90.76, 94.44 91.69, 92.93 92.93, 91.69 94.44, 90.76 96.17, 90.19 98.05, 90 100, 90.19 101.95, 90.76 103.83, 91.69 105.56, 92.93 107.07, 94.44 108.31, 96.17 109.24, 98.05 109.81, 100 110, 100.9 109.96, 200.09 101, 200.2 100.98, 200.38 100.92, 200.56 100.83, 200.71 100.71, 200.83 100.56, 200.92 100.38, 200.98 100.2, 201 100, 200.98 99.8, 200.92 99.62, 200.83 99.44, 200.71 99.29, 200.56 99.17, 200.38 99.08, 200.2 99.02, 200.09 99, 100.9 90.04, 100 90))") +} + +func TestVariableBuffer_SegmentSameDist(t *testing.T) { + checkVariableBuffer(t, "LINESTRING (100 100, 200 100)", + 10, 10, + "POLYGON ((201.95 109.81, 203.83 109.24, 205.56 108.31, 207.07 107.07, 208.31 105.56, 209.24 103.83, 209.81 101.95, 210 100, 209.81 98.05, 209.24 96.17, 208.31 94.44, 207.07 92.93, 205.56 91.69, 203.83 90.76, 201.95 90.19, 200 90, 100 90, 98.05 90.19, 96.17 90.76, 94.44 91.69, 92.93 92.93, 91.69 94.44, 90.76 96.17, 90.19 98.05, 90 100, 90.19 101.95, 90.76 103.83, 91.69 105.56, 92.93 107.07, 94.44 108.31, 96.17 109.24, 98.05 109.81, 100 110, 200 110, 201.95 109.81))") +} + +func TestVariableBuffer_OneSegment(t *testing.T) { + checkVariableBuffer(t, "LINESTRING (100 100, 200 100)", + 10, 30, + "POLYGON ((200 130, 205.85 129.42, 211.48 127.72, 216.67 124.94, 221.21 121.21, 224.94 116.67, 227.72 111.48, 229.42 105.85, 230 100, 229.42 94.15, 227.72 88.52, 224.94 83.33, 221.21 78.79, 216.67 75.06, 211.48 72.28, 205.85 70.58, 200 70, 194 70.61, 98 90.2, 96.17 90.76, 94.44 91.69, 92.93 92.93, 91.69 94.44, 90.76 96.17, 90.19 98.05, 90 100, 90.19 101.95, 90.76 103.83, 91.69 105.56, 92.93 107.07, 94.44 108.31, 96.17 109.24, 98 109.8, 194 129.39, 200 130))") +} + +func TestVariableBuffer_Segments2(t *testing.T) { + checkVariableBuffer(t, "LINESTRING( 0 0, 40 40, 60 -20)", + 10, 20, + "POLYGON ((79.62 -16.1, 80 -20, 79.62 -23.9, 78.48 -27.65, 76.63 -31.11, 74.14 -34.14, 71.11 -36.63, 67.65 -38.48, 63.9 -39.62, 60 -40, 56.1 -39.62, 52.35 -38.48, 48.89 -36.63, 45.86 -34.14, 43.37 -31.11, 41.52 -27.65, 40.56 -24.72, 31.31 13.38, 6.46 -7.64, 5.56 -8.31, 3.83 -9.24, 1.95 -9.81, 0 -10, -1.95 -9.81, -3.83 -9.24, -5.56 -8.31, -7.07 -7.07, -8.31 -5.56, -9.24 -3.83, -9.81 -1.95, -10 0, -9.81 1.95, -9.24 3.83, -8.31 5.56, -7.64 6.46, 28.76 49.5, 29.59 50.41, 31.82 52.24, 34.37 53.6, 37.13 54.44, 40 54.72, 42.87 54.44, 45.63 53.6, 48.18 52.24, 50.41 50.41, 52.24 48.18, 53.53 45.8, 78.38 -12.11, 79.62 -16.1))") +} + +func TestVariableBuffer_LargeDistance(t *testing.T) { + checkVariableBuffer(t, "LINESTRING( 0 0, 10 10)", + 1, 200, + "POLYGON ((206.16 -29.02, 194.78 -66.54, 176.29 -101.11, 151.42 -131.42, 121.11 -156.29, 86.54 -174.78, 49.02 -186.16, 10 -190, -29.02 -186.16, -66.54 -174.78, -101.11 -156.29, -131.42 -131.42, -156.29 -101.11, -174.78 -66.54, -186.16 -29.02, -190 10, -186.16 49.02, -174.78 86.54, -156.29 121.11, -131.42 151.42, -101.11 176.29, -66.54 194.78, -29.02 206.16, 10 210, 49.02 206.16, 86.54 194.78, 121.11 176.29, 151.42 151.42, 176.29 121.11, 194.78 86.54, 206.16 49.02, 210 10, 206.16 -29.02))") +} + +func TestVariableBuffer_ZeroDistanceAtVertex(t *testing.T) { + checkVariableBufferWithDistances(t, "LINESTRING( 10 10, 20 20, 30 30)", + []float64{5, 0, 5}, + "MULTIPOLYGON (((5.1 10.98, 5.38 11.91, 5.84 12.78, 6.46 13.54, 7.22 14.16, 7.94 14.56, 20 20, 14.56 7.94, 14.16 7.22, 13.54 6.46, 12.78 5.84, 11.91 5.38, 10.98 5.1, 10 5, 9.02 5.1, 8.09 5.38, 7.22 5.84, 6.46 6.46, 5.84 7.22, 5.38 8.09, 5.1 9.02, 5 10, 5.1 10.98)), ((25.44 32.06, 25.84 32.78, 26.46 33.54, 27.22 34.16, 28.09 34.62, 29.02 34.9, 30 35, 30.98 34.9, 31.91 34.62, 32.78 34.16, 33.54 33.54, 34.16 32.78, 34.62 31.91, 34.9 30.98, 35 30, 34.9 29.02, 34.62 28.09, 34.16 27.22, 33.54 26.46, 32.78 25.84, 32.06 25.44, 20 20, 25.44 32.06)))") +} + +func TestVariableBuffer_ZeroDistancesForSegment(t *testing.T) { + checkVariableBufferWithDistances(t, "LINESTRING( 10 10, 20 20, 30 30, 40 40)", + []float64{5, 0, 0, 5}, + "MULTIPOLYGON (((5.1 10.98, 5.38 11.91, 5.84 12.78, 6.46 13.54, 7.22 14.16, 7.94 14.56, 20 20, 14.56 7.94, 14.16 7.22, 13.54 6.46, 12.78 5.84, 11.91 5.38, 10.98 5.1, 10 5, 9.02 5.1, 8.09 5.38, 7.22 5.84, 6.46 6.46, 5.84 7.22, 5.38 8.09, 5.1 9.02, 5 10, 5.1 10.98)), ((35.44 42.06, 35.84 42.78, 36.46 43.54, 37.22 44.16, 38.09 44.62, 39.02 44.9, 40 45, 40.98 44.9, 41.91 44.62, 42.78 44.16, 43.54 43.54, 44.16 42.78, 44.62 41.91, 44.9 40.98, 45 40, 44.9 39.02, 44.62 38.09, 44.16 37.22, 43.54 36.46, 42.78 35.84, 42.06 35.44, 30 30, 35.44 42.06)))") +} + +// see https://github.com/locationtech/jts/issues/998 +func TestVariableBuffer_Issue998_Spike(t *testing.T) { + checkVariableBufferWithDistances(t, "LINESTRING (0.024520295 69.50077743, 0.000508719 74.50086084, 0 76.39546845)", + []float64{6.47, 6.9, 7}, + "POLYGON ((-6.87 77.76, -6.47 79.07, -5.82 80.28, -4.95 81.35, -3.89 82.22, -2.68 82.86, -1.37 83.26, 0 83.4, 1.37 83.26, 2.68 82.86, 3.89 82.22, 4.95 81.35, 5.82 80.28, 6.47 79.07, 6.87 77.76, 7 76.4, 6.99 76.03, 6.89 74.14, 6.88 74.08, 6.88 73.94, 6.47 68.98, 6.37 68.24, 6 67.02, 5.4 65.91, 4.6 64.93, 3.62 64.12, 2.5 63.52, 1.29 63.16, 0.02 63.03, -1.24 63.16, -2.45 63.52, -3.57 64.12, -4.55 64.93, -5.36 65.91, -5.95 67.02, -6.32 68.24, -6.42 68.91, -6.87 73.87, -6.88 74.05, -6.89 74.13, -6.99 76.02, -7 76.4, -6.87 77.76))") +} + +func TestVariableBuffer_NoReverseSpike(t *testing.T) { + checkVariableBufferWithDistances(t, "LINESTRING (0 70, 0 80)", + []float64{4, 7}, + "POLYGON ((-6.87 78.63, -7 80, -6.87 81.37, -6.47 82.68, -5.82 83.89, -4.95 84.95, -3.89 85.82, -2.68 86.47, -1.37 86.87, 0 87, 1.37 86.87, 2.68 86.47, 3.89 85.82, 4.95 84.95, 5.82 83.89, 6.47 82.68, 6.87 81.37, 7 80, 6.87 78.63, 6.68 77.9, 3.82 68.8, 3.7 68.47, 3.33 67.78, 2.83 67.17, 2.22 66.67, 1.53 66.3, 0.78 66.08, 0 66, -0.78 66.08, -1.53 66.3, -2.22 66.67, -2.83 67.17, -3.33 67.78, -3.7 68.47, -3.82 68.8, -6.68 77.9, -6.87 78.63))") +} + +func TestVariableBuffer_NoShortCapSegments(t *testing.T) { + checkVariableBufferWithDistances(t, "LINESTRING (6.85 78.25, 18 87)", + []float64{5, 9}, + "POLYGON ((11.64 93.36, 13 94.48, 14.56 95.31, 16.24 95.83, 18 96, 19.76 95.83, 21.44 95.31, 23 94.48, 24.36 93.36, 25.48 92, 26.31 90.44, 26.83 88.76, 27 87, 26.83 85.24, 26.31 83.56, 25.48 82, 24.36 80.64, 23 79.52, 21.33 78.64, 8.7 73.61, 7.83 73.35, 6.85 73.25, 5.87 73.35, 4.94 73.63, 4.07 74.09, 3.31 74.71, 2.69 75.47, 2.23 76.34, 1.95 77.27, 1.85 78.25, 1.95 79.23, 2.23 80.16, 2.78 81.15, 10.67 92.22, 11.64 93.36))") +} + +// ================================================================ + +var variableBuffer_testFactory = Geom_NewGeometryFactoryDefault() + +func checkVariableBuffer(t *testing.T, wkt string, startDist, endDist float64, wktExpected string) { + t.Helper() + geom := variableBufferRead(wkt) + result := OperationBuffer_VariableBuffer_Buffer(geom, startDist, endDist) + checkVariableBufferResult(t, result, wktExpected) +} + +func checkVariableBufferWithDistances(t *testing.T, wkt string, dist []float64, wktExpected string) { + t.Helper() + geom := variableBufferRead(wkt) + result := OperationBuffer_VariableBuffer_BufferWithDistances(geom, dist) + checkVariableBufferResult(t, result, wktExpected) +} + +func checkVariableBufferResult(t *testing.T, actual *Geom_Geometry, wktExpected string) { + t.Helper() + expected := variableBufferRead(wktExpected) + variableBufferCheckEqual(t, expected, actual, variableBuffer_DEFAULT_TOLERANCE) +} + +func variableBufferRead(wkt string) *Geom_Geometry { + reader := Io_NewWKTReaderWithFactory(variableBuffer_testFactory) + g, err := reader.Read(wkt) + if err != nil { + panic(err) + } + return g +} + +func variableBufferCheckEqual(t *testing.T, expected, actual *Geom_Geometry, tolerance float64) { + t.Helper() + if expected.IsEmpty() && actual.IsEmpty() { + return + } + if expected.IsEmpty() != actual.IsEmpty() { + t.Fatalf("expected empty=%v, got empty=%v\nexpected: %s\ngot: %s", + expected.IsEmpty(), actual.IsEmpty(), + expected.String(), actual.String()) + } + if !expected.EqualsNorm(actual) { + hd := AlgorithmDistance_DiscreteHausdorffDistance_Distance(expected, actual) + if hd > tolerance { + t.Fatalf("Hausdorff distance %v exceeds tolerance %v\nexpected: %s\ngot: %s", + hd, tolerance, + expected.String(), actual.String()) + } + } +} diff --git a/internal/jtsport/jts/operation_distance_base_distance_test.go b/internal/jtsport/jts/operation_distance_base_distance_test.go new file mode 100644 index 00000000..6695bd53 --- /dev/null +++ b/internal/jtsport/jts/operation_distance_base_distance_test.go @@ -0,0 +1,109 @@ +package jts + +import ( + "testing" + + "github.com/peterstace/simplefeatures/internal/jtsport/junit" +) + +// operationDistance_baseDistanceTest provides abstract test methods for distance tests. +type operationDistance_baseDistanceTest struct { + t *testing.T + distanceFn func(g1, g2 *Geom_Geometry) float64 + isWithinDistFn func(g1, g2 *Geom_Geometry, distance float64) bool + nearestPtsFn func(g1, g2 *Geom_Geometry) []*Geom_Coordinate +} + +func (bt *operationDistance_baseDistanceTest) testDisjointCollinearSegments() { + g1 := operationDistance_baseDistanceTest_read("LINESTRING (0.0 0.0, 9.9 1.4)") + g2 := operationDistance_baseDistanceTest_read("LINESTRING (11.88 1.68, 21.78 3.08)") + + dist := bt.distanceFn(g1, g2) + junit.AssertEqualsFloat64(bt.t, 1.9996999774966246, dist, 0.0001) + + junit.AssertTrue(bt.t, !bt.isWithinDistFn(g1, g2, 1)) + junit.AssertTrue(bt.t, bt.isWithinDistFn(g1, g2, 3)) +} + +func (bt *operationDistance_baseDistanceTest) testPolygonsDisjoint() { + g1 := operationDistance_baseDistanceTest_read("POLYGON ((40 320, 200 380, 320 80, 40 40, 40 320), (180 280, 80 280, 100 100, 220 140, 180 280))") + g2 := operationDistance_baseDistanceTest_read("POLYGON ((160 240, 120 240, 120 160, 160 140, 160 240))") + junit.AssertEqualsFloat64(bt.t, 18.97366596, bt.distanceFn(g1, g2), 1e-5) + + junit.AssertTrue(bt.t, !bt.isWithinDistFn(g1, g2, 0)) + junit.AssertTrue(bt.t, !bt.isWithinDistFn(g1, g2, 10)) + junit.AssertTrue(bt.t, bt.isWithinDistFn(g1, g2, 20)) +} + +func (bt *operationDistance_baseDistanceTest) testPolygonsOverlapping() { + g1 := operationDistance_baseDistanceTest_read("POLYGON ((40 320, 200 380, 320 80, 40 40, 40 320), (180 280, 80 280, 100 100, 220 140, 180 280))") + g3 := operationDistance_baseDistanceTest_read("POLYGON ((160 240, 120 240, 120 160, 180 100, 160 240))") + + junit.AssertEqualsFloat64(bt.t, 0.0, bt.distanceFn(g1, g3), 1e-9) + junit.AssertTrue(bt.t, bt.isWithinDistFn(g1, g3, 0.0)) +} + +func (bt *operationDistance_baseDistanceTest) testLinesIdentical() { + l1 := operationDistance_baseDistanceTest_read("LINESTRING(10 10, 20 20, 30 40)") + junit.AssertEqualsFloat64(bt.t, 0.0, bt.distanceFn(l1, l1), 1e-5) + + junit.AssertTrue(bt.t, bt.isWithinDistFn(l1, l1, 0)) +} + +func (bt *operationDistance_baseDistanceTest) testEmpty() { + g1 := operationDistance_baseDistanceTest_read("POINT (0 0)") + g2 := operationDistance_baseDistanceTest_read("POLYGON EMPTY") + junit.AssertEqualsFloat64(bt.t, 0.0, g1.Distance(g2), 0.0) +} + +func (bt *operationDistance_baseDistanceTest) testClosestPoints1() { + bt.checkDistanceNearestPoints("POLYGON ((200 180, 60 140, 60 260, 200 180))", "POINT (140 280)", 57.05597791103589, Geom_NewCoordinateWithXY(111.6923076923077, 230.46153846153845), Geom_NewCoordinateWithXY(140, 280)) +} + +func (bt *operationDistance_baseDistanceTest) testClosestPoints2() { + bt.checkDistanceNearestPoints("POLYGON ((200 180, 60 140, 60 260, 200 180))", "MULTIPOINT ((140 280), (140 320))", 57.05597791103589, Geom_NewCoordinateWithXY(111.6923076923077, 230.46153846153845), Geom_NewCoordinateWithXY(140, 280)) +} + +func (bt *operationDistance_baseDistanceTest) testClosestPoints3() { + bt.checkDistanceNearestPoints("LINESTRING (100 100, 200 100, 200 200, 100 200, 100 100)", "POINT (10 10)", 127.27922061357856, Geom_NewCoordinateWithXY(100, 100), Geom_NewCoordinateWithXY(10, 10)) +} + +func (bt *operationDistance_baseDistanceTest) testClosestPoints4() { + bt.checkDistanceNearestPoints("LINESTRING (100 100, 200 200)", "LINESTRING (100 200, 200 100)", 0.0, Geom_NewCoordinateWithXY(150, 150), Geom_NewCoordinateWithXY(150, 150)) +} + +func (bt *operationDistance_baseDistanceTest) testClosestPoints5() { + bt.checkDistanceNearestPoints("LINESTRING (100 100, 200 200)", "LINESTRING (150 121, 200 0)", 20.506096654409877, Geom_NewCoordinateWithXY(135.5, 135.5), Geom_NewCoordinateWithXY(150, 121)) +} + +func (bt *operationDistance_baseDistanceTest) testClosestPoints6() { + bt.checkDistanceNearestPoints("POLYGON ((76 185, 125 283, 331 276, 324 122, 177 70, 184 155, 69 123, 76 185), (267 237, 148 248, 135 185, 223 189, 251 151, 286 183, 267 237))", "LINESTRING (153 204, 185 224, 209 207, 238 222, 254 186)", 13.788860460124573, Geom_NewCoordinateWithXY(139.4956500724988, 206.78661188980183), Geom_NewCoordinateWithXY(153, 204)) +} + +func (bt *operationDistance_baseDistanceTest) testClosestPoints7() { + bt.checkDistanceNearestPoints("POLYGON ((76 185, 125 283, 331 276, 324 122, 177 70, 184 155, 69 123, 76 185), (267 237, 148 248, 135 185, 223 189, 251 151, 286 183, 267 237))", "LINESTRING (120 215, 185 224, 209 207, 238 222, 254 186)", 0.0, Geom_NewCoordinateWithXY(120, 215), Geom_NewCoordinateWithXY(120, 215)) +} + +const operationDistance_baseDistanceTest_TOLERANCE = 1e-10 + +func (bt *operationDistance_baseDistanceTest) checkDistanceNearestPoints(wkt0, wkt1 string, distance float64, p0, p1 *Geom_Coordinate) { + g0 := operationDistance_baseDistanceTest_read(wkt0) + g1 := operationDistance_baseDistanceTest_read(wkt1) + + nearestPoints := bt.nearestPtsFn(g0, g1) + + junit.AssertEqualsFloat64(bt.t, distance, nearestPoints[0].Distance(nearestPoints[1]), operationDistance_baseDistanceTest_TOLERANCE) + junit.AssertEqualsFloat64(bt.t, p0.X, nearestPoints[0].X, operationDistance_baseDistanceTest_TOLERANCE) + junit.AssertEqualsFloat64(bt.t, p0.Y, nearestPoints[0].Y, operationDistance_baseDistanceTest_TOLERANCE) + junit.AssertEqualsFloat64(bt.t, p1.X, nearestPoints[1].X, operationDistance_baseDistanceTest_TOLERANCE) + junit.AssertEqualsFloat64(bt.t, p1.Y, nearestPoints[1].Y, operationDistance_baseDistanceTest_TOLERANCE) +} + +func operationDistance_baseDistanceTest_read(wkt string) *Geom_Geometry { + reader := Io_NewWKTReader() + geom, err := reader.Read(wkt) + if err != nil { + panic(err) + } + return geom +} diff --git a/internal/jtsport/jts/operation_distance_connected_element_location_filter.go b/internal/jtsport/jts/operation_distance_connected_element_location_filter.go new file mode 100644 index 00000000..500d3bfd --- /dev/null +++ b/internal/jtsport/jts/operation_distance_connected_element_location_filter.go @@ -0,0 +1,46 @@ +package jts + +import "github.com/peterstace/simplefeatures/internal/jtsport/java" + +// OperationDistance_ConnectedElementLocationFilter extracts a single point +// from each connected element in a Geometry (e.g. a polygon, linestring or +// point) and returns them in a list. The elements of the list are +// GeometryLocations. Empty geometries do not provide a location item. +type OperationDistance_ConnectedElementLocationFilter struct { + locations []*OperationDistance_GeometryLocation +} + +var _ Geom_GeometryFilter = (*OperationDistance_ConnectedElementLocationFilter)(nil) + +func (f *OperationDistance_ConnectedElementLocationFilter) IsGeom_GeometryFilter() {} + +// OperationDistance_ConnectedElementLocationFilter_GetLocations returns a list +// containing a point from each Polygon, LineString, and Point found inside the +// specified geometry. Thus, if the specified geometry is not a +// GeometryCollection, an empty list will be returned. The elements of the list +// are GeometryLocations. +func OperationDistance_ConnectedElementLocationFilter_GetLocations(geom *Geom_Geometry) []*OperationDistance_GeometryLocation { + var locations []*OperationDistance_GeometryLocation + filter := &OperationDistance_ConnectedElementLocationFilter{locations: locations} + geom.ApplyGeometryFilter(filter) + return filter.locations +} + +// operationDistance_NewConnectedElementLocationFilter constructs a +// ConnectedElementLocationFilter. +func operationDistance_NewConnectedElementLocationFilter(locations []*OperationDistance_GeometryLocation) *OperationDistance_ConnectedElementLocationFilter { + return &OperationDistance_ConnectedElementLocationFilter{locations: locations} +} + +// Filter implements the GeometryFilter interface. +func (f *OperationDistance_ConnectedElementLocationFilter) Filter(geom *Geom_Geometry) { + // Empty geometries do not provide a location. + if geom.IsEmpty() { + return + } + if java.InstanceOf[*Geom_Point](geom) || + java.InstanceOf[*Geom_LineString](geom) || + java.InstanceOf[*Geom_Polygon](geom) { + f.locations = append(f.locations, OperationDistance_NewGeometryLocation(geom, 0, geom.GetCoordinate())) + } +} diff --git a/internal/jtsport/jts/operation_distance_connected_element_point_filter.go b/internal/jtsport/jts/operation_distance_connected_element_point_filter.go new file mode 100644 index 00000000..52af5eed --- /dev/null +++ b/internal/jtsport/jts/operation_distance_connected_element_point_filter.go @@ -0,0 +1,40 @@ +package jts + +import "github.com/peterstace/simplefeatures/internal/jtsport/java" + +// OperationDistance_ConnectedElementPointFilter extracts a single point from +// each connected element in a Geometry (e.g. a polygon, linestring or point) +// and returns them in a list. +type OperationDistance_ConnectedElementPointFilter struct { + pts []*Geom_Coordinate +} + +var _ Geom_GeometryFilter = (*OperationDistance_ConnectedElementPointFilter)(nil) + +func (f *OperationDistance_ConnectedElementPointFilter) IsGeom_GeometryFilter() {} + +// OperationDistance_ConnectedElementPointFilter_GetCoordinates returns a list +// containing a Coordinate from each Polygon, LineString, and Point found +// inside the specified geometry. Thus, if the specified geometry is not a +// GeometryCollection, an empty list will be returned. +func OperationDistance_ConnectedElementPointFilter_GetCoordinates(geom *Geom_Geometry) []*Geom_Coordinate { + var pts []*Geom_Coordinate + filter := &OperationDistance_ConnectedElementPointFilter{pts: pts} + geom.ApplyGeometryFilter(filter) + return filter.pts +} + +// operationDistance_NewConnectedElementPointFilter constructs a +// ConnectedElementPointFilter. +func operationDistance_NewConnectedElementPointFilter(pts []*Geom_Coordinate) *OperationDistance_ConnectedElementPointFilter { + return &OperationDistance_ConnectedElementPointFilter{pts: pts} +} + +// Filter implements the GeometryFilter interface. +func (f *OperationDistance_ConnectedElementPointFilter) Filter(geom *Geom_Geometry) { + if java.InstanceOf[*Geom_Point](geom) || + java.InstanceOf[*Geom_LineString](geom) || + java.InstanceOf[*Geom_Polygon](geom) { + f.pts = append(f.pts, geom.GetCoordinate()) + } +} diff --git a/internal/jtsport/jts/operation_distance_distance_op.go b/internal/jtsport/jts/operation_distance_distance_op.go new file mode 100644 index 00000000..d713b661 --- /dev/null +++ b/internal/jtsport/jts/operation_distance_distance_op.go @@ -0,0 +1,380 @@ +package jts + +import ( + "math" + + "github.com/peterstace/simplefeatures/internal/jtsport/java" +) + +// OperationDistance_DistanceOp_Distance computes the distance between the +// nearest points of two geometries. +func OperationDistance_DistanceOp_Distance(g0, g1 *Geom_Geometry) float64 { + distOp := OperationDistance_NewDistanceOp(g0, g1) + return distOp.Distance() +} + +// OperationDistance_DistanceOp_IsWithinDistance tests whether two geometries +// lie within a given distance of each other. +func OperationDistance_DistanceOp_IsWithinDistance(g0, g1 *Geom_Geometry, distance float64) bool { + // Check envelope distance for a short-circuit negative result. + envDist := g0.GetEnvelopeInternal().Distance(g1.GetEnvelopeInternal()) + if envDist > distance { + return false + } + + // MD - could improve this further with a positive short-circuit based on envelope MinMaxDist + + distOp := OperationDistance_NewDistanceOpWithTerminate(g0, g1, distance) + return distOp.Distance() <= distance +} + +// OperationDistance_DistanceOp_NearestPoints computes the nearest points of +// two geometries. The points are presented in the same order as the input +// Geometries. +func OperationDistance_DistanceOp_NearestPoints(g0, g1 *Geom_Geometry) []*Geom_Coordinate { + distOp := OperationDistance_NewDistanceOp(g0, g1) + return distOp.NearestPoints() +} + +// OperationDistance_DistanceOp_ClosestPoints computes the closest points of +// two geometries. The points are presented in the same order as the input +// Geometries. +// Deprecated: renamed to NearestPoints. +func OperationDistance_DistanceOp_ClosestPoints(g0, g1 *Geom_Geometry) []*Geom_Coordinate { + distOp := OperationDistance_NewDistanceOp(g0, g1) + return distOp.NearestPoints() +} + +// OperationDistance_DistanceOp finds two points on two Geometries which lie +// within a given distance, or else are the nearest points on the geometries +// (in which case this also provides the distance between the geometries). +// +// The distance computation also finds a pair of points in the input geometries +// which have the minimum distance between them. If a point lies in the interior +// of a line segment, the coordinate computed is a close approximation to the +// exact point. +// +// Empty geometry collection components are ignored. +// +// The algorithms used are straightforward O(n^2) comparisons. This worst-case +// performance could be improved on by using Voronoi techniques or spatial +// indexes. +type OperationDistance_DistanceOp struct { + // Input. + geom [2]*Geom_Geometry + terminateDistance float64 + // Working. + ptLocator *Algorithm_PointLocator + minDistanceLocation [2]*OperationDistance_GeometryLocation + minDistance float64 +} + +// OperationDistance_NewDistanceOp constructs a DistanceOp that computes the +// distance and nearest points between the two specified geometries. +func OperationDistance_NewDistanceOp(g0, g1 *Geom_Geometry) *OperationDistance_DistanceOp { + return OperationDistance_NewDistanceOpWithTerminate(g0, g1, 0.0) +} + +// OperationDistance_NewDistanceOpWithTerminate constructs a DistanceOp that +// computes the distance and nearest points between the two specified +// geometries. +func OperationDistance_NewDistanceOpWithTerminate(g0, g1 *Geom_Geometry, terminateDistance float64) *OperationDistance_DistanceOp { + return &OperationDistance_DistanceOp{ + geom: [2]*Geom_Geometry{g0, g1}, + terminateDistance: terminateDistance, + ptLocator: Algorithm_NewPointLocator(), + minDistance: math.MaxFloat64, + } +} + +// Distance reports the distance between the nearest points on the input +// geometries. +// +// Returns the distance between the geometries or 0 if either input geometry is +// empty. Panics if either input geometry is null. +func (op *OperationDistance_DistanceOp) Distance() float64 { + if op.geom[0] == nil || op.geom[1] == nil { + panic("null geometries are not supported") + } + if op.geom[0].IsEmpty() || op.geom[1].IsEmpty() { + return 0.0 + } + + // Optimization for Point/Point case. + if java.InstanceOf[*Geom_Point](op.geom[0]) && java.InstanceOf[*Geom_Point](op.geom[1]) { + return op.geom[0].GetCoordinate().Distance(op.geom[1].GetCoordinate()) + } + + op.computeMinDistance() + return op.minDistance +} + +// NearestPoints reports the coordinates of the nearest points in the input +// geometries. The points are presented in the same order as the input +// Geometries. +func (op *OperationDistance_DistanceOp) NearestPoints() []*Geom_Coordinate { + op.computeMinDistance() + nearestPts := []*Geom_Coordinate{ + op.minDistanceLocation[0].GetCoordinate(), + op.minDistanceLocation[1].GetCoordinate(), + } + return nearestPts +} + +// ClosestPoints returns a pair of Coordinates of the nearest points. +// Deprecated: renamed to NearestPoints. +func (op *OperationDistance_DistanceOp) ClosestPoints() []*Geom_Coordinate { + return op.NearestPoints() +} + +// NearestLocations reports the locations of the nearest points in the input +// geometries. The locations are presented in the same order as the input +// Geometries. +func (op *OperationDistance_DistanceOp) NearestLocations() [2]*OperationDistance_GeometryLocation { + op.computeMinDistance() + return op.minDistanceLocation +} + +// ClosestLocations returns a pair of GeometryLocations for the nearest points. +// Deprecated: renamed to NearestLocations. +func (op *OperationDistance_DistanceOp) ClosestLocations() [2]*OperationDistance_GeometryLocation { + return op.NearestLocations() +} + +func (op *OperationDistance_DistanceOp) updateMinDistance(locGeom [2]*OperationDistance_GeometryLocation, flip bool) { + // If not set then don't update. + if locGeom[0] == nil { + return + } + + if flip { + op.minDistanceLocation[0] = locGeom[1] + op.minDistanceLocation[1] = locGeom[0] + } else { + op.minDistanceLocation[0] = locGeom[0] + op.minDistanceLocation[1] = locGeom[1] + } +} + +func (op *OperationDistance_DistanceOp) computeMinDistance() { + // Only compute once! + if op.minDistanceLocation[0] != nil { + return + } + + op.computeContainmentDistance() + if op.minDistance <= op.terminateDistance { + return + } + op.computeFacetDistance() +} + +func (op *OperationDistance_DistanceOp) computeContainmentDistance() { + var locPtPoly [2]*OperationDistance_GeometryLocation + // Test if either geometry has a vertex inside the other. + op.computeContainmentDistanceForIndex(0, &locPtPoly) + if op.minDistance <= op.terminateDistance { + return + } + op.computeContainmentDistanceForIndex(1, &locPtPoly) +} + +func (op *OperationDistance_DistanceOp) computeContainmentDistanceForIndex(polyGeomIndex int, locPtPoly *[2]*OperationDistance_GeometryLocation) { + polyGeom := op.geom[polyGeomIndex] + // If no polygon then nothing to do. + if polyGeom.GetDimension() < 2 { + return + } + + locationsIndex := 1 - polyGeomIndex + polys := GeomUtil_PolygonExtracter_GetPolygons(polyGeom) + if len(polys) > 0 { + insideLocs := OperationDistance_ConnectedElementLocationFilter_GetLocations(op.geom[locationsIndex]) + op.computeContainmentDistanceLocsPolys(insideLocs, polys, locPtPoly) + if op.minDistance <= op.terminateDistance { + // This assignment is determined by the order of the args in the computeInside call above. + op.minDistanceLocation[locationsIndex] = locPtPoly[0] + op.minDistanceLocation[polyGeomIndex] = locPtPoly[1] + return + } + } +} + +func (op *OperationDistance_DistanceOp) computeContainmentDistanceLocsPolys(locs []*OperationDistance_GeometryLocation, polys []*Geom_Polygon, locPtPoly *[2]*OperationDistance_GeometryLocation) { + for i := 0; i < len(locs); i++ { + loc := locs[i] + for j := 0; j < len(polys); j++ { + op.computeContainmentDistanceLocPoly(loc, polys[j], locPtPoly) + if op.minDistance <= op.terminateDistance { + return + } + } + } +} + +func (op *OperationDistance_DistanceOp) computeContainmentDistanceLocPoly(ptLoc *OperationDistance_GeometryLocation, poly *Geom_Polygon, locPtPoly *[2]*OperationDistance_GeometryLocation) { + pt := ptLoc.GetCoordinate() + // If pt is not in exterior, distance to geom is 0. + if Geom_Location_Exterior != op.ptLocator.Locate(pt, poly.Geom_Geometry) { + op.minDistance = 0.0 + locPtPoly[0] = ptLoc + locPtPoly[1] = OperationDistance_NewGeometryLocationInsideArea(poly.Geom_Geometry, pt) + return + } +} + +// computeFacetDistance computes distance between facets (lines and points) of +// input geometries. +func (op *OperationDistance_DistanceOp) computeFacetDistance() { + var locGeom [2]*OperationDistance_GeometryLocation + + // Geometries are not wholly inside, so compute distance from lines and + // points of one to lines and points of the other. + lines0 := GeomUtil_LinearComponentExtracter_GetLines(op.geom[0]) + lines1 := GeomUtil_LinearComponentExtracter_GetLines(op.geom[1]) + + pts0 := GeomUtil_PointExtracter_GetPoints(op.geom[0]) + pts1 := GeomUtil_PointExtracter_GetPoints(op.geom[1]) + + // Exit whenever minDistance goes LE than terminateDistance. + op.computeMinDistanceLines(lines0, lines1, &locGeom) + op.updateMinDistance(locGeom, false) + if op.minDistance <= op.terminateDistance { + return + } + + locGeom[0] = nil + locGeom[1] = nil + op.computeMinDistanceLinesPoints(lines0, pts1, &locGeom) + op.updateMinDistance(locGeom, false) + if op.minDistance <= op.terminateDistance { + return + } + + locGeom[0] = nil + locGeom[1] = nil + op.computeMinDistanceLinesPoints(lines1, pts0, &locGeom) + op.updateMinDistance(locGeom, true) + if op.minDistance <= op.terminateDistance { + return + } + + locGeom[0] = nil + locGeom[1] = nil + op.computeMinDistancePoints(pts0, pts1, &locGeom) + op.updateMinDistance(locGeom, false) +} + +func (op *OperationDistance_DistanceOp) computeMinDistanceLines(lines0, lines1 []*Geom_LineString, locGeom *[2]*OperationDistance_GeometryLocation) { + for i := 0; i < len(lines0); i++ { + line0 := lines0[i] + for j := 0; j < len(lines1); j++ { + line1 := lines1[j] + op.computeMinDistanceLineToLine(line0, line1, locGeom) + if op.minDistance <= op.terminateDistance { + return + } + } + } +} + +func (op *OperationDistance_DistanceOp) computeMinDistancePoints(points0, points1 []*Geom_Point, locGeom *[2]*OperationDistance_GeometryLocation) { + for i := 0; i < len(points0); i++ { + pt0 := points0[i] + if pt0.IsEmpty() { + continue + } + for j := 0; j < len(points1); j++ { + pt1 := points1[j] + if pt1.IsEmpty() { + continue + } + dist := pt0.GetCoordinate().Distance(pt1.GetCoordinate()) + if dist < op.minDistance { + op.minDistance = dist + locGeom[0] = OperationDistance_NewGeometryLocation(pt0.Geom_Geometry, 0, pt0.GetCoordinate()) + locGeom[1] = OperationDistance_NewGeometryLocation(pt1.Geom_Geometry, 0, pt1.GetCoordinate()) + } + if op.minDistance <= op.terminateDistance { + return + } + } + } +} + +func (op *OperationDistance_DistanceOp) computeMinDistanceLinesPoints(lines []*Geom_LineString, points []*Geom_Point, locGeom *[2]*OperationDistance_GeometryLocation) { + for i := 0; i < len(lines); i++ { + line := lines[i] + for j := 0; j < len(points); j++ { + pt := points[j] + if pt.IsEmpty() { + continue + } + op.computeMinDistanceLineToPoint(line, pt, locGeom) + if op.minDistance <= op.terminateDistance { + return + } + } + } +} + +func (op *OperationDistance_DistanceOp) computeMinDistanceLineToLine(line0, line1 *Geom_LineString, locGeom *[2]*OperationDistance_GeometryLocation) { + if line0.GetEnvelopeInternal().Distance(line1.GetEnvelopeInternal()) > op.minDistance { + return + } + coord0 := line0.GetCoordinates() + coord1 := line1.GetCoordinates() + // Brute force approach! + for i := 0; i < len(coord0)-1; i++ { + // Short-circuit if line segment is far from line. + segEnv0 := Geom_NewEnvelopeFromCoordinates(coord0[i], coord0[i+1]) + if segEnv0.Distance(line1.GetEnvelopeInternal()) > op.minDistance { + continue + } + + for j := 0; j < len(coord1)-1; j++ { + // Short-circuit if line segments are far apart. + segEnv1 := Geom_NewEnvelopeFromCoordinates(coord1[j], coord1[j+1]) + if segEnv0.Distance(segEnv1) > op.minDistance { + continue + } + + dist := Algorithm_Distance_SegmentToSegment( + coord0[i], coord0[i+1], + coord1[j], coord1[j+1]) + if dist < op.minDistance { + op.minDistance = dist + seg0 := Geom_NewLineSegmentFromCoordinates(coord0[i], coord0[i+1]) + seg1 := Geom_NewLineSegmentFromCoordinates(coord1[j], coord1[j+1]) + closestPt := seg0.ClosestPoints(seg1) + locGeom[0] = OperationDistance_NewGeometryLocation(line0.Geom_Geometry, i, closestPt[0]) + locGeom[1] = OperationDistance_NewGeometryLocation(line1.Geom_Geometry, j, closestPt[1]) + } + if op.minDistance <= op.terminateDistance { + return + } + } + } +} + +func (op *OperationDistance_DistanceOp) computeMinDistanceLineToPoint(line *Geom_LineString, pt *Geom_Point, locGeom *[2]*OperationDistance_GeometryLocation) { + if line.GetEnvelopeInternal().Distance(pt.GetEnvelopeInternal()) > op.minDistance { + return + } + coord0 := line.GetCoordinates() + coord := pt.GetCoordinate() + // Brute force approach! + for i := 0; i < len(coord0)-1; i++ { + dist := Algorithm_Distance_PointToSegment(coord, coord0[i], coord0[i+1]) + if dist < op.minDistance { + op.minDistance = dist + seg := Geom_NewLineSegmentFromCoordinates(coord0[i], coord0[i+1]) + segClosestPoint := seg.ClosestPoint(coord) + locGeom[0] = OperationDistance_NewGeometryLocation(line.Geom_Geometry, i, segClosestPoint) + locGeom[1] = OperationDistance_NewGeometryLocation(pt.Geom_Geometry, 0, coord) + } + if op.minDistance <= op.terminateDistance { + return + } + } +} diff --git a/internal/jtsport/jts/operation_distance_distance_test.go b/internal/jtsport/jts/operation_distance_distance_test.go new file mode 100644 index 00000000..fd9ef85c --- /dev/null +++ b/internal/jtsport/jts/operation_distance_distance_test.go @@ -0,0 +1,78 @@ +package jts + +import "testing" + +func TestDistanceOp_DisjointCollinearSegments(t *testing.T) { + bt := newOperationDistance_distanceTest(t) + bt.testDisjointCollinearSegments() +} + +func TestDistanceOp_PolygonsDisjoint(t *testing.T) { + bt := newOperationDistance_distanceTest(t) + bt.testPolygonsDisjoint() +} + +func TestDistanceOp_PolygonsOverlapping(t *testing.T) { + bt := newOperationDistance_distanceTest(t) + bt.testPolygonsOverlapping() +} + +func TestDistanceOp_LinesIdentical(t *testing.T) { + bt := newOperationDistance_distanceTest(t) + bt.testLinesIdentical() +} + +func TestDistanceOp_Empty(t *testing.T) { + bt := newOperationDistance_distanceTest(t) + bt.testEmpty() +} + +func TestDistanceOp_ClosestPoints1(t *testing.T) { + bt := newOperationDistance_distanceTest(t) + bt.testClosestPoints1() +} + +func TestDistanceOp_ClosestPoints2(t *testing.T) { + bt := newOperationDistance_distanceTest(t) + bt.testClosestPoints2() +} + +func TestDistanceOp_ClosestPoints3(t *testing.T) { + bt := newOperationDistance_distanceTest(t) + bt.testClosestPoints3() +} + +func TestDistanceOp_ClosestPoints4(t *testing.T) { + bt := newOperationDistance_distanceTest(t) + bt.testClosestPoints4() +} + +func TestDistanceOp_ClosestPoints5(t *testing.T) { + bt := newOperationDistance_distanceTest(t) + bt.testClosestPoints5() +} + +func TestDistanceOp_ClosestPoints6(t *testing.T) { + bt := newOperationDistance_distanceTest(t) + bt.testClosestPoints6() +} + +func TestDistanceOp_ClosestPoints7(t *testing.T) { + bt := newOperationDistance_distanceTest(t) + bt.testClosestPoints7() +} + +func newOperationDistance_distanceTest(t *testing.T) *operationDistance_baseDistanceTest { + return &operationDistance_baseDistanceTest{ + t: t, + distanceFn: func(g1, g2 *Geom_Geometry) float64 { + return g1.Distance(g2) + }, + isWithinDistFn: func(g1, g2 *Geom_Geometry, distance float64) bool { + return g1.IsWithinDistance(g2, distance) + }, + nearestPtsFn: func(g1, g2 *Geom_Geometry) []*Geom_Coordinate { + return OperationDistance_DistanceOp_NearestPoints(g1, g2) + }, + } +} diff --git a/internal/jtsport/jts/operation_distance_geometry_location.go b/internal/jtsport/jts/operation_distance_geometry_location.go new file mode 100644 index 00000000..9b4ef080 --- /dev/null +++ b/internal/jtsport/jts/operation_distance_geometry_location.go @@ -0,0 +1,67 @@ +package jts + +import "strconv" + +// OperationDistance_GeometryLocation_INSIDE_AREA is a special value of +// segmentIndex used for locations inside area geometries. These locations are +// not located on a segment, and thus do not have an associated segment index. +const OperationDistance_GeometryLocation_INSIDE_AREA = -1 + +// OperationDistance_GeometryLocation represents the location of a point on a +// Geometry. Maintains both the actual point location (which may not be exact, +// if the point is not a vertex) as well as information about the component and +// segment index where the point occurs. Locations inside area Geometries will +// not have an associated segment index, so in this case the segment index will +// have the sentinel value of INSIDE_AREA. +type OperationDistance_GeometryLocation struct { + component *Geom_Geometry + segIndex int + pt *Geom_Coordinate +} + +// OperationDistance_NewGeometryLocation constructs a GeometryLocation +// specifying a point on a geometry, as well as the segment that the point is +// on (or INSIDE_AREA if the point is not on a segment). +func OperationDistance_NewGeometryLocation(component *Geom_Geometry, segIndex int, pt *Geom_Coordinate) *OperationDistance_GeometryLocation { + return &OperationDistance_GeometryLocation{ + component: component, + segIndex: segIndex, + pt: pt, + } +} + +// OperationDistance_NewGeometryLocationInsideArea constructs a GeometryLocation +// specifying a point inside an area geometry. +func OperationDistance_NewGeometryLocationInsideArea(component *Geom_Geometry, pt *Geom_Coordinate) *OperationDistance_GeometryLocation { + return OperationDistance_NewGeometryLocation(component, OperationDistance_GeometryLocation_INSIDE_AREA, pt) +} + +// GetGeometryComponent returns the geometry component on (or in) which this +// location occurs. +func (gl *OperationDistance_GeometryLocation) GetGeometryComponent() *Geom_Geometry { + return gl.component +} + +// GetSegmentIndex returns the segment index for this location. If the location +// is inside an area, the index will have the value INSIDE_AREA. +func (gl *OperationDistance_GeometryLocation) GetSegmentIndex() int { + return gl.segIndex +} + +// GetCoordinate returns the Coordinate of this location. +func (gl *OperationDistance_GeometryLocation) GetCoordinate() *Geom_Coordinate { + return gl.pt +} + +// IsInsideArea tests whether this location represents a point inside an area +// geometry. +func (gl *OperationDistance_GeometryLocation) IsInsideArea() bool { + return gl.segIndex == OperationDistance_GeometryLocation_INSIDE_AREA +} + +// String returns a string representation of this GeometryLocation. +func (gl *OperationDistance_GeometryLocation) String() string { + return gl.component.GetGeometryType() + + "[" + strconv.Itoa(gl.segIndex) + "]" + + "-" + Io_WKTWriter_ToPoint(gl.pt) +} diff --git a/internal/jtsport/jts/operation_overlayng_overlay_graph.go b/internal/jtsport/jts/operation_overlayng_overlay_graph.go index 6247d868..3777d891 100644 --- a/internal/jtsport/jts/operation_overlayng_overlay_graph.go +++ b/internal/jtsport/jts/operation_overlayng_overlay_graph.go @@ -1,5 +1,7 @@ package jts +import "sort" + // OperationOverlayng_OverlayGraph is a planar graph of edges, representing the // topology resulting from an overlay operation. Each source edge is // represented by a pair of OverlayEdges, with opposite (symmetric) orientation. @@ -32,6 +34,13 @@ func (og *OperationOverlayng_OverlayGraph) GetNodeEdges() []*OperationOverlayng_ for _, edge := range og.nodeMap { result = append(result, edge) } + // TRANSLITERATION NOTE: Sort by coordinate for deterministic ordering. + // Go's map iteration order is randomized, while Java's HashMap iteration + // order is consistent within a single JVM run. This sorting ensures the + // Go code produces deterministic results. + sort.Slice(result, func(i, j int) bool { + return result[i].Orig().CompareTo(result[j].Orig()) < 0 + }) return result } diff --git a/internal/jtsport/jts/operation_relateng_relate_edge.go b/internal/jtsport/jts/operation_relateng_relate_edge.go index 74d127fd..3ca022a2 100644 --- a/internal/jtsport/jts/operation_relateng_relate_edge.go +++ b/internal/jtsport/jts/operation_relateng_relate_edge.go @@ -306,7 +306,8 @@ func (e *OperationRelateng_RelateEdge) Location(isA bool, position int) int { return e.bLocLine } } - panic("should never reach here") + Util_Assert_ShouldNeverReachHere() + return operationRelateng_RelateEdge_LOC_UNKNOWN } func (e *OperationRelateng_RelateEdge) dimension(isA bool) int { diff --git a/internal/jtsport/jts/operation_valid_indexed_nested_hole_tester.go b/internal/jtsport/jts/operation_valid_indexed_nested_hole_tester.go new file mode 100644 index 00000000..6da8a9a2 --- /dev/null +++ b/internal/jtsport/jts/operation_valid_indexed_nested_hole_tester.go @@ -0,0 +1,76 @@ +package jts + +// OperationValid_IndexedNestedHoleTester tests whether any holes of a Polygon are +// nested inside another hole, using a spatial index to speed up the comparisons. +// +// The logic assumes that the holes do not overlap and have no collinear segments +// (so they are properly nested, and there are no duplicate holes). +// +// The situation where every vertex of a hole touches another hole +// is invalid because either the hole is nested, +// or else it disconnects the polygon interior. +// This class detects the nested situation. +// The disconnected interior situation must be checked elsewhere. +type OperationValid_IndexedNestedHoleTester struct { + polygon *Geom_Polygon + index Index_SpatialIndex + nestedPt *Geom_Coordinate +} + +// OperationValid_NewIndexedNestedHoleTester creates a new IndexedNestedHoleTester for +// the given polygon. +func OperationValid_NewIndexedNestedHoleTester(poly *Geom_Polygon) *OperationValid_IndexedNestedHoleTester { + tester := &OperationValid_IndexedNestedHoleTester{ + polygon: poly, + } + tester.loadIndex() + return tester +} + +func (t *OperationValid_IndexedNestedHoleTester) loadIndex() { + t.index = IndexStrtree_NewSTRtree() + + for i := 0; i < t.polygon.GetNumInteriorRing(); i++ { + hole := t.polygon.GetInteriorRingN(i) + env := hole.GetEnvelopeInternal() + t.index.Insert(env, hole) + } +} + +// GetNestedPoint gets a point on a nested hole, if one exists. +// +// Returns a point on a nested hole, or nil if none are nested. +func (t *OperationValid_IndexedNestedHoleTester) GetNestedPoint() *Geom_Coordinate { + return t.nestedPt +} + +// IsNested tests if any hole is nested (contained) within another hole. +// This is invalid. +// The nested point will be set to reflect this. +// +// Returns true if some hole is nested. +func (t *OperationValid_IndexedNestedHoleTester) IsNested() bool { + for i := 0; i < t.polygon.GetNumInteriorRing(); i++ { + hole := t.polygon.GetInteriorRingN(i) + + results := t.index.Query(hole.GetEnvelopeInternal()) + for _, result := range results { + testHole := result.(*Geom_LinearRing) + if hole == testHole { + continue + } + + // Hole is not fully covered by test hole, so cannot be nested + if !testHole.GetEnvelopeInternal().CoversEnvelope(hole.GetEnvelopeInternal()) { + continue + } + + if OperationValid_PolygonTopologyAnalyzer_IsRingNested(hole, testHole) { + //TODO: find a hole point known to be inside + t.nestedPt = hole.GetCoordinateN(0) + return true + } + } + } + return false +} diff --git a/internal/jtsport/jts/operation_valid_indexed_nested_polygon_tester.go b/internal/jtsport/jts/operation_valid_indexed_nested_polygon_tester.go new file mode 100644 index 00000000..1a544484 --- /dev/null +++ b/internal/jtsport/jts/operation_valid_indexed_nested_polygon_tester.go @@ -0,0 +1,156 @@ +package jts + +import "github.com/peterstace/simplefeatures/internal/jtsport/java" + +// OperationValid_IndexedNestedPolygonTester tests whether a MultiPolygon has any +// element polygon improperly nested inside another polygon, using a spatial +// index to speed up the comparisons. +// +// The logic assumes that the polygons do not overlap and have no collinear segments. +// So the polygon rings may touch at discrete points, +// but they are properly nested, and there are no duplicate rings. +type OperationValid_IndexedNestedPolygonTester struct { + multiPoly *Geom_MultiPolygon + index Index_SpatialIndex + locators []*AlgorithmLocate_IndexedPointInAreaLocator + nestedPt *Geom_Coordinate +} + +// OperationValid_NewIndexedNestedPolygonTester creates a new IndexedNestedPolygonTester +// for the given MultiPolygon. +func OperationValid_NewIndexedNestedPolygonTester(multiPoly *Geom_MultiPolygon) *OperationValid_IndexedNestedPolygonTester { + tester := &OperationValid_IndexedNestedPolygonTester{ + multiPoly: multiPoly, + } + tester.loadIndex() + return tester +} + +func (t *OperationValid_IndexedNestedPolygonTester) loadIndex() { + t.index = IndexStrtree_NewSTRtree() + + for i := 0; i < t.multiPoly.GetNumGeometries(); i++ { + poly := java.Cast[*Geom_Polygon](t.multiPoly.GetGeometryN(i)) + env := poly.GetEnvelopeInternal() + t.index.Insert(env, i) + } +} + +func (t *OperationValid_IndexedNestedPolygonTester) getLocator(polyIndex int) *AlgorithmLocate_IndexedPointInAreaLocator { + if t.locators == nil { + t.locators = make([]*AlgorithmLocate_IndexedPointInAreaLocator, t.multiPoly.GetNumGeometries()) + } + locator := t.locators[polyIndex] + if locator == nil { + locator = AlgorithmLocate_NewIndexedPointInAreaLocator(t.multiPoly.GetGeometryN(polyIndex)) + t.locators[polyIndex] = locator + } + return locator +} + +// GetNestedPoint gets a point on a nested polygon, if one exists. +// +// Returns a point on a nested polygon, or nil if none are nested. +func (t *OperationValid_IndexedNestedPolygonTester) GetNestedPoint() *Geom_Coordinate { + return t.nestedPt +} + +// IsNested tests if any polygon is improperly nested (contained) within another polygon. +// This is invalid. +// The nested point will be set to reflect this. +// +// Returns true if some polygon is nested. +func (t *OperationValid_IndexedNestedPolygonTester) IsNested() bool { + for i := 0; i < t.multiPoly.GetNumGeometries(); i++ { + poly := java.Cast[*Geom_Polygon](t.multiPoly.GetGeometryN(i)) + shell := poly.GetExteriorRing() + + results := t.index.Query(poly.GetEnvelopeInternal()) + for _, result := range results { + polyIndex := result.(int) + possibleOuterPoly := java.Cast[*Geom_Polygon](t.multiPoly.GetGeometryN(polyIndex)) + + if poly == possibleOuterPoly { + continue + } + // If polygon is not fully covered by candidate polygon it cannot be nested + if !possibleOuterPoly.GetEnvelopeInternal().CoversEnvelope(poly.GetEnvelopeInternal()) { + continue + } + + t.nestedPt = t.findNestedPoint(shell, possibleOuterPoly, t.getLocator(polyIndex)) + if t.nestedPt != nil { + return true + } + } + } + return false +} + +// findNestedPoint finds an improperly nested point, if one exists. +// +// shell is the test polygon shell +// possibleOuterPoly is a polygon which may contain it +// locator is the locator for the outer polygon +// +// Returns a nested point, if one exists, or nil. +func (t *OperationValid_IndexedNestedPolygonTester) findNestedPoint(shell *Geom_LinearRing, + possibleOuterPoly *Geom_Polygon, locator *AlgorithmLocate_IndexedPointInAreaLocator) *Geom_Coordinate { + // Try checking two points, since checking point location is fast. + shellPt0 := shell.GetCoordinateN(0) + loc0 := locator.Locate(shellPt0) + if loc0 == Geom_Location_Exterior { + return nil + } + if loc0 == Geom_Location_Interior { + return shellPt0 + } + + shellPt1 := shell.GetCoordinateN(1) + loc1 := locator.Locate(shellPt1) + if loc1 == Geom_Location_Exterior { + return nil + } + if loc1 == Geom_Location_Interior { + return shellPt1 + } + + // The shell points both lie on the boundary of + // the polygon. + // Nesting can be checked via the topology of the incident edges. + return operationValid_IndexedNestedPolygonTester_findIncidentSegmentNestedPoint(shell, possibleOuterPoly) +} + +// operationValid_IndexedNestedPolygonTester_findIncidentSegmentNestedPoint finds a point of a shell +// segment which lies inside a polygon, if any. +// The shell is assumed to touch the polygon only at shell vertices, +// and does not cross the polygon. +// +// shell is the shell to test +// poly is the polygon to test against +// +// Returns an interior segment point, or nil if the shell is nested correctly. +func operationValid_IndexedNestedPolygonTester_findIncidentSegmentNestedPoint(shell *Geom_LinearRing, poly *Geom_Polygon) *Geom_Coordinate { + polyShell := poly.GetExteriorRing() + if polyShell.IsEmpty() { + return nil + } + + if !OperationValid_PolygonTopologyAnalyzer_IsRingNested(shell, polyShell) { + return nil + } + + // Check if the shell is inside a hole (if there are any). + // If so this is valid. + for i := 0; i < poly.GetNumInteriorRing(); i++ { + hole := poly.GetInteriorRingN(i) + if hole.GetEnvelopeInternal().CoversEnvelope(shell.GetEnvelopeInternal()) && + OperationValid_PolygonTopologyAnalyzer_IsRingNested(shell, hole) { + return nil + } + } + + // The shell is contained in the polygon, but is not contained in a hole. + // This is invalid. + return shell.GetCoordinateN(0) +} diff --git a/internal/jtsport/jts/operation_valid_is_valid_op.go b/internal/jtsport/jts/operation_valid_is_valid_op.go new file mode 100644 index 00000000..49f11382 --- /dev/null +++ b/internal/jtsport/jts/operation_valid_is_valid_op.go @@ -0,0 +1,510 @@ +package jts + +import ( + "math" + + "github.com/peterstace/simplefeatures/internal/jtsport/java" +) + +const operationValid_IsValidOp_MIN_SIZE_LINESTRING = 2 +const operationValid_IsValidOp_MIN_SIZE_RING = 4 + +// OperationValid_IsValidOp_IsValid tests whether a Geometry is valid. +func OperationValid_IsValidOp_IsValid(geom *Geom_Geometry) bool { + isValidOp := OperationValid_NewIsValidOp(geom) + return isValidOp.IsValid() +} + +// OperationValid_IsValidOp_IsValidCoordinate checks whether a coordinate is valid for processing. +// Coordinates are valid if their x and y ordinates are in the +// range of the floating point representation. +func OperationValid_IsValidOp_IsValidCoordinate(coord *Geom_Coordinate) bool { + if math.IsNaN(coord.X) { + return false + } + if math.IsInf(coord.X, 0) { + return false + } + if math.IsNaN(coord.Y) { + return false + } + if math.IsInf(coord.Y, 0) { + return false + } + return true +} + +// OperationValid_IsValidOp implements the algorithms required to compute the isValid() method +// for Geometrys. +// See the documentation for the various geometry types for a specification of validity. +type OperationValid_IsValidOp struct { + // The geometry being validated + inputGeometry *Geom_Geometry + // If the following condition is TRUE JTS will validate inverted shells and exverted holes + // (the ESRI SDE model) + isInvertedRingValid bool + + validErr *OperationValid_TopologyValidationError +} + +// OperationValid_NewIsValidOp creates a new validator for a geometry. +func OperationValid_NewIsValidOp(inputGeometry *Geom_Geometry) *OperationValid_IsValidOp { + return &OperationValid_IsValidOp{ + inputGeometry: inputGeometry, + isInvertedRingValid: false, + } +} + +// SetSelfTouchingRingFormingHoleValid sets whether polygons using Self-Touching Rings to form +// holes are reported as valid. +// If this flag is set, the following Self-Touching conditions +// are treated as being valid: +// - inverted shell - the shell ring self-touches to create a hole touching the shell +// - exverted hole - a hole ring self-touches to create two holes touching at a point +// +// The default (following the OGC SFS standard) +// is that this condition is not valid (false). +// +// Self-Touching Rings which disconnect the +// the polygon interior are still considered to be invalid +// (these are invalid under the SFS, and many other +// spatial models as well). +// This includes: +// - exverted ("bow-tie") shells which self-touch at a single point +// - inverted shells with the inversion touching the shell at another point +// - exverted holes with exversion touching the hole at another point +// - inverted ("C-shaped") holes which self-touch at a single point causing an island to be formed +// - inverted shells or exverted holes which form part of a chain of touching rings +// (which disconnect the interior) +func (op *OperationValid_IsValidOp) SetSelfTouchingRingFormingHoleValid(isValid bool) { + op.isInvertedRingValid = isValid +} + +// IsValid tests the validity of the input geometry. +func (op *OperationValid_IsValidOp) IsValid() bool { + return op.isValidGeometry(op.inputGeometry) +} + +// GetValidationError computes the validity of the geometry, +// and if not valid returns the validation error for the geometry, +// or nil if the geometry is valid. +func (op *OperationValid_IsValidOp) GetValidationError() *OperationValid_TopologyValidationError { + op.isValidGeometry(op.inputGeometry) + return op.validErr +} + +func (op *OperationValid_IsValidOp) logInvalid(code int, pt *Geom_Coordinate) { + op.validErr = OperationValid_NewTopologyValidationError(code, pt) +} + +func (op *OperationValid_IsValidOp) hasInvalidError() bool { + return op.validErr != nil + +} + +func (op *OperationValid_IsValidOp) isValidGeometry(g *Geom_Geometry) bool { + op.validErr = nil + + // empty geometries are always valid + if g.IsEmpty() { + return true + } + + if java.InstanceOf[*Geom_Point](g) { + return op.isValidPoint(java.Cast[*Geom_Point](g)) + } + if java.InstanceOf[*Geom_MultiPoint](g) { + return op.isValidMultiPoint(java.Cast[*Geom_MultiPoint](g)) + } + if java.InstanceOf[*Geom_LinearRing](g) { + return op.isValidLinearRing(java.Cast[*Geom_LinearRing](g)) + } + if java.InstanceOf[*Geom_LineString](g) { + return op.isValidLineString(java.Cast[*Geom_LineString](g)) + } + if java.InstanceOf[*Geom_Polygon](g) { + return op.isValidPolygon(java.Cast[*Geom_Polygon](g)) + } + if java.InstanceOf[*Geom_MultiPolygon](g) { + return op.isValidMultiPolygon(java.Cast[*Geom_MultiPolygon](g)) + } + if java.InstanceOf[*Geom_GeometryCollection](g) { + return op.isValidGeometryCollection(java.Cast[*Geom_GeometryCollection](g)) + } + + // geometry type not known + panic("unsupported geometry type") +} + +// isValidPoint tests validity of a Point. +func (op *OperationValid_IsValidOp) isValidPoint(g *Geom_Point) bool { + op.checkCoordinatesValid(g.GetCoordinates()) + if op.hasInvalidError() { + return false + } + return true +} + +// isValidMultiPoint tests validity of a MultiPoint. +func (op *OperationValid_IsValidOp) isValidMultiPoint(g *Geom_MultiPoint) bool { + op.checkCoordinatesValid(g.GetCoordinates()) + if op.hasInvalidError() { + return false + } + return true +} + +// isValidLineString tests validity of a LineString. +// Almost anything goes for linestrings! +func (op *OperationValid_IsValidOp) isValidLineString(g *Geom_LineString) bool { + op.checkCoordinatesValid(g.GetCoordinates()) + if op.hasInvalidError() { + return false + } + op.checkPointSize(g, operationValid_IsValidOp_MIN_SIZE_LINESTRING) + if op.hasInvalidError() { + return false + } + return true +} + +// isValidLinearRing tests validity of a LinearRing. +func (op *OperationValid_IsValidOp) isValidLinearRing(g *Geom_LinearRing) bool { + op.checkCoordinatesValid(g.GetCoordinates()) + if op.hasInvalidError() { + return false + } + + op.checkRingClosed(g) + if op.hasInvalidError() { + return false + } + + op.checkRingPointSize(g) + if op.hasInvalidError() { + return false + } + + op.checkRingSimple(g) + return op.validErr == nil +} + +// isValidPolygon tests the validity of a polygon. +// Sets the validErr flag. +func (op *OperationValid_IsValidOp) isValidPolygon(g *Geom_Polygon) bool { + op.checkCoordinatesValidForPolygon(g) + if op.hasInvalidError() { + return false + } + + op.checkRingsClosed(g) + if op.hasInvalidError() { + return false + } + + op.checkRingsPointSize(g) + if op.hasInvalidError() { + return false + } + + areaAnalyzer := OperationValid_NewPolygonTopologyAnalyzer(g.Geom_Geometry, op.isInvertedRingValid) + + op.checkAreaIntersections(areaAnalyzer) + if op.hasInvalidError() { + return false + } + + op.checkHolesInShell(g) + if op.hasInvalidError() { + return false + } + + op.checkHolesNotNested(g) + if op.hasInvalidError() { + return false + } + + op.checkInteriorConnected(areaAnalyzer) + if op.hasInvalidError() { + return false + } + + return true +} + +// isValidMultiPolygon tests validity of a MultiPolygon. +func (op *OperationValid_IsValidOp) isValidMultiPolygon(g *Geom_MultiPolygon) bool { + for i := 0; i < g.GetNumGeometries(); i++ { + p := java.Cast[*Geom_Polygon](g.GetGeometryN(i)) + op.checkCoordinatesValidForPolygon(p) + if op.hasInvalidError() { + return false + } + + op.checkRingsClosed(p) + if op.hasInvalidError() { + return false + } + op.checkRingsPointSize(p) + if op.hasInvalidError() { + return false + } + } + + areaAnalyzer := OperationValid_NewPolygonTopologyAnalyzer(g.Geom_Geometry, op.isInvertedRingValid) + + op.checkAreaIntersections(areaAnalyzer) + if op.hasInvalidError() { + return false + } + + for i := 0; i < g.GetNumGeometries(); i++ { + p := java.Cast[*Geom_Polygon](g.GetGeometryN(i)) + op.checkHolesInShell(p) + if op.hasInvalidError() { + return false + } + } + for i := 0; i < g.GetNumGeometries(); i++ { + p := java.Cast[*Geom_Polygon](g.GetGeometryN(i)) + op.checkHolesNotNested(p) + if op.hasInvalidError() { + return false + } + } + op.checkShellsNotNested(g) + if op.hasInvalidError() { + return false + } + + op.checkInteriorConnected(areaAnalyzer) + if op.hasInvalidError() { + return false + } + + return true +} + +// isValidGeometryCollection tests validity of a GeometryCollection. +func (op *OperationValid_IsValidOp) isValidGeometryCollection(gc *Geom_GeometryCollection) bool { + for i := 0; i < gc.GetNumGeometries(); i++ { + if !op.isValidGeometry(gc.GetGeometryN(i)) { + return false + } + } + return true +} + +func (op *OperationValid_IsValidOp) checkCoordinatesValid(coords []*Geom_Coordinate) { + for i := 0; i < len(coords); i++ { + if !OperationValid_IsValidOp_IsValidCoordinate(coords[i]) { + op.logInvalid(OperationValid_TopologyValidationError_INVALID_COORDINATE, coords[i]) + return + } + } +} + +func (op *OperationValid_IsValidOp) checkCoordinatesValidForPolygon(poly *Geom_Polygon) { + op.checkCoordinatesValid(poly.GetExteriorRing().GetCoordinates()) + if op.hasInvalidError() { + return + } + for i := 0; i < poly.GetNumInteriorRing(); i++ { + op.checkCoordinatesValid(poly.GetInteriorRingN(i).GetCoordinates()) + if op.hasInvalidError() { + return + } + } +} + +func (op *OperationValid_IsValidOp) checkRingClosed(ring *Geom_LinearRing) { + if ring.IsEmpty() { + return + } + if !ring.IsClosed() { + var pt *Geom_Coordinate + if ring.GetNumPoints() >= 1 { + pt = ring.GetCoordinateN(0) + } + op.logInvalid(OperationValid_TopologyValidationError_RING_NOT_CLOSED, pt) + return + } +} + +func (op *OperationValid_IsValidOp) checkRingsClosed(poly *Geom_Polygon) { + op.checkRingClosed(poly.GetExteriorRing()) + if op.hasInvalidError() { + return + } + for i := 0; i < poly.GetNumInteriorRing(); i++ { + op.checkRingClosed(poly.GetInteriorRingN(i)) + if op.hasInvalidError() { + return + } + } +} + +func (op *OperationValid_IsValidOp) checkRingsPointSize(poly *Geom_Polygon) { + op.checkRingPointSize(poly.GetExteriorRing()) + if op.hasInvalidError() { + return + } + for i := 0; i < poly.GetNumInteriorRing(); i++ { + op.checkRingPointSize(poly.GetInteriorRingN(i)) + if op.hasInvalidError() { + return + } + } +} + +func (op *OperationValid_IsValidOp) checkRingPointSize(ring *Geom_LinearRing) { + if ring.IsEmpty() { + return + } + op.checkPointSize(ring.Geom_LineString, operationValid_IsValidOp_MIN_SIZE_RING) +} + +// checkPointSize checks the number of non-repeated points is at least a given size. +func (op *OperationValid_IsValidOp) checkPointSize(line *Geom_LineString, minSize int) { + if !op.isNonRepeatedSizeAtLeast(line, minSize) { + var pt *Geom_Coordinate + if line.GetNumPoints() >= 1 { + pt = line.GetCoordinateN(0) + } + op.logInvalid(OperationValid_TopologyValidationError_TOO_FEW_POINTS, pt) + } +} + +// isNonRepeatedSizeAtLeast tests if the number of non-repeated points in a line +// is at least a given minimum size. +func (op *OperationValid_IsValidOp) isNonRepeatedSizeAtLeast(line *Geom_LineString, minSize int) bool { + numPts := 0 + var prevPt *Geom_Coordinate + for i := 0; i < line.GetNumPoints(); i++ { + if numPts >= minSize { + return true + } + pt := line.GetCoordinateN(i) + if prevPt == nil || !pt.Equals2D(prevPt) { + numPts++ + } + prevPt = pt + } + return numPts >= minSize +} + +func (op *OperationValid_IsValidOp) checkAreaIntersections(areaAnalyzer *OperationValid_PolygonTopologyAnalyzer) { + if areaAnalyzer.HasInvalidIntersection() { + op.logInvalid(areaAnalyzer.GetInvalidCode(), + areaAnalyzer.GetInvalidLocation()) + return + } +} + +// checkRingSimple checks whether a ring self-intersects (except at its endpoints). +func (op *OperationValid_IsValidOp) checkRingSimple(ring *Geom_LinearRing) { + intPt := OperationValid_PolygonTopologyAnalyzer_FindSelfIntersection(ring) + if intPt != nil { + op.logInvalid(OperationValid_TopologyValidationError_RING_SELF_INTERSECTION, + intPt) + } +} + +// checkHolesInShell tests that each hole is inside the polygon shell. +// This routine assumes that the holes have previously been tested +// to ensure that all vertices lie on the shell or on the same side of it +// (i.e. that the hole rings do not cross the shell ring). +// Given this, a simple point-in-polygon test of a single point in the hole can be used, +// provided the point is chosen such that it does not lie on the shell. +func (op *OperationValid_IsValidOp) checkHolesInShell(poly *Geom_Polygon) { + // skip test if no holes are present + if poly.GetNumInteriorRing() <= 0 { + return + } + + shell := poly.GetExteriorRing() + isShellEmpty := shell.IsEmpty() + + for i := 0; i < poly.GetNumInteriorRing(); i++ { + hole := poly.GetInteriorRingN(i) + if hole.IsEmpty() { + continue + } + + var invalidPt *Geom_Coordinate + if isShellEmpty { + invalidPt = hole.GetCoordinate() + } else { + invalidPt = op.findHoleOutsideShellPoint(hole, shell) + } + if invalidPt != nil { + op.logInvalid(OperationValid_TopologyValidationError_HOLE_OUTSIDE_SHELL, + invalidPt) + return + } + } +} + +// findHoleOutsideShellPoint checks if a polygon hole lies inside its shell +// and if not returns a point indicating this. +// The hole is known to be wholly inside or outside the shell, +// so it suffices to find a single point which is interior or exterior, +// or check the edge topology at a point on the boundary of the shell. +func (op *OperationValid_IsValidOp) findHoleOutsideShellPoint(hole, shell *Geom_LinearRing) *Geom_Coordinate { + holePt0 := hole.GetCoordinateN(0) + // If hole envelope is not covered by shell, it must be outside + if !shell.GetEnvelopeInternal().CoversEnvelope(hole.GetEnvelopeInternal()) { + //TODO: find hole pt outside shell env + return holePt0 + } + + if OperationValid_PolygonTopologyAnalyzer_IsRingNested(hole, shell) { + return nil + } + //TODO: find hole point outside shell + return holePt0 +} + +// checkHolesNotNested checks if any polygon hole is nested inside another. +// Assumes that holes do not cross (overlap), +// This is checked earlier. +func (op *OperationValid_IsValidOp) checkHolesNotNested(poly *Geom_Polygon) { + // skip test if no holes are present + if poly.GetNumInteriorRing() <= 0 { + return + } + + nestedTester := OperationValid_NewIndexedNestedHoleTester(poly) + if nestedTester.IsNested() { + op.logInvalid(OperationValid_TopologyValidationError_NESTED_HOLES, + nestedTester.GetNestedPoint()) + } +} + +// checkShellsNotNested checks that no element polygon is in the interior of another element polygon. +// +// Preconditions: +// - shells do not partially overlap +// - shells do not touch along an edge +// - no duplicate rings exist +// +// These have been confirmed by the PolygonTopologyAnalyzer. +func (op *OperationValid_IsValidOp) checkShellsNotNested(mp *Geom_MultiPolygon) { + // skip test if only one shell present + if mp.GetNumGeometries() <= 1 { + return + } + + nestedTester := OperationValid_NewIndexedNestedPolygonTester(mp) + if nestedTester.IsNested() { + op.logInvalid(OperationValid_TopologyValidationError_NESTED_SHELLS, + nestedTester.GetNestedPoint()) + } +} + +func (op *OperationValid_IsValidOp) checkInteriorConnected(analyzer *OperationValid_PolygonTopologyAnalyzer) { + if analyzer.IsInteriorDisconnected() { + op.logInvalid(OperationValid_TopologyValidationError_DISCONNECTED_INTERIOR, + analyzer.GetDisconnectionLocation()) + } +} diff --git a/internal/jtsport/jts/operation_valid_is_valid_test.go b/internal/jtsport/jts/operation_valid_is_valid_test.go new file mode 100644 index 00000000..c12b6292 --- /dev/null +++ b/internal/jtsport/jts/operation_valid_is_valid_test.go @@ -0,0 +1,186 @@ +package jts + +import ( + "math" + "testing" + + "github.com/peterstace/simplefeatures/internal/jtsport/junit" +) + +var operationValidIsValidTest_precisionModel = Geom_NewPrecisionModel() +var operationValidIsValidTest_geometryFactory = Geom_NewGeometryFactoryWithPrecisionModelAndSRID(operationValidIsValidTest_precisionModel, 0) +var operationValidIsValidTest_reader = Io_NewWKTReaderWithFactory(operationValidIsValidTest_geometryFactory) + +func TestIsValidInvalidCoordinate(t *testing.T) { + badCoord := Geom_NewCoordinateWithXY(1.0, math.NaN()) + pts := []*Geom_Coordinate{Geom_NewCoordinateWithXY(0.0, 0.0), badCoord} + line := operationValidIsValidTest_geometryFactory.CreateLineStringFromCoordinates(pts) + isValidOp := OperationValid_NewIsValidOp(line.Geom_Geometry) + valid := isValidOp.IsValid() + err := isValidOp.GetValidationError() + errCoord := err.GetCoordinate() + + junit.AssertEquals(t, OperationValid_TopologyValidationError_INVALID_COORDINATE, err.GetErrorType()) + junit.AssertTrue(t, math.IsNaN(errCoord.Y)) + junit.AssertEquals(t, false, valid) +} + +func TestIsValidZeroAreaPolygon(t *testing.T) { + isValid_checkInvalid(t, "POLYGON((0 0, 0 0, 0 0, 0 0, 0 0))") +} + +func TestIsValidValidSimplePolygon(t *testing.T) { + isValid_checkValid(t, "POLYGON ((10 89, 90 89, 90 10, 10 10, 10 89))") +} + +func TestIsValidInvalidSimplePolygonRingSelfIntersection(t *testing.T) { + isValid_checkInvalidWithCode(t, OperationValid_TopologyValidationError_SELF_INTERSECTION, + "POLYGON ((10 90, 90 10, 90 90, 10 10, 10 90))") +} + +func TestIsValidInvalidPolygonInverted(t *testing.T) { + isValid_checkInvalidWithCode(t, OperationValid_TopologyValidationError_RING_SELF_INTERSECTION, + "POLYGON ((70 250, 40 500, 100 400, 70 250, 80 350, 60 350, 70 250))") +} + +func TestIsValidInvalidPolygonSelfCrossing(t *testing.T) { + isValid_checkInvalidWithCode(t, OperationValid_TopologyValidationError_SELF_INTERSECTION, + "POLYGON ((70 250, 70 500, 80 400, 40 400, 70 250))") +} + +func TestIsValidSimplePolygonHole(t *testing.T) { + isValid_checkValid(t, + "POLYGON ((10 90, 90 90, 90 10, 10 10, 10 90), (60 20, 20 70, 90 90, 60 20))") +} + +func TestIsValidPolygonTouchingHoleAtVertex(t *testing.T) { + isValid_checkValid(t, + "POLYGON ((240 260, 40 260, 40 80, 240 80, 240 260), (140 180, 40 260, 140 240, 140 180))") +} + +func TestIsValidPolygonMultipleHolesTouchAtSamePoint(t *testing.T) { + isValid_checkValid(t, + "POLYGON ((10 90, 90 90, 90 10, 10 10, 10 90), (40 80, 60 80, 50 50, 40 80), (20 60, 20 40, 50 50, 20 60), (40 20, 60 20, 50 50, 40 20))") +} + +func TestIsValidPolygonHoleOutsideShellAllTouch(t *testing.T) { + isValid_checkInvalidWithCode(t, OperationValid_TopologyValidationError_HOLE_OUTSIDE_SHELL, + "POLYGON ((10 10, 30 10, 30 50, 70 50, 70 10, 90 10, 90 90, 10 90, 10 10), (50 50, 30 10, 70 10, 50 50))") +} + +func TestIsValidPolygonHoleOutsideShellDoubleTouch(t *testing.T) { + isValid_checkInvalidWithCode(t, OperationValid_TopologyValidationError_HOLE_OUTSIDE_SHELL, + "POLYGON ((10 90, 90 90, 90 10, 10 10, 10 90), (20 80, 80 80, 80 20, 20 20, 20 80), (90 70, 150 50, 90 20, 110 40, 90 70))") +} + +func TestIsValidPolygonNestedHolesAllTouch(t *testing.T) { + isValid_checkInvalidWithCode(t, OperationValid_TopologyValidationError_NESTED_HOLES, + "POLYGON ((10 90, 90 90, 90 10, 10 10, 10 90), (20 80, 80 80, 80 20, 20 20, 20 80), (50 80, 80 50, 50 20, 20 50, 50 80))") +} + +func TestIsValidInvalidPolygonHoleProperIntersection(t *testing.T) { + isValid_checkInvalidWithCode(t, OperationValid_TopologyValidationError_SELF_INTERSECTION, + "POLYGON ((10 90, 50 50, 10 10, 10 90), (20 50, 60 70, 60 30, 20 50))") +} + +func TestIsValidInvalidPolygonDisconnectedInterior(t *testing.T) { + isValid_checkInvalidWithCode(t, OperationValid_TopologyValidationError_DISCONNECTED_INTERIOR, + "POLYGON ((10 90, 90 90, 90 10, 10 10, 10 90), (20 80, 30 80, 20 20, 20 80), (80 30, 20 20, 80 20, 80 30), (80 80, 30 80, 80 30, 80 80))") +} + +func TestIsValidValidMultiPolygonTouchAtVertices(t *testing.T) { + isValid_checkValid(t, + "MULTIPOLYGON (((10 10, 10 90, 90 90, 90 10, 80 80, 50 20, 20 80, 10 10)), ((90 10, 10 10, 50 20, 90 10)))") +} + +func TestIsValidInvalidMultiPolygonHoleOverlapCrossing(t *testing.T) { + isValid_checkInvalidWithCode(t, OperationValid_TopologyValidationError_SELF_INTERSECTION, + "MULTIPOLYGON (((20 380, 420 380, 420 20, 20 20, 20 380), (220 340, 180 240, 60 200, 140 100, 340 60, 300 240, 220 340)), ((60 200, 340 60, 220 340, 60 200)))") +} + +func TestIsValidValidMultiPolygonTouchAtVerticesSegments(t *testing.T) { + isValid_checkValid(t, + "MULTIPOLYGON (((60 40, 90 10, 90 90, 10 90, 10 10, 40 40, 60 40)), ((50 40, 20 20, 80 20, 50 40)))") +} + +func TestIsValidInvalidMultiPolygonNestedAllTouchAtVertices(t *testing.T) { + isValid_checkInvalidWithCode(t, OperationValid_TopologyValidationError_NESTED_SHELLS, + "MULTIPOLYGON (((10 10, 20 30, 10 90, 90 90, 80 30, 90 10, 50 20, 10 10)), ((80 30, 20 30, 50 20, 80 30)))") +} + +func TestIsValidValidMultiPolygonHoleTouchVertices(t *testing.T) { + isValid_checkValid(t, + "MULTIPOLYGON (((20 380, 420 380, 420 20, 20 20, 20 380), (220 340, 80 320, 60 200, 140 100, 340 60, 300 240, 220 340)), ((60 200, 340 60, 220 340, 60 200)))") +} + +func TestIsValidLineString(t *testing.T) { + isValid_checkInvalid(t, "LINESTRING(0 0, 0 0)") +} + +func TestIsValidLinearRingTriangle(t *testing.T) { + isValid_checkValid(t, "LINEARRING (100 100, 150 200, 200 100, 100 100)") +} + +func TestIsValidLinearRingSelfCrossing(t *testing.T) { + isValid_checkInvalidWithCode(t, OperationValid_TopologyValidationError_RING_SELF_INTERSECTION, + "LINEARRING (150 100, 300 300, 100 300, 350 100, 150 100)") +} + +func TestIsValidLinearRingSelfCrossing2(t *testing.T) { + isValid_checkInvalidWithCode(t, OperationValid_TopologyValidationError_RING_SELF_INTERSECTION, + "LINEARRING (0 0, 100 100, 100 0, 0 100, 0 0)") +} + +// TestIsValidPolygonHoleWithRepeatedShellPointTouch tests that repeated points at nodes are handled correctly. +// +// See https://github.com/locationtech/jts/issues/843 +func TestIsValidPolygonHoleWithRepeatedShellPointTouch(t *testing.T) { + isValid_checkValid(t, "POLYGON ((90 10, 10 10, 50 90, 50 90, 90 10), (50 90, 60 30, 40 30, 50 90))") +} + +func TestIsValidPolygonHoleWithRepeatedShellPointTouchMultiple(t *testing.T) { + isValid_checkValid(t, "POLYGON ((90 10, 10 10, 50 90, 50 90, 50 90, 50 90, 90 10), (50 90, 60 30, 40 30, 50 90))") +} + +func TestIsValidPolygonHoleWithRepeatedTouchEndPoint(t *testing.T) { + isValid_checkValid(t, "POLYGON ((90 10, 10 10, 50 90, 90 10, 90 10), (90 10, 40 30, 60 50, 90 10))") +} + +func TestIsValidPolygonHoleWithRepeatedHolePointTouch(t *testing.T) { + isValid_checkValid(t, "POLYGON ((50 90, 10 10, 90 10, 50 90), (50 90, 50 90, 60 40, 60 40, 40 40, 50 90))") +} + +//============================================= + +func isValid_checkValid(t *testing.T, wkt string) { + t.Helper() + isValid_checkValidExpected(t, true, wkt) +} + +func isValid_checkValidExpected(t *testing.T, isExpectedValid bool, wkt string) { + t.Helper() + geom := isValid_read(wkt) + isValid := geom.IsValid() + junit.AssertEquals(t, isExpectedValid, isValid) +} + +func isValid_checkInvalid(t *testing.T, wkt string) { + t.Helper() + isValid_checkValidExpected(t, false, wkt) +} + +func isValid_checkInvalidWithCode(t *testing.T, expectedErrType int, wkt string) { + t.Helper() + geom := isValid_read(wkt) + validOp := OperationValid_NewIsValidOp(geom) + err := validOp.GetValidationError() + junit.AssertEquals(t, expectedErrType, err.GetErrorType()) +} + +func isValid_read(wkt string) *Geom_Geometry { + g, err := operationValidIsValidTest_reader.Read(wkt) + if err != nil { + panic(err) + } + return g +} diff --git a/internal/jtsport/jts/operation_valid_polygon_intersection_analyzer.go b/internal/jtsport/jts/operation_valid_polygon_intersection_analyzer.go new file mode 100644 index 00000000..c3dd340c --- /dev/null +++ b/internal/jtsport/jts/operation_valid_polygon_intersection_analyzer.go @@ -0,0 +1,223 @@ +package jts + +// operationValid_PolygonIntersectionAnalyzer_noInvalidIntersection is the +// sentinel value indicating no invalid intersection was found. +const operationValid_PolygonIntersectionAnalyzer_noInvalidIntersection = -1 + +// OperationValid_PolygonIntersectionAnalyzer finds and analyzes intersections +// in and between polygons, to determine if they are valid. +// +// The Noding_SegmentStrings which are analyzed can have OperationValid_PolygonRings +// attached. If so they will be updated with intersection information +// to support further validity analysis which must be done after +// basic intersection validity has been confirmed. +type OperationValid_PolygonIntersectionAnalyzer struct { + isInvertedRingValid bool + + li *Algorithm_LineIntersector + invalidCode int + invalidLocation *Geom_Coordinate + + hasDoubleTouch bool + doubleTouchLocation *Geom_Coordinate +} + +var _ Noding_SegmentIntersector = (*OperationValid_PolygonIntersectionAnalyzer)(nil) + +// OperationValid_NewPolygonIntersectionAnalyzer creates a new analyzer, +// allowing for the mode where inverted rings are valid. +func OperationValid_NewPolygonIntersectionAnalyzer(isInvertedRingValid bool) *OperationValid_PolygonIntersectionAnalyzer { + return &OperationValid_PolygonIntersectionAnalyzer{ + isInvertedRingValid: isInvertedRingValid, + li: Algorithm_NewRobustLineIntersector().Algorithm_LineIntersector, + invalidCode: operationValid_PolygonIntersectionAnalyzer_noInvalidIntersection, + } +} + +// TRANSLITERATION NOTE: Marker method required for Go interface implementation. +// Java implements SegmentIntersector interface implicitly. +func (pia *OperationValid_PolygonIntersectionAnalyzer) IsNoding_SegmentIntersector() {} + +// IsDone reports whether the client needs to continue testing all +// intersections in an arrangement. +func (pia *OperationValid_PolygonIntersectionAnalyzer) IsDone() bool { + return pia.IsInvalid() || pia.hasDoubleTouch +} + +// IsInvalid reports whether an invalid intersection was found. +func (pia *OperationValid_PolygonIntersectionAnalyzer) IsInvalid() bool { + return pia.invalidCode >= 0 +} + +// GetInvalidCode returns the code indicating the type of invalid intersection, +// or -1 if none was found. +func (pia *OperationValid_PolygonIntersectionAnalyzer) GetInvalidCode() int { + return pia.invalidCode +} + +// GetInvalidLocation returns the location of the invalid intersection. +func (pia *OperationValid_PolygonIntersectionAnalyzer) GetInvalidLocation() *Geom_Coordinate { + return pia.invalidLocation +} + +// HasDoubleTouch reports whether a double touch was found between rings. +func (pia *OperationValid_PolygonIntersectionAnalyzer) HasDoubleTouch() bool { + return pia.hasDoubleTouch +} + +// GetDoubleTouchLocation returns the location of the double touch. +func (pia *OperationValid_PolygonIntersectionAnalyzer) GetDoubleTouchLocation() *Geom_Coordinate { + return pia.doubleTouchLocation +} + +// ProcessIntersections is called by clients to process intersections for +// two segments of the SegmentStrings being intersected. +func (pia *OperationValid_PolygonIntersectionAnalyzer) ProcessIntersections(ss0 Noding_SegmentString, segIndex0 int, ss1 Noding_SegmentString, segIndex1 int) { + // don't test a segment with itself + isSameSegString := ss0 == ss1 + isSameSegment := isSameSegString && segIndex0 == segIndex1 + if isSameSegment { + return + } + + code := pia.findInvalidIntersection(ss0, segIndex0, ss1, segIndex1) + // Ensure that invalidCode is only set once, + // since the short-circuiting in SegmentIntersector is not guaranteed + // to happen immediately. + if code != operationValid_PolygonIntersectionAnalyzer_noInvalidIntersection { + pia.invalidCode = code + pia.invalidLocation = pia.li.GetIntersection(0) + } +} + +func (pia *OperationValid_PolygonIntersectionAnalyzer) findInvalidIntersection(ss0 Noding_SegmentString, segIndex0 int, ss1 Noding_SegmentString, segIndex1 int) int { + p00 := ss0.GetCoordinate(segIndex0) + p01 := ss0.GetCoordinate(segIndex0 + 1) + p10 := ss1.GetCoordinate(segIndex1) + p11 := ss1.GetCoordinate(segIndex1 + 1) + + pia.li.ComputeIntersection(p00, p01, p10, p11) + + if !pia.li.HasIntersection() { + return operationValid_PolygonIntersectionAnalyzer_noInvalidIntersection + } + + isSameSegString := ss0 == ss1 + + // Check for an intersection in the interior of both segments. + // Collinear intersections by definition contain an interior intersection. + if pia.li.IsProper() || pia.li.GetIntersectionNum() >= 2 { + return OperationValid_TopologyValidationError_SELF_INTERSECTION + } + + // Now know there is exactly one intersection, + // at a vertex of at least one segment. + intPt := pia.li.GetIntersection(0) + + // If segments are adjacent the intersection must be their common endpoint. + // (since they are not collinear). + // This is valid. + isAdjacentSegments := isSameSegString && operationValid_PolygonIntersectionAnalyzer_isAdjacentInRing(ss0, segIndex0, segIndex1) + // Assert: intersection is an endpoint of both segs + if isAdjacentSegments { + return operationValid_PolygonIntersectionAnalyzer_noInvalidIntersection + } + + // Under OGC semantics, rings cannot self-intersect. + // So the intersection is invalid. + // + // The return of RING_SELF_INTERSECTION is to match the previous IsValid semantics. + if isSameSegString && !pia.isInvertedRingValid { + return OperationValid_TopologyValidationError_RING_SELF_INTERSECTION + } + + // Optimization: don't analyze intPts at the endpoint of a segment. + // This is because they are also start points, so don't need to be + // evaluated twice. + // This simplifies following logic, by removing the segment endpoint case. + if intPt.Equals2D(p01) || intPt.Equals2D(p11) { + return operationValid_PolygonIntersectionAnalyzer_noInvalidIntersection + } + + // Check topology of a vertex intersection. + // The ring(s) must not cross. + e00 := p00 + e01 := p01 + if intPt.Equals2D(p00) { + e00 = operationValid_PolygonIntersectionAnalyzer_prevCoordinateInRing(ss0, segIndex0) + e01 = p01 + } + e10 := p10 + e11 := p11 + if intPt.Equals2D(p10) { + e10 = operationValid_PolygonIntersectionAnalyzer_prevCoordinateInRing(ss1, segIndex1) + e11 = p11 + } + hasCrossing := Algorithm_PolygonNodeTopology_IsCrossing(intPt, e00, e01, e10, e11) + if hasCrossing { + return OperationValid_TopologyValidationError_SELF_INTERSECTION + } + + // If allowing inverted rings, record a self-touch to support later checking + // that it does not disconnect the interior. + if isSameSegString && pia.isInvertedRingValid { + pia.addSelfTouch(ss0, intPt, e00, e01, e10, e11) + } + + // If the rings are in the same polygon + // then record the touch to support connected interior checking. + // + // Also check for an invalid double-touch situation, + // if the rings are different. + isDoubleTouch := pia.addDoubleTouch(ss0, ss1, intPt) + if isDoubleTouch && !isSameSegString { + pia.hasDoubleTouch = true + pia.doubleTouchLocation = intPt + // TODO: for poly-hole or hole-hole touch, check if it has bad topology. If so return invalid code + } + + return operationValid_PolygonIntersectionAnalyzer_noInvalidIntersection +} + +func (pia *OperationValid_PolygonIntersectionAnalyzer) addDoubleTouch(ss0, ss1 Noding_SegmentString, intPt *Geom_Coordinate) bool { + return OperationValid_PolygonRing_AddTouch(ss0.GetData().(*OperationValid_PolygonRing), ss1.GetData().(*OperationValid_PolygonRing), intPt) +} + +func (pia *OperationValid_PolygonIntersectionAnalyzer) addSelfTouch(ss Noding_SegmentString, intPt, e00, e01, e10, e11 *Geom_Coordinate) { + polyRing := ss.GetData().(*OperationValid_PolygonRing) + if polyRing == nil { + panic("SegmentString missing PolygonRing data when checking self-touches") + } + polyRing.AddSelfTouch(intPt, e00, e01, e10, e11) +} + +// operationValid_PolygonIntersectionAnalyzer_prevCoordinateInRing gets the +// coordinate previous to the given index for a segment string for a ring +// (wrapping if the index is 0). +func operationValid_PolygonIntersectionAnalyzer_prevCoordinateInRing(ringSS Noding_SegmentString, segIndex int) *Geom_Coordinate { + prevIndex := segIndex - 1 + if prevIndex < 0 { + prevIndex = ringSS.Size() - 2 + } + return ringSS.GetCoordinate(prevIndex) +} + +// operationValid_PolygonIntersectionAnalyzer_isAdjacentInRing tests if two +// segments in a closed SegmentString are adjacent. This handles determining +// adjacency across the start/end of the ring. +func operationValid_PolygonIntersectionAnalyzer_isAdjacentInRing(ringSS Noding_SegmentString, segIndex0, segIndex1 int) bool { + delta := segIndex1 - segIndex0 + if delta < 0 { + delta = -delta + } + if delta <= 1 { + return true + } + // A string with N vertices has maximum segment index of N-2. + // If the delta is at least N-2, the segments must be + // at the start and end of the string and thus adjacent. + if delta >= ringSS.Size()-2 { + return true + } + return false +} diff --git a/internal/jtsport/jts/operation_valid_polygon_ring.go b/internal/jtsport/jts/operation_valid_polygon_ring.go new file mode 100644 index 00000000..39dc0045 --- /dev/null +++ b/internal/jtsport/jts/operation_valid_polygon_ring.go @@ -0,0 +1,361 @@ +package jts + +// OperationValid_PolygonRing_IsShell tests if a polygon ring represents a shell. +func OperationValid_PolygonRing_IsShell(polyRing *OperationValid_PolygonRing) bool { + if polyRing == nil { + return true + } + return polyRing.IsShell() +} + +// OperationValid_PolygonRing_AddTouch records a touch location between two rings, +// and checks if the rings already touch in a different location. +func OperationValid_PolygonRing_AddTouch(ring0, ring1 *OperationValid_PolygonRing, pt *Geom_Coordinate) bool { + //--- skip if either polygon does not have holes + if ring0 == nil || ring1 == nil { + return false + } + + //--- only record touches within a polygon + if !ring0.IsSamePolygon(ring1) { + return false + } + + if !ring0.isOnlyTouch(ring1, pt) { + return true + } + if !ring1.isOnlyTouch(ring0, pt) { + return true + } + + ring0.addTouch(ring1, pt) + ring1.addTouch(ring0, pt) + return false +} + +// OperationValid_PolygonRing_FindHoleCycleLocation finds a location (if any) where a chain of holes forms a cycle +// in the ring touch graph. +// The shell may form part of the chain as well. +// This indicates that a set of holes disconnects the interior of a polygon. +func OperationValid_PolygonRing_FindHoleCycleLocation(polyRings []*OperationValid_PolygonRing) *Geom_Coordinate { + for _, polyRing := range polyRings { + if !polyRing.isInTouchSet() { + holeCycleLoc := polyRing.findHoleCycleLocation() + if holeCycleLoc != nil { + return holeCycleLoc + } + } + } + return nil +} + +// OperationValid_PolygonRing_FindInteriorSelfNode finds a location of an interior self-touch in a list of rings, +// if one exists. +// This indicates that a self-touch disconnects the interior of a polygon, +// which is invalid. +func OperationValid_PolygonRing_FindInteriorSelfNode(polyRings []*OperationValid_PolygonRing) *Geom_Coordinate { + for _, polyRing := range polyRings { + interiorSelfNode := polyRing.FindInteriorSelfNode() + if interiorSelfNode != nil { + return interiorSelfNode + } + } + return nil +} + +// OperationValid_PolygonRing is a ring of a polygon being analyzed for topological validity. +// The shell and hole rings of valid polygons touch only at discrete points. +// The "touch" relationship induces a graph over the set of rings. +// The interior of a valid polygon must be connected. +// This is the case if there is no "chain" of touching rings +// (which would partition off part of the interior). +// This is equivalent to the touch graph having no cycles. +// Thus the touch graph of a valid polygon is a forest - a set of disjoint trees. +// +// Also, in a valid polygon two rings can touch only at a single location, +// since otherwise they disconnect a portion of the interior between them. +// This is checked as the touches relation is built +// (so the touch relation representation for a polygon ring does not need to support +// more than one touch location for each adjacent ring). +// +// The cycle detection algorithm works for polygon rings which also contain self-touches +// (inverted shells and exverted holes). +// +// Polygons with no holes do not need to be checked for +// a connected interior, unless self-touches are allowed. +// The class also records the topology at self-touch nodes, +// to support checking if an invalid self-touch disconnects the polygon. +type OperationValid_PolygonRing struct { + id int + shell *OperationValid_PolygonRing + ring *Geom_LinearRing + + // The root of the touch graph tree containing this ring. + // Serves as the id for the graph partition induced by the touch relation. + touchSetRoot *OperationValid_PolygonRing + + // lazily created + // The set of PolygonRingTouch links for this ring. + // The set of all touches in the rings of a polygon forms the polygon touch graph. + // This supports detecting touch cycles, which reveal the condition of a disconnected interior. + // Only a single touch is recorded between any two rings, + // since more than one touch between two rings indicates interior disconnection as well. + touches map[int]*operationValid_PolygonRingTouch + + // The set of self-nodes in this ring. + // This supports checking valid ring self-touch topology. + selfNodes []*operationValid_PolygonRingSelfNode +} + +// OperationValid_NewPolygonRing creates a ring for a polygon shell. +func OperationValid_NewPolygonRing(ring *Geom_LinearRing) *OperationValid_PolygonRing { + pr := &OperationValid_PolygonRing{ + ring: ring, + id: -1, + } + pr.shell = pr + return pr +} + +// OperationValid_NewPolygonRingWithIndexAndShell creates a ring for a polygon hole. +func OperationValid_NewPolygonRingWithIndexAndShell(ring *Geom_LinearRing, index int, shell *OperationValid_PolygonRing) *OperationValid_PolygonRing { + return &OperationValid_PolygonRing{ + ring: ring, + id: index, + shell: shell, + } +} + +func (pr *OperationValid_PolygonRing) IsSamePolygon(ring *OperationValid_PolygonRing) bool { + return pr.shell == ring.shell +} + +func (pr *OperationValid_PolygonRing) IsShell() bool { + return pr.shell == pr +} + +func (pr *OperationValid_PolygonRing) isInTouchSet() bool { + return pr.touchSetRoot != nil +} + +func (pr *OperationValid_PolygonRing) setTouchSetRoot(ring *OperationValid_PolygonRing) { + pr.touchSetRoot = ring +} + +func (pr *OperationValid_PolygonRing) getTouchSetRoot() *OperationValid_PolygonRing { + return pr.touchSetRoot +} + +func (pr *OperationValid_PolygonRing) hasTouches() bool { + return pr.touches != nil && len(pr.touches) > 0 +} + +func (pr *OperationValid_PolygonRing) getTouches() []*operationValid_PolygonRingTouch { + result := make([]*operationValid_PolygonRingTouch, 0, len(pr.touches)) + for _, touch := range pr.touches { + result = append(result, touch) + } + return result +} + +func (pr *OperationValid_PolygonRing) addTouch(ring *OperationValid_PolygonRing, pt *Geom_Coordinate) { + if pr.touches == nil { + pr.touches = make(map[int]*operationValid_PolygonRingTouch) + } + _, exists := pr.touches[ring.id] + if !exists { + pr.touches[ring.id] = operationValid_newPolygonRingTouch(ring, pt) + } +} + +func (pr *OperationValid_PolygonRing) AddSelfTouch(origin, e00, e01, e10, e11 *Geom_Coordinate) { + if pr.selfNodes == nil { + pr.selfNodes = make([]*operationValid_PolygonRingSelfNode, 0) + } + pr.selfNodes = append(pr.selfNodes, operationValid_newPolygonRingSelfNode(origin, e00, e01, e10, e11)) +} + +// isOnlyTouch tests if this ring touches a given ring at +// the single point specified. +func (pr *OperationValid_PolygonRing) isOnlyTouch(ring *OperationValid_PolygonRing, pt *Geom_Coordinate) bool { + //--- no touches for this ring + if pr.touches == nil { + return true + } + //--- no touches for other ring + touch, exists := pr.touches[ring.id] + if !exists { + return true + } + //--- the rings touch - check if point is the same + return touch.isAtLocation(pt) +} + +// findHoleCycleLocation detects whether the subgraph of holes linked by touch to this ring +// contains a hole cycle. +// If no cycles are detected, the set of touching rings is a tree. +// The set is marked using this ring as the root. +func (pr *OperationValid_PolygonRing) findHoleCycleLocation() *Geom_Coordinate { + //--- the touch set including this ring is already processed + if pr.isInTouchSet() { + return nil + } + + //--- scan the touch set tree rooted at this ring + // Assert: this.touchSetRoot is null + root := pr + root.setTouchSetRoot(root) + + if !pr.hasTouches() { + return nil + } + + touchStack := make([]*operationValid_PolygonRingTouch, 0) + touchStack = operationValid_polygonRing_init(root, touchStack) + + for len(touchStack) > 0 { + // pop + touch := touchStack[len(touchStack)-1] + touchStack = touchStack[:len(touchStack)-1] + + holeCyclePt := pr.scanForHoleCycle(touch, root, &touchStack) + if holeCyclePt != nil { + return holeCyclePt + } + } + return nil +} + +func operationValid_polygonRing_init(root *OperationValid_PolygonRing, touchStack []*operationValid_PolygonRingTouch) []*operationValid_PolygonRingTouch { + for _, touch := range root.getTouches() { + touch.getRing().setTouchSetRoot(root) + touchStack = append(touchStack, touch) + } + return touchStack +} + +// scanForHoleCycle scans for a hole cycle starting at a given touch. +func (pr *OperationValid_PolygonRing) scanForHoleCycle(currentTouch *operationValid_PolygonRingTouch, root *OperationValid_PolygonRing, touchStack *[]*operationValid_PolygonRingTouch) *Geom_Coordinate { + ring := currentTouch.getRing() + currentPt := currentTouch.getCoordinate() + + // Scan the touched rings + // Either they form a hole cycle, or they are added to the touch set + // and pushed on the stack for scanning + for _, touch := range ring.getTouches() { + // Don't check touches at the entry point + // to avoid trivial cycles. + // They will already be processed or on the stack + // from the previous ring (which touched + // all the rings at that point as well) + if currentPt.Equals2D(touch.getCoordinate()) { + continue + } + + // Test if the touched ring has already been + // reached via a different touch path. + // This is indicated by it already being marked as + // part of the touch set. + // This indicates a hole cycle has been found. + touchRing := touch.getRing() + if touchRing.getTouchSetRoot() == root { + return touch.getCoordinate() + } + + touchRing.setTouchSetRoot(root) + + *touchStack = append(*touchStack, touch) + } + return nil +} + +// FindInteriorSelfNode finds the location of an invalid interior self-touch in this ring, +// if one exists. +func (pr *OperationValid_PolygonRing) FindInteriorSelfNode() *Geom_Coordinate { + if pr.selfNodes == nil { + return nil + } + + // Determine if the ring interior is on the Right. + // This is the case if the ring is a shell and is CW, + // or is a hole and is CCW. + isCCW := Algorithm_Orientation_IsCCW(pr.ring.GetCoordinates()) + isInteriorOnRight := pr.IsShell() != isCCW + + for _, selfNode := range pr.selfNodes { + if !selfNode.isExterior(isInteriorOnRight) { + return selfNode.getCoordinate() + } + } + return nil +} + +func (pr *OperationValid_PolygonRing) String() string { + return pr.ring.String() +} + +// operationValid_PolygonRingTouch records a point where a PolygonRing touches another one. +// This forms an edge in the induced ring touch graph. +type operationValid_PolygonRingTouch struct { + ring *OperationValid_PolygonRing + touchPt *Geom_Coordinate +} + +func operationValid_newPolygonRingTouch(ring *OperationValid_PolygonRing, pt *Geom_Coordinate) *operationValid_PolygonRingTouch { + return &operationValid_PolygonRingTouch{ + ring: ring, + touchPt: pt, + } +} + +func (prt *operationValid_PolygonRingTouch) getCoordinate() *Geom_Coordinate { + return prt.touchPt +} + +func (prt *operationValid_PolygonRingTouch) getRing() *OperationValid_PolygonRing { + return prt.ring +} + +func (prt *operationValid_PolygonRingTouch) isAtLocation(pt *Geom_Coordinate) bool { + return prt.touchPt.Equals2D(pt) +} + +// operationValid_PolygonRingSelfNode represents a ring self-touch node, recording the node (intersection point) +// and the endpoints of the four adjacent segments. +// This is used to evaluate validity of self-touching nodes, when they are allowed. +type operationValid_PolygonRingSelfNode struct { + nodePt *Geom_Coordinate + e00 *Geom_Coordinate + e01 *Geom_Coordinate + e10 *Geom_Coordinate + //e11 *Geom_Coordinate +} + +func operationValid_newPolygonRingSelfNode(nodePt, e00, e01, e10, e11 *Geom_Coordinate) *operationValid_PolygonRingSelfNode { + return &operationValid_PolygonRingSelfNode{ + nodePt: nodePt, + e00: e00, + e01: e01, + e10: e10, + //e11: e11, + } +} + +// getCoordinate returns the node point. +func (sn *operationValid_PolygonRingSelfNode) getCoordinate() *Geom_Coordinate { + return sn.nodePt +} + +// isExterior tests if a self-touch has the segments of each half of the touch +// lying in the exterior of a polygon. +// This is a valid self-touch. +// It applies to both shells and holes. +// Only one of the four possible cases needs to be tested, +// since the situation has full symmetry. +func (sn *operationValid_PolygonRingSelfNode) isExterior(isInteriorOnRight bool) bool { + // Note that either corner and either of the other edges could be used to test. + // The situation is fully symmetrical. + isInteriorSeg := Algorithm_PolygonNodeTopology_IsInteriorSegment(sn.nodePt, sn.e00, sn.e01, sn.e10) + isExterior := isInteriorOnRight != isInteriorSeg + return isExterior +} diff --git a/internal/jtsport/jts/operation_valid_polygon_topology_analyzer.go b/internal/jtsport/jts/operation_valid_polygon_topology_analyzer.go new file mode 100644 index 00000000..edc89f79 --- /dev/null +++ b/internal/jtsport/jts/operation_valid_polygon_topology_analyzer.go @@ -0,0 +1,322 @@ +package jts + +import "github.com/peterstace/simplefeatures/internal/jtsport/java" + +// OperationValid_PolygonTopologyAnalyzer_IsRingNested tests whether a ring is nested inside another ring. +// +// Preconditions: +// - The rings do not cross (i.e. the test is wholly inside or outside the target) +// - The rings may touch at discrete points only +// - The target ring does not self-cross, but it may self-touch +// +// If the test ring start point is properly inside or outside, that provides the result. +// Otherwise the start point is on the target ring, +// and the incident start segment (accounting for repeated points) is +// tested for its topology relative to the target ring. +func OperationValid_PolygonTopologyAnalyzer_IsRingNested(test, target *Geom_LinearRing) bool { + p0 := test.GetCoordinateN(0) + targetPts := target.GetCoordinates() + loc := Algorithm_PointLocation_LocateInRing(p0, targetPts) + if loc == Geom_Location_Exterior { + return false + } + if loc == Geom_Location_Interior { + return true + } + + // The start point is on the boundary of the ring. + // Use the topology at the node to check if the segment + // is inside or outside the ring. + p1 := operationValid_PolygonTopologyAnalyzer_findNonEqualVertex(test, p0) + return operationValid_PolygonTopologyAnalyzer_isIncidentSegmentInRing(p0, p1, targetPts) +} + +func operationValid_PolygonTopologyAnalyzer_findNonEqualVertex(ring *Geom_LinearRing, p *Geom_Coordinate) *Geom_Coordinate { + i := 1 + next := ring.GetCoordinateN(i) + for next.Equals2D(p) && i < ring.GetNumPoints()-1 { + i += 1 + next = ring.GetCoordinateN(i) + } + return next +} + +// operationValid_PolygonTopologyAnalyzer_isIncidentSegmentInRing tests whether a touching segment is interior to a ring. +// +// Preconditions: +// - The segment does not intersect the ring other than at the endpoints +// - The segment vertex p0 lies on the ring +// - The ring does not self-cross, but it may self-touch +// +// This works for both shells and holes, but the caller must know +// the ring role. +func operationValid_PolygonTopologyAnalyzer_isIncidentSegmentInRing(p0, p1 *Geom_Coordinate, ringPts []*Geom_Coordinate) bool { + index := operationValid_PolygonTopologyAnalyzer_intersectingSegIndex(ringPts, p0) + if index < 0 { + panic("Segment vertex does not intersect ring") + } + rPrev := operationValid_PolygonTopologyAnalyzer_findRingVertexPrev(ringPts, index, p0) + rNext := operationValid_PolygonTopologyAnalyzer_findRingVertexNext(ringPts, index, p0) + // If ring orientation is not normalized, flip the corner orientation + isInteriorOnRight := !Algorithm_Orientation_IsCCW(ringPts) + if !isInteriorOnRight { + temp := rPrev + rPrev = rNext + rNext = temp + } + return Algorithm_PolygonNodeTopology_IsInteriorSegment(p0, rPrev, rNext, p1) +} + +// operationValid_PolygonTopologyAnalyzer_findRingVertexPrev finds the ring vertex previous to a node point on a ring +// (which is contained in the index'th segment, +// as either the start vertex or an interior point). +// Repeated points are skipped over. +func operationValid_PolygonTopologyAnalyzer_findRingVertexPrev(ringPts []*Geom_Coordinate, index int, node *Geom_Coordinate) *Geom_Coordinate { + iPrev := index + prev := ringPts[iPrev] + for node.Equals2D(prev) { + iPrev = operationValid_PolygonTopologyAnalyzer_ringIndexPrev(ringPts, iPrev) + prev = ringPts[iPrev] + } + return prev +} + +// operationValid_PolygonTopologyAnalyzer_findRingVertexNext finds the ring vertex next from a node point on a ring +// (which is contained in the index'th segment, +// as either the start vertex or an interior point). +// Repeated points are skipped over. +func operationValid_PolygonTopologyAnalyzer_findRingVertexNext(ringPts []*Geom_Coordinate, index int, node *Geom_Coordinate) *Geom_Coordinate { + //-- safe, since index is always the start of a ring segment + iNext := index + 1 + next := ringPts[iNext] + for node.Equals2D(next) { + iNext = operationValid_PolygonTopologyAnalyzer_ringIndexNext(ringPts, iNext) + next = ringPts[iNext] + } + return next +} + +func operationValid_PolygonTopologyAnalyzer_ringIndexPrev(ringPts []*Geom_Coordinate, index int) int { + if index == 0 { + return len(ringPts) - 2 + } + return index - 1 +} + +func operationValid_PolygonTopologyAnalyzer_ringIndexNext(ringPts []*Geom_Coordinate, index int) int { + if index >= len(ringPts)-2 { + return 0 + } + return index + 1 +} + +// operationValid_PolygonTopologyAnalyzer_intersectingSegIndex computes the index of the segment which intersects a given point. +func operationValid_PolygonTopologyAnalyzer_intersectingSegIndex(ringPts []*Geom_Coordinate, pt *Geom_Coordinate) int { + for i := 0; i < len(ringPts)-1; i++ { + if Algorithm_PointLocation_IsOnSegment(pt, ringPts[i], ringPts[i+1]) { + //-- check if pt is the start point of the next segment + if pt.Equals2D(ringPts[i+1]) { + return i + 1 + } + return i + } + } + return -1 +} + +// OperationValid_PolygonTopologyAnalyzer_FindSelfIntersection finds a self-intersection (if any) in a LinearRing. +func OperationValid_PolygonTopologyAnalyzer_FindSelfIntersection(ring *Geom_LinearRing) *Geom_Coordinate { + ata := OperationValid_NewPolygonTopologyAnalyzerFromLinearRing(ring, false) + if ata.HasInvalidIntersection() { + return ata.GetInvalidLocation() + } + return nil +} + +// OperationValid_PolygonTopologyAnalyzer analyzes the topology of polygonal geometry +// to determine whether it is valid. +// +// Analyzing polygons with inverted rings (shells or exverted holes) +// is performed if specified. +// Inverted rings may cause a disconnected interior due to a self-touch; +// this is reported by IsInteriorDisconnectedBySelfTouch(). +type OperationValid_PolygonTopologyAnalyzer struct { + isInvertedRingValid bool + + intFinder *OperationValid_PolygonIntersectionAnalyzer + polyRings []*OperationValid_PolygonRing + disconnectionPt *Geom_Coordinate +} + +// OperationValid_NewPolygonTopologyAnalyzer creates a new analyzer for a Polygon or MultiPolygon. +func OperationValid_NewPolygonTopologyAnalyzer(geom *Geom_Geometry, isInvertedRingValid bool) *OperationValid_PolygonTopologyAnalyzer { + pta := &OperationValid_PolygonTopologyAnalyzer{ + isInvertedRingValid: isInvertedRingValid, + } + pta.analyze(geom) + return pta +} + +// TRANSLITERATION NOTE: This constructor is added for Go convenience. Java's +// constructor accepts Geometry which includes LinearRing, but Go's type system +// requires a separate constructor to accept *Geom_LinearRing directly without +// requiring the caller to access the embedded Geom_Geometry. +func OperationValid_NewPolygonTopologyAnalyzerFromLinearRing(ring *Geom_LinearRing, isInvertedRingValid bool) *OperationValid_PolygonTopologyAnalyzer { + pta := &OperationValid_PolygonTopologyAnalyzer{ + isInvertedRingValid: isInvertedRingValid, + } + pta.analyze(ring.Geom_Geometry) + return pta +} + +func (pta *OperationValid_PolygonTopologyAnalyzer) HasInvalidIntersection() bool { + return pta.intFinder.IsInvalid() +} + +func (pta *OperationValid_PolygonTopologyAnalyzer) GetInvalidCode() int { + return pta.intFinder.GetInvalidCode() +} + +func (pta *OperationValid_PolygonTopologyAnalyzer) GetInvalidLocation() *Geom_Coordinate { + return pta.intFinder.GetInvalidLocation() +} + +// IsInteriorDisconnected tests whether the interior of the polygonal geometry is +// disconnected. +// If true, the disconnection location is available from +// GetDisconnectionLocation(). +func (pta *OperationValid_PolygonTopologyAnalyzer) IsInteriorDisconnected() bool { + // May already be set by a double-touching hole + if pta.disconnectionPt != nil { + return true + } + if pta.isInvertedRingValid { + pta.CheckInteriorDisconnectedBySelfTouch() + if pta.disconnectionPt != nil { + return true + } + } + pta.CheckInteriorDisconnectedByHoleCycle() + if pta.disconnectionPt != nil { + return true + } + return false +} + +// GetDisconnectionLocation gets a location where the polygonal interior is disconnected. +// IsInteriorDisconnected() must be called first. +func (pta *OperationValid_PolygonTopologyAnalyzer) GetDisconnectionLocation() *Geom_Coordinate { + return pta.disconnectionPt +} + +// CheckInteriorDisconnectedByHoleCycle tests whether any polygon with holes has a disconnected interior +// by virtue of the holes (and possibly shell) forming a hole cycle. +// +// This is a global check, which relies on determining +// the touching graph of all holes in a polygon. +// +// If inverted rings disconnect the interior +// via a self-touch, this is checked by the PolygonIntersectionAnalyzer. +// If inverted rings are part of a hole cycle +// this is detected here as well. +func (pta *OperationValid_PolygonTopologyAnalyzer) CheckInteriorDisconnectedByHoleCycle() { + // PolyRings will be null for empty, no hole or LinearRing inputs + if pta.polyRings != nil { + pta.disconnectionPt = OperationValid_PolygonRing_FindHoleCycleLocation(pta.polyRings) + } +} + +// CheckInteriorDisconnectedBySelfTouch tests if an area interior is disconnected by a self-touching ring. +// This must be evaluated after other self-intersections have been analyzed +// and determined to not exist, since the logic relies on +// the rings not self-crossing (winding). +// +// If self-touching rings are not allowed, +// then the self-touch will previously trigger a self-intersection error. +func (pta *OperationValid_PolygonTopologyAnalyzer) CheckInteriorDisconnectedBySelfTouch() { + if pta.polyRings != nil { + pta.disconnectionPt = OperationValid_PolygonRing_FindInteriorSelfNode(pta.polyRings) + } +} + +func (pta *OperationValid_PolygonTopologyAnalyzer) analyze(geom *Geom_Geometry) { + if geom.IsEmpty() { + return + } + segStrings := operationValid_PolygonTopologyAnalyzer_createSegmentStrings(geom, pta.isInvertedRingValid) + pta.polyRings = operationValid_PolygonTopologyAnalyzer_getPolygonRings(segStrings) + pta.intFinder = pta.analyzeIntersections(segStrings) + + if pta.intFinder.HasDoubleTouch() { + pta.disconnectionPt = pta.intFinder.GetDoubleTouchLocation() + return + } +} + +func (pta *OperationValid_PolygonTopologyAnalyzer) analyzeIntersections(segStrings []Noding_SegmentString) *OperationValid_PolygonIntersectionAnalyzer { + segInt := OperationValid_NewPolygonIntersectionAnalyzer(pta.isInvertedRingValid) + noder := Noding_NewMCIndexNoder() + noder.SetSegmentIntersector(segInt) + noder.ComputeNodes(segStrings) + return segInt +} + +func operationValid_PolygonTopologyAnalyzer_createSegmentStrings(geom *Geom_Geometry, isInvertedRingValid bool) []Noding_SegmentString { + segStrings := make([]Noding_SegmentString, 0) + if java.InstanceOf[*Geom_LinearRing](geom) { + ring := java.Cast[*Geom_LinearRing](geom) + segStrings = append(segStrings, operationValid_PolygonTopologyAnalyzer_createSegString(ring, nil)) + return segStrings + } + for i := 0; i < geom.GetNumGeometries(); i++ { + poly := java.Cast[*Geom_Polygon](geom.GetGeometryN(i)) + if poly.IsEmpty() { + continue + } + hasHoles := poly.GetNumInteriorRing() > 0 + + //--- polygons with no holes do not need connected interior analysis + var shellRing *OperationValid_PolygonRing + if hasHoles || isInvertedRingValid { + shellRing = OperationValid_NewPolygonRing(poly.GetExteriorRing()) + } + segStrings = append(segStrings, operationValid_PolygonTopologyAnalyzer_createSegString(poly.GetExteriorRing(), shellRing)) + + for j := 0; j < poly.GetNumInteriorRing(); j++ { + hole := poly.GetInteriorRingN(j) + if hole.IsEmpty() { + continue + } + holeRing := OperationValid_NewPolygonRingWithIndexAndShell(hole, j, shellRing) + segStrings = append(segStrings, operationValid_PolygonTopologyAnalyzer_createSegString(hole, holeRing)) + } + } + return segStrings +} + +func operationValid_PolygonTopologyAnalyzer_getPolygonRings(segStrings []Noding_SegmentString) []*OperationValid_PolygonRing { + var polyRings []*OperationValid_PolygonRing + for _, ss := range segStrings { + data := ss.GetData() + polyRing, ok := data.(*OperationValid_PolygonRing) + if ok && polyRing != nil { + if polyRings == nil { + polyRings = make([]*OperationValid_PolygonRing, 0) + } + polyRings = append(polyRings, polyRing) + } + } + return polyRings +} + +func operationValid_PolygonTopologyAnalyzer_createSegString(ring *Geom_LinearRing, polyRing *OperationValid_PolygonRing) Noding_SegmentString { + pts := ring.GetCoordinates() + + //--- repeated points must be removed for accurate intersection detection + if Geom_CoordinateArrays_HasRepeatedPoints(pts) { + pts = Geom_CoordinateArrays_RemoveRepeatedPoints(pts) + } + + ss := Noding_NewBasicSegmentString(pts, polyRing) + return ss +} diff --git a/internal/jtsport/jts/operation_valid_topology_validation_error.go b/internal/jtsport/jts/operation_valid_topology_validation_error.go new file mode 100644 index 00000000..e4cb3b32 --- /dev/null +++ b/internal/jtsport/jts/operation_valid_topology_validation_error.go @@ -0,0 +1,120 @@ +package jts + +// OperationValid_TopologyValidationError_ERROR is not used. +// Deprecated. +const OperationValid_TopologyValidationError_ERROR = 0 + +// OperationValid_TopologyValidationError_REPEATED_POINT is no longer used - +// repeated points are considered valid as per the SFS. +// Deprecated. +const OperationValid_TopologyValidationError_REPEATED_POINT = 1 + +// OperationValid_TopologyValidationError_HOLE_OUTSIDE_SHELL indicates that a +// hole of a polygon lies partially or completely in the exterior of the shell. +const OperationValid_TopologyValidationError_HOLE_OUTSIDE_SHELL = 2 + +// OperationValid_TopologyValidationError_NESTED_HOLES indicates that a hole +// lies in the interior of another hole in the same polygon. +const OperationValid_TopologyValidationError_NESTED_HOLES = 3 + +// OperationValid_TopologyValidationError_DISCONNECTED_INTERIOR indicates that +// the interior of a polygon is disjoint (often caused by set of contiguous +// holes splitting the polygon into two parts). +const OperationValid_TopologyValidationError_DISCONNECTED_INTERIOR = 4 + +// OperationValid_TopologyValidationError_SELF_INTERSECTION indicates that two +// rings of a polygonal geometry intersect. +const OperationValid_TopologyValidationError_SELF_INTERSECTION = 5 + +// OperationValid_TopologyValidationError_RING_SELF_INTERSECTION indicates that +// a ring self-intersects. +const OperationValid_TopologyValidationError_RING_SELF_INTERSECTION = 6 + +// OperationValid_TopologyValidationError_NESTED_SHELLS indicates that a polygon +// component of a MultiPolygon lies inside another polygonal component. +const OperationValid_TopologyValidationError_NESTED_SHELLS = 7 + +// OperationValid_TopologyValidationError_DUPLICATE_RINGS indicates that a +// polygonal geometry contains two rings which are identical. +const OperationValid_TopologyValidationError_DUPLICATE_RINGS = 8 + +// OperationValid_TopologyValidationError_TOO_FEW_POINTS indicates that either +// a LineString contains a single point or a LinearRing contains 2 or 3 points. +const OperationValid_TopologyValidationError_TOO_FEW_POINTS = 9 + +// OperationValid_TopologyValidationError_INVALID_COORDINATE indicates that the +// X or Y ordinate of a Coordinate is not a valid numeric value (e.g. NaN). +const OperationValid_TopologyValidationError_INVALID_COORDINATE = 10 + +// OperationValid_TopologyValidationError_RING_NOT_CLOSED indicates that a ring +// is not correctly closed (the first and the last coordinate are different). +const OperationValid_TopologyValidationError_RING_NOT_CLOSED = 11 + +// operationValid_TopologyValidationError_errMsg contains messages corresponding +// to error codes. +var operationValid_TopologyValidationError_errMsg = []string{ + "Topology Validation Error", + "Repeated Point", + "Hole lies outside shell", + "Holes are nested", + "Interior is disconnected", + "Self-intersection", + "Ring Self-intersection", + "Nested shells", + "Duplicate Rings", + "Too few distinct points in geometry component", + "Invalid Coordinate", + "Ring is not closed", +} + +// OperationValid_TopologyValidationError contains information about the nature +// and location of a Geometry validation error. +type OperationValid_TopologyValidationError struct { + errorType int + pt *Geom_Coordinate +} + +// OperationValid_NewTopologyValidationError creates a validation error with the +// given type and location. +func OperationValid_NewTopologyValidationError(errorType int, pt *Geom_Coordinate) *OperationValid_TopologyValidationError { + var ptCopy *Geom_Coordinate + if pt != nil { + ptCopy = pt.Copy() + } + return &OperationValid_TopologyValidationError{ + errorType: errorType, + pt: ptCopy, + } +} + +// OperationValid_NewTopologyValidationErrorWithType creates a validation error +// of the given type with a null location. +func OperationValid_NewTopologyValidationErrorWithType(errorType int) *OperationValid_TopologyValidationError { + return OperationValid_NewTopologyValidationError(errorType, nil) +} + +// GetCoordinate returns the location of this error (on the Geometry containing +// the error). +func (e *OperationValid_TopologyValidationError) GetCoordinate() *Geom_Coordinate { + return e.pt +} + +// GetErrorType gets the type of this error. +func (e *OperationValid_TopologyValidationError) GetErrorType() int { + return e.errorType +} + +// GetMessage gets an error message describing this error. The error message +// does not describe the location of the error. +func (e *OperationValid_TopologyValidationError) GetMessage() string { + return operationValid_TopologyValidationError_errMsg[e.errorType] +} + +// String gets a message describing the type and location of this error. +func (e *OperationValid_TopologyValidationError) String() string { + locStr := "" + if e.pt != nil { + locStr = " at or near point " + e.pt.String() + } + return e.GetMessage() + locStr +} diff --git a/internal/jtsport/jts/operation_valid_valid_closed_ring_test.go b/internal/jtsport/jts/operation_valid_valid_closed_ring_test.go new file mode 100644 index 00000000..3540063c --- /dev/null +++ b/internal/jtsport/jts/operation_valid_valid_closed_ring_test.go @@ -0,0 +1,65 @@ +package jts + +import ( + "testing" + + "github.com/peterstace/simplefeatures/internal/jtsport/java" + "github.com/peterstace/simplefeatures/internal/jtsport/junit" +) + +var operationValidValidClosedRingTest_rdr = Io_NewWKTReader() + +func TestValidClosedRingBadLinearRing(t *testing.T) { + ring := java.Cast[*Geom_LinearRing](operationValidValidClosedRingTest_fromWKT("LINEARRING (0 0, 0 10, 10 10, 10 0, 0 0)")) + operationValidValidClosedRingTest_updateNonClosedRing(ring) + operationValidValidClosedRingTest_checkIsValid(t, ring.Geom_Geometry, false) +} + +func TestValidClosedRingGoodLinearRing(t *testing.T) { + ring := java.Cast[*Geom_LinearRing](operationValidValidClosedRingTest_fromWKT("LINEARRING (0 0, 0 10, 10 10, 10 0, 0 0)")) + operationValidValidClosedRingTest_checkIsValid(t, ring.Geom_Geometry, true) +} + +func TestValidClosedRingBadPolygonShell(t *testing.T) { + poly := java.Cast[*Geom_Polygon](operationValidValidClosedRingTest_fromWKT("POLYGON ((0 0, 0 10, 10 10, 10 0, 0 0))")) + operationValidValidClosedRingTest_updateNonClosedRing(poly.GetExteriorRing()) + operationValidValidClosedRingTest_checkIsValid(t, poly.Geom_Geometry, false) +} + +func TestValidClosedRingBadPolygonHole(t *testing.T) { + poly := java.Cast[*Geom_Polygon](operationValidValidClosedRingTest_fromWKT("POLYGON ((0 0, 0 10, 10 10, 10 0, 0 0), (1 1, 2 1, 2 2, 1 2, 1 1) ))")) + operationValidValidClosedRingTest_updateNonClosedRing(poly.GetInteriorRingN(0)) + operationValidValidClosedRingTest_checkIsValid(t, poly.Geom_Geometry, false) +} + +func TestValidClosedRingGoodPolygon(t *testing.T) { + poly := java.Cast[*Geom_Polygon](operationValidValidClosedRingTest_fromWKT("POLYGON ((0 0, 0 10, 10 10, 10 0, 0 0))")) + operationValidValidClosedRingTest_checkIsValid(t, poly.Geom_Geometry, true) +} + +func TestValidClosedRingBadGeometryCollection(t *testing.T) { + gc := java.Cast[*Geom_GeometryCollection](operationValidValidClosedRingTest_fromWKT("GEOMETRYCOLLECTION ( POLYGON ((0 0, 0 10, 10 10, 10 0, 0 0), (1 1, 2 1, 2 2, 1 2, 1 1) )), POINT(0 0) )")) + poly := java.Cast[*Geom_Polygon](gc.GetGeometryN(0)) + operationValidValidClosedRingTest_updateNonClosedRing(poly.GetInteriorRingN(0)) + operationValidValidClosedRingTest_checkIsValid(t, poly.Geom_Geometry, false) +} + +func operationValidValidClosedRingTest_checkIsValid(t *testing.T, geom *Geom_Geometry, expected bool) { + t.Helper() + validator := OperationValid_NewIsValidOp(geom) + isValid := validator.IsValid() + junit.AssertTrue(t, isValid == expected) +} + +func operationValidValidClosedRingTest_fromWKT(wkt string) *Geom_Geometry { + geom, err := operationValidValidClosedRingTest_rdr.Read(wkt) + if err != nil { + panic(err) + } + return geom +} + +func operationValidValidClosedRingTest_updateNonClosedRing(ring *Geom_LinearRing) { + pts := ring.GetCoordinates() + pts[0].X += 0.0001 +} diff --git a/internal/jtsport/jts/operation_valid_valid_self_touching_ring_test.go b/internal/jtsport/jts/operation_valid_valid_self_touching_ring_test.go new file mode 100644 index 00000000..4a8114c2 --- /dev/null +++ b/internal/jtsport/jts/operation_valid_valid_self_touching_ring_test.go @@ -0,0 +1,143 @@ +package jts + +import ( + "testing" + + "github.com/peterstace/simplefeatures/internal/jtsport/junit" +) + +// Tests that IsValidOp validates polygons with +// Self-Touching Rings (inverted shells or exverted holes). +// Mainly tests that configuring IsValidOp to allow validating +// the STR validates polygons with this condition, and does not validate +// polygons with other kinds of self-intersection (such as ones with Disconnected Interiors). +// Includes some basic tests to confirm that other invalid cases remain detected correctly, +// but most of this testing is left to the existing XML validation tests. + +var operationValidValidSelfTouchingRingTest_rdr = Io_NewWKTReader() + +// TestValidSelfTouchingRingShellAndHoleSelfTouch tests a geometry with both a shell self-touch and a hole self-touch. +// This is valid if STR is allowed, but invalid in OGC +func TestValidSelfTouchingRingShellAndHoleSelfTouch(t *testing.T) { + wkt := "POLYGON ((0 0, 0 340, 320 340, 320 0, 120 0, 180 100, 60 100, 120 0, 0 0), (80 300, 80 180, 200 180, 200 240, 280 200, 280 280, 200 240, 200 300, 80 300))" + validSelfTouchingRingTest_checkIsValidSTR(t, wkt, true) + validSelfTouchingRingTest_checkIsValidOGC(t, wkt, false) +} + +func TestValidSelfTouchingRingShellTouchAtHole(t *testing.T) { + wkt := "POLYGON ((10 90, 90 90, 90 10, 50 50, 80 50, 80 80, 10 10, 10 90), (40 80, 20 60, 50 50, 40 80))" + validSelfTouchingRingTest_checkIsValidSTR(t, wkt, true) + validSelfTouchingRingTest_checkIsValidOGC(t, wkt, false) +} + +func TestValidSelfTouchingRingShellTouchInChain(t *testing.T) { + wkt := "POLYGON ((10 90, 90 90, 90 10, 10 10, 10 90, 20 70, 30 70, 30 50, 40 50, 40 70, 30 70, 30 80, 10 90))" + validSelfTouchingRingTest_checkIsValidSTR(t, wkt, true) + validSelfTouchingRingTest_checkIsValidOGC(t, wkt, false) +} + +func TestValidSelfTouchingRingHoleTouchInChain(t *testing.T) { + wkt := "POLYGON ((10 90, 90 90, 90 10, 10 10, 10 90), (20 20, 80 20, 80 50, 70 20, 70 50, 60 20, 60 50, 50 20, 50 50, 40 20, 40 50, 30 20, 30 50, 20 20))" + validSelfTouchingRingTest_checkIsValidSTR(t, wkt, true) + validSelfTouchingRingTest_checkIsValidOGC(t, wkt, false) +} + +// TestValidSelfTouchingRingShellHoleAndHoleHoleTouch tests a geometry representing the same area as in testShellAndHoleSelfTouch +// but using a shell-hole touch and a hole-hole touch. +// This is valid in OGC. +func TestValidSelfTouchingRingShellHoleAndHoleHoleTouch(t *testing.T) { + wkt := "POLYGON ((0 0, 0 340, 320 340, 320 0, 120 0, 0 0), (120 0, 180 100, 60 100, 120 0), (80 300, 80 180, 200 180, 200 240, 200 300, 80 300), (200 240, 280 200, 280 280, 200 240))" + validSelfTouchingRingTest_checkIsValidSTR(t, wkt, true) + validSelfTouchingRingTest_checkIsValidOGC(t, wkt, true) +} + +// TestValidSelfTouchingRingShellSelfTouchHoleOverlappingHole tests an overlapping hole condition, where one of the holes is created by a shell self-touch. +// This is never valid. +func TestValidSelfTouchingRingShellSelfTouchHoleOverlappingHole(t *testing.T) { + wkt := "POLYGON ((0 0, 220 0, 220 200, 120 200, 140 100, 80 100, 120 200, 0 200, 0 0), (200 80, 20 80, 120 200, 200 80))" + validSelfTouchingRingTest_checkIsValidSTR(t, wkt, false) + validSelfTouchingRingTest_checkIsValidOGC(t, wkt, false) +} + +// TestValidSelfTouchingRingDisconnectedInteriorShellSelfTouchAtNonVertex ensures that the Disconnected Interior condition is not validated +func TestValidSelfTouchingRingDisconnectedInteriorShellSelfTouchAtNonVertex(t *testing.T) { + wkt := "POLYGON ((40 180, 40 60, 240 60, 240 180, 140 60, 40 180))" + validSelfTouchingRingTest_checkIsValidSTR(t, wkt, false) + validSelfTouchingRingTest_checkIsValidOGC(t, wkt, false) +} + +func TestValidSelfTouchingRingDisconnectedInteriorShellSelfTouchAtVertex(t *testing.T) { + wkt := "POLYGON ((20 20, 20 100, 140 100, 140 180, 260 180, 260 100, 140 100, 140 20, 20 20))" + validSelfTouchingRingTest_checkIsValidSTR(t, wkt, false) + validSelfTouchingRingTest_checkIsValidOGC(t, wkt, false) +} + +func TestValidSelfTouchingRingDisconnectedInteriorShellTouchAtVertices(t *testing.T) { + wkt := "POLYGON ((10 10, 90 10, 50 50, 80 70, 90 10, 90 90, 10 90, 10 10, 50 50, 20 70, 10 10))" + validSelfTouchingRingTest_checkIsValidSTR(t, wkt, false) + validSelfTouchingRingTest_checkIsValidOGC(t, wkt, false) +} + +func TestValidSelfTouchingRingDisconnectedInteriorHoleTouch(t *testing.T) { + wkt := "POLYGON ((10 90, 90 90, 90 10, 10 10, 10 90), (20 20, 20 80, 80 80, 80 30, 30 30, 70 40, 70 70, 20 20))" + validSelfTouchingRingTest_checkIsValidSTR(t, wkt, false) + validSelfTouchingRingTest_checkIsValidOGC(t, wkt, false) +} + +func TestValidSelfTouchingRingShellCross(t *testing.T) { + wkt := "POLYGON ((20 20, 120 20, 120 220, 240 220, 240 120, 20 120, 20 20))" + validSelfTouchingRingTest_checkIsValidSTR(t, wkt, false) + validSelfTouchingRingTest_checkIsValidOGC(t, wkt, false) +} + +func TestValidSelfTouchingRingShellCrossAndSTR(t *testing.T) { + wkt := "POLYGON ((20 20, 120 20, 120 220, 180 220, 140 160, 200 160, 180 220, 240 220, 240 120, 20 120, 20 20))" + validSelfTouchingRingTest_checkIsValidSTR(t, wkt, false) + validSelfTouchingRingTest_checkIsValidOGC(t, wkt, false) +} + +func TestValidSelfTouchingRingExvertedHoleStarTouchHoleCycle(t *testing.T) { + wkt := "POLYGON ((10 90, 90 90, 90 10, 10 10, 10 90), (20 80, 50 30, 80 80, 80 30, 20 30, 20 80), (40 70, 50 70, 50 30, 40 70), (40 20, 60 20, 50 30, 40 20), (40 80, 20 80, 40 70, 40 80))" + validSelfTouchingRingTest_checkInvalidSTR(t, wkt, OperationValid_TopologyValidationError_DISCONNECTED_INTERIOR) + //checkIsValidOGC(wkt, false); +} + +func TestValidSelfTouchingRingExvertedHoleStarTouch(t *testing.T) { + wkt := "POLYGON ((10 90, 90 90, 90 10, 10 10, 10 90), (20 80, 50 30, 80 80, 80 30, 20 30, 20 80), (40 70, 50 70, 50 30, 40 70), (40 20, 60 20, 50 30, 40 20))" + validSelfTouchingRingTest_checkIsValidSTR(t, wkt, true) + validSelfTouchingRingTest_checkIsValidOGC(t, wkt, false) +} + +func validSelfTouchingRingTest_checkInvalidSTR(t *testing.T, wkt string, expectedErrType int) { + t.Helper() + geom := validSelfTouchingRingTest_read(wkt) + validOp := OperationValid_NewIsValidOp(geom) + validOp.SetSelfTouchingRingFormingHoleValid(true) + err := validOp.GetValidationError() + junit.AssertEquals(t, expectedErrType, err.GetErrorType()) +} + +func validSelfTouchingRingTest_checkIsValidOGC(t *testing.T, wkt string, expected bool) { + t.Helper() + geom := validSelfTouchingRingTest_read(wkt) + validator := OperationValid_NewIsValidOp(geom) + isValid := validator.IsValid() + junit.AssertTrue(t, isValid == expected) +} + +func validSelfTouchingRingTest_checkIsValidSTR(t *testing.T, wkt string, expected bool) { + t.Helper() + geom := validSelfTouchingRingTest_read(wkt) + validator := OperationValid_NewIsValidOp(geom) + validator.SetSelfTouchingRingFormingHoleValid(true) + isValid := validator.IsValid() + junit.AssertTrue(t, isValid == expected) +} + +func validSelfTouchingRingTest_read(wkt string) *Geom_Geometry { + g, err := operationValidValidSelfTouchingRingTest_rdr.Read(wkt) + if err != nil { + panic(err) + } + return g +} diff --git a/internal/jtsport/jts/stubs.go b/internal/jtsport/jts/stubs.go index cf6c4fb1..97cc2aba 100644 --- a/internal/jtsport/jts/stubs.go +++ b/internal/jtsport/jts/stubs.go @@ -58,44 +58,6 @@ func jtstestUtilIo_wktOrWKBReader_isHexDigit(ch rune) bool { return false } -// ============================================================================= -// STUB: noding package stubs for EdgeNodingValidator -// ============================================================================= - -// STUB: Noding_FastNodingValidator validates that a collection of -// SegmentStrings is correctly noded. -type Noding_FastNodingValidator struct { - segStrings []*Noding_BasicSegmentString - isValid bool - checked bool -} - -// Noding_NewFastNodingValidator creates a new FastNodingValidator. -func Noding_NewFastNodingValidator(segStrings []*Noding_BasicSegmentString) *Noding_FastNodingValidator { - return &Noding_FastNodingValidator{ - segStrings: segStrings, - isValid: true, - } -} - -// CheckValid checks whether the supplied segment strings are correctly noded. -// Panics with TopologyException if they are not. -func (fnv *Noding_FastNodingValidator) CheckValid() { - if fnv.checked { - return - } - fnv.checked = true - // STUB: Full implementation would check for interior intersections using - // MCIndexNoder and NodingIntersectionFinder. For now, we assume valid. - fnv.isValid = true -} - -// IsValid returns true if the segment strings are correctly noded. -func (fnv *Noding_FastNodingValidator) IsValid() bool { - fnv.CheckValid() - return fnv.isValid -} - // ============================================================================= // STUB: precision package stubs for SnapOverlayOp // The precision package is optional but needed by SnapOverlayOp for the @@ -204,29 +166,6 @@ func JtstestUtil_StringUtil_EscapeHTML(s string) string { panic("JtstestUtil_StringUtil_EscapeHTML not yet ported") } -// ============================================================================= -// STUB: operation/valid package stubs -// ============================================================================= - -// STUB: OperationValid_IsValidOp_IsValid - operation/valid/IsValidOp not yet ported. -func OperationValid_IsValidOp_IsValid(g *Geom_Geometry) bool { - panic("operation/valid/IsValidOp not yet ported") -} - -// ============================================================================= -// STUB: operation/distance package stubs -// ============================================================================= - -// STUB: OperationDistance_DistanceOp_Distance - operation/distance/DistanceOp not yet ported. -func OperationDistance_DistanceOp_Distance(g1, g2 *Geom_Geometry) float64 { - panic("operation/distance/DistanceOp not yet ported") -} - -// STUB: OperationDistance_DistanceOp_IsWithinDistance - operation/distance/DistanceOp not yet ported. -func OperationDistance_DistanceOp_IsWithinDistance(g1, g2 *Geom_Geometry, distance float64) bool { - panic("operation/distance/DistanceOp not yet ported") -} - // ============================================================================= // STUB: algorithm package stubs for Centroid and InteriorPoint // ============================================================================= @@ -241,25 +180,6 @@ func Algorithm_InteriorPoint_GetInteriorPoint(g *Geom_Geometry) *Geom_Coordinate panic("algorithm/InteriorPoint not yet ported") } -// ============================================================================= -// STUB: operation/buffer package stubs -// ============================================================================= - -// STUB: OperationBuffer_BufferOp_BufferOp - operation/buffer/BufferOp not yet ported. -func OperationBuffer_BufferOp_BufferOp(g *Geom_Geometry, distance float64) *Geom_Geometry { - panic("operation/buffer/BufferOp not yet ported") -} - -// STUB: OperationBuffer_BufferOp_BufferOpWithQuadrantSegments - operation/buffer/BufferOp not yet ported. -func OperationBuffer_BufferOp_BufferOpWithQuadrantSegments(g *Geom_Geometry, distance float64, quadrantSegments int) *Geom_Geometry { - panic("operation/buffer/BufferOp not yet ported") -} - -// STUB: OperationBuffer_BufferOp_BufferOpWithQuadrantSegmentsAndEndCapStyle - operation/buffer/BufferOp not yet ported. -func OperationBuffer_BufferOp_BufferOpWithQuadrantSegmentsAndEndCapStyle(g *Geom_Geometry, distance float64, quadrantSegments, endCapStyle int) *Geom_Geometry { - panic("operation/buffer/BufferOp not yet ported") -} - // ============================================================================= // STUB: algorithm package stubs for ConvexHull // ============================================================================= @@ -279,32 +199,6 @@ func (ch *Algorithm_ConvexHull) GetConvexHull() *Geom_Geometry { panic("algorithm/ConvexHull not yet ported") } -// ============================================================================= -// STUB: operation/buffer package stubs for BufferParameters -// ============================================================================= - -const OperationBuffer_BufferParameters_JOIN_MITRE = 2 - -// STUB: OperationBuffer_BufferParameters - operation/buffer/BufferParameters not yet ported. -type OperationBuffer_BufferParameters struct { - joinStyle int -} - -// STUB: OperationBuffer_NewBufferParameters - operation/buffer/BufferParameters not yet ported. -func OperationBuffer_NewBufferParameters() *OperationBuffer_BufferParameters { - return &OperationBuffer_BufferParameters{} -} - -// STUB: SetJoinStyle - operation/buffer/BufferParameters not yet ported. -func (bp *OperationBuffer_BufferParameters) SetJoinStyle(joinStyle int) { - bp.joinStyle = joinStyle -} - -// STUB: OperationBuffer_BufferOp_BufferOpWithParams - operation/buffer/BufferOp not yet ported. -func OperationBuffer_BufferOp_BufferOpWithParams(g *Geom_Geometry, distance float64, params *OperationBuffer_BufferParameters) *Geom_Geometry { - panic("operation/buffer/BufferOp not yet ported") -} - // ============================================================================= // STUB: densify package stubs // ============================================================================= diff --git a/internal/jtsport/jts/util_debug.go b/internal/jtsport/jts/util_debug.go new file mode 100644 index 00000000..193b8088 --- /dev/null +++ b/internal/jtsport/jts/util_debug.go @@ -0,0 +1,277 @@ +package jts + +import ( + "fmt" + "io" + "os" + "strings" +) + +var Util_Debug_DEBUG_PROPERTY_NAME = "jts.debug" +var Util_Debug_DEBUG_PROPERTY_VALUE_ON = "on" +var Util_Debug_DEBUG_PROPERTY_VALUE_TRUE = "true" + +var util_debug_debugOn = false + +// TRANSLITERATION NOTE: In Java, the static initializer reads the system property +// "jts.debug" at class load time. In Go, we use an IIFE in a package-level var +// to read the environment variable at package initialization. +var _ = func() struct{} { + debugValue := os.Getenv(Util_Debug_DEBUG_PROPERTY_NAME) + if debugValue != "" { + if strings.EqualFold(debugValue, Util_Debug_DEBUG_PROPERTY_VALUE_ON) || + strings.EqualFold(debugValue, Util_Debug_DEBUG_PROPERTY_VALUE_TRUE) { + util_debug_debugOn = true + } + } + return struct{}{} +}() + +var util_debug_stopwatch = Util_NewStopwatch() +var util_debug_lastTimePrinted int64 + +// Util_Debug_Main prints the status of debugging to stdout. +func Util_Debug_Main(args []string) { + status := "OFF" + if util_debug_debugOn { + status = "ON" + } + fmt.Println("JTS Debugging is " + status) +} + +var util_debug_debug = util_newDebug() +var util_debug_fact = Geom_NewGeometryFactoryDefault() + +const util_debug_DEBUG_LINE_TAG = "D! " + +type util_Debug struct { + out io.Writer + watchObj any +} + +func Util_Debug_IsDebugging() bool { + return util_debug_debugOn +} + +func Util_Debug_ToLine(p0, p1 *Geom_Coordinate) *Geom_LineString { + return util_debug_fact.CreateLineStringFromCoordinates([]*Geom_Coordinate{p0, p1}) +} + +func Util_Debug_ToLine3(p0, p1, p2 *Geom_Coordinate) *Geom_LineString { + return util_debug_fact.CreateLineStringFromCoordinates([]*Geom_Coordinate{p0, p1, p2}) +} + +func Util_Debug_ToLine4(p0, p1, p2, p3 *Geom_Coordinate) *Geom_LineString { + return util_debug_fact.CreateLineStringFromCoordinates([]*Geom_Coordinate{p0, p1, p2, p3}) +} + +func Util_Debug_PrintString(str string) { + if !util_debug_debugOn { + return + } + util_debug_debug.instancePrintString(str) +} + +func Util_Debug_Print(obj any) { + if !util_debug_debugOn { + return + } + util_debug_debug.instancePrint(obj) +} + +func Util_Debug_PrintIf(isTrue bool, obj any) { + if !util_debug_debugOn { + return + } + if !isTrue { + return + } + util_debug_debug.instancePrint(obj) +} + +func Util_Debug_Println(obj any) { + if !util_debug_debugOn { + return + } + util_debug_debug.instancePrint(obj) + util_debug_debug.println() +} + +func Util_Debug_ResetTime() { + util_debug_stopwatch.Reset() + util_debug_lastTimePrinted = util_debug_stopwatch.GetTime() +} + +func Util_Debug_PrintTime(tag string) { + if !util_debug_debugOn { + return + } + time := util_debug_stopwatch.GetTime() + elapsedTime := time - util_debug_lastTimePrinted + util_debug_debug.instancePrint( + util_debug_formatField(Util_Stopwatch_GetTimeStringFromMillis(time), 10) + + " (" + util_debug_formatField(Util_Stopwatch_GetTimeStringFromMillis(elapsedTime), 10) + " ) " + + tag) + util_debug_debug.println() + util_debug_lastTimePrinted = time +} + +func util_debug_formatField(s string, fieldLen int) string { + nPad := fieldLen - len(s) + if nPad <= 0 { + return s + } + padStr := util_debug_spaces(nPad) + s + return padStr[len(padStr)-fieldLen:] +} + +func util_debug_spaces(n int) string { + ch := make([]byte, n) + for i := 0; i < n; i++ { + ch[i] = ' ' + } + return string(ch) +} + +func Util_Debug_Equals(c1, c2 *Geom_Coordinate, tolerance float64) bool { + return c1.Distance(c2) <= tolerance +} + +// Util_Debug_AddWatch adds an object to be watched. +// A watched object can be printed out at any time. +// Currently only supports one watched object at a time. +func Util_Debug_AddWatch(obj any) { + util_debug_debug.instanceAddWatch(obj) +} + +func Util_Debug_PrintWatch() { + util_debug_debug.instancePrintWatch() +} + +func Util_Debug_PrintIfWatch(obj any) { + util_debug_debug.instancePrintIfWatch(obj) +} + +func Util_Debug_BreakIf(cond bool) { + if cond { + util_debug_doBreak() + } +} + +func Util_Debug_BreakIfEqual(o1, o2 any) { + if o1 == o2 { + util_debug_doBreak() + } +} + +func Util_Debug_BreakIfEqualCoords(p0, p1 *Geom_Coordinate, tolerance float64) { + if p0.Distance(p1) <= tolerance { + util_debug_doBreak() + } +} + +func util_debug_doBreak() { + // Put breakpoint on following statement to break here + return +} + +func Util_Debug_HasSegment(geom Geom_Geometry, p0, p1 *Geom_Coordinate) bool { + filter := util_debug_newSegmentFindingFilter(p0, p1) + geom.ApplyCoordinateSequenceFilter(filter) + return filter.hasSegment() +} + +type util_debug_SegmentFindingFilter struct { + p0, p1 *Geom_Coordinate + hasSegmentVal bool +} + +func util_debug_newSegmentFindingFilter(p0, p1 *Geom_Coordinate) *util_debug_SegmentFindingFilter { + return &util_debug_SegmentFindingFilter{ + p0: p0, + p1: p1, + hasSegmentVal: false, + } +} + +func (f *util_debug_SegmentFindingFilter) hasSegment() bool { + return f.hasSegmentVal +} + +func (f *util_debug_SegmentFindingFilter) Filter(seq Geom_CoordinateSequence, i int) { + if i == 0 { + return + } + f.hasSegmentVal = f.p0.Equals2D(seq.GetCoordinate(i-1)) && + f.p1.Equals2D(seq.GetCoordinate(i)) +} + +func (f *util_debug_SegmentFindingFilter) IsDone() bool { + return f.hasSegmentVal +} + +func (f *util_debug_SegmentFindingFilter) IsGeometryChanged() bool { + return false +} + +// Implement Geom_CoordinateSequenceFilter interface marker +func (f *util_debug_SegmentFindingFilter) IsGeom_CoordinateSequenceFilter() {} + +func util_newDebug() *util_Debug { + return &util_Debug{ + out: os.Stdout, + } +} + +func (d *util_Debug) instancePrintWatch() { + if d.watchObj == nil { + return + } + d.instancePrint(d.watchObj) +} + +func (d *util_Debug) instancePrintIfWatch(obj any) { + if obj != d.watchObj { + return + } + if d.watchObj == nil { + return + } + d.instancePrint(d.watchObj) +} + +func (d *util_Debug) instancePrint(obj any) { + // TRANSLITERATION NOTE: Java uses reflection to check if the object has a + // print(PrintStream) method and calls it if present. Go doesn't have this + // capability, so we use a type switch for known types and fall back to + // fmt.Sprint for others. + switch v := obj.(type) { + case []any: + for _, item := range v { + d.instancePrintObject(item) + } + default: + d.instancePrintObject(obj) + } +} + +func (d *util_Debug) instancePrintObject(obj any) { + // TRANSLITERATION NOTE: In Java, this method uses reflection to find and + // invoke a print(PrintStream) method on the object. Since Go doesn't have + // this capability, we print using fmt.Sprint and rely on the object's + // String() method if it implements fmt.Stringer. + d.instancePrintString(fmt.Sprint(obj)) +} + +func (d *util_Debug) println() { + fmt.Fprintln(d.out) +} + +func (d *util_Debug) instanceAddWatch(obj any) { + d.watchObj = obj +} + +func (d *util_Debug) instancePrintString(str string) { + fmt.Fprint(d.out, util_debug_DEBUG_LINE_TAG) + fmt.Fprint(d.out, str) +} diff --git a/internal/jtsport/jts/util_stopwatch.go b/internal/jtsport/jts/util_stopwatch.go new file mode 100644 index 00000000..bb46fd3a --- /dev/null +++ b/internal/jtsport/jts/util_stopwatch.go @@ -0,0 +1,78 @@ +package jts + +import ( + "fmt" + "time" +) + +// Util_Stopwatch implements a timer function which can compute +// elapsed time as well as split times. +type Util_Stopwatch struct { + startTimestamp time.Time + totalTime int64 + isRunning bool +} + +func Util_NewStopwatch() *Util_Stopwatch { + sw := &Util_Stopwatch{ + totalTime: 0, + isRunning: false, + } + sw.Start() + return sw +} + +func (sw *Util_Stopwatch) Start() { + if sw.isRunning { + return + } + sw.startTimestamp = time.Now() + sw.isRunning = true +} + +func (sw *Util_Stopwatch) Stop() int64 { + if sw.isRunning { + sw.updateTotalTime() + sw.isRunning = false + } + return sw.totalTime +} + +func (sw *Util_Stopwatch) Reset() { + sw.totalTime = 0 + sw.startTimestamp = time.Now() +} + +func (sw *Util_Stopwatch) Split() int64 { + if sw.isRunning { + sw.updateTotalTime() + } + return sw.totalTime +} + +func (sw *Util_Stopwatch) updateTotalTime() { + endTimestamp := time.Now() + elapsedTime := endTimestamp.Sub(sw.startTimestamp).Milliseconds() + sw.startTimestamp = endTimestamp + sw.totalTime += elapsedTime +} + +func (sw *Util_Stopwatch) GetTime() int64 { + sw.updateTotalTime() + return sw.totalTime +} + +func (sw *Util_Stopwatch) GetTimeString() string { + totalTime := sw.GetTime() + return Util_Stopwatch_GetTimeStringFromMillis(totalTime) +} + +func Util_Stopwatch_GetTimeStringFromMillis(timeMillis int64) string { + var totalTimeStr string + if timeMillis < 10000 { + totalTimeStr = fmt.Sprintf("%d ms", timeMillis) + } else { + totalTimeStr = fmt.Sprintf("%v s", float64(timeMillis)/1000.0) + } + return totalTimeStr +} diff --git a/internal/jtsport/xmltest/runner_test.go b/internal/jtsport/xmltest/runner_test.go index 202ef2a0..cf7e00aa 100644 --- a/internal/jtsport/xmltest/runner_test.go +++ b/internal/jtsport/xmltest/runner_test.go @@ -60,22 +60,17 @@ func TestXMLTestSuite(t *testing.T) { } func isUnsupportedOp(opName string) bool { + // Operations that are still stubbed (not yet ported). unsupported := []string{ - "buffer", - "buffermitredjoin", - "convexhull", - "densify", - "distance", - "getcentroid", - "getinteriorpoint", - "getlength", - "isvalid", - "iswithindistance", - "minclearance", - "minclearanceline", - "polygonize", - "simplifydp", - "simplifytp", + "convexhull", // algorithm/ConvexHull not ported + "densify", // densify/Densifier not ported + "getcentroid", // algorithm/Centroid not ported + "getinteriorpoint", // algorithm/InteriorPoint not ported + "minclearance", // precision/MinimumClearance not ported + "minclearanceline", // precision/MinimumClearance not ported + "polygonize", // operation/polygonize/Polygonizer not ported + "simplifydp", // simplify/DouglasPeuckerSimplifier not ported + "simplifytp", // simplify/TopologyPreservingSimplifier not ported } for _, u := range unsupported { if opName == u { diff --git a/internal/test/test.go b/internal/test/test.go index ae5cfe90..b1acac43 100644 --- a/internal/test/test.go +++ b/internal/test/test.go @@ -10,6 +10,13 @@ import ( "github.com/peterstace/simplefeatures/geom" ) +func FromWKT(tb testing.TB, wkt string) geom.Geometry { + tb.Helper() + g, err := geom.UnmarshalWKT(wkt) + NoErr(tb, err) + return g +} + func Eq[T comparable](tb testing.TB, got, want T) { tb.Helper() if got != want { @@ -59,10 +66,16 @@ func ExactEquals(tb testing.TB, got, want geom.Geometry, opts ...geom.ExactEqual } } -func NotExactEquals(tb testing.TB, got, want geom.Geometry, opts ...geom.ExactEqualsOption) { +func ExactEqualsWKT(tb testing.TB, got geom.Geometry, wantWKT string, opts ...geom.ExactEqualsOption) { + tb.Helper() + want := FromWKT(tb, wantWKT) + ExactEquals(tb, got, want, opts...) +} + +func NotExactEquals(tb testing.TB, got, doNotWant geom.Geometry, opts ...geom.ExactEqualsOption) { tb.Helper() - if geom.ExactEquals(got, want, opts...) { - tb.Fatalf("geometries should not be exactly equal:\n got: %v\nwant: %v", got.AsText(), want.AsText()) + if geom.ExactEquals(got, doNotWant, opts...) { + tb.Fatalf("geometries should not be exactly equal:\n got: %v\ndoNotWant: %v", got.AsText(), doNotWant.AsText()) } } From 5ac104fb7ce2aceb3f003f2961f87e9b5dd47b70 Mon Sep 17 00:00:00 2001 From: Peter Stace Date: Fri, 13 Feb 2026 10:45:53 +1100 Subject: [PATCH 2/9] Fix logical merge conflict --- internal/test/test.go | 9 --------- 1 file changed, 9 deletions(-) diff --git a/internal/test/test.go b/internal/test/test.go index b1acac43..2fdcef6f 100644 --- a/internal/test/test.go +++ b/internal/test/test.go @@ -79,15 +79,6 @@ func NotExactEquals(tb testing.TB, got, doNotWant geom.Geometry, opts ...geom.Ex } } -func ExactEqualsWKT(tb testing.TB, got geom.Geometry, wantWKT string, opts ...geom.ExactEqualsOption) { - tb.Helper() - want, err := geom.UnmarshalWKT(wantWKT) - if err != nil { - tb.Fatalf("failed to unmarshal WKT '%s': %v", wantWKT, err) - } - ExactEquals(tb, got, want, opts...) -} - func DeepEqual(tb testing.TB, a, b any) { tb.Helper() if !reflect.DeepEqual(a, b) { From a8b6e20464b00b3b4658878c53861d7f546704eb Mon Sep 17 00:00:00 2001 From: Peter Stace Date: Fri, 13 Feb 2026 10:52:28 +1100 Subject: [PATCH 3/9] Use tolerance of 1e-10 for buffer tests This is to account for floating point differences on different architectures. --- geom/alg_buffer_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/geom/alg_buffer_test.go b/geom/alg_buffer_test.go index ed624a02..60f8d208 100644 --- a/geom/alg_buffer_test.go +++ b/geom/alg_buffer_test.go @@ -161,7 +161,7 @@ func TestBuffer(t *testing.T) { in := test.FromWKT(t, tc.input) got, err := geom.Buffer(in, tc.dist, tc.opts...) test.NoErr(t, err) - test.ExactEqualsWKT(t, got, tc.want) + test.ExactEqualsWKT(t, got, tc.want, geom.ToleranceXY(1e-10)) }) } } From a42289a2c4178acd3af4cd60fa8dc6d5ca4556d0 Mon Sep 17 00:00:00 2001 From: Peter Stace Date: Fri, 13 Feb 2026 10:58:45 +1100 Subject: [PATCH 4/9] Tweak CHANGELOG.md wording --- CHANGELOG.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 40cad904..53a7fed6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,9 @@ - Add `Buffer` function that computes the buffer of a geometry at a given radius. Options are available for controlling quad segments, end cap style (round, flat, square), join style (round, mitre, bevel), single-sided mode, - and simplify factor. The implementation is based on a port of JTS. + and simplify factor. The implementation is based on a port of JTS. This means + that `Buffer` is now available natively in Go without need for the GEOS + dependency. - Change `GeometryCollection.Dimension()` to return -1 for empty geometry collections (previously returned 0). This change is consistent with GEOS. From 6c77807e57cec5b3bd01daea0eaec70889433017 Mon Sep 17 00:00:00 2001 From: Peter Stace Date: Fri, 13 Feb 2026 10:59:36 +1100 Subject: [PATCH 5/9] Restore function ordering in test.go --- internal/test/test.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/internal/test/test.go b/internal/test/test.go index 2fdcef6f..28a32261 100644 --- a/internal/test/test.go +++ b/internal/test/test.go @@ -66,12 +66,6 @@ func ExactEquals(tb testing.TB, got, want geom.Geometry, opts ...geom.ExactEqual } } -func ExactEqualsWKT(tb testing.TB, got geom.Geometry, wantWKT string, opts ...geom.ExactEqualsOption) { - tb.Helper() - want := FromWKT(tb, wantWKT) - ExactEquals(tb, got, want, opts...) -} - func NotExactEquals(tb testing.TB, got, doNotWant geom.Geometry, opts ...geom.ExactEqualsOption) { tb.Helper() if geom.ExactEquals(got, doNotWant, opts...) { @@ -79,6 +73,12 @@ func NotExactEquals(tb testing.TB, got, doNotWant geom.Geometry, opts ...geom.Ex } } +func ExactEqualsWKT(tb testing.TB, got geom.Geometry, wantWKT string, opts ...geom.ExactEqualsOption) { + tb.Helper() + want := FromWKT(tb, wantWKT) + ExactEquals(tb, got, want, opts...) +} + func DeepEqual(tb testing.TB, a, b any) { tb.Helper() if !reflect.DeepEqual(a, b) { From b5d4643178812c0b60bc95c4dd8702fe5233e491 Mon Sep 17 00:00:00 2001 From: Peter Stace Date: Fri, 13 Feb 2026 11:01:36 +1100 Subject: [PATCH 6/9] Clean up unneeded comments --- internal/jtsport/xmltest/runner_test.go | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/internal/jtsport/xmltest/runner_test.go b/internal/jtsport/xmltest/runner_test.go index cf7e00aa..fdf38269 100644 --- a/internal/jtsport/xmltest/runner_test.go +++ b/internal/jtsport/xmltest/runner_test.go @@ -60,17 +60,16 @@ func TestXMLTestSuite(t *testing.T) { } func isUnsupportedOp(opName string) bool { - // Operations that are still stubbed (not yet ported). unsupported := []string{ - "convexhull", // algorithm/ConvexHull not ported - "densify", // densify/Densifier not ported - "getcentroid", // algorithm/Centroid not ported - "getinteriorpoint", // algorithm/InteriorPoint not ported - "minclearance", // precision/MinimumClearance not ported - "minclearanceline", // precision/MinimumClearance not ported - "polygonize", // operation/polygonize/Polygonizer not ported - "simplifydp", // simplify/DouglasPeuckerSimplifier not ported - "simplifytp", // simplify/TopologyPreservingSimplifier not ported + "convexhull", + "densify", + "getcentroid", + "getinteriorpoint", + "minclearance", + "minclearanceline", + "polygonize", + "simplifydp", + "simplifytp", } for _, u := range unsupported { if opName == u { From 85f2cd65d83cdb013a1f2dd0e189d7f71843e7e5 Mon Sep 17 00:00:00 2001 From: Peter Stace Date: Fri, 13 Feb 2026 11:11:22 +1100 Subject: [PATCH 7/9] Use square brackets to refer to code elements in comments --- geom/alg_buffer.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/geom/alg_buffer.go b/geom/alg_buffer.go index 85705655..b6b87d8f 100644 --- a/geom/alg_buffer.go +++ b/geom/alg_buffer.go @@ -18,7 +18,7 @@ import ( // Since true buffer curves may contain circular arcs, computed buffer polygons // are only approximations to the true geometry. The user can control the // accuracy of the approximation by specifying the number of segments used to -// approximate quarter circles (via BufferQuadSegments). +// approximate quarter circles (via [BufferQuadSegments]). // // An error may be returned in pathological cases of numerical degeneracy. func Buffer(g Geometry, radius float64, opts ...BufferOption) (Geometry, error) { @@ -47,7 +47,7 @@ func Buffer(g Geometry, radius float64, opts ...BufferOption) (Geometry, error) return result, err } -// BufferOption allows the behaviour of the Buffer operation to be modified. +// BufferOption allows the behaviour of the [Buffer] operation to be modified. type BufferOption func(*bufferOptionSet) type bufferOptionSet struct { From 971220cac9f72d3bd51175421b3bdb546a720e9a Mon Sep 17 00:00:00 2001 From: Peter Stace Date: Fri, 13 Feb 2026 11:17:55 +1100 Subject: [PATCH 8/9] Use "distance" terminology consistently --- CHANGELOG.md | 2 +- geom/alg_buffer.go | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 53a7fed6..4fd76f8d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,7 @@ ## Unreleased - Add `Buffer` function that computes the buffer of a geometry at a given - radius. Options are available for controlling quad segments, end cap style + distance. Options are available for controlling quad segments, end cap style (round, flat, square), join style (round, mitre, bevel), single-sided mode, and simplify factor. The implementation is based on a port of JTS. This means that `Buffer` is now available natively in Go without need for the GEOS diff --git a/geom/alg_buffer.go b/geom/alg_buffer.go index b6b87d8f..5c156f7f 100644 --- a/geom/alg_buffer.go +++ b/geom/alg_buffer.go @@ -4,7 +4,7 @@ import ( "github.com/peterstace/simplefeatures/internal/jtsport/jts" ) -// Buffer returns a geometry that contains all points within the given radius +// Buffer returns a geometry that contains all points within the given distance // of the input geometry. // // In GIS, the positive (or negative) buffer of a geometry is defined as @@ -21,7 +21,7 @@ import ( // approximate quarter circles (via [BufferQuadSegments]). // // An error may be returned in pathological cases of numerical degeneracy. -func Buffer(g Geometry, radius float64, opts ...BufferOption) (Geometry, error) { +func Buffer(g Geometry, distance float64, opts ...BufferOption) (Geometry, error) { optSet := newBufferOptionSet(opts) params := jts.OperationBuffer_NewBufferParameters() @@ -39,7 +39,7 @@ func Buffer(g Geometry, radius float64, opts ...BufferOption) (Geometry, error) if err != nil { return wrap(err, "converting geometry to JTS") } - jtsResult := jts.OperationBuffer_BufferOp_BufferOpWithParams(jtsG, radius, params) + jtsResult := jts.OperationBuffer_BufferOp_BufferOpWithParams(jtsG, distance, params) wkbWriter := jts.Io_NewWKBWriter() result, err = UnmarshalWKB(wkbWriter.Write(jtsResult), NoValidate{}) return wrap(err, "converting JTS buffer result to simplefeatures") From 369967094cffe02eebf0b77145fa8341bf019512 Mon Sep 17 00:00:00 2001 From: Peter Stace Date: Fri, 13 Feb 2026 11:24:07 +1100 Subject: [PATCH 9/9] Simplify buffer options setup --- geom/alg_buffer.go | 79 ++++++++++++++-------------------------------- 1 file changed, 23 insertions(+), 56 deletions(-) diff --git a/geom/alg_buffer.go b/geom/alg_buffer.go index 5c156f7f..29139535 100644 --- a/geom/alg_buffer.go +++ b/geom/alg_buffer.go @@ -22,15 +22,10 @@ import ( // // An error may be returned in pathological cases of numerical degeneracy. func Buffer(g Geometry, distance float64, opts ...BufferOption) (Geometry, error) { - optSet := newBufferOptionSet(opts) - params := jts.OperationBuffer_NewBufferParameters() - params.SetQuadrantSegments(optSet.quadSegments) - params.SetEndCapStyle(optSet.endCapStyle) - params.SetJoinStyle(optSet.joinStyle) - params.SetMitreLimit(optSet.mitreLimit) - params.SetSingleSided(optSet.isSingleSided) - params.SetSimplifyFactor(optSet.simplifyFactor) + for _, opt := range opts { + opt(params) + } var result Geometry err := catch(func() error { @@ -48,68 +43,44 @@ func Buffer(g Geometry, distance float64, opts ...BufferOption) (Geometry, error } // BufferOption allows the behaviour of the [Buffer] operation to be modified. -type BufferOption func(*bufferOptionSet) - -type bufferOptionSet struct { - quadSegments int - endCapStyle int - joinStyle int - mitreLimit float64 - isSingleSided bool - simplifyFactor float64 -} - -func newBufferOptionSet(opts []BufferOption) bufferOptionSet { - bos := bufferOptionSet{ - quadSegments: jts.OperationBuffer_BufferParameters_DEFAULT_QUADRANT_SEGMENTS, - endCapStyle: jts.OperationBuffer_BufferParameters_CAP_ROUND, - joinStyle: jts.OperationBuffer_BufferParameters_JOIN_ROUND, - mitreLimit: jts.OperationBuffer_BufferParameters_DEFAULT_MITRE_LIMIT, - isSingleSided: false, - simplifyFactor: jts.OperationBuffer_BufferParameters_DEFAULT_SIMPLIFY_FACTOR, - } - for _, opt := range opts { - opt(&bos) - } - return bos -} +type BufferOption func(*jts.OperationBuffer_BufferParameters) // BufferQuadSegments sets the number of segments used to approximate a quarter // circle. It defaults to 8. func BufferQuadSegments(quadSegments int) BufferOption { - return func(bos *bufferOptionSet) { - bos.quadSegments = quadSegments + return func(p *jts.OperationBuffer_BufferParameters) { + p.SetQuadrantSegments(quadSegments) } } // BufferEndCapRound sets the end cap style to 'round'. It is 'round' by // default. func BufferEndCapRound() BufferOption { - return func(bos *bufferOptionSet) { - bos.endCapStyle = jts.OperationBuffer_BufferParameters_CAP_ROUND + return func(p *jts.OperationBuffer_BufferParameters) { + p.SetEndCapStyle(jts.OperationBuffer_BufferParameters_CAP_ROUND) } } // BufferEndCapFlat sets the end cap style to 'flat'. It is 'round' by default. func BufferEndCapFlat() BufferOption { - return func(bos *bufferOptionSet) { - bos.endCapStyle = jts.OperationBuffer_BufferParameters_CAP_FLAT + return func(p *jts.OperationBuffer_BufferParameters) { + p.SetEndCapStyle(jts.OperationBuffer_BufferParameters_CAP_FLAT) } } // BufferEndCapSquare sets the end cap style to 'square'. It is 'round' by // default. func BufferEndCapSquare() BufferOption { - return func(bos *bufferOptionSet) { - bos.endCapStyle = jts.OperationBuffer_BufferParameters_CAP_SQUARE + return func(p *jts.OperationBuffer_BufferParameters) { + p.SetEndCapStyle(jts.OperationBuffer_BufferParameters_CAP_SQUARE) } } // BufferJoinStyleRound sets the join style to 'round'. It is 'round' by // default. func BufferJoinStyleRound() BufferOption { - return func(bos *bufferOptionSet) { - bos.joinStyle = jts.OperationBuffer_BufferParameters_JOIN_ROUND + return func(p *jts.OperationBuffer_BufferParameters) { + p.SetJoinStyle(jts.OperationBuffer_BufferParameters_JOIN_ROUND) } } @@ -117,17 +88,17 @@ func BufferJoinStyleRound() BufferOption { // default. The mitreLimit controls how far a mitre join can extend from the // join point. Corners with a ratio which exceed the limit will be beveled. func BufferJoinStyleMitre(mitreLimit float64) BufferOption { - return func(bos *bufferOptionSet) { - bos.joinStyle = jts.OperationBuffer_BufferParameters_JOIN_MITRE - bos.mitreLimit = mitreLimit + return func(p *jts.OperationBuffer_BufferParameters) { + p.SetJoinStyle(jts.OperationBuffer_BufferParameters_JOIN_MITRE) + p.SetMitreLimit(mitreLimit) } } // BufferJoinStyleBevel sets the join style to 'bevel'. It is 'round' by // default. func BufferJoinStyleBevel() BufferOption { - return func(bos *bufferOptionSet) { - bos.joinStyle = jts.OperationBuffer_BufferParameters_JOIN_BEVEL + return func(p *jts.OperationBuffer_BufferParameters) { + p.SetJoinStyle(jts.OperationBuffer_BufferParameters_JOIN_BEVEL) } } @@ -137,8 +108,8 @@ func BufferJoinStyleBevel() BufferOption { // indicates the left-hand side, negative indicates the right-hand side. // The end cap style is ignored for single-sided buffers and forced to flat. func BufferSingleSided() BufferOption { - return func(bos *bufferOptionSet) { - bos.isSingleSided = true + return func(p *jts.OperationBuffer_BufferParameters) { + p.SetSingleSided(true) } } @@ -149,11 +120,7 @@ func BufferSingleSided() BufferOption { // relatively good accuracy for the generated buffer. Larger values sacrifice // accuracy in return for performance. The default is 0.01. func BufferSimplifyFactor(factor float64) BufferOption { - return func(bos *bufferOptionSet) { - if factor < 0 { - bos.simplifyFactor = 0 - } else { - bos.simplifyFactor = factor - } + return func(p *jts.OperationBuffer_BufferParameters) { + p.SetSimplifyFactor(factor) } }