-
Notifications
You must be signed in to change notification settings - Fork 26
DRAFT: JTS Port #680
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Closed
Closed
DRAFT: JTS Port #680
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,7 @@ | ||
| run: | ||
| timeout: 5m | ||
| skip-dirs: | ||
| - internal/jtsport/jts | ||
|
|
||
| issues: | ||
| exclude-rules: | ||
|
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| # CLAUDE.md |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| This software includes code derived from JTS (Java Topology Suite) | ||
| Copyright (c) 2016-2024 LocationTech and Contributors | ||
| Licensed under the Eclipse Distribution License 1.0 | ||
| See: https://github.com/locationtech/jts. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,20 +1,37 @@ | ||
| package geom | ||
|
|
||
| import "github.com/peterstace/simplefeatures/internal/jtsport/jts" | ||
|
|
||
| // TODO: Optimise operations by comparing bounding boxes first. | ||
|
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Need to create an issue to track this TODO. |
||
|
|
||
| // hasGC determines if either argument is a GeometryCollection. The JTS port | ||
| // doesn't have full support for GeometryCollections, so we need to handle them | ||
| // specially. | ||
| // | ||
| // Specifically, the JTS port has the following restrictions for binary overlay | ||
| // operations: | ||
| // | ||
| // 1. All input elements must have the same dimension. | ||
| // | ||
| // 2. Polygons in a GeometryCollection must not overlap. | ||
| func hasGC(a, b Geometry) bool { | ||
| return a.IsGeometryCollection() || b.IsGeometryCollection() | ||
| } | ||
|
|
||
| // Union returns a geometry that represents the parts from either geometry A or | ||
| // geometry B (or both). An error may be returned in pathological cases of | ||
| // numerical degeneracy. | ||
| func Union(a, b Geometry) (Geometry, error) { | ||
| if a.IsEmpty() && b.IsEmpty() { | ||
| return Geometry{}, nil | ||
| if hasGC(a, b) { | ||
| return gcAwareUnion(a, b) | ||
| } | ||
| if a.IsEmpty() { | ||
| return UnaryUnion(b) | ||
| } | ||
| if b.IsEmpty() { | ||
| return UnaryUnion(a) | ||
| } | ||
| g, err := setOp(a, or, b) | ||
| return g, wrap(err, "executing union") | ||
| return jtsOverlayOp(a, b, jts.OperationOverlayng_OverlayNG_UNION) | ||
| } | ||
|
|
||
| func gcAwareUnion(a, b Geometry) (Geometry, error) { | ||
| // UnaryUnion supports arbitrary GeometryCollections. | ||
| gc := NewGeometryCollection([]Geometry{a, b}) | ||
| return UnaryUnion(gc.AsGeometry()) | ||
| } | ||
|
|
||
| // Intersection returns a geometry that represents the parts that are common to | ||
|
|
@@ -24,8 +41,64 @@ func Intersection(a, b Geometry) (Geometry, error) { | |
| if a.IsEmpty() || b.IsEmpty() { | ||
| return Geometry{}, nil | ||
| } | ||
| g, err := setOp(a, and, b) | ||
| return g, wrap(err, "executing intersection") | ||
| if hasGC(a, b) { | ||
| return gcAwareIntersection(a, b) | ||
| } | ||
| return jtsOverlayOp(a, b, jts.OperationOverlayng_OverlayNG_INTERSECTION) | ||
| } | ||
|
|
||
| func gcAwareIntersection(a, b Geometry) (Geometry, error) { | ||
| partsA, partsB, err := prepareOverlayInputParts(a, b) | ||
| if err != nil { | ||
| return Geometry{}, err | ||
| } | ||
|
|
||
| // The total result is the union of the intersections across the Cartesian | ||
| // product of parts. | ||
| var results []Geometry | ||
| for _, partA := range partsA { | ||
| for _, partB := range partsB { | ||
| result, err := jtsOverlayOp(partA, partB, jts.OperationOverlayng_OverlayNG_INTERSECTION) | ||
| if err != nil { | ||
| return Geometry{}, err | ||
| } | ||
| results = append(results, result) | ||
| } | ||
| } | ||
| return UnaryUnion(NewGeometryCollection(results).AsGeometry()) | ||
| } | ||
|
|
||
| func explodeGeometryCollections(dst []Geometry, g Geometry) []Geometry { | ||
| if gc, ok := g.AsGeometryCollection(); ok { | ||
| for i := 0; i < gc.NumGeometries(); i++ { | ||
| dst = explodeGeometryCollections(dst, gc.GeometryN(i)) | ||
| } | ||
| return dst | ||
| } | ||
| return append(dst, g) | ||
| } | ||
|
|
||
| func prepareOverlayInputParts(a, b Geometry) ([]Geometry, []Geometry, error) { | ||
| // Normalize GC inputs by unioning their parts. | ||
| if a.IsGeometryCollection() { | ||
| var err error | ||
| a, err = UnaryUnion(a) | ||
| if err != nil { | ||
| return nil, nil, err | ||
| } | ||
| } | ||
| if b.IsGeometryCollection() { | ||
| var err error | ||
| b, err = UnaryUnion(b) | ||
| if err != nil { | ||
| return nil, nil, err | ||
| } | ||
| } | ||
|
|
||
| // Extract non-GC parts from each input. | ||
| partsA := explodeGeometryCollections(nil, a) | ||
| partsB := explodeGeometryCollections(nil, b) | ||
| return partsA, partsB, nil | ||
| } | ||
|
|
||
| // Difference returns a geometry that represents the parts of input geometry A | ||
|
|
@@ -35,11 +108,57 @@ func Difference(a, b Geometry) (Geometry, error) { | |
| if a.IsEmpty() { | ||
| return Geometry{}, nil | ||
| } | ||
| if b.IsEmpty() { | ||
| return UnaryUnion(a) | ||
| if hasGC(a, b) { | ||
| return gcAwareDifference(a, b) | ||
| } | ||
| g, err := setOp(a, andNot, b) | ||
| return g, wrap(err, "executing difference") | ||
| return jtsOverlayOp(a, b, jts.OperationOverlayng_OverlayNG_DIFFERENCE) | ||
| } | ||
|
|
||
| func gcAwareDifference(a, b Geometry) (Geometry, error) { | ||
| partsA, partsB, err := prepareOverlayInputParts(a, b) | ||
| if err != nil { | ||
| return Geometry{}, err | ||
| } | ||
|
|
||
| // The total result is the union of each part of A after each part of B has | ||
| // been removed (sequentially). | ||
| var results []Geometry | ||
| for _, partA := range partsA { | ||
| result := partA | ||
| for _, partB := range partsB { | ||
| var err error | ||
| result, err = jtsOverlayOp(result, partB, jts.OperationOverlayng_OverlayNG_DIFFERENCE) | ||
| if err != nil { | ||
| return Geometry{}, err | ||
| } | ||
| if result.IsEmpty() { | ||
| break | ||
| } | ||
| } | ||
| results = append(results, result) | ||
| } | ||
| return UnaryUnion(NewGeometryCollection(results).AsGeometry()) | ||
| } | ||
|
|
||
| // jtsOverlayOp invokes the JTS port's overlay operation with the given opCode. | ||
| func jtsOverlayOp(a, b Geometry, opCode int) (Geometry, error) { | ||
| var result Geometry | ||
| err := catch(func() error { | ||
| wkbReader := jts.Io_NewWKBReader() | ||
| jtsA, err := wkbReader.ReadBytes(a.AsBinary()) | ||
| if err != nil { | ||
| return wrap(err, "converting geometry A to JTS") | ||
| } | ||
| jtsB, err := wkbReader.ReadBytes(b.AsBinary()) | ||
| if err != nil { | ||
| return wrap(err, "converting geometry B to JTS") | ||
| } | ||
| jtsResult := jts.OperationOverlayng_OverlayNGRobust_Overlay(jtsA, jtsB, opCode) | ||
| wkbWriter := jts.Io_NewWKBWriter() | ||
| result, err = UnmarshalWKB(wkbWriter.Write(jtsResult), NoValidate{}) | ||
| return wrap(err, "converting JTS overlay result to simplefeatures") | ||
| }) | ||
| return result, err | ||
| } | ||
|
|
||
| // SymmetricDifference returns a geometry that represents the parts of geometry | ||
|
|
@@ -55,14 +174,25 @@ func SymmetricDifference(a, b Geometry) (Geometry, error) { | |
| if b.IsEmpty() { | ||
| return UnaryUnion(a) | ||
| } | ||
| g, err := setOp(a, xor, b) | ||
| return g, wrap(err, "executing symmetric difference") | ||
|
|
||
| diffAB, err := Difference(a, b) | ||
| if err != nil { | ||
| return Geometry{}, err | ||
| } | ||
| diffBA, err := Difference(b, a) | ||
| if err != nil { | ||
| return Geometry{}, err | ||
| } | ||
| return Union(diffAB, diffBA) | ||
| } | ||
|
|
||
| // UnaryUnion is a single input variant of the Union function, unioning | ||
| // together the components of the input geometry. | ||
| func UnaryUnion(g Geometry) (Geometry, error) { | ||
| return setOp(g, or, Geometry{}) | ||
| if g.IsEmpty() { | ||
| return Geometry{}, nil | ||
| } | ||
| return jtsUnaryUnion(g) | ||
| } | ||
|
|
||
| // UnionMany unions together the input geometries. | ||
|
|
@@ -71,19 +201,19 @@ func UnionMany(gs []Geometry) (Geometry, error) { | |
| return UnaryUnion(gc.AsGeometry()) | ||
| } | ||
|
|
||
| func setOp(a Geometry, include func([2]bool) bool, b Geometry) (Geometry, error) { | ||
| overlay := newDCELFromGeometries(a, b) | ||
| g, err := overlay.extractGeometry(include) | ||
| if err != nil { | ||
| return Geometry{}, wrap(err, "internal error extracting geometry") | ||
| } | ||
| if err := g.Validate(); err != nil { | ||
| return Geometry{}, wrap(err, "invalid geometry produced by overlay") | ||
| } | ||
| return g, nil | ||
| // jtsUnaryUnion invokes the JTS port's unary union operation. | ||
| func jtsUnaryUnion(g Geometry) (Geometry, error) { | ||
| 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.OperationOverlayng_OverlayNGRobust_Union(jtsG) | ||
| wkbWriter := jts.Io_NewWKBWriter() | ||
| result, err = UnmarshalWKB(wkbWriter.Write(jtsResult), NoValidate{}) | ||
| return wrap(err, "converting JTS union result to simplefeatures") | ||
| }) | ||
| return result, err | ||
| } | ||
|
|
||
| func or(b [2]bool) bool { return b[0] || b[1] } | ||
| func and(b [2]bool) bool { return b[0] && b[1] } | ||
| func xor(b [2]bool) bool { return b[0] != b[1] } | ||
| func andNot(b [2]bool) bool { return b[0] && !b[1] } | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Need to track this TODO in an issue.