Skip to content

Commit

Permalink
add support for empty geometry
Browse files Browse the repository at this point in the history
  • Loading branch information
mscno committed Feb 29, 2024
1 parent 8eae8e7 commit 1622394
Show file tree
Hide file tree
Showing 31 changed files with 2,140 additions and 287 deletions.
1 change: 1 addition & 0 deletions cmd/feature.geojson
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"id":"1000001","type":"Feature","geometry":{"type":"MultiPolygon","coordinates":[[[[-83.537385,33.9659119],[-83.5084519,33.931233],[-83.4155119,33.918541],[-83.275933,33.847977],[-83.306619,33.811444],[-83.28034,33.7617739],[-83.29145,33.7343149],[-83.406189,33.698307],[-83.479523,33.802265],[-83.505928,33.81776],[-83.533165,33.820923],[-83.647031,33.9061979],[-83.537385,33.9659119]]],[[[-83.537385,33.9659119],[-83.5084519,33.931233],[-83.4155119,33.918541],[-83.275933,33.847977],[-83.306619,33.811444],[-83.28034,33.7617739],[-83.29145,33.7343149],[-83.406189,33.698307],[-83.479523,33.802265],[-83.505928,33.81776],[-83.533165,33.820923],[-83.647031,33.9061979],[-83.537385,33.9659119]]],[[[-83.537385,33.9659119],[-83.5084519,33.931233],[-83.4155119,33.918541],[-83.275933,33.847977],[-83.306619,33.811444],[-83.28034,33.7617739],[-83.29145,33.7343149],[-83.406189,33.698307],[-83.479523,33.802265],[-83.505928,33.81776],[-83.533165,33.820923],[-83.647031,33.9061979],[-83.537385,33.9659119]]]]},"properties":{"AREA":"13219","COLORKEY":"#03E174","area":"13219","index":1109}}
1 change: 1 addition & 0 deletions cmd/feature.multipolygon.geobuf.base64.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
0a04415245410a08434f4c4f524b45590a04617265610a05696e646578100218072ab9020afb010806120703010c010c010c1aed01b3acd69c06dea5f6c302e6a823c9aa2af0b9718fbf0f9ab1aa01cf9156d7ba25a3cc2c8c8a20f9d03cf7c70dbbc221fb878c019dfa2bb7c259b8f37ee39d208cf512e39f219cee03c7ff8a019a8c68b3acd69c06dea5f6c302e6a823c9aa2af0b9718fbf0f9ab1aa01cf9156d7ba25a3cc2c8c8a20f9d03cf7c70dbbc221fb878c019dfa2bb7c259b8f37ee39d208cf512e39f219cee03c7ff8a019a8c68b3acd69c06dea5f6c302e6a823c9aa2af0b9718fbf0f9ab1aa01cf9156d7ba25a3cc2c8c8a20f9d03cf7c70dbbc221fb878c019dfa2bb7c259b8f37ee39d208cf512e39f219cee03c7ff8a019a8c686a070a0531333231396a090a07233033453137346a070a0531333231396a09110000000000549140720800000101020203035a0731303030303031
1 change: 1 addition & 0 deletions cmd/feature.point.geobuf.base64.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
100218032a0c0a0a08011a06b6930fb0cf1c
1 change: 1 addition & 0 deletions cmd/feature.point_with_precision.geobuf.base64.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
100218062a0f0a0d08011a09f0ddaf76aa9bccdf01
80 changes: 80 additions & 0 deletions cmd/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package main

import (
"encoding/hex"
"github.com/mscno/go-geobuf"
"github.com/paulmach/orb"
"github.com/paulmach/orb/geojson"
"google.golang.org/protobuf/proto"
"log"
"os"
)

func main() {
demoPoint()
demoPointWithPrecision()
demoMultiPolygon()

}

func demoMultiPolygon() {
f, err := os.ReadFile("cmd/feature.geojson")
if err != nil {
log.Fatal(err)
}

feature, err := geojson.UnmarshalFeature(f)
if err != nil {
log.Fatal(err)
}

data, err := geobuf.Encode(feature)
if err != nil {
log.Fatal(err)
}

payload, err := proto.Marshal(data)
if err != nil {
log.Fatal(err)
}

payloadBase64 := hex.EncodeToString(payload)

err = os.WriteFile("cmd/feature.multipolygon.geobuf.base64.txt", []byte(payloadBase64), 0644)
}

func demoPoint() {
p := orb.Point([2]float64{124.123, 234.456})
feature := geojson.NewFeature(p)
data, err := geobuf.Encode(feature)
if err != nil {
log.Fatal(err)
}

payload, err := proto.Marshal(data)
if err != nil {
log.Fatal(err)
}

payloadBase64 := hex.EncodeToString(payload)

err = os.WriteFile("cmd/feature.point.geobuf.base64.txt", []byte(payloadBase64), 0644)
}

func demoPointWithPrecision() {
p := orb.Point([2]float64{124.123, 234.456_789})
feature := geojson.NewFeature(p)
data, err := geobuf.Encode(feature, geobuf.WithPrecision(6))
if err != nil {
log.Fatal(err)
}

payload, err := proto.Marshal(data)
if err != nil {
log.Fatal(err)
}

payloadBase64 := hex.EncodeToString(payload)

err = os.WriteFile("cmd/feature.point_with_precision.geobuf.base64.txt", []byte(payloadBase64), 0644)
}
10 changes: 5 additions & 5 deletions decode.go
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
package geobuf

import (
"github.com/mscno/go-geobuf/geobufpb"
"github.com/mscno/go-geobuf/internal/decode"
"github.com/mscno/go-geobuf/proto"
"github.com/paulmach/orb/geojson"
)

func Decode(msg *proto.Data) (interface{}, error) {
func Decode(msg *geobufpb.Data) (interface{}, error) {
switch v := msg.DataType.(type) {
case *proto.Data_Geometry_:
case *geobufpb.Data_Geometry_:
geo := v.Geometry
return decode.DecodeGeometry(geo, msg.Precision, msg.Dimensions), nil
case *proto.Data_Feature_:
case *geobufpb.Data_Feature_:
return decode.DecodeFeature(msg, v.Feature, msg.Precision, msg.Dimensions), nil
case *proto.Data_FeatureCollection_:
case *geobufpb.Data_FeatureCollection_:
collection := geojson.NewFeatureCollection()
for _, feature := range v.FeatureCollection.Features {
collection.Append(decode.DecodeFeature(msg, feature, msg.Precision, msg.Dimensions))
Expand Down
87 changes: 87 additions & 0 deletions elixir/geobufpb/geobuf.pb.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
defmodule Geobuf.Data.Geometry.Type do
@moduledoc false

use Protobuf, enum: true, syntax: :proto3, protoc_gen_elixir_version: "0.12.0"

field :EMPTY, 0
field :POINT, 1
field :MULTIPOINT, 2
field :LINESTRING, 3
field :MULTILINESTRING, 4
field :POLYGON, 5
field :MULTIPOLYGON, 6
field :GEOMETRYCOLLECTION, 7
end

defmodule Geobuf.Data.Feature do
@moduledoc false

use Protobuf, syntax: :proto3, protoc_gen_elixir_version: "0.12.0"

oneof :id_type, 0

field :geometry, 1, type: Geobuf.Data.Geometry
field :id, 11, type: :string, oneof: 0
field :int_id, 12, type: :sint64, json_name: "intId", oneof: 0
field :values, 13, repeated: true, type: Geobuf.Data.Value
field :properties, 14, repeated: true, type: :uint32
field :custom_properties, 15, repeated: true, type: :uint32, json_name: "customProperties"
end

defmodule Geobuf.Data.Geometry do
@moduledoc false

use Protobuf, syntax: :proto3, protoc_gen_elixir_version: "0.12.0"

field :type, 1, type: Geobuf.Data.Geometry.Type, enum: true
field :lengths, 2, repeated: true, type: :uint32
field :coords, 3, repeated: true, type: :sint64
field :geometries, 4, repeated: true, type: Geobuf.Data.Geometry
field :values, 13, repeated: true, type: Geobuf.Data.Value
field :custom_properties, 15, repeated: true, type: :uint32, json_name: "customProperties"
end

defmodule Geobuf.Data.FeatureCollection do
@moduledoc false

use Protobuf, syntax: :proto3, protoc_gen_elixir_version: "0.12.0"

field :features, 1, repeated: true, type: Geobuf.Data.Feature
field :values, 13, repeated: true, type: Geobuf.Data.Value
field :custom_properties, 15, repeated: true, type: :uint32, json_name: "customProperties"
end

defmodule Geobuf.Data.Value do
@moduledoc false

use Protobuf, syntax: :proto3, protoc_gen_elixir_version: "0.12.0"

oneof :value_type, 0

field :string_value, 1, type: :string, json_name: "stringValue", oneof: 0
field :double_value, 2, type: :double, json_name: "doubleValue", oneof: 0
field :pos_int_value, 3, type: :uint64, json_name: "posIntValue", oneof: 0
field :neg_int_value, 4, type: :uint64, json_name: "negIntValue", oneof: 0
field :bool_value, 5, type: :bool, json_name: "boolValue", oneof: 0
field :json_value, 6, type: :bytes, json_name: "jsonValue", oneof: 0
end

defmodule Geobuf.Data do
@moduledoc false

use Protobuf, syntax: :proto3, protoc_gen_elixir_version: "0.12.0"

oneof :data_type, 0

field :keys, 1, repeated: true, type: :string
field :dimensions, 2, type: :uint32
field :precision, 3, type: :uint32

field :feature_collection, 4,
type: Geobuf.Data.FeatureCollection,
json_name: "featureCollection",
oneof: 0

field :feature, 5, type: Geobuf.Data.Feature, oneof: 0
field :geometry, 6, type: Geobuf.Data.Geometry, oneof: 0
end
22 changes: 20 additions & 2 deletions encode.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
package geobuf

import (
"errors"
geoproto "github.com/mscno/go-geobuf/geobufpb"
"github.com/mscno/go-geobuf/internal/encode"
"github.com/mscno/go-geobuf/internal/math"
geoproto "github.com/mscno/go-geobuf/proto"
"github.com/paulmach/orb/geojson"
)

var ErrUnsupportedType = errors.New("unsupported type: object is not geojson")
var ErrNilInput = errors.New("invalid input: object is nil")

type EncodingOption func(o *encode.EncodingConfig)

func WithPrecision(precision uint) EncodingOption {
Expand All @@ -28,6 +32,12 @@ func WithKeys(keys []string) EncodingOption {
}
}

func WithAllowEmptyGeometry(allow bool) EncodingOption {
return func(o *encode.EncodingConfig) {
o.AllowEmptyGeometry = allow
}
}

func Encode(obj interface{}, opts ...EncodingOption) (*geoproto.Data, error) {
cfg := &encode.EncodingConfig{
Dimension: 2,
Expand All @@ -53,6 +63,8 @@ func Encode(obj interface{}, opts ...EncodingOption) (*geoproto.Data, error) {
}

switch t := obj.(type) {
case nil:
return nil, ErrNilInput
case *geojson.FeatureCollection:
collection, err := encode.EncodeFeatureCollection(t, cfg)
if err != nil {
Expand All @@ -70,9 +82,15 @@ func Encode(obj interface{}, opts ...EncodingOption) (*geoproto.Data, error) {
Feature: feature,
}
case *geojson.Geometry:
geom, err := encode.EncodeGeometry(t.Geometry(), cfg)
if err != nil {
return nil, err
}
data.DataType = &geoproto.Data_Geometry_{
Geometry: encode.EncodeGeometry(t.Geometry(), cfg),
Geometry: geom,
}
default:
return nil, ErrUnsupportedType
}

return data, nil
Expand Down
68 changes: 68 additions & 0 deletions encode_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package geobuf

import (
"github.com/mscno/go-geobuf/internal/encode"
"github.com/paulmach/orb/geojson"
"github.com/stretchr/testify/require"
"testing"
)

func TestEncodeEmpty(t *testing.T) {
_, err := Encode(nil)
require.Error(t, err)
require.ErrorIs(t, err, ErrNilInput)
}

func TestEncodeBadType(t *testing.T) {
_, err := Encode("bad type")
require.Error(t, err)
require.ErrorIs(t, err, ErrUnsupportedType)
}

func TestEncodeEmptyFeatureCollection(t *testing.T) {
fc := geojson.NewFeatureCollection()
data, err := Encode(fc)
require.NoError(t, err)
require.NotNil(t, data)
}

func TestEncodeEmptyFeature(t *testing.T) {
fc := geojson.NewFeature(nil)
data, err := Encode(fc, WithAllowEmptyGeometry(true))
require.NoError(t, err)
require.NotNil(t, data)

fc2, err := Decode(data)
require.NoError(t, err)
require.NotNil(t, fc2)
fc3, ok := fc2.(*geojson.Feature)
require.True(t, ok)
require.NotNil(t, fc3)
require.Nil(t, fc3.Geometry)
}

var emptyGeometryFeature = `{"type":"Feature","geometry":null}`

func TestEncodeEmptyGeometryFeature(t *testing.T) {
f, err := geojson.UnmarshalFeature([]byte(emptyGeometryFeature))

data, err := Encode(f, WithAllowEmptyGeometry(false))
require.Error(t, err)
require.ErrorIs(t, err, encode.ErrEmptyGeometry)

data, err = Encode(f)
require.Error(t, err)
require.ErrorIs(t, err, encode.ErrEmptyGeometry)

data, err = Encode(f, WithAllowEmptyGeometry(true))
require.NoError(t, err)
require.NotNil(t, data)

f2, err := Decode(data)
require.NoError(t, err)
require.NotNil(t, f2)
f3, ok := f2.(*geojson.Feature)
require.True(t, ok)
require.NotNil(t, f3)
require.Nil(t, f3.Geometry)
}
Loading

0 comments on commit 1622394

Please sign in to comment.