Skip to content

Commit

Permalink
[go] fix geometry parts build (#355)
Browse files Browse the repository at this point in the history
* [go] fix geometry parts build

* [go] add separate test for multipolygon
  • Loading branch information
durandAD committed Apr 9, 2024
1 parent 2828e07 commit ac924bd
Show file tree
Hide file tree
Showing 3 changed files with 146 additions and 22 deletions.
127 changes: 109 additions & 18 deletions src/go/flatgeobuf_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,9 @@ func TestCreateFGBFileAndBasicSearch(t *testing.T) {

var mockFile bytes.Buffer
wr := writer.NewWriter(header, true, fgen(), hu)
wr.Write(&mockFile)
if _, err := wr.Write(&mockFile); err != nil {
t.Errorf("failed to write in buffer: %v", err)
}

// TEST:
// check header metadata
Expand All @@ -119,16 +121,7 @@ func TestCreateFGBFileAndBasicSearch(t *testing.T) {
t.Errorf("Incorrect header metadata: got %q, want %q", meta, hu.MetadataStr)
}

type test struct {
searchMinX float64
searchMinY float64
searchMaxX float64
searchMaxY float64

expectedResultProperties []int
}

tests := []test{
tests := []testSearch{
{searchMinX: 0.5, searchMinY: 0.5, searchMaxX: 0.6, searchMaxY: 0.6,
expectedResultProperties: []int{1}},
{searchMinX: -0.6, searchMinY: -0.1, searchMaxX: -0.5, searchMaxY: 0.1,
Expand All @@ -139,6 +132,84 @@ func TestCreateFGBFileAndBasicSearch(t *testing.T) {
expectedResultProperties: []int{}},
}

runTestSearchInFGB(t, fgbFile, tests)

// Check writer.WithMemory produces the same result
var got bytes.Buffer
mwr := writer.NewWriter(hgen(), true, fgen(), hu, writer.WithMemory())
if _, err = mwr.Write(&got); err != nil {
t.Errorf("failed to write with WithMemory: %v", err)
}

if !reflect.DeepEqual(got.Bytes(), mockFile.Bytes()) {
t.Error("Unexpected results using writer.WithMemory")
}
}

func TestFGBFileWithMultiPolygon(t *testing.T) {
// SETUP:
// Create a mock fgb file
//

// four multipolygon features that are composed of squares, each
// feature being in a quadrant of the cartesian plan, the uint32 property
// attached to each is the quadrant number
fgen := func() *fg {
return &fg{
Features: []*writer.Feature{
createMultiSquareFeature([][4]float64{{1, 1, 2, 2}, {2, 2, 3, 3}}, 1),
createMultiSquareFeature([][4]float64{{-2, 1, -1, 2}, {-3, 2, -2, 3}}, 2),
createMultiSquareFeature([][4]float64{{-2, -2, -1, -1}, {-2, -2, -3, -3}}, 3),
createMultiSquareFeature([][4]float64{{1, -2, 2, -1}, {2, -3, 3, -2}}, 4),
},
}
}

hgen := func() *writer.Header {
headerBuilder := flatbuffers.NewBuilder(0)
header := writer.NewHeader(headerBuilder).
SetName("Households ShapeFile Data").
SetTitle("Households ShapeFile Data").
SetGeometryType(flattypes.GeometryTypeMultiPolygon)
householdsCol := writer.NewColumn(headerBuilder).
SetName("Households").
SetType(flattypes.ColumnTypeUInt)
header.SetColumns([]*writer.Column{householdsCol})
return header
}

header := hgen()

var mockFile bytes.Buffer
wr := writer.NewWriter(header, true, fgen(), nil)
if _, err := wr.Write(&mockFile); err != nil {
t.Errorf("failed to write in buffer: %v", err)
}

// TEST:
// run search cases where we expect 0, 1, 2, and 4 results
//

fgbFile, err := NewWithData(mockFile.Bytes())
if err != nil {
t.Fatalf("failed to create FlatGeoBuf: %v", err)
}

tests := []testSearch{
{searchMinX: 1.5, searchMinY: 1.5, searchMaxX: 1.6, searchMaxY: 1.6,
expectedResultProperties: []int{1}},
{searchMinX: -1.6, searchMinY: -1.1, searchMaxX: -1.5, searchMaxY: 1.1,
expectedResultProperties: []int{2, 3}},
{searchMinX: -1.1, searchMinY: -1.1, searchMaxX: 1.1, searchMaxY: 1.1,
expectedResultProperties: []int{1, 2, 3, 4}},
{searchMinX: 3.5, searchMinY: 3.5, searchMaxX: 4.5, searchMaxY: 4.5,
expectedResultProperties: []int{}},
}

runTestSearchInFGB(t, fgbFile, tests)
}

func runTestSearchInFGB(t *testing.T, fgbFile *FlatGeoBuf, tests []testSearch) {
for _, test := range tests {
features, _ := fgbFile.Search(test.searchMinX, test.searchMinY, test.searchMaxX, test.searchMaxY)
if got, want := len(features), len(test.expectedResultProperties); got != want {
Expand All @@ -157,14 +228,15 @@ func TestCreateFGBFileAndBasicSearch(t *testing.T) {
t.Errorf("Unexpected search results. (want = %v, got = %v)", wantSet, gotSet)
}
}
}

// Check writer.WithMemory produces the same result
var got bytes.Buffer
mwr := writer.NewWriter(hgen(), true, fgen(), hu, writer.WithMemory())
mwr.Write(&got)
if !reflect.DeepEqual(got.Bytes(), mockFile.Bytes()) {
t.Error("Unexpected results using writer.WithMemory")
}
type testSearch struct {
searchMinX float64
searchMinY float64
searchMaxX float64
searchMaxY float64

expectedResultProperties []int
}

func createSquareFeature(xmin, ymin, xmax, ymax float64, propertyCount uint16) *writer.Feature {
Expand All @@ -181,6 +253,25 @@ func createSquareFeature(xmin, ymin, xmax, ymax float64, propertyCount uint16) *
return writer.NewFeature(featureBuilder).SetProperties(properties).SetGeometry(geo)
}

func createMultiSquareFeature(multiSquares [][4]float64, propertyCount uint16) *writer.Feature {
properties := make([]byte, 5)
binary.LittleEndian.PutUint16(properties[1:], propertyCount)

featureBuilder := flatbuffers.NewBuilder(0)
parts := make([]writer.Geometry, len(multiSquares))
for i, coords := range multiSquares {
xmin, ymin, xmax, ymax := coords[0], coords[1], coords[2], coords[3]
parts[i] = *writer.NewGeometry(featureBuilder).SetXY([]float64{
xmin, ymin,
xmin, ymax,
xmax, ymax,
xmax, ymin,
})
}
geo := writer.NewGeometry(featureBuilder).SetXY([]float64{}).SetParts(parts)
return writer.NewFeature(featureBuilder).SetProperties(properties).SetGeometry(geo)
}

type fg struct {
Features []*writer.Feature
idx int
Expand Down
11 changes: 8 additions & 3 deletions src/go/writer/geometry.go
Original file line number Diff line number Diff line change
Expand Up @@ -105,11 +105,16 @@ func (g *Geometry) Build() flatbuffers.UOffsetT {
}
tmOffset := g.builder.EndVector(len(g.tm))

flattypes.GeometryStartPartsVector(g.builder, len(g.parts))
partsOffsets := make([]flatbuffers.UOffsetT, 0, len(g.parts))
for i := len(g.parts) - 1; i >= 0; i-- {
g.builder.PrependUOffsetT(g.parts[i].Build())
partsOffsets = append(partsOffsets, g.parts[i].Build())
}
partsOffset := g.builder.EndVector(len(g.parts))

flattypes.GeometryStartPartsVector(g.builder, len(partsOffsets))
for _, partOffset := range partsOffsets {
g.builder.PrependUOffsetT(partOffset)
}
partsOffset := g.builder.EndVector(len(partsOffsets))

flattypes.GeometryStart(g.builder)

Expand Down
30 changes: 29 additions & 1 deletion src/go/writer/geometry_extended.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,42 @@ func NewGeometryExtended(geometry *Geometry) *GeometryExtended {
}

func (g *GeometryExtended) BoundingBox() (minX, minY, maxX, maxY float64) {
if len(g.parts) == 0 {
return geomBoundingBox(g.Geometry)
}
minX = math.Inf(1)
minY = math.Inf(1)
maxX = math.Inf(-1)
maxY = math.Inf(-1)
for i := 0; i < len(g.parts); i++ {
partMinX, partMinY, partMaxX, partMaxY := geomBoundingBox(&g.parts[i])
if partMinX < minX {
minX = partMinX
}
if partMinY < minY {
minY = partMinY
}
if partMaxX > maxX {
maxX = partMaxX
}
if partMaxY > maxY {
maxY = partMaxY
}
}
return
}

func geomBoundingBox(g *Geometry) (minX, minY, maxX, maxY float64) {
firstPartEnd := len(g.xy)
if firstPartEnd == 0 {
// Nothing to do.
return
}

if len(g.ends) != 0 {
firstPartEnd = int(g.ends[0])
// Ends being the number of point we multiply by 2 to get the last
// x y coordinates of the first part
firstPartEnd = int(g.ends[0]) * 2
}
firstPart := g.xy[0:firstPartEnd]

Expand Down

0 comments on commit ac924bd

Please sign in to comment.