diff --git a/src/go/flatgeobuf_test.go b/src/go/flatgeobuf_test.go index 2522e597..6ca30ba8 100644 --- a/src/go/flatgeobuf_test.go +++ b/src/go/flatgeobuf_test.go @@ -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 @@ -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, @@ -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 { @@ -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 { @@ -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 diff --git a/src/go/writer/geometry.go b/src/go/writer/geometry.go index 21a8f071..4cc7ea70 100644 --- a/src/go/writer/geometry.go +++ b/src/go/writer/geometry.go @@ -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) diff --git a/src/go/writer/geometry_extended.go b/src/go/writer/geometry_extended.go index 02c1a3b4..85c846d6 100644 --- a/src/go/writer/geometry_extended.go +++ b/src/go/writer/geometry_extended.go @@ -13,6 +13,32 @@ 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. @@ -20,7 +46,9 @@ func (g *GeometryExtended) BoundingBox() (minX, minY, maxX, maxY float64) { } 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]