Skip to content
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

GEOS: Wrap Make Valid #433

Merged
merged 5 commits into from
Nov 22, 2021
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ __Special thanks to Albert Teoh and Sameera Perera for contributing to this rele
- Fixes a bug in Simplify where invalid interior rings would be omitted rather
than producing an error.

- Adds a wrapper in the `geos` package for the `GEOSMakeValid_r` function
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What was the motivation for wrapping the GEOSMakeValid_r function? What challenges are there to implementing this natively in Go?

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's something that Ken may find useful in his current work (might be better to take that discussion offline for further details).

The main challenges involved there would be coming up with an actual algorithm to do it (possibly that's the hard part) and the implementing it. It would most likely involve a DCEL data structure (which we have already implemented). I'd ballpark estimate 50hrs to 100hrs of effort would be required.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If there are plans to do this, maybe it's worth creating a ticket for this work; but if it's not in scope of the goals of simplefeatures, then all good.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good call, it's definitely in scope. I've created a ticket here: #435

(exposed as `MakeValid`).

## v0.34.0

2021-11-02
Expand Down
27 changes: 27 additions & 0 deletions geos/entrypoints.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,17 @@ package geos
#cgo LDFLAGS: -lgeos_c
#cgo CFLAGS: -Wall
#include "geos_c.h"

#define MAKE_VALID_MIN_VERSION "3.8.0"
#define MAKE_VALID_MISSING ( \
GEOS_VERSION_MAJOR < 3 || \
(GEOS_VERSION_MAJOR == 3 && GEOS_VERSION_MINOR < 8) \
)
#if MAKE_VALID_MISSING
// This stub implementation always fails:
GEOSGeometry *GEOSMakeValid_r(GEOSContextHandle_t handle, const GEOSGeometry* g) { return NULL; }
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this needed to prevent a compile-time error?

Copy link
Owner Author

@peterstace peterstace Nov 21, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, that's correct. Prior versions of the GEOS library don't define the symbol at all, so we can define a stub version conditionally if the GEOS library doesn't do it for us (of course, that stub implementation wouldn't actually do anything, but we can check for that condition before invoking it).

#endif

*/
import "C"

Expand Down Expand Up @@ -309,3 +320,19 @@ func SymmetricDifference(a, b geom.Geometry, opts ...geom.ConstructorOption) (ge
})
return result, wrap(err, "executing GEOSSymDifference_r")
}

// MakeValid can be used to convert an invalid geometry into a valid geometry.
// It does this by keeping the original control points and constructing a new
// geometry that is valid and similar (but not the same as) the original
// invalid geometry. If the input geometry is valid, then it is returned
// unaltered.
func MakeValid(g geom.Geometry, opts ...geom.ConstructorOption) (geom.Geometry, error) {
if C.MAKE_VALID_MISSING != 0 {
return geom.Geometry{}, unsupportedGEOSVersionError{
C.MAKE_VALID_MIN_VERSION, "MakeValid"}
}
result, err := unaryOpG(g, opts, func(ctx C.GEOSContextHandle_t, g *C.GEOSGeometry) *C.GEOSGeometry {
return C.GEOSMakeValid_r(ctx, g)
})
return result, wrap(err, "executing GEOSMakeValid_r")
}
27 changes: 25 additions & 2 deletions geos/entrypoints_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@ import (
"github.com/peterstace/simplefeatures/geom"
)

func geomFromWKT(t *testing.T, wkt string) geom.Geometry {
func geomFromWKT(t *testing.T, wkt string, opts ...geom.ConstructorOption) geom.Geometry {
t.Helper()
geom, err := geom.UnmarshalWKT(wkt)
geom, err := geom.UnmarshalWKT(wkt, opts...)
if err != nil {
t.Fatalf("could not unmarshal WKT:\n wkt: %s\n err: %v", wkt, err)
}
Expand Down Expand Up @@ -767,3 +767,26 @@ func TestSymmetricDifference(t *testing.T) {

expectGeomEq(t, got, want, geom.IgnoreOrder)
}

func TestMakeValid(t *testing.T) {
for i, tt := range []struct {
input string
wantOutput string
}{
{
"POLYGON((0 0,2 2,2 0,0 2,0 0))",
"MULTIPOLYGON(((0 0,0 2,1 1,0 0)),((2 0,1 1,2 2,2 0)))",
},
} {
t.Run(strconv.Itoa(i), func(t *testing.T) {
in := geomFromWKT(t, tt.input, geom.DisableAllValidations)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a way to assert that the input geometry is invalid?

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, that's possible. Added in 65c2837

gotGeom, err := MakeValid(in)
if _, ok := err.(unsupportedGEOSVersionError); ok {
t.Skip(err)
}
expectNoErr(t, err)
wantGeom := geomFromWKT(t, tt.wantOutput)
expectGeomEq(t, gotGeom, wantGeom)
})
}
}
20 changes: 20 additions & 0 deletions geos/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,29 @@ package geos

import "fmt"

// #include "geos_c.h"
import "C"

func wrap(err error, format string, args ...interface{}) error {
if err == nil {
return nil
}
return fmt.Errorf(format+": %w", append(args, err)...)
}

var currentGEOSVersion = fmt.Sprintf(
"%d.%d.%d",
C.GEOS_VERSION_MAJOR,
C.GEOS_VERSION_MINOR,
C.GEOS_VERSION_PATCH,
)

type unsupportedGEOSVersionError struct {
requiredGEOSVersion string
operation string
}

func (e unsupportedGEOSVersionError) Error() string {
return fmt.Sprintf("%s is unsupported in GEOS %s, requires at least GEOS %s",
e.operation, currentGEOSVersion, e.requiredGEOSVersion)
}
2 changes: 1 addition & 1 deletion geos/handle.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ char *marshal(GEOSContextHandle_t handle, const GEOSGeometry *g, size_t *size, c
GEOSGeometry const *noop(GEOSContextHandle_t handle, const GEOSGeometry *g) {
return g;
}

*/
import "C"

Expand Down Expand Up @@ -62,6 +61,7 @@ func newHandle() (*handle, error) {
h.release()
return nil, errors.New("malloc failed")
}
C.memset((unsafe.Pointer)(h.errBuf), 0, 1024)
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was a bug I noticed while I was here. Malloc is not going to actually zero out the memory content... (I've been spoilt by Go).


h.context = C.sf_init(unsafe.Pointer(h.errBuf))
if h.context == nil {
Expand Down