From 93f81edb4e1846566e6c25088a45830b3ac16317 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Esteban=20Zim=C3=A1nyi?= Date: Sat, 29 Aug 2026 19:06:51 +0200 Subject: [PATCH 1/2] Name a temporal property's type as the standard defines it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A TemporalProperty document names its type with one of the five tokens the standard declares — `components/schemas/temporalProperty/properties/type` admits TBoolean, TText, TInteger, TReal and TImage — so an integer property is written TInteger and a boolean one TBoolean, where they read TInt and TBool. The token is the tier's internal name for the type as well, so the aggregation table and the window dispatch carry the same spelling and a stream query over either property resolves as before; the spellings a caller may write when storing a property are unchanged, `tint` and `tbool` among them. A test reads the admitted set out of the vendored schema and checks the token of every spelling the tier accepts, which is what the suite lacked: the plural TemporalProperties document was validated against its schema and the singular one was not. --- ats_schema_test.go | 69 +++++++++++++++++++++++++++++++++++++++++++ main.go | 6 ++-- stream.go | 12 ++++---- stream_engine_meos.go | 2 +- stream_meos_test.go | 2 +- tproperties_test.go | 8 ++--- 6 files changed, 84 insertions(+), 15 deletions(-) diff --git a/ats_schema_test.go b/ats_schema_test.go index d94c721..cdce40c 100644 --- a/ats_schema_test.go +++ b/ats_schema_test.go @@ -396,3 +396,72 @@ func TestATSSchemaBundleMatchesOGC(t *testing.T) { ogcBundlePath, ogcBundleURL, len(vendored), len(published)) } } + +// /conf/movingfeatures/tproperty-get-success — the `type` a TemporalProperty +// document names is one the standard defines. +// +// ⛔ THE ADMITTED SET IS READ OUT OF THE VENDORED DOCUMENT, never written here: a +// constant chosen in this file would agree with whatever the tier does and assert +// nothing. The schema states the enum once, at +// components/schemas/temporalProperty/properties/type. +// +// ⛔ THE WHOLE DOCUMENT CANNOT BE VALIDATED THE WAY ITS SIBLINGS ARE, and the +// reason is the standard's, not the tier's: `temporalPrimitiveValue` declares +// `datetimes` an array of `minItems: 2` beside `values` as a single scalar +// (`oneOf` number/string/boolean), so no document carrying a value per instant +// satisfies it; and its `interpolation` enum reads Discrete/Step/Linear/Regression +// where the temporal-geometry half of the same document names Stepwise. This +// asserts the cell that has one authority, which is the type token. +func TestATSSchemaTemporalPropertyType(t *testing.T) { + f, err := os.Open(ogcBundlePath) + if err != nil { + t.Fatal(err) + } + defer f.Close() + var doc struct { + Components struct { + Schemas struct { + TemporalProperty struct { + Properties struct { + Type struct { + Enum []string `json:"enum"` + } `json:"type"` + } `json:"properties"` + } `json:"temporalProperty"` + } `json:"schemas"` + } `json:"components"` + } + if err := json.NewDecoder(f).Decode(&doc); err != nil { + t.Fatalf("reading %s: %v", ogcBundlePath, err) + } + admitted := doc.Components.Schemas.TemporalProperty.Properties.Type.Enum + if len(admitted) == 0 { + t.Fatal("the vendored document declares no type enum, so this test would assert nothing") + } + in := func(s string) bool { + for _, a := range admitted { + if a == s { + return true + } + } + return false + } + // Every spelling the tier accepts, so a token is checked per stored type rather + // than for the one type a single case would happen to cover. + for _, stored := range []string{ + "TReal", "tfloat", "measure", "number", + "TInteger", "tint", "integer", "int", + "TText", "tstring", "text", "string", + "TBoolean", "tbool", "boolean", "bool", + } { + tt, ok := tPropType(stored) + if !ok { + t.Errorf("the tier does not resolve the stored type %q", stored) + continue + } + if !in(tt.ogc) { + t.Errorf("a %q property is written as type %q, which the standard does not define; it admits %v", + stored, tt.ogc, admitted) + } + } +} diff --git a/main.go b/main.go index e3d9aa6..cf286cf 100644 --- a/main.go +++ b/main.go @@ -704,11 +704,11 @@ func tPropType(t string) (tType, bool) { case "", "treal", "tfloat", "measure", "real", "float", "double", "number": return tType{"MovingFloat", "vfloat", "tfloat", "TReal", "Linear"}, true case "tint", "tinteger", "integer", "int": - return tType{"MovingInteger", "vint", "tint", "TInt", "Step"}, true + return tType{"MovingInteger", "vint", "tint", "TInteger", "Step"}, true case "ttext", "tstring", "text", "string": return tType{"MovingText", "vtext", "ttext", "TText", "Discrete"}, true case "tbool", "tboolean", "boolean", "bool": - return tType{"MovingBoolean", "vbool", "tbool", "TBool", "Step"}, true + return tType{"MovingBoolean", "vbool", "tbool", "TBoolean", "Step"}, true } return tType{}, false } @@ -1138,7 +1138,7 @@ func apiDoc(w http.ResponseWriter, r *http.Request) { "/collections/{cid}/items/{fid}/tproperties": map[string]any{ "get": withParams(op("Stored temporal properties of a feature"), limitParam, datetimeParam, subTemporalValueParam), - "post": op("Add one or more temporal properties (TReal | TInt | TText | TBool) to a feature"), + "post": op("Add one or more temporal properties (TReal | TInteger | TText | TBoolean) to a feature"), }, "/collections/{cid}/items/{fid}/tproperties/{pname}": map[string]any{ "get": withParams(op("A stored temporal property as an OGC temporalProperty"), diff --git a/stream.go b/stream.go index e8a5d3f..cf329b2 100644 --- a/stream.go +++ b/stream.go @@ -32,7 +32,7 @@ import ( // Instant is one stream record: a temporal-property value at a timestamp. The // timestamp is carried verbatim as its ISO-8601 string so it round-trips // through MEOS without a parse/format step. V holds a numeric value (TReal / -// TInt); S holds a text value (TText) or "t"/"f" (TBool). +// TInteger); S holds a text value (TText) or "t"/"f" (TBoolean). type Instant struct { T string `json:"datetime"` V float64 `json:"value"` @@ -51,7 +51,7 @@ type QuerySpec struct { Arg float64 // operand for scalar ops (add/sub/mul/div); ignored otherwise Agg string // a window aggregation, e.g. "AVG" or "MAX" Window Window // the window over which Agg is computed - Ptype string // the property's OGC type (TReal | TInt | TText | TBool) + Ptype string // the property's OGC type (TReal | TInteger | TText | TBoolean) Interval time.Duration // pacing between emitted records Engine string // per-query engine override ("flink"|"kafka"|"meos-local"); empty = the process default } @@ -72,10 +72,10 @@ type Window struct { // the window's values; text and boolean aggregations reduce the MEOS value // arrays. var aggByType = map[string]map[string]bool{ - "TReal": {"COUNT": true, "SUM": true, "AVG": true, "MIN": true, "MAX": true}, - "TInt": {"COUNT": true, "SUM": true, "AVG": true, "MIN": true, "MAX": true}, - "TText": {"COUNT": true, "COUNT_DISTINCT": true}, - "TBool": {"COUNT": true, "ANY": true, "ALL": true, "COUNT_TRUE": true, "COUNT_FALSE": true}, + "TReal": {"COUNT": true, "SUM": true, "AVG": true, "MIN": true, "MAX": true}, + "TInteger": {"COUNT": true, "SUM": true, "AVG": true, "MIN": true, "MAX": true}, + "TText": {"COUNT": true, "COUNT_DISTINCT": true}, + "TBoolean": {"COUNT": true, "ANY": true, "ALL": true, "COUNT_TRUE": true, "COUNT_FALSE": true}, } // aggregations is the union of all valid aggregation names (the engine validates diff --git a/stream_engine_meos.go b/stream_engine_meos.go index aae9dd1..05248c6 100644 --- a/stream_engine_meos.go +++ b/stream_engine_meos.go @@ -361,7 +361,7 @@ func windowAggregate(ptype, agg string, win []Instant) (any, int, error) { switch ptype { case "TText": return textAggregate(agg, win) - case "TBool": + case "TBoolean": return boolAggregate(agg, win) default: return numAggregate(agg, win) diff --git a/stream_meos_test.go b/stream_meos_test.go index 786ba58..13d30dd 100644 --- a/stream_meos_test.go +++ b/stream_meos_test.go @@ -127,7 +127,7 @@ func TestMeosTextBoolAggregate(t *testing.T) { }{{"ANY", true}, {"ALL", false}, {"COUNT_TRUE", 1.0}} { ctx, cancel := context.WithCancel(context.Background()) src := make(chan Instant, 3) - h, err := e.Submit(ctx, QuerySpec{Ptype: "TBool", Agg: c.agg, Window: Window{Type: "COUNT", Size: 3}}, src) + h, err := e.Submit(ctx, QuerySpec{Ptype: "TBoolean", Agg: c.agg, Window: Window{Type: "COUNT", Size: 3}}, src) if err != nil { t.Fatal(err) } diff --git a/tproperties_test.go b/tproperties_test.go index 3832f9d..7e97fe5 100644 --- a/tproperties_test.go +++ b/tproperties_test.go @@ -12,12 +12,12 @@ func TestTPropType(t *testing.T) { "": {"MovingFloat", "vfloat", "tfloat", "TReal", "Linear"}, "TReal": {"MovingFloat", "vfloat", "tfloat", "TReal", "Linear"}, "measure": {"MovingFloat", "vfloat", "tfloat", "TReal", "Linear"}, - "TInt": {"MovingInteger", "vint", "tint", "TInt", "Step"}, - "integer": {"MovingInteger", "vint", "tint", "TInt", "Step"}, + "TInt": {"MovingInteger", "vint", "tint", "TInteger", "Step"}, + "integer": {"MovingInteger", "vint", "tint", "TInteger", "Step"}, "TText": {"MovingText", "vtext", "ttext", "TText", "Discrete"}, "string": {"MovingText", "vtext", "ttext", "TText", "Discrete"}, - "TBool": {"MovingBoolean", "vbool", "tbool", "TBool", "Step"}, - "BOOLEAN ": {"MovingBoolean", "vbool", "tbool", "TBool", "Step"}, + "TBool": {"MovingBoolean", "vbool", "tbool", "TBoolean", "Step"}, + "BOOLEAN ": {"MovingBoolean", "vbool", "tbool", "TBoolean", "Step"}, } for in, want := range cases { got, ok := tPropType(in) From 32427cb1f006b009b4ca32caad89eb7da8a4b874 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Esteban=20Zim=C3=A1nyi?= Date: Sat, 29 Aug 2026 19:02:13 +0200 Subject: [PATCH 2/2] Write one sample document per resource from the service itself MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The service writes its own sample documents: `mfapi -emit ` reads every resource it serves through the routing table it serves them with, and writes each answer, so a sample states what the code answers rather than what its author believed it answers. The emitter names no collection, feature or property of its own — it reads the collections the service lists and takes the first, then that collection's features, then that feature's temporal properties — so it answers for whatever database it is pointed at. `samples/` holds the thirteen documents the conformance fixture produces, with an index naming each one, the request that produced it and the status the tier answered with; a status other than 200 is a sample too, since the tier answers 501 for an acceleration under linear interpolation. The conformance job re-emits the set and fails when a committed document is not what the service writes, normalising only the TemporalProperties `timeStamp`, which the standard defines as the moment the document was written. --- .github/workflows/go.yml | 35 ++ .gitignore | 3 + main.go | 15 + samples.go | 205 +++++++ samples/README.md | 41 ++ samples/api-definition.json | 563 ++++++++++++++++++++ samples/collection.json | 47 ++ samples/collections.json | 59 ++ samples/conformance-declaration.json | 11 + samples/landing-page.json | 27 + samples/moving-feature-collection.json | 147 +++++ samples/moving-feature.json | 100 ++++ samples/temporal-geometry-acceleration.json | 5 + samples/temporal-geometry-distance.json | 31 ++ samples/temporal-geometry-sequence.json | 48 ++ samples/temporal-geometry-velocity.json | 31 ++ samples/temporal-properties.json | 60 +++ samples/temporal-property.json | 26 + 18 files changed, 1454 insertions(+) create mode 100644 samples.go create mode 100644 samples/README.md create mode 100644 samples/api-definition.json create mode 100644 samples/collection.json create mode 100644 samples/collections.json create mode 100644 samples/conformance-declaration.json create mode 100644 samples/landing-page.json create mode 100644 samples/moving-feature-collection.json create mode 100644 samples/moving-feature.json create mode 100644 samples/temporal-geometry-acceleration.json create mode 100644 samples/temporal-geometry-distance.json create mode 100644 samples/temporal-geometry-sequence.json create mode 100644 samples/temporal-geometry-velocity.json create mode 100644 samples/temporal-properties.json create mode 100644 samples/temporal-property.json diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index b7bbe04..e340997 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -102,3 +102,38 @@ jobs: exit 1 fi echo "$groups live conformance test group(s) ran against MobilityDB" + + # ⛔ A SAMPLE THAT HAS DRIFTED IS WORSE THAN NO SAMPLE: it states what the + # service used to answer, and a reader has no way to tell. Re-emitting here + # and diffing against what is committed is what keeps samples/ the current + # answer rather than a snapshot somebody forgot to refresh. + # + # The TemporalProperties document carries the time it was written, which the + # standard defines it to carry, so that one line is normalised on both sides. + - name: The committed samples are what the service writes + run: | + set -o pipefail + go run . -emit samples.emitted + norm() { sed -E 's/"timeStamp": "[^"]*"/"timeStamp": ""/' "$1"; } + rc=0 + for f in samples/*; do + b=$(basename "$f") + if [ ! -f "samples.emitted/$b" ]; then + echo "::error::samples/$b is no longer a resource the service serves"; rc=1; continue + fi + if ! diff -q <(norm "$f") <(norm "samples.emitted/$b") >/dev/null; then + echo "::error::samples/$b is not what the service writes; re-run: go run . -emit samples" + diff -u <(norm "$f") <(norm "samples.emitted/$b") | head -40 + rc=1 + fi + done + for f in samples.emitted/*; do + b=$(basename "$f") + if [ ! -f "samples/$b" ]; then + echo "::error::the service serves a resource samples/ does not carry: $b"; rc=1 + fi + done + if [ "$rc" -eq 0 ]; then + echo "$(ls samples/*.json | wc -l) sample documents are what the service writes" + fi + exit $rc diff --git a/.gitignore b/.gitignore index 9b526d1..610d6b9 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,6 @@ MobilityAPI-go mfapi mfapi-meos live.log + +# the sample set the CI freshness check re-emits for comparison +samples.emitted/ diff --git a/main.go b/main.go index cf286cf..09e4509 100644 --- a/main.go +++ b/main.go @@ -17,6 +17,7 @@ import ( "context" "encoding/json" "errors" + "flag" "fmt" "log" "net/http" @@ -71,6 +72,9 @@ func envInt(k string, def int) int { } func main() { + emit := flag.String("emit", "", "write one sample document per resource into this directory, then exit") + flag.Parse() + dsn := os.Getenv("MFAPI_DSN") if dsn == "" { dsn = "postgres:///mfapi_demo?host=/tmp&port=5432&user=esteban" @@ -85,6 +89,17 @@ func main() { log.Fatal("db ping: ", err) } + // The samples are written by the service, through the routing table it + // serves, so they are what the code answers rather than an illustration of + // it. See samples.go. + if *emit != "" { + if err := emitSamples(*emit); err != nil { + log.Fatal("emitting samples: ", err) + } + log.Printf("sample documents written to %s", *emit) + return + } + if broker := os.Getenv("MFAPI_MQTT_BROKER"); broker != "" { if _, err := startMQTTIngest(broker, "mfapi-"+strconv.Itoa(os.Getpid())); err != nil { log.Printf("MQTT ingestion disabled: %v", err) diff --git a/samples.go b/samples.go new file mode 100644 index 0000000..fe24c0d --- /dev/null +++ b/samples.go @@ -0,0 +1,205 @@ +// The sample documents: one per resource the tier serves, written by the tier +// itself. +// +// A sample assembled by hand states what its author believed the service +// answers. These state what it answers, because every one is the body of a real +// request through the service's own routing table. Regenerating rewrites the +// whole set, so a sample that has drifted from the code cannot survive one. +// +// The emitter names no collection, feature or property of its own. It reads the +// collections the service lists and takes the first, then that collection's +// features and takes the first, then that feature's temporal properties — the +// walk a client makes through the documents' own contents. So it answers for +// whatever database it is pointed at, and against +// tutorial/setup/load_conformance.sql it writes the conformance sample set. +// +// MFAPI_DSN=... mfapi -emit samples +package main + +import ( + "bytes" + "encoding/json" + "fmt" + "net/http" + "net/http/httptest" + "os" + "path/filepath" + "strconv" + "strings" +) + +// sample is one resource of the tier: the request that reads it, the OGC +// resource the answer is an instance of, and what the tier answered. +type sample struct { + file string // basename the document is written under + path string // the request the tier answers + what string // the resource this document is an instance of + code int + body []byte +} + +// firstMember reads the first element of a named array out of a document and +// returns one of its fields as text. It is how the emitter finds a collection, +// a feature and a temporal property without naming any of them. +func firstMember(body []byte, array, field string) (string, error) { + var doc map[string]any + if err := json.Unmarshal(body, &doc); err != nil { + return "", fmt.Errorf("the %s document is not JSON: %w", array, err) + } + members, ok := doc[array].([]any) + if !ok || len(members) == 0 { + return "", fmt.Errorf("the document carries no %s to sample", array) + } + m, ok := members[0].(map[string]any) + if !ok { + return "", fmt.Errorf("the first member of %s is not an object", array) + } + switch v := m[field].(type) { + case string: + return v, nil + case float64: + return strconv.FormatFloat(v, 'f', -1, 64), nil + case nil: + return "", fmt.Errorf("the first member of %s carries no %s", array, field) + default: + return fmt.Sprint(v), nil + } +} + +// emitSamples writes one document per resource into dir, plus an index naming +// each one and the request that produced it. +func emitSamples(dir string) error { + mux := newMux() + read := func(path string) (int, []byte) { + rec := httptest.NewRecorder() + mux.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, path, nil)) + return rec.Code, rec.Body.Bytes() + } + // A resource the walk needs in order to reach the next one has to answer, or + // the set below it would be silently missing rather than wrong. + require := func(path string) ([]byte, error) { + code, body := read(path) + if code != http.StatusOK { + return nil, fmt.Errorf("GET %s answered %d, so the walk cannot continue: %s", + path, code, strings.TrimSpace(string(body))) + } + return body, nil + } + + cols, err := require("/collections") + if err != nil { + return err + } + cid, err := firstMember(cols, "collections", "id") + if err != nil { + return err + } + collection := "/collections/" + cid + + items, err := require(collection + "/items") + if err != nil { + return err + } + fid, err := firstMember(items, "features", "id") + if err != nil { + return err + } + feature := collection + "/items/" + fid + + props, err := require(feature + "/tproperties") + if err != nil { + return err + } + pname, err := firstMember(props, "temporalProperties", "name") + if err != nil { + return err + } + + // A feature's temporal primitive geometries are addressed by their 1-based + // position, which is what sequenceN takes, so the first one is 1. + const firstSequence = "1" + tgseq := feature + "/tgsequence/" + firstSequence + + samples := []sample{ + {file: "landing-page", path: "/", what: "the landing page"}, + {file: "api-definition", path: "/api", what: "the API definition"}, + {file: "conformance-declaration", path: "/conformance", what: "the conformance declaration"}, + {file: "collections", path: "/collections", what: "the Collections document"}, + {file: "collection", path: collection, what: "a Collection document"}, + {file: "moving-feature-collection", path: collection + "/items", what: "a MovingFeatureCollection document"}, + {file: "moving-feature", path: feature, what: "a MovingFeature document"}, + {file: "temporal-geometry-sequence", path: feature + "/tgsequence", what: "a TemporalGeometrySequence document"}, + {file: "temporal-geometry-distance", path: tgseq + "/distance", what: "the distance a temporal primitive geometry covers"}, + {file: "temporal-geometry-velocity", path: tgseq + "/velocity", what: "the velocity along a temporal primitive geometry"}, + {file: "temporal-geometry-acceleration", path: tgseq + "/acceleration", what: "the acceleration along a temporal primitive geometry"}, + {file: "temporal-properties", path: feature + "/tproperties", what: "a TemporalProperties document"}, + {file: "temporal-property", path: feature + "/tproperties/" + pname, what: "a TemporalProperty document"}, + } + + if err := os.MkdirAll(dir, 0o755); err != nil { + return err + } + for i := range samples { + s := &samples[i] + s.code, s.body = read(s.path) + name, out := s.file+".json", &bytes.Buffer{} + if json.Valid(s.body) { + if err := json.Indent(out, s.body, "", " "); err != nil { + return fmt.Errorf("indenting %s: %w", s.path, err) + } + out.WriteByte('\n') + } else { + // A response the service does not write as JSON is kept verbatim: the + // sample is the answer, not a rendering of it. + name = s.file + ".txt" + out.Write(s.body) + } + if err := os.WriteFile(filepath.Join(dir, name), out.Bytes(), 0o644); err != nil { + return err + } + } + return writeSampleIndex(dir, cid, fid, pname, samples) +} + +// writeSampleIndex writes the README naming every sample, the request that +// produced it and the status the tier answered with. +func writeSampleIndex(dir, cid, fid, pname string, samples []sample) error { + var b strings.Builder + b.WriteString(`# Sample documents + +Every file here is the body of one request through the service's own routing +table, written by the service itself: + + MFAPI_DSN=... mfapi -emit samples + +so a sample states what the code answers rather than what its author believed +it answers. Regenerating rewrites the whole set. + +These were read from the conformance fixture +(` + "`tutorial/setup/load_conformance.sql`" + `), whose collection is ` + "`" + cid + "`" + `, whose +first feature is ` + "`" + fid + "`" + ` and whose first temporal property is ` + "`" + pname + "`" + `. Pointed at +another database the emitter names that database's own resources instead: it +takes the first collection the service lists, that collection's first feature +and that feature's first temporal property. + +A status other than 200 is a sample too. The tier answers 501 for an +acceleration under linear interpolation, because a velocity that is constant on +every segment has no acceleration the standard's motion model can carry, and +saying so is the honest answer rather than a zero. + +⛔ The ` + "`timeStamp`" + ` of the TemporalProperties document is the moment the +document was written, which the standard defines it to be, so that one line +differs between two regenerations of an unchanged service. + +| file | status | request | resource | +|---|---|---|---| +`) + for _, s := range samples { + name := s.file + ".json" + if !json.Valid(s.body) { + name = s.file + ".txt" + } + fmt.Fprintf(&b, "| [`%s`](%s) | %d | `GET %s` | %s |\n", name, name, s.code, s.path, s.what) + } + return os.WriteFile(filepath.Join(dir, "README.md"), []byte(b.String()), 0o644) +} diff --git a/samples/README.md b/samples/README.md new file mode 100644 index 0000000..f7a276c --- /dev/null +++ b/samples/README.md @@ -0,0 +1,41 @@ +# Sample documents + +Every file here is the body of one request through the service's own routing +table, written by the service itself: + + MFAPI_DSN=... mfapi -emit samples + +so a sample states what the code answers rather than what its author believed +it answers. Regenerating rewrites the whole set. + +These were read from the conformance fixture +(`tutorial/setup/load_conformance.sql`), whose collection is `conformance`, whose +first feature is `1` and whose first temporal property is `anchored`. Pointed at +another database the emitter names that database's own resources instead: it +takes the first collection the service lists, that collection's first feature +and that feature's first temporal property. + +A status other than 200 is a sample too. The tier answers 501 for an +acceleration under linear interpolation, because a velocity that is constant on +every segment has no acceleration the standard's motion model can carry, and +saying so is the honest answer rather than a zero. + +⛔ The `timeStamp` of the TemporalProperties document is the moment the +document was written, which the standard defines it to be, so that one line +differs between two regenerations of an unchanged service. + +| file | status | request | resource | +|---|---|---|---| +| [`landing-page.json`](landing-page.json) | 200 | `GET /` | the landing page | +| [`api-definition.json`](api-definition.json) | 200 | `GET /api` | the API definition | +| [`conformance-declaration.json`](conformance-declaration.json) | 200 | `GET /conformance` | the conformance declaration | +| [`collections.json`](collections.json) | 200 | `GET /collections` | the Collections document | +| [`collection.json`](collection.json) | 200 | `GET /collections/conformance` | a Collection document | +| [`moving-feature-collection.json`](moving-feature-collection.json) | 200 | `GET /collections/conformance/items` | a MovingFeatureCollection document | +| [`moving-feature.json`](moving-feature.json) | 200 | `GET /collections/conformance/items/1` | a MovingFeature document | +| [`temporal-geometry-sequence.json`](temporal-geometry-sequence.json) | 200 | `GET /collections/conformance/items/1/tgsequence` | a TemporalGeometrySequence document | +| [`temporal-geometry-distance.json`](temporal-geometry-distance.json) | 200 | `GET /collections/conformance/items/1/tgsequence/1/distance` | the distance a temporal primitive geometry covers | +| [`temporal-geometry-velocity.json`](temporal-geometry-velocity.json) | 200 | `GET /collections/conformance/items/1/tgsequence/1/velocity` | the velocity along a temporal primitive geometry | +| [`temporal-geometry-acceleration.json`](temporal-geometry-acceleration.json) | 501 | `GET /collections/conformance/items/1/tgsequence/1/acceleration` | the acceleration along a temporal primitive geometry | +| [`temporal-properties.json`](temporal-properties.json) | 200 | `GET /collections/conformance/items/1/tproperties` | a TemporalProperties document | +| [`temporal-property.json`](temporal-property.json) | 200 | `GET /collections/conformance/items/1/tproperties/anchored` | a TemporalProperty document | diff --git a/samples/api-definition.json b/samples/api-definition.json new file mode 100644 index 0000000..9194e91 --- /dev/null +++ b/samples/api-definition.json @@ -0,0 +1,563 @@ +{ + "info": { + "description": "OGC API – Moving Features over MobilityDB", + "title": "MobilityAPI-go", + "version": "1.0.0" + }, + "openapi": "3.0.3", + "paths": { + "/": { + "get": { + "responses": { + "200": { + "description": "OK" + } + }, + "summary": "Landing page" + } + }, + "/api": { + "get": { + "responses": { + "200": { + "description": "OK" + } + }, + "summary": "API definition" + } + }, + "/collections": { + "get": { + "parameters": [ + { + "description": "The optional limit parameter limits the number of items presented in the response document.", + "explode": false, + "in": "query", + "name": "limit", + "required": false, + "schema": { + "default": 100, + "maximum": 10000, + "minimum": 1, + "type": "integer" + }, + "style": "form" + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "summary": "Moving feature collections" + }, + "post": { + "responses": { + "200": { + "description": "OK" + } + }, + "summary": "Register a new collection" + } + }, + "/collections/{cid}": { + "delete": { + "responses": { + "200": { + "description": "OK" + } + }, + "summary": "Delete a collection" + }, + "get": { + "responses": { + "200": { + "description": "OK" + } + }, + "summary": "Collection metadata" + }, + "put": { + "responses": { + "200": { + "description": "OK" + } + }, + "summary": "Replace collection metadata" + } + }, + "/collections/{cid}/bulk": { + "post": { + "responses": { + "200": { + "description": "OK" + } + }, + "summary": "Bulk ingest (extension): a batch of (vehicleId, position, time) observations as GeoJSON Points or GeoParquet, optionally gzip/deflate/br/zstd-compressed; each is appended as one instant" + } + }, + "/collections/{cid}/export": { + "get": { + "responses": { + "200": { + "description": "OK" + } + }, + "summary": "Bulk lakehouse export: NDJSON, or ?format=parquet (WKB + bbox/time sidecar)" + } + }, + "/collections/{cid}/items": { + "get": { + "parameters": [ + { + "description": "The optional limit parameter limits the number of items presented in the response document.", + "explode": false, + "in": "query", + "name": "limit", + "required": false, + "schema": { + "default": 100, + "maximum": 10000, + "minimum": 1, + "type": "integer" + }, + "style": "form" + }, + { + "description": "Only features whose geometry intersects the bounding box are selected.", + "explode": false, + "in": "query", + "name": "bbox", + "required": false, + "schema": { + "items": { + "type": "number" + }, + "maxItems": 6, + "minItems": 4, + "type": "array" + }, + "style": "form" + }, + { + "description": "Either a date-time or an interval. Only features that have a temporal geometry or temporal property that intersects the value are selected.", + "explode": false, + "in": "query", + "name": "datetime", + "required": false, + "schema": { + "type": "string" + }, + "style": "form" + }, + { + "description": "Only a subsequence of the temporal geometry clipped to the datetime interval is returned. The datetime parameter is then a bounded interval and the leaf parameter is not used.", + "explode": false, + "in": "query", + "name": "subTrajectory", + "required": false, + "schema": { + "default": false, + "type": "boolean" + }, + "style": "form" + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "summary": "Moving features (streamed, keyset-paged; bbox/datetime/subtrajectory filters)" + }, + "post": { + "responses": { + "200": { + "description": "OK" + } + }, + "summary": "Insert a moving feature" + } + }, + "/collections/{cid}/items/{fid}": { + "delete": { + "responses": { + "200": { + "description": "OK" + } + }, + "summary": "Delete a moving feature" + }, + "get": { + "responses": { + "200": { + "description": "OK" + } + }, + "summary": "A moving feature as a Feature" + }, + "put": { + "responses": { + "200": { + "description": "OK" + } + }, + "summary": "Replace a moving feature" + } + }, + "/collections/{cid}/items/{fid}/tgsequence": { + "get": { + "parameters": [ + { + "description": "The optional limit parameter limits the number of items presented in the response document.", + "explode": false, + "in": "query", + "name": "limit", + "required": false, + "schema": { + "default": 100, + "maximum": 10000, + "minimum": 1, + "type": "integer" + }, + "style": "form" + }, + { + "description": "Only features whose geometry intersects the bounding box are selected.", + "explode": false, + "in": "query", + "name": "bbox", + "required": false, + "schema": { + "items": { + "type": "number" + }, + "maxItems": 6, + "minItems": 4, + "type": "array" + }, + "style": "form" + }, + { + "description": "Either a date-time or an interval. Only features that have a temporal geometry or temporal property that intersects the value are selected.", + "explode": false, + "in": "query", + "name": "datetime", + "required": false, + "schema": { + "type": "string" + }, + "style": "form" + }, + { + "description": "Only features with a temporal geometry or temporal property that intersect the given date-times are selected. The date-times are given as a comma-separated list.", + "explode": false, + "in": "query", + "name": "leaf", + "required": false, + "schema": { + "items": { + "format": "date-time", + "type": "string" + }, + "type": "array" + }, + "style": "form" + }, + { + "description": "Only a subsequence of the temporal geometry clipped to the datetime interval is returned. The datetime parameter is then a bounded interval and the leaf parameter is not used.", + "explode": false, + "in": "query", + "name": "subTrajectory", + "required": false, + "schema": { + "default": false, + "type": "boolean" + }, + "style": "form" + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "summary": "Temporal geometry sequence (TemporalGeometrySequence; members addressable by their 1-based id)" + }, + "post": { + "responses": { + "200": { + "description": "OK" + } + }, + "summary": "Append a temporally-disjoint member sequence" + } + }, + "/collections/{cid}/items/{fid}/tgsequence/queries": { + "get": { + "responses": { + "200": { + "description": "OK" + } + }, + "summary": "List the geometry (position) continuous queries of a feature" + }, + "post": { + "responses": { + "200": { + "description": "OK" + } + }, + "summary": "Register a geometry continuous query streaming the moving feature's position" + } + }, + "/collections/{cid}/items/{fid}/tgsequence/queries/{qid}": { + "delete": { + "responses": { + "200": { + "description": "OK" + } + }, + "summary": "Stop a geometry continuous query" + }, + "get": { + "responses": { + "200": { + "description": "OK" + } + }, + "summary": "Geometry continuous-query status (the cquery link object)" + } + }, + "/collections/{cid}/items/{fid}/tgsequence/queries/{qid}/stream": { + "get": { + "responses": { + "200": { + "description": "OK" + } + }, + "summary": "Moving-feature positions as Server-Sent Events (see api/streaming-asyncapi.yaml)" + } + }, + "/collections/{cid}/items/{fid}/tgsequence/{tgid}": { + "delete": { + "responses": { + "200": { + "description": "OK" + } + }, + "summary": "Delete a temporal primitive geometry (member sequence) by id" + } + }, + "/collections/{cid}/items/{fid}/tgsequence/{tgid}/{qtype}": { + "get": { + "responses": { + "200": { + "description": "OK" + } + }, + "summary": "Derived query on a member geometry: distance | velocity (acceleration → 501, not derivable for this motion model)" + } + }, + "/collections/{cid}/items/{fid}/tproperties": { + "get": { + "parameters": [ + { + "description": "The optional limit parameter limits the number of items presented in the response document.", + "explode": false, + "in": "query", + "name": "limit", + "required": false, + "schema": { + "default": 100, + "maximum": 10000, + "minimum": 1, + "type": "integer" + }, + "style": "form" + }, + { + "description": "Either a date-time or an interval. Only features that have a temporal geometry or temporal property that intersects the value are selected.", + "explode": false, + "in": "query", + "name": "datetime", + "required": false, + "schema": { + "type": "string" + }, + "style": "form" + }, + { + "description": "Only a subsequence of the temporal property clipped to the datetime interval is returned. The datetime parameter is then a bounded interval and the leaf parameter is not used.", + "explode": false, + "in": "query", + "name": "subTemporalValue", + "required": false, + "schema": { + "default": false, + "type": "boolean" + }, + "style": "form" + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "summary": "Stored temporal properties of a feature" + }, + "post": { + "responses": { + "200": { + "description": "OK" + } + }, + "summary": "Add one or more temporal properties (TReal | TInteger | TText | TBoolean) to a feature" + } + }, + "/collections/{cid}/items/{fid}/tproperties/{pname}": { + "delete": { + "responses": { + "200": { + "description": "OK" + } + }, + "summary": "Delete a temporal property" + }, + "get": { + "parameters": [ + { + "description": "Either a date-time or an interval. Only features that have a temporal geometry or temporal property that intersects the value are selected.", + "explode": false, + "in": "query", + "name": "datetime", + "required": false, + "schema": { + "type": "string" + }, + "style": "form" + }, + { + "description": "Only features with a temporal geometry or temporal property that intersect the given date-times are selected. The date-times are given as a comma-separated list.", + "explode": false, + "in": "query", + "name": "leaf", + "required": false, + "schema": { + "items": { + "format": "date-time", + "type": "string" + }, + "type": "array" + }, + "style": "form" + }, + { + "description": "Only a subsequence of the temporal property clipped to the datetime interval is returned. The datetime parameter is then a bounded interval and the leaf parameter is not used.", + "explode": false, + "in": "query", + "name": "subTemporalValue", + "required": false, + "schema": { + "default": false, + "type": "boolean" + }, + "style": "form" + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "summary": "A stored temporal property as an OGC temporalProperty" + }, + "post": { + "responses": { + "200": { + "description": "OK" + } + }, + "summary": "Append values to a temporal property (temporally disjoint; overlap → 409)" + } + }, + "/collections/{cid}/items/{fid}/tproperties/{pname}/ingest": { + "post": { + "responses": { + "200": { + "description": "OK" + } + }, + "summary": "Push a live record ({datetime, value}) to the live queries on the property" + } + }, + "/collections/{cid}/items/{fid}/tproperties/{pname}/queries": { + "get": { + "responses": { + "200": { + "description": "OK" + } + }, + "summary": "List the continuous queries on a temporal property" + }, + "post": { + "responses": { + "200": { + "description": "OK" + } + }, + "summary": "Register a continuous query (MF Part 4): a lifted transform (operation), or a windowed aggregation (aggregation + window: COUNT | TUMBLING | HOPPING). Set live:true to source from pushed records" + } + }, + "/collections/{cid}/items/{fid}/tproperties/{pname}/queries/{qid}": { + "delete": { + "responses": { + "200": { + "description": "OK" + } + }, + "summary": "Stop a continuous query" + }, + "get": { + "responses": { + "200": { + "description": "OK" + } + }, + "summary": "Continuous-query status (the cquery link object)" + } + }, + "/collections/{cid}/items/{fid}/tproperties/{pname}/queries/{qid}/stream": { + "get": { + "responses": { + "200": { + "description": "OK" + } + }, + "summary": "Continuous-query results as Server-Sent Events (see api/streaming-asyncapi.yaml)" + } + }, + "/collections/{cid}/items/{fid}/tproperties/{pname}/{tvid}": { + "delete": { + "responses": { + "200": { + "description": "OK" + } + }, + "summary": "Delete a temporal primitive value (member value sequence) by its 1-based id" + } + }, + "/conformance": { + "get": { + "responses": { + "200": { + "description": "OK" + } + }, + "summary": "Conformance declaration" + } + } + } +} + diff --git a/samples/collection.json b/samples/collection.json new file mode 100644 index 0000000..e5dda3d --- /dev/null +++ b/samples/collection.json @@ -0,0 +1,47 @@ +{ + "crs": [ + "http://www.opengis.net/def/crs/EPSG/0/25832" + ], + "description": "A fixed two-feature corpus for the OGC API - Moving Features Part 1 abstract tests", + "extent": { + "spatial": { + "bbox": [ + [ + 10.189934628004437, + 56.11035654255155, + 10.271202546128217, + 56.136503619599175 + ] + ], + "crs": "http://www.opengis.net/def/crs/OGC/1.3/CRS84" + }, + "temporal": { + "interval": [ + [ + "2026-01-01T08:00:00Z", + "2026-01-01T09:30:00Z" + ] + ], + "trs": "http://www.opengis.net/def/uom/ISO-8601/0/Gregorian" + } + }, + "id": "conformance", + "itemType": "movingfeature", + "links": [ + { + "href": "/collections/conformance", + "rel": "self" + }, + { + "href": "/collections/conformance/items", + "rel": "items" + }, + { + "href": "/collections/conformance/export", + "rel": "enclosure", + "type": "application/x-ndjson" + } + ], + "title": "conformance" +} + diff --git a/samples/collections.json b/samples/collections.json new file mode 100644 index 0000000..9d8bcb3 --- /dev/null +++ b/samples/collections.json @@ -0,0 +1,59 @@ +{ + "collections": [ + { + "crs": [ + "http://www.opengis.net/def/crs/EPSG/0/25832" + ], + "description": "A fixed two-feature corpus for the OGC API - Moving Features Part 1 abstract tests", + "id": "conformance", + "itemType": "movingfeature", + "links": [ + { + "href": "/collections/conformance", + "rel": "self" + }, + { + "href": "/collections/conformance/items", + "rel": "items" + }, + { + "href": "/collections/conformance/export", + "rel": "enclosure", + "type": "application/x-ndjson" + } + ], + "title": "conformance" + }, + { + "crs": [ + "http://www.opengis.net/def/crs/EPSG/0/25832" + ], + "description": "A second collection, so a delete cannot pass by acting on the only one", + "id": "conformance_alt", + "itemType": "movingfeature", + "links": [ + { + "href": "/collections/conformance_alt", + "rel": "self" + }, + { + "href": "/collections/conformance_alt/items", + "rel": "items" + }, + { + "href": "/collections/conformance_alt/export", + "rel": "enclosure", + "type": "application/x-ndjson" + } + ], + "title": "conformance_alt" + } + ], + "links": [ + { + "href": "/collections", + "rel": "self" + } + ] +} + diff --git a/samples/conformance-declaration.json b/samples/conformance-declaration.json new file mode 100644 index 0000000..fcb0f70 --- /dev/null +++ b/samples/conformance-declaration.json @@ -0,0 +1,11 @@ +{ + "conformsTo": [ + "http://www.opengis.net/spec/ogcapi-movingfeatures-1/1.0/conf/common", + "http://www.opengis.net/spec/ogcapi-movingfeatures-1/1.0/conf/mf-collection", + "http://www.opengis.net/spec/ogcapi-movingfeatures-1/1.0/conf/movingfeatures", + "http://www.opengis.net/spec/ogcapi-movingfeatures-4/1.0/conf/cquery", + "http://www.opengis.net/spec/ogcapi-common-1/1.0/conf/core", + "http://www.opengis.net/spec/ogcapi-common-2/1.0/conf/collections" + ] +} + diff --git a/samples/landing-page.json b/samples/landing-page.json new file mode 100644 index 0000000..778e7f4 --- /dev/null +++ b/samples/landing-page.json @@ -0,0 +1,27 @@ +{ + "description": "OGC API – Moving Features over MobilityDB", + "links": [ + { + "href": "/", + "rel": "self", + "type": "application/json" + }, + { + "href": "/api", + "rel": "service-desc", + "type": "application/vnd.oai.openapi+json;version=3.0" + }, + { + "href": "/conformance", + "rel": "conformance", + "type": "application/json" + }, + { + "href": "/collections", + "rel": "data", + "type": "application/json" + } + ], + "title": "MobilityAPI-go" +} + diff --git a/samples/moving-feature-collection.json b/samples/moving-feature-collection.json new file mode 100644 index 0000000..b953432 --- /dev/null +++ b/samples/moving-feature-collection.json @@ -0,0 +1,147 @@ +{ + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "id": "1", + "properties": { + "mmsi": 219000001, + "name": "Alpha" + }, + "crs": { + "type": "Name", + "properties": { + "name": "urn:ogc:def:crs:EPSG::25832" + } + }, + "trs": { + "type": "Link", + "properties": { + "href": "http://www.opengis.net/def/uom/ISO-8601/0/Gregorian", + "type": "ogcdef" + } + }, + "bbox": [ + 575000, + 6220000, + 579000, + 6222000 + ], + "time": [ + "2026-01-01T08:00:00Z", + "2026-01-01T08:45:00Z" + ], + "temporalGeometry": { + "type": "MovingPoint", + "crs": { + "type": "Name", + "properties": { + "name": "urn:ogc:def:crs:EPSG::25832" + } + }, + "coordinates": [ + [ + 575000, + 6220000 + ], + [ + 576000, + 6220500 + ], + [ + 577500, + 6221000 + ], + [ + 579000, + 6222000 + ] + ], + "datetimes": [ + "2026-01-01T08:00:00+00:00", + "2026-01-01T08:10:00+00:00", + "2026-01-01T08:25:00+00:00", + "2026-01-01T08:45:00+00:00" + ], + "lower_inc": true, + "upper_inc": true, + "interpolation": "Linear" + }, + "links": [ + { + "rel": "self", + "href": "/collections/conformance/items/1" + } + ] + }, + { + "type": "Feature", + "id": "2", + "properties": { + "mmsi": 219000002, + "name": "Bravo" + }, + "crs": { + "type": "Name", + "properties": { + "name": "urn:ogc:def:crs:EPSG::25832" + } + }, + "trs": { + "type": "Link", + "properties": { + "href": "http://www.opengis.net/def/uom/ISO-8601/0/Gregorian", + "type": "ogcdef" + } + }, + "bbox": [ + 574000, + 6219000, + 575600, + 6220100 + ], + "time": [ + "2026-01-01T09:00:00Z", + "2026-01-01T09:30:00Z" + ], + "temporalGeometry": { + "type": "MovingPoint", + "crs": { + "type": "Name", + "properties": { + "name": "urn:ogc:def:crs:EPSG::25832" + } + }, + "coordinates": [ + [ + 574000, + 6219000 + ], + [ + 574800, + 6219400 + ], + [ + 575600, + 6220100 + ] + ], + "datetimes": [ + "2026-01-01T09:00:00+00:00", + "2026-01-01T09:12:00+00:00", + "2026-01-01T09:30:00+00:00" + ], + "lower_inc": true, + "upper_inc": true, + "interpolation": "Linear" + }, + "links": [ + { + "rel": "self", + "href": "/collections/conformance/items/2" + } + ] + } + ], + "numberReturned": 2 +} diff --git a/samples/moving-feature.json b/samples/moving-feature.json new file mode 100644 index 0000000..95cc71d --- /dev/null +++ b/samples/moving-feature.json @@ -0,0 +1,100 @@ +{ + "type": "Feature", + "id": "1", + "properties": { + "mmsi": 219000001, + "name": "Alpha" + }, + "crs": { + "type": "Name", + "properties": { + "name": "urn:ogc:def:crs:EPSG::25832" + } + }, + "trs": { + "type": "Link", + "properties": { + "href": "http://www.opengis.net/def/uom/ISO-8601/0/Gregorian", + "type": "ogcdef" + } + }, + "bbox": [ + 575000, + 6220000, + 579000, + 6222000 + ], + "time": [ + "2026-01-01T08:00:00Z", + "2026-01-01T08:45:00Z" + ], + "temporalGeometry": { + "type": "MovingPoint", + "crs": { + "type": "Name", + "properties": { + "name": "urn:ogc:def:crs:EPSG::25832" + } + }, + "coordinates": [ + [ + 575000, + 6220000 + ], + [ + 576000, + 6220500 + ], + [ + 577500, + 6221000 + ], + [ + 579000, + 6222000 + ] + ], + "datetimes": [ + "2026-01-01T08:00:00+00:00", + "2026-01-01T08:10:00+00:00", + "2026-01-01T08:25:00+00:00", + "2026-01-01T08:45:00+00:00" + ], + "lower_inc": true, + "upper_inc": true, + "interpolation": "Linear" + }, + "geometry": { + "type": "LineString", + "crs": { + "type": "name", + "properties": { + "name": "urn:ogc:def:crs:EPSG::25832" + } + }, + "coordinates": [ + [ + 575000, + 6220000 + ], + [ + 576000, + 6220500 + ], + [ + 577500, + 6221000 + ], + [ + 579000, + 6222000 + ] + ] + }, + "links": [ + { + "rel": "self", + "href": "/collections/conformance/items/1" + } + ] +} diff --git a/samples/temporal-geometry-acceleration.json b/samples/temporal-geometry-acceleration.json new file mode 100644 index 0000000..df5cdb8 --- /dev/null +++ b/samples/temporal-geometry-acceleration.json @@ -0,0 +1,5 @@ +{ + "code": "501", + "description": "acceleration is not derivable: linearly interpolated position gives a piecewise-constant (Step) speed, whose derivative is zero within each segment and undefined at the vertices" +} + diff --git a/samples/temporal-geometry-distance.json b/samples/temporal-geometry-distance.json new file mode 100644 index 0000000..a266f37 --- /dev/null +++ b/samples/temporal-geometry-distance.json @@ -0,0 +1,31 @@ +{ + "name": "distance", + "type": "TReal", + "form": "http://www.opengis.net/def/uom/UCUM/0/m", + "description": "Cumulative distance travelled along the trajectory.", + "valueSequence": [ + { + "datetimes": [ + "2026-01-01T08:00:00+00:00", + "2026-01-01T08:10:00+00:00", + "2026-01-01T08:25:00+00:00", + "2026-01-01T08:45:00+00:00" + ], + "interpolation": "Linear", + "lower_inc": true, + "upper_inc": true, + "values": [ + 0, + 1118.033988749895, + 2699.172818834085, + 4501.94845656608 + ] + } + ], + "links": [ + { + "rel": "self", + "href": "/collections/conformance/items/1/tgsequence/1/distance" + } + ] +} diff --git a/samples/temporal-geometry-sequence.json b/samples/temporal-geometry-sequence.json new file mode 100644 index 0000000..3373914 --- /dev/null +++ b/samples/temporal-geometry-sequence.json @@ -0,0 +1,48 @@ +{ + "type": "TemporalGeometrySequence", + "geometrySequence": [ + { + "coordinates": [ + [ + 575000, + 6220000 + ], + [ + 576000, + 6220500 + ], + [ + 577500, + 6221000 + ], + [ + 579000, + 6222000 + ] + ], + "crs": { + "type": "Name", + "properties": { + "name": "urn:ogc:def:crs:EPSG::25832" + } + }, + "datetimes": [ + "2026-01-01T08:00:00+00:00", + "2026-01-01T08:10:00+00:00", + "2026-01-01T08:25:00+00:00", + "2026-01-01T08:45:00+00:00" + ], + "id": 1, + "interpolation": "Linear", + "lower_inc": true, + "type": "MovingPoint", + "upper_inc": true + } + ], + "links": [ + { + "rel": "self", + "href": "/collections/conformance/items/1/tgsequence" + } + ] +} diff --git a/samples/temporal-geometry-velocity.json b/samples/temporal-geometry-velocity.json new file mode 100644 index 0000000..0f53018 --- /dev/null +++ b/samples/temporal-geometry-velocity.json @@ -0,0 +1,31 @@ +{ + "name": "velocity", + "type": "TReal", + "form": "http://www.opengis.net/def/uom/UCUM/0/m_s-1", + "description": "Speed over ground (velocity magnitude), a piecewise-constant function of the trajectory.", + "valueSequence": [ + { + "datetimes": [ + "2026-01-01T08:00:00+00:00", + "2026-01-01T08:10:00+00:00", + "2026-01-01T08:25:00+00:00", + "2026-01-01T08:45:00+00:00" + ], + "interpolation": "Stepwise", + "lower_inc": true, + "upper_inc": true, + "values": [ + 1.863389981249825, + 1.756820922315766, + 1.502313031443329, + 1.502313031443329 + ] + } + ], + "links": [ + { + "rel": "self", + "href": "/collections/conformance/items/1/tgsequence/1/velocity" + } + ] +} diff --git a/samples/temporal-properties.json b/samples/temporal-properties.json new file mode 100644 index 0000000..ac79cbb --- /dev/null +++ b/samples/temporal-properties.json @@ -0,0 +1,60 @@ +{ + "links": [ + { + "href": "/collections/conformance/items/1/tproperties", + "rel": "self" + } + ], + "numberMatched": 4, + "numberReturned": 4, + "temporalProperties": [ + { + "description": "Whether the vessel is at anchor", + "links": [ + { + "href": "/collections/conformance/items/1/tproperties/anchored", + "rel": "self" + } + ], + "name": "anchored", + "type": "TBoolean" + }, + { + "description": "Course over ground", + "form": "http://www.opengis.net/def/uom/UCUM/0/deg", + "links": [ + { + "href": "/collections/conformance/items/1/tproperties/heading", + "rel": "self" + } + ], + "name": "heading", + "type": "TInteger" + }, + { + "description": "Speed over ground", + "form": "http://www.opengis.net/def/uom/UCUM/0/km_h-1", + "links": [ + { + "href": "/collections/conformance/items/1/tproperties/speed", + "rel": "self" + } + ], + "name": "speed", + "type": "TReal" + }, + { + "description": "Navigational status", + "links": [ + { + "href": "/collections/conformance/items/1/tproperties/status", + "rel": "self" + } + ], + "name": "status", + "type": "TText" + } + ], + "timeStamp": "2026-08-29T17:08:43Z" +} + diff --git a/samples/temporal-property.json b/samples/temporal-property.json new file mode 100644 index 0000000..1b98903 --- /dev/null +++ b/samples/temporal-property.json @@ -0,0 +1,26 @@ +{ + "name": "anchored", + "type": "TBoolean", + "description": "Whether the vessel is at anchor", + "valueSequence": [ + { + "datetimes": [ + "2026-01-01T08:00:00+00:00", + "2026-01-01T08:45:00+00:00" + ], + "interpolation": "Stepwise", + "lower_inc": true, + "upper_inc": true, + "values": [ + false, + true + ] + } + ], + "links": [ + { + "rel": "self", + "href": "/collections/conformance/items/1/tproperties/anchored" + } + ] +}