diff --git a/datastore/README.md b/datastore/README.md index bca38f0..6bd1789 100644 --- a/datastore/README.md +++ b/datastore/README.md @@ -240,6 +240,24 @@ $ grpcurl -d '{"standard_names": ["wind_speed", "air_temperature"], "interval": ... ``` +### Get the temporal- and spatial extent of all observations currently in the storage + +```text +$ grpcurl -plaintext -proto protobuf/datastore.proto 127.0.0.1:50050 datastore.Datastore.GetExtents +{ + "timeExtent": { + "start": "2022-12-31T00:00:00Z", + "end": "2022-12-31T23:50:00Z" + }, + "geoExtent": { + "left": -68.2758333, + "bottom": 12.13, + "right": 7.1493220605216, + "top": 55.399166666667 + } +} +``` + ## Testing the datastore service with a Python client ### Compiling the protobuf file diff --git a/datastore/dsimpl/getextents.go b/datastore/dsimpl/getextents.go new file mode 100644 index 0000000..530d27c --- /dev/null +++ b/datastore/dsimpl/getextents.go @@ -0,0 +1,20 @@ +package dsimpl + +import ( + "context" + "fmt" + + "datastore/datastore" +) + +func (svcInfo *ServiceInfo) GetExtents( + ctx context.Context, request *datastore.GetExtentsRequest) ( + *datastore.GetExtentsResponse, error) { + + response, err := svcInfo.Sbe.GetExtents(request) + if err != nil { + return nil, fmt.Errorf("svcInfo.Sbe.GetExtents() failed: %v", err) + } + + return response, nil +} diff --git a/datastore/protobuf/datastore.proto b/datastore/protobuf/datastore.proto index be7bd0b..19f5ceb 100644 --- a/datastore/protobuf/datastore.proto +++ b/datastore/protobuf/datastore.proto @@ -22,6 +22,7 @@ option go_package = "./datastore"; service Datastore { rpc PutObservations(PutObsRequest) returns (PutObsResponse); rpc GetObservations(GetObsRequest) returns (GetObsResponse); + rpc GetExtents(GetExtentsRequest) returns (GetExtentsResponse); } //--------------------------------------------------------------------------- @@ -35,6 +36,13 @@ message Polygon { // horizontal area; three or more points repeated Point points = 1; } +message BoundingBox { + double left = 1; + double bottom = 2; + double right = 3; + double top = 4; +} + message TimeInterval { google.protobuf.Timestamp start = 1; google.protobuf.Timestamp end = 2; @@ -127,9 +135,21 @@ message GetObsRequest { // TODO: add search filters for other metadata } - message GetObsResponse { int32 status = 1; string error = 2; // any error description (empty on success) repeated Metadata2 observations = 3; } + +//--------------------------------------------------------------------------- + +message GetExtentsRequest { + // currently no args +} + +message GetExtentsResponse { + int32 status = 1; + string error = 2; // any error description (empty on success) + TimeInterval temporal_extent = 3; + BoundingBox spatial_extent = 4; +} diff --git a/datastore/storagebackend/postgresql/getextents.go b/datastore/storagebackend/postgresql/getextents.go new file mode 100644 index 0000000..8f4ba3d --- /dev/null +++ b/datastore/storagebackend/postgresql/getextents.go @@ -0,0 +1,74 @@ +package postgresql + +import ( + "database/sql" + "datastore/datastore" + "fmt" + "time" + + _ "github.com/lib/pq" + "google.golang.org/protobuf/types/known/timestamppb" +) + +// getTemporalExtent gets the current temporal extent of all observations in the storage. +func getTemporalExtent(db *sql.DB) (*datastore.TimeInterval, error) { + query := "SELECT min(obstime_instant), max(obstime_instant) FROM observation" + row := db.QueryRow(query) + + var start, end time.Time + + err := row.Scan(&start, &end) + if err != nil { + return nil, fmt.Errorf("row.Scan() failed: %v", err) + } + + return &datastore.TimeInterval{ + Start: timestamppb.New(start), + End: timestamppb.New(end), + }, nil +} + +// getSpatialExtent gets the current horizontally spatial extent of all observations in the storage. +func getSpatialExtent(db *sql.DB) (*datastore.BoundingBox, error) { + query := ` + SELECT ST_XMin(ext), ST_YMin(ext), ST_XMax(ext), ST_YMax(ext) + FROM (SELECT ST_Extent(point::geometry) AS ext FROM geo_point) t + ` + row := db.QueryRow(query) + + var xmin, ymin, xmax, ymax float64 + + err := row.Scan(&xmin, &ymin, &xmax, &ymax) + if err != nil { + return nil, fmt.Errorf("row.Scan() failed: %v", err) + } + + return &datastore.BoundingBox{ + Left: xmin, + Bottom: ymin, + Right: xmax, + Top: ymax, + }, nil +} + +// GetExtents ... (see documentation in StorageBackend interface) +func (sbe *PostgreSQL) GetExtents(request *datastore.GetExtentsRequest) ( + *datastore.GetExtentsResponse, error) { + + var err error + + temporalExtent, err := getTemporalExtent(sbe.Db) + if err != nil { + return nil, fmt.Errorf("getTemporalExtent() failed: %v", err) + } + + spatialExtent, err := getSpatialExtent(sbe.Db) + if err != nil { + return nil, fmt.Errorf("getSpatialExtent() failed: %v", err) + } + + return &datastore.GetExtentsResponse{ + TemporalExtent: temporalExtent, + SpatialExtent: spatialExtent, + }, nil +} diff --git a/datastore/storagebackend/storagebackend.go b/datastore/storagebackend/storagebackend.go index ac66364..da0115c 100644 --- a/datastore/storagebackend/storagebackend.go +++ b/datastore/storagebackend/storagebackend.go @@ -17,4 +17,8 @@ type StorageBackend interface { // GetObservations retrieves observations from the storage. // Returns nil upon success, otherwise error. GetObservations(*datastore.GetObsRequest) (*datastore.GetObsResponse, error) + + // GetExtents gets the time- and geo extents of all currently stored observations. + // Returns nil upon success, otherwise error. + GetExtents(*datastore.GetExtentsRequest) (*datastore.GetExtentsResponse, error) }