diff --git a/Makefile b/Makefile index b5ede9eb..b5fb6bb8 100644 --- a/Makefile +++ b/Makefile @@ -4,6 +4,7 @@ LDFLAGS=-s -w cli: go build -ldflags="$(LDFLAGS)" -mod $(GOMOD) -o bin/http-server cmd/http-server/main.go go build -ldflags="$(LDFLAGS)" -mod $(GOMOD) -o bin/grpc-server cmd/grpc-server/main.go + go build -ldflags="$(LDFLAGS)" -mod $(GOMOD) -o bin/grpc-client cmd/grpc-client/main.go go build -ldflags="$(LDFLAGS)" -mod $(GOMOD) -o bin/update-hierarchies cmd/update-hierarchies/main.go go build -ldflags="$(LDFLAGS)" -mod $(GOMOD) -o bin/pip cmd/pip/main.go @@ -14,3 +15,7 @@ httpd: go run cmd/http-server/main.go \ -enable-www \ -spatial-database-uri "sqlite://?dsn=$(DSN)" + +grpcd: + go run cmd/grpc-server/main.go \ + 'sqlite://?dsn=$(DSN)' diff --git a/README.md b/README.md index 8ba3d4b6..f24ce0a8 100644 --- a/README.md +++ b/README.md @@ -26,9 +26,7 @@ Here's an example of the creating a compatible SQLite database for all the [admi ``` $> ./bin/wof-sqlite-index-features \ -index-alt-files \ - -rtree \ - -spr \ - -properties \ + -spatial-tables \ -timings \ -dsn /usr/local/ca-alt.db \ -mode repo:// \ @@ -381,36 +379,10 @@ $> go run -mod vendor cmd/query/main.go \ _Big thanks to @psanford 's [sqlitevfshttp](https://github.com/psanford/sqlite3vfshttp) package for making this possible._ -## Interfaces - -This package implements the following [go-whosonfirst-spatial](#) interfaces. - -### spatial.SpatialDatabase - -``` -import ( - "github.com/whosonfirst/go-whosonfirst-spatial/database" - _ "github.com/whosonfirst/go-whosonfirst-spatial-sqlite" -) - -db, err := database.NewSpatialDatabase(ctx, "sqlite://?dsn={DSN}") -``` - -### spatial.PropertiesReader - -``` -import ( - "github.com/whosonfirst/go-whosonfirst-spatial/properties" - _ "github.com/whosonfirst/go-whosonfirst-spatial-sqlite" -) +### http-server -pr, err := properties.NewPropertiesReader(ctx, "sqlite://?dsn={DSN}") ``` - -## server - -``` -> ./bin/server -h +> ./bin/http-server -h -authenticator-uri string A valid sfomuseum/go-http-auth URI. (default "null://") -cors-allow-credentials @@ -466,7 +438,7 @@ pr, err := properties.NewPropertiesReader(ctx, "sqlite://?dsn={DSN}") For example: ``` -$> bin/server \ +$> bin/http-server \ -enable-www \ -spatial-database-uri 'sqlite:///?dsn=modernc:///usr/local/data/sfomuseum-data-architecture.db' ``` @@ -482,7 +454,7 @@ When you visit `http://localhost:8080` in your web browser you should see someth If you don't need, or want, to expose a user-facing interface simply remove the `-enable-www` and `-nextzen-apikey` flags. For example: ``` -$> bin/server \ +$> bin/http-server \ -spatial-database-uri 'sqlite:///?dsn=modernc:///usr/local/data/sfomuseum-data-architecture.db' ``` @@ -527,7 +499,7 @@ By default, results are returned as a list of ["standard places response"](https ``` -$> bin/server \ +$> bin/http-server \ -enable-geojson \ -spatial-database-uri 'sqlite:///?dsn=modernc:///usr/local/data/sfomuseum-data-architecture.db' ``` @@ -578,10 +550,97 @@ $> curl -s -XPOST -H 'Accept: application/geo+json' 'http://localhost:8080/api/p } ``` +### grpc-server + +``` +$> ./bin/grpc-server -h + -custom-placetypes string + A JSON-encoded string containing custom placetypes defined using the syntax described in the whosonfirst/go-whosonfirst-placetypes repository. + -enable-custom-placetypes + Enable wof:placetype values that are not explicitly defined in the whosonfirst/go-whosonfirst-placetypes repository. + -host string + The host to listen for requests on (default "localhost") + -is-wof + Input data is WOF-flavoured GeoJSON. (Pass a value of '0' or 'false' if you need to index non-WOF documents. (default true) + -iterator-uri string + A valid whosonfirst/go-whosonfirst-iterate/v2 URI. Supported schemes are: directory://, featurecollection://, file://, filelist://, geojsonl://, null://, repo://. (default "repo://") + -port int + The port to listen for requests on (default 8082) + -properties-reader-uri string + A valid whosonfirst/go-reader.Reader URI. Available options are: [fs:// null:// repo:// sqlite:// stdin://]. If the value is {spatial-database-uri} then the value of the '-spatial-database-uri' implements the reader.Reader interface and will be used. + -spatial-database-uri string + A valid whosonfirst/go-whosonfirst-spatial/data.SpatialDatabase URI. options are: [rtree:// sqlite://] (default "rtree://") +``` + +For example: + +``` +$> ./bin/grpc-server -spatial-database-uri 'sqlite://?dsn=modernc:///usr/local/data/arch.db' +2024/07/19 10:52:47 Listening on localhost:8082 +``` + +And then in another terminal: + +``` +$> ./bin/grpc-client -latitude 37.621131 -longitude -122.384292 | jq '.places[]["name"]' +"San Francisco International Airport" +``` + +### grpc-client + +``` +$> ./bin/grpc-client -h + -alternate-geometry value + One or more alternate geometry labels (wof:alt_label) values to filter results by. + -cessation string + A valid EDTF date string. + -geometries string + Valid options are: all, alt, default. (default "all") + -host string + The host of the gRPC server to connect to. (default "localhost") + -inception string + A valid EDTF date string. + -is-ceased value + One or more existential flags (-1, 0, 1) to filter results by. + -is-current value + One or more existential flags (-1, 0, 1) to filter results by. + -is-deprecated value + One or more existential flags (-1, 0, 1) to filter results by. + -is-superseded value + One or more existential flags (-1, 0, 1) to filter results by. + -is-superseding value + One or more existential flags (-1, 0, 1) to filter results by. + -latitude float + A valid latitude. + -longitude float + A valid longitude. + -null + Emit results to /dev/null + -placetype value + One or more place types to filter results by. + -port int + The port of the gRPC server to connect to. (default 8082) + -property value + One or more Who's On First properties to append to each result. + -sort-uri value + Zero or more whosonfirst/go-whosonfirst-spr/sort URIs. + -stdout + Emit results to STDOUT (default true) +``` + +For example: + +``` +$> ./bin/grpc-client -latitude 37.621131 -longitude -122.384292 | jq '.places[]["name"]' +"San Francisco International Airport" +``` + ## See also -* https://www.sqlite.org/rtree.html * https://github.com/whosonfirst/go-whosonfirst-spatial +* https://github.com/whosonfirst/go-whosonfirst-spatial-www +* https://github.com/whosonfirst/go-whosonfirst-spatial-grpc * https://github.com/whosonfirst/go-whosonfirst-sqlite * https://github.com/whosonfirst/go-whosonfirst-sqlite-features +* https://github.com/whosonfirst/go-whosonfirst-sqlite-features-index * https://github.com/whosonfirst/go-reader diff --git a/cmd/grpc-client/main.go b/cmd/grpc-client/main.go new file mode 100644 index 00000000..87218073 --- /dev/null +++ b/cmd/grpc-client/main.go @@ -0,0 +1,20 @@ +package main + +import ( + "context" + "log" + + "github.com/whosonfirst/go-whosonfirst-spatial-grpc/app/client" +) + +func main() { + + ctx := context.Background() + logger := log.Default() + + err := client.Run(ctx, logger) + + if err != nil { + logger.Fatalf("Failed to run client, %v", err) + } +} diff --git a/go.mod b/go.mod index e558d87b..0d50e510 100644 --- a/go.mod +++ b/go.mod @@ -11,7 +11,7 @@ require ( github.com/whosonfirst/go-ioutil v1.0.2 github.com/whosonfirst/go-reader v1.0.2 github.com/whosonfirst/go-whosonfirst-spatial v0.10.0 - github.com/whosonfirst/go-whosonfirst-spatial-grpc v0.1.1 + github.com/whosonfirst/go-whosonfirst-spatial-grpc v0.1.2 github.com/whosonfirst/go-whosonfirst-spatial-www v0.2.0 github.com/whosonfirst/go-whosonfirst-spr/v2 v2.3.7 github.com/whosonfirst/go-whosonfirst-sqlite-features/v2 v2.0.3 diff --git a/go.sum b/go.sum index a1ef8d45..9522f0cf 100644 --- a/go.sum +++ b/go.sum @@ -341,8 +341,8 @@ github.com/whosonfirst/go-whosonfirst-sources v0.1.0 h1:JuKLa6KWke22jBfJ1pM9WQHo github.com/whosonfirst/go-whosonfirst-sources v0.1.0/go.mod h1:EUMHyGzUmqPPxlMmOp+28BFeoBdxxE0HCKRd67lkqGM= github.com/whosonfirst/go-whosonfirst-spatial v0.10.0 h1:l6OwMOy5HazP8vf0EDmgjIGXPPofIX6FHpY/JOX24NE= github.com/whosonfirst/go-whosonfirst-spatial v0.10.0/go.mod h1:ZYUHliaEMtm3R43ZBTgdRVGhnF92LbrBehUSmR2T7go= -github.com/whosonfirst/go-whosonfirst-spatial-grpc v0.1.1 h1:20MqWjgiWCHIyAhT28PEgMqoBrhwC6lZ4RPdv4DXo1w= -github.com/whosonfirst/go-whosonfirst-spatial-grpc v0.1.1/go.mod h1:giIqrHTO6grd1Geu3gBa2EY2D+GoVSZAY2l9oJYbtRk= +github.com/whosonfirst/go-whosonfirst-spatial-grpc v0.1.2 h1:0a+lES3zv/1iz0ppqMiuLIMxHW+IxO4VTwV7yIo+mw0= +github.com/whosonfirst/go-whosonfirst-spatial-grpc v0.1.2/go.mod h1:giIqrHTO6grd1Geu3gBa2EY2D+GoVSZAY2l9oJYbtRk= github.com/whosonfirst/go-whosonfirst-spatial-www v0.2.0 h1:LpjuvmBznHQnYbYquo0YPYSJy0dab2sBRP5t6I5A9HU= github.com/whosonfirst/go-whosonfirst-spatial-www v0.2.0/go.mod h1:yoPKlKjXg0fdRnMuZhcuurn2mBKnN3dMli/4w8TYGas= github.com/whosonfirst/go-whosonfirst-spr-geojson v0.0.8 h1:K0rCaKo0xvOtuS/x9r62bEfIK3OTdZqbDmgG+A3xDSs= diff --git a/vendor/github.com/whosonfirst/go-whosonfirst-spatial-grpc/app/client/client.go b/vendor/github.com/whosonfirst/go-whosonfirst-spatial-grpc/app/client/client.go new file mode 100644 index 00000000..64e0d460 --- /dev/null +++ b/vendor/github.com/whosonfirst/go-whosonfirst-spatial-grpc/app/client/client.go @@ -0,0 +1,106 @@ +package client + +import ( + "context" + "encoding/json" + "flag" + "fmt" + + "github.com/whosonfirst/go-whosonfirst-spatial-grpc/request" + "github.com/whosonfirst/go-whosonfirst-spatial-grpc/spatial" + "github.com/whosonfirst/go-whosonfirst-spatial/pip" + "google.golang.org/grpc" + "io" + "log" + "os" +) + +func Run(ctx context.Context, logger *log.Logger) error { + + fs, err := DefaultFlagSet() + + if err != nil { + return fmt.Errorf("Failed to create default flagset, %w", err) + } + + return RunWithFlagSet(ctx, fs, logger) +} + +func RunWithFlagSet(ctx context.Context, fs *flag.FlagSet, logger *log.Logger) error { + + opts, err := RunOptionsFromFlagSet(ctx, fs) + + if err != nil { + return fmt.Errorf("Failed to derive options from flagset, %w", err) + } + + return RunWithOptions(ctx, opts, logger) +} + +func RunWithOptions(ctx context.Context, opts *RunOptions, logger *log.Logger) error { + + pip_req := &pip.PointInPolygonRequest{ + Latitude: opts.Latitude, + Longitude: opts.Longitude, + Placetypes: opts.Placetypes, + Geometries: opts.Geometries, + AlternateGeometries: opts.AlternateGeometries, + IsCurrent: opts.IsCurrent, + IsCeased: opts.IsCeased, + IsDeprecated: opts.IsDeprecated, + IsSuperseded: opts.IsSuperseded, + IsSuperseding: opts.IsSuperseding, + InceptionDate: opts.InceptionDate, + CessationDate: opts.CessationDate, + Properties: opts.Properties, + Sort: opts.Sort, + } + + spatial_req, err := request.NewPointInPolygonRequest(pip_req) + + if err != nil { + return fmt.Errorf("Failed to create spatial PIP request, %w", err) + } + + var grpc_opts []grpc.DialOption + grpc_opts = append(grpc_opts, grpc.WithInsecure()) + + addr := fmt.Sprintf("%s:%d", opts.Host, opts.Port) + + conn, err := grpc.Dial(addr, grpc_opts...) + + if err != nil { + return fmt.Errorf("Failed to dial '%s', %v", addr, err) + } + + defer conn.Close() + + client := spatial.NewSpatialClient(conn) + + stream, err := client.PointInPolygon(ctx, spatial_req) + + if err != nil { + return fmt.Errorf("Failed to perform point in polygon operation, %w", err) + } + + writers := make([]io.Writer, 0) + + if opts.Stdout { + writers = append(writers, os.Stdout) + } + + if opts.Null { + writers = append(writers, io.Discard) + } + + wr := io.MultiWriter(writers...) + + enc := json.NewEncoder(wr) + err = enc.Encode(stream) + + if err != nil { + return fmt.Errorf("Failed to encode stream, %v", err) + } + + return nil +} diff --git a/vendor/github.com/whosonfirst/go-whosonfirst-spatial-grpc/app/client/flags.go b/vendor/github.com/whosonfirst/go-whosonfirst-spatial-grpc/app/client/flags.go new file mode 100644 index 00000000..e6b2bdff --- /dev/null +++ b/vendor/github.com/whosonfirst/go-whosonfirst-spatial-grpc/app/client/flags.go @@ -0,0 +1,69 @@ +package client + +import ( + "flag" + + "github.com/sfomuseum/go-flags/flagset" + "github.com/sfomuseum/go-flags/multi" +) + +var host string +var port int + +var latitude float64 +var longitude float64 +var geometries string + +var inception string +var cessation string + +var props multi.MultiString +var placetypes multi.MultiString +var alt_geoms multi.MultiString + +var is_current multi.MultiInt64 +var is_ceased multi.MultiInt64 +var is_deprecated multi.MultiInt64 +var is_superseded multi.MultiInt64 +var is_superseding multi.MultiInt64 + +var sort_uris multi.MultiString + +var stdout bool +var null bool + +func DefaultFlagSet() (*flag.FlagSet, error) { + + fs := flagset.NewFlagSet("client") + + fs.StringVar(&host, "host", "localhost", "The host of the gRPC server to connect to.") + fs.IntVar(&port, "port", 8082, "The port of the gRPC server to connect to.") + + fs.BoolVar(&stdout, "stdout", true, "Emit results to STDOUT") + fs.BoolVar(&null, "null", false, "Emit results to /dev/null") + + // query flags + + fs.Float64Var(&latitude, "latitude", 0.0, "A valid latitude.") + fs.Float64Var(&longitude, "longitude", 0.0, "A valid longitude.") + + fs.StringVar(&geometries, "geometries", "all", "Valid options are: all, alt, default.") + + fs.StringVar(&inception, "inception", "", "A valid EDTF date string.") + fs.StringVar(&cessation, "cessation", "", "A valid EDTF date string.") + + fs.Var(&props, "property", "One or more Who's On First properties to append to each result.") + fs.Var(&placetypes, "placetype", "One or more place types to filter results by.") + + fs.Var(&alt_geoms, "alternate-geometry", "One or more alternate geometry labels (wof:alt_label) values to filter results by.") + + fs.Var(&is_current, "is-current", "One or more existential flags (-1, 0, 1) to filter results by.") + fs.Var(&is_ceased, "is-ceased", "One or more existential flags (-1, 0, 1) to filter results by.") + fs.Var(&is_deprecated, "is-deprecated", "One or more existential flags (-1, 0, 1) to filter results by.") + fs.Var(&is_superseded, "is-superseded", "One or more existential flags (-1, 0, 1) to filter results by.") + fs.Var(&is_superseding, "is-superseding", "One or more existential flags (-1, 0, 1) to filter results by.") + + fs.Var(&sort_uris, "sort-uri", "Zero or more whosonfirst/go-whosonfirst-spr/sort URIs.") + + return fs, nil +} diff --git a/vendor/github.com/whosonfirst/go-whosonfirst-spatial-grpc/app/client/options.go b/vendor/github.com/whosonfirst/go-whosonfirst-spatial-grpc/app/client/options.go new file mode 100644 index 00000000..e25ba006 --- /dev/null +++ b/vendor/github.com/whosonfirst/go-whosonfirst-spatial-grpc/app/client/options.go @@ -0,0 +1,63 @@ +package client + +import ( + "context" + "flag" + + "github.com/sfomuseum/go-flags/flagset" +) + +type RunOptions struct { + Host string `json:"host"` + Port int `json:"port"` + Stdout bool `json:"stdout"` + Null bool `json:"null"` + Latitude float64 `json:"latitude"` + Longitude float64 `json:"longitude"` + Placetypes []string `json:"placetypes,omitempty"` + Geometries string `json:"geometries,omitempty"` + AlternateGeometries []string `json:"alternate_geometries,omitempty"` + IsCurrent []int64 `json:"is_current,omitempty"` + IsCeased []int64 `json:"is_ceased,omitempty"` + IsDeprecated []int64 `json:"is_deprecated,omitempty"` + IsSuperseded []int64 `json:"is_superseded,omitempty"` + IsSuperseding []int64 `json:"is_superseding,omitempty"` + InceptionDate string `json:"inception_date,omitempty"` + CessationDate string `json:"cessation_date,omitempty"` + Properties []string `json:"properties,omitempty"` + Sort []string `json:"sort,omitempty"` +} + +func RunOptionsFromFlagSet(ctx context.Context, fs *flag.FlagSet) (*RunOptions, error) { + + flagset.Parse(fs) + + err := flagset.SetFlagsFromEnvVars(fs, "WHOSONFIRST") + + if err != nil { + return nil, err + } + + opts := &RunOptions{ + Host: host, + Port: port, + Stdout: stdout, + Null: null, + Latitude: latitude, + Longitude: longitude, + Placetypes: placetypes, + Geometries: geometries, + AlternateGeometries: alt_geoms, + IsCurrent: is_current, + IsCeased: is_ceased, + IsDeprecated: is_deprecated, + IsSuperseded: is_superseded, + IsSuperseding: is_superseding, + InceptionDate: inception, + CessationDate: cessation, + Properties: props, + Sort: sort_uris, + } + + return opts, nil +} diff --git a/vendor/github.com/whosonfirst/go-whosonfirst-spatial-grpc/app/server/flags.go b/vendor/github.com/whosonfirst/go-whosonfirst-spatial-grpc/app/server/flags.go index 7cd7b753..f753c2df 100644 --- a/vendor/github.com/whosonfirst/go-whosonfirst-spatial-grpc/app/server/flags.go +++ b/vendor/github.com/whosonfirst/go-whosonfirst-spatial-grpc/app/server/flags.go @@ -29,8 +29,8 @@ func DefaultFlagSet() (*flag.FlagSet, error) { fs := flagset.NewFlagSet("server") - fs.StringVar(&host, "host", "localhost", "...") - fs.IntVar(&port, "port", 8082, "...") + fs.StringVar(&host, "host", "localhost", "The host to listen for requests on") + fs.IntVar(&port, "port", 8082, "The port to listen for requests on") available_databases := database.Schemes() desc_databases := fmt.Sprintf("A valid whosonfirst/go-whosonfirst-spatial/data.SpatialDatabase URI. options are: %s", available_databases) diff --git a/vendor/modules.txt b/vendor/modules.txt index 83b2497f..4c292e6e 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -503,8 +503,9 @@ github.com/whosonfirst/go-whosonfirst-spatial/geo github.com/whosonfirst/go-whosonfirst-spatial/hierarchy github.com/whosonfirst/go-whosonfirst-spatial/hierarchy/filter github.com/whosonfirst/go-whosonfirst-spatial/pip -# github.com/whosonfirst/go-whosonfirst-spatial-grpc v0.1.1 +# github.com/whosonfirst/go-whosonfirst-spatial-grpc v0.1.2 ## explicit; go 1.22.2 +github.com/whosonfirst/go-whosonfirst-spatial-grpc/app/client github.com/whosonfirst/go-whosonfirst-spatial-grpc/app/server github.com/whosonfirst/go-whosonfirst-spatial-grpc/request github.com/whosonfirst/go-whosonfirst-spatial-grpc/server