15 |
16 |
17 |
--------------------------------------------------------------------------------
/internal/engine/testdata/readfile-gzipped.txt.gz:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PDOK/gokoala/7e31a0320491af419fb89300d02e154ce5e597f8/internal/engine/testdata/readfile-gzipped.txt.gz
--------------------------------------------------------------------------------
/internal/engine/testdata/readfile-plain.txt:
--------------------------------------------------------------------------------
1 | foobar
--------------------------------------------------------------------------------
/internal/engine/testdata/test-resources-dir/foo.txt:
--------------------------------------------------------------------------------
1 | bar
--------------------------------------------------------------------------------
/internal/engine/testdata/test-resources-dir/thumbnail.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PDOK/gokoala/7e31a0320491af419fb89300d02e154ce5e597f8/internal/engine/testdata/test-resources-dir/thumbnail.jpg
--------------------------------------------------------------------------------
/internal/engine/util/filereader.go:
--------------------------------------------------------------------------------
1 | package util
2 |
3 | import (
4 | "bytes"
5 | "compress/gzip"
6 | "errors"
7 | "io"
8 | "io/fs"
9 | "log"
10 | "os"
11 | )
12 |
13 | // ReadFile read a plain or gzipped file and return contents as string
14 | func ReadFile(filePath string) string {
15 | gzipFile := filePath + ".gz"
16 | var fileContents string
17 | if _, err := os.Stat(gzipFile); !errors.Is(err, fs.ErrNotExist) {
18 | fileContents, err = readGzipContents(gzipFile)
19 | if err != nil {
20 | log.Fatalf("unable to decompress gzip file %s", gzipFile)
21 | }
22 | } else {
23 | fileContents, err = readPlainContents(filePath)
24 | if err != nil {
25 | log.Fatalf("unable to read file %s", filePath)
26 | }
27 | }
28 | return fileContents
29 | }
30 |
31 | // decompress gzip files, return contents as string
32 | func readGzipContents(filePath string) (string, error) {
33 | gzipFile, err := os.Open(filePath)
34 | if err != nil {
35 | return "", err
36 | }
37 | defer func(gzipFile *os.File) {
38 | err := gzipFile.Close()
39 | if err != nil {
40 | log.Println("failed to close gzip file")
41 | }
42 | }(gzipFile)
43 | gzipReader, err := gzip.NewReader(gzipFile)
44 | if err != nil {
45 | return "", err
46 | }
47 | defer func(gzipReader *gzip.Reader) {
48 | err := gzipReader.Close()
49 | if err != nil {
50 | log.Println("failed to close gzip reader")
51 | }
52 | }(gzipReader)
53 | var buffer bytes.Buffer
54 | _, err = io.Copy(&buffer, gzipReader) //nolint:gosec
55 | if err != nil {
56 | return "", err
57 | }
58 | return buffer.String(), nil
59 | }
60 |
61 | // read file, return contents as string
62 | func readPlainContents(filePath string) (string, error) {
63 | file, err := os.Open(filePath)
64 | if err != nil {
65 | return "", err
66 | }
67 | defer func(file *os.File) {
68 | err := file.Close()
69 | if err != nil {
70 | log.Println("failed to close file")
71 | }
72 | }(file)
73 | var buffer bytes.Buffer
74 | _, err = io.Copy(&buffer, file)
75 | if err != nil {
76 | return "", err
77 | }
78 | return buffer.String(), nil
79 | }
80 |
--------------------------------------------------------------------------------
/internal/engine/util/filereader_test.go:
--------------------------------------------------------------------------------
1 | package util
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/stretchr/testify/assert"
7 | )
8 |
9 | func TestReadFile(t *testing.T) {
10 | tests := []struct {
11 | name string
12 | filePath string
13 | wantErr bool
14 | }{
15 | {
16 | name: "Test read gzip file",
17 | filePath: "../testdata/readfile-gzipped.txt",
18 | wantErr: false,
19 | },
20 | {
21 | name: "Test read plain file",
22 | filePath: "../testdata/readfile-plain.txt",
23 | wantErr: false,
24 | },
25 | }
26 | for _, tt := range tests {
27 | t.Run(tt.name, func(t *testing.T) {
28 | got := ReadFile(tt.filePath)
29 | assert.Equal(t, "foobar", got)
30 | })
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/internal/engine/util/json.go:
--------------------------------------------------------------------------------
1 | package util
2 |
3 | import (
4 | "bytes"
5 | "encoding/json"
6 | "log"
7 |
8 | "dario.cat/mergo"
9 | )
10 |
11 | func PrettyPrintJSON(content []byte, name string) []byte {
12 | var pretty bytes.Buffer
13 | if err := json.Indent(&pretty, content, "", " "); err != nil {
14 | log.Print(string(content))
15 | log.Fatalf("invalid json in %s: %v, see json output above", name, err)
16 | }
17 | return pretty.Bytes()
18 | }
19 |
20 | // MergeJSON merges the two JSON byte slices. It returns an error if x1 or x2 cannot be JSON-unmarshalled,
21 | // or the merged JSON is invalid.
22 | //
23 | // Optionally, an orderBy function can be provided to alter the key order in the resulting JSON
24 | func MergeJSON(x1, x2 []byte, orderBy func(output map[string]any) any) ([]byte, error) {
25 | var j1 map[string]any
26 | err := json.Unmarshal(x1, &j1)
27 | if err != nil {
28 | return nil, err
29 | }
30 | var j2 map[string]any
31 | err = json.Unmarshal(x2, &j2)
32 | if err != nil {
33 | return nil, err
34 | }
35 | err = mergo.Merge(&j1, &j2)
36 | if err != nil {
37 | return nil, err
38 | }
39 | if orderBy != nil {
40 | return json.Marshal(orderBy(j1))
41 | }
42 | return json.Marshal(j1)
43 | }
44 |
--------------------------------------------------------------------------------
/internal/engine/util/maps.go:
--------------------------------------------------------------------------------
1 | package util
2 |
3 | // Keys returns the keys of the map m. The keys will be an indeterminate order.
4 | func Keys[M ~map[K]V, K comparable, V any](input M) []K {
5 | output := make([]K, 0, len(input))
6 | for k := range input {
7 | output = append(output, k)
8 | }
9 | return output
10 | }
11 |
12 | // Inverse switches the values to keys and the keys to values.
13 | func Inverse(input map[string]string) map[string]string {
14 | output := make(map[string]string)
15 | for k, v := range input {
16 | output[v] = k
17 | }
18 | return output
19 | }
20 |
21 | // Cast turns a map[K]V to a map[K]any, so values will downcast to 'any' type.
22 | func Cast[M ~map[K]V, K comparable, V any](input M) map[K]any {
23 | output := make(map[K]any, len(input))
24 | for k, v := range input {
25 | output[k] = v
26 | }
27 | return output
28 | }
29 |
--------------------------------------------------------------------------------
/internal/ogc/README.md:
--------------------------------------------------------------------------------
1 | # OGC API
2 |
3 | OGC APIs are constructed by different building blocks. These building blocks
4 | are composed of the different [OGC API standards](https://ogcapi.ogc.org/).
5 | Each OGC building block resides in its own Go package.
6 |
7 | When coding we try to use the naming convention as used by the OGC, so it is clear
8 | which specification or part is referred to in code.
9 |
10 | ## Coding
11 |
12 | ### Templates
13 |
14 | We use templates to generate static/pre-defined API responses based on
15 | the given GoKoala configuration file. Lots of OGC API responses can be
16 | statically generated. Generation happens at startup and results are served
17 | from memory when an API request is received. Benefits of this approach are:
18 |
19 | - Lightning fast responses to API calls since everything is served from memory
20 | - Fail fast since validation is performed during startup
21 |
22 | #### Duplication
23 |
24 | We will have duplication between JSON and HTML templates: that's ok. They're
25 | different representations of the same data. Don't try to be clever and
26 | "optimize" it. The duplication is pretty obvious/visible since the files only
27 | differ by extension, so it's clear any changes need to be done in both
28 | representations. Having independent files keeps the templates simple and
29 | flexible.
30 |
31 | #### IDE support
32 |
33 | See [README](../../README.md) in the root.
34 |
35 | #### Tip: handling JSON
36 |
37 | When generating JSON arrays using templates you need to be aware of trailing
38 | commas. The last element in an array must not contain a comma. To prevent this,
39 | either:
40 |
41 | - Add the comma in front of array items
42 | - Use the index of a `range` to check array position and place the comma based
43 | on the index
44 | - The most comprehensive solution is to use:
45 |
46 | ```jinja
47 | {{ $first := true }}
48 | {{ range $_, $element := .}}
49 | {{if not $first}}, {{else}} {{$first = false}} {{end}}
50 | {{$element.Name}}
51 | {{end}}
52 | ```
53 |
--------------------------------------------------------------------------------
/internal/ogc/common/geospatial/README.md:
--------------------------------------------------------------------------------
1 | # Geospatial data resources / collections.
2 |
3 | For GoKoala devs: If you want to implement collections support in one of the OGC building blocks
4 | in GoKoala (see `ogc` package) you'll need to perform the following tasks:
5 |
6 | Config:
7 | - Expand / add yaml tag in `engine.Config.OgcAPI` to allow users to configure collections
8 |
9 | OpenAPI
10 | - Materialize the collections as API endpoints by looping over the collection in the OpenAPI template
11 | for that specific OGC building block. For example for OGC tiles you'll need to
12 | create `/collection/{collectionId}/tiles` endpoints in OpenAPI. Note `/collection/{collectionId}` endpoint
13 | are already implemented in OpenAPI by this package.
14 |
15 | Responses:
16 | - Expand the `collections` and `collection` [templates](./templates).
17 | - Implement an endpoint in your specific OGC API building block to serve the CONTENTS of a collection
18 | (e.g. `/collection/{collectionId}/tiles`)
19 |
20 | Testing:
21 | - Add unit tests
22 |
23 |
--------------------------------------------------------------------------------
/internal/ogc/features/datasources/geopackage/backend_cloud.go:
--------------------------------------------------------------------------------
1 | //go:build cgo && !darwin && !windows
2 |
3 | package geopackage
4 |
5 | import (
6 | "fmt"
7 | "log"
8 |
9 | "github.com/PDOK/gokoala/config"
10 | "github.com/google/uuid"
11 |
12 | cloudsqlitevfs "github.com/PDOK/go-cloud-sqlite-vfs"
13 | "github.com/jmoiron/sqlx"
14 | )
15 |
16 | // Cloud-Backed SQLite (CBS) GeoPackage in Azure or Google object storage
17 | type cloudGeoPackage struct {
18 | db *sqlx.DB
19 | cloudVFS *cloudsqlitevfs.VFS
20 | }
21 |
22 | func newCloudBackedGeoPackage(gpkg *config.GeoPackageCloud) geoPackageBackend {
23 | cacheDir, err := gpkg.CacheDir()
24 | if err != nil {
25 | log.Fatalf("invalid cache dir, error: %v", err)
26 | }
27 | cacheSize, err := gpkg.Cache.MaxSizeAsBytes()
28 | if err != nil {
29 | log.Fatalf("invalid cache size provided, error: %v", err)
30 | }
31 |
32 | msg := fmt.Sprintf("Cloud-Backed GeoPackage '%s' in container '%s' on '%s'",
33 | gpkg.File, gpkg.Container, gpkg.Connection)
34 |
35 | log.Printf("connecting to %s\n", msg)
36 | vfsName := uuid.New().String() // important: each geopackage must use a unique VFS name
37 | vfs, err := cloudsqlitevfs.NewVFS(vfsName, gpkg.Connection, gpkg.User, gpkg.Auth,
38 | gpkg.Container, cacheDir, cacheSize, gpkg.LogHTTPRequests)
39 | if err != nil {
40 | log.Fatalf("failed to connect with %s, error: %v", msg, err)
41 | }
42 | log.Printf("connected to %s\n", msg)
43 |
44 | conn := fmt.Sprintf("/%s/%s?vfs=%s&mode=ro&_cache_size=%d", gpkg.Container, gpkg.File, vfsName, gpkg.InMemoryCacheSize)
45 | db, err := sqlx.Open(sqliteDriverName, conn)
46 | if err != nil {
47 | log.Fatalf("failed to open %s, error: %v", msg, err)
48 | }
49 |
50 | return &cloudGeoPackage{db, &vfs}
51 | }
52 |
53 | func (g *cloudGeoPackage) getDB() *sqlx.DB {
54 | return g.db
55 | }
56 |
57 | func (g *cloudGeoPackage) close() {
58 | err := g.db.Close()
59 | if err != nil {
60 | log.Printf("failed to close GeoPackage: %v", err)
61 | }
62 | if g.cloudVFS != nil {
63 | err = g.cloudVFS.Close()
64 | if err != nil {
65 | log.Printf("failed to close Cloud-Backed GeoPackage: %v", err)
66 | }
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/internal/ogc/features/datasources/geopackage/backend_cloud_darwin.go:
--------------------------------------------------------------------------------
1 | //go:build darwin
2 |
3 | package geopackage
4 |
5 | import (
6 | "log"
7 |
8 | "github.com/PDOK/gokoala/config"
9 | )
10 |
11 | // Dummy implementation to make compilation on macOS work. We don't support cloud-backed
12 | // sqlite/geopackages on macOS since the LLVM linker on macOS doesn't support the
13 | // '--allow-multiple-definition' flag. This flag is required since both the 'mattn' sqlite
14 | // driver and 'go-cloud-sqlite-vfs' contain a copy of the sqlite C-code, which causes
15 | // duplicate symbols (aka multiple definitions).
16 | func newCloudBackedGeoPackage(_ *config.GeoPackageCloud) geoPackageBackend {
17 | log.Fatalf("Cloud backed GeoPackage isn't supported on darwin/macos")
18 | return nil
19 | }
20 |
--------------------------------------------------------------------------------
/internal/ogc/features/datasources/geopackage/backend_cloud_windows.go:
--------------------------------------------------------------------------------
1 | //go:build windows
2 |
3 | package geopackage
4 |
5 | import (
6 | "log"
7 |
8 | "github.com/PDOK/gokoala/config"
9 | )
10 |
11 | // Dummy implementation to make compilation on window work.
12 | func newCloudBackedGeoPackage(_ *config.GeoPackageCloud) geoPackageBackend {
13 | log.Fatalf("Cloud backed GeoPackage isn't supported on windows")
14 | return nil
15 | }
16 |
--------------------------------------------------------------------------------
/internal/ogc/features/datasources/geopackage/backend_local.go:
--------------------------------------------------------------------------------
1 | package geopackage
2 |
3 | import (
4 | "fmt"
5 | "log"
6 | "time"
7 |
8 | "github.com/PDOK/gokoala/config"
9 | "github.com/PDOK/gokoala/internal/engine"
10 | "github.com/jmoiron/sqlx"
11 | )
12 |
13 | // GeoPackage on local disk
14 | type localGeoPackage struct {
15 | db *sqlx.DB
16 | }
17 |
18 | func newLocalGeoPackage(gpkg *config.GeoPackageLocal) geoPackageBackend {
19 | if gpkg.Download != nil {
20 | downloadGeoPackage(gpkg)
21 | }
22 | conn := fmt.Sprintf("file:%s?immutable=1&_cache_size=%d", gpkg.File, gpkg.InMemoryCacheSize)
23 | db, err := sqlx.Open(sqliteDriverName, conn)
24 | if err != nil {
25 | log.Fatalf("failed to open GeoPackage: %v", err)
26 | }
27 | log.Printf("connected to local GeoPackage: %s", gpkg.File)
28 |
29 | return &localGeoPackage{db}
30 | }
31 |
32 | func downloadGeoPackage(gpkg *config.GeoPackageLocal) {
33 | url := *gpkg.Download.From.URL
34 | log.Printf("start download of GeoPackage: %s", url.String())
35 | downloadTime, err := engine.Download(url, gpkg.File, gpkg.Download.Parallelism, gpkg.Download.TLSSkipVerify,
36 | gpkg.Download.Timeout.Duration, gpkg.Download.RetryDelay.Duration, gpkg.Download.RetryMaxDelay.Duration, gpkg.Download.MaxRetries)
37 | if err != nil {
38 | log.Fatalf("failed to download GeoPackage: %v", err)
39 | }
40 | log.Printf("successfully downloaded GeoPackage to %s in %s", gpkg.File, downloadTime.Round(time.Second))
41 | }
42 |
43 | func (g *localGeoPackage) getDB() *sqlx.DB {
44 | return g.db
45 | }
46 |
47 | func (g *localGeoPackage) close() {
48 | err := g.db.Close()
49 | if err != nil {
50 | log.Printf("failed to close GeoPackage: %v", err)
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/internal/ogc/features/datasources/geopackage/stmtcache.go:
--------------------------------------------------------------------------------
1 | package geopackage
2 |
3 | import (
4 | "context"
5 | "log"
6 |
7 | lru "github.com/hashicorp/golang-lru/v2"
8 | "github.com/jmoiron/sqlx"
9 | )
10 |
11 | var preparedStmtCacheSize = 25
12 |
13 | // PreparedStatementCache is thread safe
14 | type PreparedStatementCache struct {
15 | cache *lru.Cache[string, *sqlx.NamedStmt]
16 | }
17 |
18 | // NewCache creates a new PreparedStatementCache that will evict least-recently used (LRU) statements.
19 | func NewCache() *PreparedStatementCache {
20 | cache, _ := lru.NewWithEvict[string, *sqlx.NamedStmt](preparedStmtCacheSize,
21 | func(_ string, stmt *sqlx.NamedStmt) {
22 | if stmt != nil {
23 | _ = stmt.Close()
24 | }
25 | })
26 |
27 | return &PreparedStatementCache{cache: cache}
28 | }
29 |
30 | // Lookup gets a prepared statement from the cache for the given query, or creates a new one and adds it to the cache
31 | func (c *PreparedStatementCache) Lookup(ctx context.Context, db *sqlx.DB, query string) (*sqlx.NamedStmt, error) {
32 | cachedStmt, ok := c.cache.Get(query)
33 | if !ok {
34 | stmt, err := db.PrepareNamedContext(ctx, query)
35 | if err != nil {
36 | return nil, err
37 | }
38 | c.cache.Add(query, stmt)
39 | return stmt, nil
40 | }
41 | return cachedStmt, nil
42 | }
43 |
44 | // Close purges the cache, and closes remaining prepared statements
45 | func (c *PreparedStatementCache) Close() {
46 | log.Printf("closing %d prepared statements", c.cache.Len())
47 | c.cache.Purge()
48 | }
49 |
--------------------------------------------------------------------------------
/internal/ogc/features/datasources/geopackage/stmtcache_test.go:
--------------------------------------------------------------------------------
1 | package geopackage
2 |
3 | import (
4 | "sync"
5 | "testing"
6 |
7 | "github.com/jmoiron/sqlx"
8 | "github.com/stretchr/testify/assert"
9 | )
10 |
11 | func TestPreparedStatementCache(t *testing.T) {
12 | tests := []struct {
13 | name string
14 | query string
15 | }{
16 | {
17 | name: "First query is a cache miss",
18 | query: "SELECT * FROM main.sqlite_master WHERE name = :n",
19 | },
20 | {
21 | name: "Second query is a cache hit",
22 | query: "SELECT * FROM main.sqlite_master WHERE name = :n",
23 | },
24 | }
25 | for _, tt := range tests {
26 | t.Run(tt.name, func(t *testing.T) {
27 | c := NewCache()
28 | assert.NotNil(t, c)
29 |
30 | db, err := sqlx.Connect("sqlite3", ":memory:")
31 | assert.NoError(t, err)
32 |
33 | stmt, err := c.Lookup(t.Context(), db, tt.query)
34 | assert.NoError(t, err)
35 | assert.NotNil(t, stmt)
36 |
37 | c.Close()
38 | })
39 | }
40 |
41 | t.Run("Concurrent access to the cache", func(t *testing.T) {
42 | var wg sync.WaitGroup
43 |
44 | c := NewCache()
45 | assert.NotNil(t, c)
46 |
47 | db, err := sqlx.Connect("sqlite3", ":memory:")
48 | assert.NoError(t, err)
49 |
50 | // Run multiple goroutines that will access the cache concurrently.
51 | for i := 0; i < 25; i++ {
52 | wg.Add(1)
53 | go func() {
54 | defer wg.Done()
55 | stmt1, err := c.Lookup(t.Context(), db, "SELECT * FROM main.sqlite_master WHERE name = :n")
56 | assert.NoError(t, err)
57 | assert.NotNil(t, stmt1)
58 |
59 | stmt2, err := c.Lookup(t.Context(), db, "SELECT * FROM main.sqlite_master WHERE type = :t")
60 | assert.NoError(t, err)
61 | assert.NotNil(t, stmt2)
62 | }()
63 | }
64 | wg.Wait() // Wait for all goroutines to finish.
65 |
66 | assert.Equal(t, 2, c.cache.Len())
67 | c.Close()
68 | })
69 | }
70 |
--------------------------------------------------------------------------------
/internal/ogc/features/datasources/geopackage/testdata/3d-geoms.gpkg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PDOK/gokoala/7e31a0320491af419fb89300d02e154ce5e597f8/internal/ogc/features/datasources/geopackage/testdata/3d-geoms.gpkg
--------------------------------------------------------------------------------
/internal/ogc/features/datasources/geopackage/testdata/bag-temporal.gpkg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PDOK/gokoala/7e31a0320491af419fb89300d02e154ce5e597f8/internal/ogc/features/datasources/geopackage/testdata/bag-temporal.gpkg
--------------------------------------------------------------------------------
/internal/ogc/features/datasources/geopackage/testdata/bag.gpkg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PDOK/gokoala/7e31a0320491af419fb89300d02e154ce5e597f8/internal/ogc/features/datasources/geopackage/testdata/bag.gpkg
--------------------------------------------------------------------------------
/internal/ogc/features/datasources/geopackage/testdata/external-fid.gpkg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PDOK/gokoala/7e31a0320491af419fb89300d02e154ce5e597f8/internal/ogc/features/datasources/geopackage/testdata/external-fid.gpkg
--------------------------------------------------------------------------------
/internal/ogc/features/datasources/geopackage/testdata/null-empty-geoms.gpkg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PDOK/gokoala/7e31a0320491af419fb89300d02e154ce5e597f8/internal/ogc/features/datasources/geopackage/testdata/null-empty-geoms.gpkg
--------------------------------------------------------------------------------
/internal/ogc/features/datasources/geopackage/testdata/roads.gpkg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PDOK/gokoala/7e31a0320491af419fb89300d02e154ce5e597f8/internal/ogc/features/datasources/geopackage/testdata/roads.gpkg
--------------------------------------------------------------------------------
/internal/ogc/features/datasources/geopackage/warmup.go:
--------------------------------------------------------------------------------
1 | package geopackage
2 |
3 | import (
4 | "errors"
5 | "fmt"
6 | "log"
7 |
8 | "github.com/PDOK/gokoala/config"
9 |
10 | "github.com/jmoiron/sqlx"
11 | )
12 |
13 | // warmUpFeatureTables executes a warmup query to speedup subsequent queries.
14 | // This encompasses traversing index(es) to fill the local cache.
15 | func warmUpFeatureTables(
16 | configuredCollections config.GeoSpatialCollections,
17 | featureTableByCollectionID map[string]*featureTable,
18 | db *sqlx.DB) error {
19 |
20 | for collID, table := range featureTableByCollectionID {
21 | if table == nil {
22 | return errors.New("given table can't be nil")
23 | }
24 | for _, coll := range configuredCollections {
25 | if coll.ID == collID && coll.Features != nil {
26 | if err := warmUpFeatureTable(table.TableName, db); err != nil {
27 | return err
28 | }
29 | break
30 | }
31 | }
32 | }
33 | return nil
34 | }
35 |
36 | func warmUpFeatureTable(tableName string, db *sqlx.DB) error {
37 | query := fmt.Sprintf(`
38 | select minx,maxx,miny,maxy from %[1]s where minx <= 0 and maxx >= 0 and miny <= 0 and maxy >= 0
39 | `, tableName)
40 |
41 | log.Printf("start warm-up of feature table '%s'", tableName)
42 | _, err := db.Exec(query)
43 | if err != nil {
44 | return fmt.Errorf("failed to warm-up feature table '%s': %w", tableName, err)
45 | }
46 | log.Printf("end warm-up of feature table '%s'", tableName)
47 | return nil
48 | }
49 |
--------------------------------------------------------------------------------
/internal/ogc/features/datasources/postgis/postgis_test.go:
--------------------------------------------------------------------------------
1 | package postgis
2 |
3 | import (
4 | neturl "net/url"
5 | "testing"
6 |
7 | "github.com/PDOK/gokoala/internal/ogc/features/datasources"
8 | "github.com/PDOK/gokoala/internal/ogc/features/domain"
9 | "github.com/stretchr/testify/assert"
10 | )
11 |
12 | // PostGIS !!! Placeholder implementation, for future reference !!!
13 | func TestPostGIS(t *testing.T) {
14 | pg := PostGIS{}
15 | url, _ := neturl.Parse("http://example.com")
16 | p := domain.NewProfile(domain.RelAsLink, *url, []string{})
17 |
18 | t.Run("GetFeatureIDs", func(t *testing.T) {
19 | ids, cursors, err := pg.GetFeatureIDs(t.Context(), "", datasources.FeaturesCriteria{})
20 | assert.NoError(t, err)
21 | assert.Empty(t, ids)
22 | assert.NotNil(t, cursors)
23 | })
24 |
25 | t.Run("GetFeaturesByID", func(t *testing.T) {
26 | fc, err := pg.GetFeaturesByID(t.Context(), "", nil, p)
27 | assert.NoError(t, err)
28 | assert.NotNil(t, fc)
29 | })
30 |
31 | t.Run("GetFeatures", func(t *testing.T) {
32 | fc, cursors, err := pg.GetFeatures(t.Context(), "", datasources.FeaturesCriteria{}, p)
33 | assert.NoError(t, err)
34 | assert.Nil(t, fc)
35 | assert.NotNil(t, cursors)
36 | })
37 |
38 | t.Run("GetFeature", func(t *testing.T) {
39 | f, err := pg.GetFeature(t.Context(), "", 0, p)
40 | assert.NoError(t, err)
41 | assert.Nil(t, f)
42 | })
43 |
44 | t.Run("GetSchema", func(t *testing.T) {
45 | schema, err := pg.GetSchema("")
46 | assert.NoError(t, err)
47 | assert.Nil(t, schema)
48 | })
49 | }
50 |
--------------------------------------------------------------------------------
/internal/ogc/features/domain/jsonfg.go:
--------------------------------------------------------------------------------
1 | package domain
2 |
3 | import (
4 | "github.com/twpayne/go-geom/encoding/geojson"
5 | )
6 |
7 | const (
8 | ConformanceJSONFGCore = "http://www.opengis.net/spec/json-fg-1/0.2/conf/core"
9 | )
10 |
11 | // JSONFGFeatureCollection FeatureCollection according to the JSON-FG standard
12 | // Note: fields in this struct are sorted for optimal memory usage (field alignment)
13 | type JSONFGFeatureCollection struct {
14 | Type featureCollectionType `json:"type"`
15 | Timestamp string `json:"timeStamp,omitempty"`
16 | CoordRefSys string `json:"coordRefSys"`
17 | Links []Link `json:"links,omitempty"`
18 | ConformsTo []string `json:"conformsTo"`
19 | Features []*JSONFGFeature `json:"features"`
20 | NumberReturned int `json:"numberReturned"`
21 | }
22 |
23 | // JSONFGFeature Feature according to the JSON-FG standard
24 | // Note: fields in this struct are sorted for optimal memory usage (field alignment)
25 | type JSONFGFeature struct {
26 | // We expect feature ids to be auto-incrementing integers (which is the default in geopackages)
27 | // since we use it for cursor-based pagination.
28 | ID string `json:"id"`
29 | Type featureType `json:"type"`
30 | Time any `json:"time"`
31 | // We don't implement the JSON-FG "3D" conformance class. So Place only
32 | // supports simple/2D geometries, no 3D geometries like Polyhedron, Prism, etc.
33 | Place *geojson.Geometry `json:"place"` // may only contain non-WGS84 geometries
34 | Geometry *geojson.Geometry `json:"geometry"` // may only contain WGS84 geometries
35 | Properties FeatureProperties `json:"properties"`
36 | CoordRefSys string `json:"coordRefSys,omitempty"`
37 | Links []Link `json:"links,omitempty"`
38 | ConformsTo []string `json:"conformsTo,omitempty"`
39 | }
40 |
--------------------------------------------------------------------------------
/internal/ogc/features/domain/spatialref.go:
--------------------------------------------------------------------------------
1 | package domain
2 |
3 | import (
4 | "fmt"
5 | "strconv"
6 | "strings"
7 | )
8 |
9 | const (
10 | CrsURIPrefix = "http://www.opengis.net/def/crs/"
11 | UndefinedSRID = 0
12 | WGS84SRID = 100000 // We use the SRID for CRS84 (WGS84) as defined in the GeoPackage, instead of EPSG:4326 (due to axis order). In time, we may need to read this value dynamically from the GeoPackage.
13 | WGS84CodeOGC = "CRS84"
14 | WGS84CrsURI = CrsURIPrefix + "OGC/1.3/" + WGS84CodeOGC
15 | )
16 |
17 | // SRID Spatial Reference System Identifier: a unique value to unambiguously identify a spatial coordinate system.
18 | // For example '28992' in https://www.opengis.net/def/crs/EPSG/0/28992
19 | type SRID int
20 |
21 | func (s SRID) GetOrDefault() int {
22 | val := int(s)
23 | if val <= 0 {
24 | return WGS84SRID
25 | }
26 | return val
27 | }
28 |
29 | func EpsgToSrid(srs string) (SRID, error) {
30 | prefix := "EPSG:"
31 | srsCode, found := strings.CutPrefix(srs, prefix)
32 | if !found {
33 | return -1, fmt.Errorf("expected SRS to start with '%s', got %s", prefix, srs)
34 | }
35 | srid, err := strconv.Atoi(srsCode)
36 | if err != nil {
37 | return -1, fmt.Errorf("expected EPSG code to have numeric value, got %s", srsCode)
38 | }
39 | return SRID(srid), nil
40 | }
41 |
42 | // ContentCrs the coordinate reference system (represented as a URI) of the content/output to return.
43 | type ContentCrs string
44 |
45 | // ToLink returns link target conforming to RFC 8288
46 | func (c ContentCrs) ToLink() string {
47 | return fmt.Sprintf("<%s>", c)
48 | }
49 |
50 | func (c ContentCrs) IsWGS84() bool {
51 | return string(c) == WGS84CrsURI
52 | }
53 |
--------------------------------------------------------------------------------
/internal/ogc/features/domain/spatialref_test.go:
--------------------------------------------------------------------------------
1 | package domain
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/stretchr/testify/assert"
7 | )
8 |
9 | func TestGetOrDefault(t *testing.T) {
10 | tests := []struct {
11 | name string
12 | srid SRID
13 | expected int
14 | }{
15 | {"Positive SRID", SRID(28992), 28992},
16 | {"Zero SRID", SRID(0), WGS84SRID},
17 | {"Negative SRID", SRID(-1), WGS84SRID},
18 | }
19 |
20 | for _, tt := range tests {
21 | t.Run(tt.name, func(t *testing.T) {
22 | assert.Equal(t, tt.expected, tt.srid.GetOrDefault())
23 | })
24 | }
25 | }
26 |
27 | func TestEpsgToSrid(t *testing.T) {
28 | tests := []struct {
29 | name string
30 | srs string
31 | expected SRID
32 | expectError bool
33 | }{
34 | {"Valid EPSG", "EPSG:28992", SRID(28992), false},
35 | {"Invalid prefix", "INVALID:28992", SRID(-1), true},
36 | {"Non-numeric EPSG code", "EPSG:ABC", SRID(-1), true},
37 | }
38 | for _, tt := range tests {
39 | t.Run(tt.name, func(t *testing.T) {
40 | result, err := EpsgToSrid(tt.srs)
41 | if tt.expectError {
42 | assert.Error(t, err)
43 | } else {
44 | assert.NoError(t, err)
45 | assert.Equal(t, tt.expected, result)
46 | }
47 | })
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/internal/ogc/features/testdata/config_benchmark.yaml:
--------------------------------------------------------------------------------
1 | ---
2 | version: 1.0.2
3 | title: OGC API Features
4 | abstract: Config specific for BAG benchmark
5 | baseUrl: http://localhost:8080
6 | serviceIdentifier: Bench
7 | license:
8 | name: CC0
9 | url: https://www.tldrlegal.com/license/creative-commons-cc0-1-0-universal
10 | ogcApi:
11 | features:
12 | validateResponses: false # improves performance
13 | datasources:
14 | defaultWGS84:
15 | geopackage:
16 | local:
17 | file: ./examples/resources/addresses-crs84.gpkg
18 | additional:
19 | - srs: EPSG:28992
20 | geopackage:
21 | local:
22 | file: ./examples/resources/addresses-rd.gpkg
23 | - srs: EPSG:3035
24 | geopackage:
25 | local:
26 | file: ./examples/resources/addresses-etrs89.gpkg
27 | collections:
28 | - id: dutch-addresses
29 | tableName: addresses # name of the feature table (optional), when omitted collection ID is used.
30 | metadata:
31 | description: addresses
32 | temporalProperties:
33 | startDate: validfrom
34 | endDate: validto
35 | extent:
36 | srs: EPSG:4326
37 | interval: [ "\"1970-01-01T00:00:00Z\"", "null" ]
38 |
--------------------------------------------------------------------------------
/internal/ogc/features/testdata/config_features_3d_geoms.yaml:
--------------------------------------------------------------------------------
1 | ---
2 | version: 1.0.2
3 | title: OGC API Features
4 | abstract: Test to verify support for XYZ geoms
5 | baseUrl: http://localhost:8080
6 | serviceIdentifier: Feats
7 | license:
8 | name: CC0
9 | url: https://www.tldrlegal.com/license/creative-commons-cc0-1-0-universal
10 | ogcApi:
11 | features:
12 | datasources:
13 | defaultWGS84:
14 | geopackage:
15 | local:
16 | file: ./internal/ogc/features/datasources/geopackage/testdata/3d-geoms.gpkg
17 | collections:
18 | - id: foo
19 | metadata:
20 | title: Foo
21 | description: Contains 3D linestrings
22 | - id: bar
23 | metadata:
24 | title: Bar
25 | description: Contains 3D multipoints
26 |
--------------------------------------------------------------------------------
/internal/ogc/features/testdata/config_features_bag.yaml:
--------------------------------------------------------------------------------
1 | ---
2 | version: 1.0.2
3 | title: OGC API Features
4 | abstract: Contains a slimmed-down/example version of the BAG-dataset
5 | baseUrl: http://localhost:8080
6 | serviceIdentifier: Feats
7 | license:
8 | name: CC0
9 | url: https://www.tldrlegal.com/license/creative-commons-cc0-1-0-universal
10 | ogcApi:
11 | features:
12 | datasources:
13 | defaultWGS84:
14 | geopackage:
15 | local:
16 | file: ./internal/ogc/features/datasources/geopackage/testdata/bag.gpkg
17 | fid: feature_id
18 | queryTimeout: 15m # pretty high to allow debugging
19 | collections:
20 | - id: foo
21 | tableName: ligplaatsen
22 | filters:
23 | properties:
24 | - name: straatnaam
25 | - name: postcode
26 | metadata:
27 | title: Foooo
28 | description: Foooo
29 | - id: bar
30 | tableName: ligplaatsen
31 | metadata:
32 | title: Barrr
33 | description: Barrr
34 | tableName: ligplaatsen
35 | - id: baz
36 |
--------------------------------------------------------------------------------
/internal/ogc/features/testdata/config_features_bag_allowed_values.yaml:
--------------------------------------------------------------------------------
1 | ---
2 | version: 1.0.2
3 | title: OGC API Features
4 | abstract: Contains a slimmed-down/example version of the BAG-dataset
5 | baseUrl: http://localhost:8080
6 | serviceIdentifier: Feats
7 | license:
8 | name: CC0
9 | url: https://www.tldrlegal.com/license/creative-commons-cc0-1-0-universal
10 | ogcApi:
11 | features:
12 | datasources:
13 | defaultWGS84:
14 | geopackage:
15 | local:
16 | file: ./internal/ogc/features/datasources/geopackage/testdata/bag.gpkg
17 | fid: feature_id
18 | collections:
19 | - id: foo
20 | tableName: ligplaatsen
21 | filters:
22 | properties:
23 | - name: straatnaam
24 | allowedValues:
25 | - Silodam
26 | - Westerdok
27 | - name: type
28 | indexRequired: false
29 | deriveAllowedValuesFromDatasource: true
30 | - name: postcode
31 | metadata:
32 | title: Foo
33 | description: Example collection to test property filters with allowed values restriction
34 | - id: bar
35 | tableName: standplaatsen
36 | filters:
37 | properties:
38 | - name: straatnaam
39 | indexRequired: false
40 | deriveAllowedValuesFromDatasource: true
41 | - name: type
42 | indexRequired: false
43 | deriveAllowedValuesFromDatasource: false
44 | metadata:
45 | title: Bar
46 | description: Example collection to test property filters with allowed values restriction
47 | tableName: ligplaatsen
48 |
--------------------------------------------------------------------------------
/internal/ogc/features/testdata/config_features_bag_invalid_filters.yaml:
--------------------------------------------------------------------------------
1 | ---
2 | version: 1.0.2
3 | title: OGC API Features
4 | abstract: Contains a slimmed-down/example version of the BAG-dataset
5 | baseUrl: http://localhost:8080
6 | serviceIdentifier: Feats
7 | license:
8 | name: CC0
9 | url: https://www.tldrlegal.com/license/creative-commons-cc0-1-0-universal
10 | ogcApi:
11 | features:
12 | datasources:
13 | defaultWGS84:
14 | geopackage:
15 | local:
16 | file: ./internal/ogc/features/datasources/geopackage/testdata/bag.gpkg
17 | fid: feature_id
18 | queryTimeout: 15m # pretty high to allow debugging
19 | collections:
20 | - id: foo
21 | tableName: ligplaatsen
22 | filters:
23 | properties:
24 | - name: straatnaam
25 | - name: invalid_this_does_not_exist_in_gpkg
26 | - name: postcode
27 | metadata:
28 | title: Foooo
29 | description: Foooo
30 |
--------------------------------------------------------------------------------
/internal/ogc/features/testdata/config_features_bag_long_description.yaml:
--------------------------------------------------------------------------------
1 | ---
2 | version: 1.0.2
3 | title: OGC API Features
4 | abstract: Contains a slimmed-down/example version of the BAG-dataset
5 | baseUrl: http://localhost:8080
6 | serviceIdentifier: Feats
7 | license:
8 | name: CC0
9 | url: https://www.tldrlegal.com/license/creative-commons-cc0-1-0-universal
10 | ogcApi:
11 | features:
12 | datasources:
13 | defaultWGS84:
14 | geopackage:
15 | local:
16 | file: ./internal/ogc/features/datasources/geopackage/testdata/bag.gpkg
17 | fid: feature_id
18 | queryTimeout: 15m # pretty high to allow debugging
19 | collections:
20 | - id: foo
21 | tableName: ligplaatsen
22 | filters:
23 | properties:
24 | - name: straatnaam
25 | - name: postcode
26 | metadata:
27 | title: Foooo
28 | description: >-
29 | This description of collection Foooo is short.
30 | - id: bar
31 | tableName: ligplaatsen
32 | metadata:
33 | title: Barrr
34 | description: >-
35 | This description of collection Barrr is quite long, and as such would distract the user from the rest of the content on overview pages.
36 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec accumsan lectus id ipsum condimentum pretium. Aenean cursus et diam aliquam
37 | vestibulum. Cras at est risus. Suspendisse venenatis dignissim aliquet. Maecenas rhoncus mi vulputate mi ullamcorper tincidunt.
38 | Aliquam aliquet risus ut convallis finibus. Curabitur ut ultrices erat. Suspendisse et vehicula arcu, a lacinia ligula. Orci posuere.
39 | tableName: ligplaatsen
40 |
--------------------------------------------------------------------------------
/internal/ogc/features/testdata/config_features_bag_multiple_feature_tables.yaml:
--------------------------------------------------------------------------------
1 | ---
2 | version: 1.0.2
3 | title: OGC API Features
4 | abstract: Contains a slimmed-down/example version of the BAG-dataset
5 | baseUrl: http://localhost:8080
6 | serviceIdentifier: Feats
7 | license:
8 | name: CC0
9 | url: https://www.tldrlegal.com/license/creative-commons-cc0-1-0-universal
10 | ogcApi:
11 | features:
12 | datasources:
13 | defaultWGS84:
14 | geopackage:
15 | local:
16 | file: ./internal/ogc/features/datasources/geopackage/testdata/bag.gpkg
17 | fid: feature_id
18 | queryTimeout: 15m # pretty high to allow debugging
19 | collections:
20 | - id: ligplaatsen
21 | filters:
22 | properties:
23 | - name: straatnaam
24 | - name: postcode
25 | - id: standplaatsen
26 |
--------------------------------------------------------------------------------
/internal/ogc/features/testdata/config_features_bag_temporal.yaml:
--------------------------------------------------------------------------------
1 | ---
2 | version: 1.0.2
3 | title: OGC API Features
4 | abstract: Contains a slimmed-down/example version of the BAG-dataset
5 | baseUrl: http://localhost:8080
6 | serviceIdentifier: Feats
7 | license:
8 | name: CC0
9 | url: https://www.tldrlegal.com/license/creative-commons-cc0-1-0-universal
10 | ogcApi:
11 | features:
12 | datasources:
13 | defaultWGS84:
14 | geopackage:
15 | local:
16 | file: ./internal/ogc/features/datasources/geopackage/testdata/bag-temporal.gpkg
17 | fid: feature_id
18 | collections:
19 | - id: ligplaatsen
20 | metadata:
21 | description: ligplaatsen
22 | extent:
23 | srs: EPSG:4326
24 | interval: [ "\"1970-01-01T00:00:00Z\"", "null" ]
25 | temporalProperties:
26 | startDate: datum_strt
27 | endDate: datum_eind
28 | - id: standplaatsen
29 | metadata:
30 | description: standplaatsen
31 | extent:
32 | srs: EPSG:4326
33 | interval: [ "\"1970-01-01T00:00:00Z\"", "null" ]
34 | temporalProperties:
35 | startDate: datum_strt
36 | endDate: datum_eind
37 | - id: verblijfsobjecten
38 | metadata:
39 | description: verblijfsobjecten
40 | extent:
41 | srs: EPSG:4326
42 | interval: [ "\"1970-01-01T00:00:00Z\"", "null" ]
43 | temporalProperties:
44 | startDate: datum_strt
45 | endDate: datum_eind
46 |
--------------------------------------------------------------------------------
/internal/ogc/features/testdata/config_features_external_fid.yaml:
--------------------------------------------------------------------------------
1 | ---
2 | version: 1.0.2
3 | title: OGC API Features
4 | abstract: Example dataset with external FIDs and relations between features
5 | baseUrl: http://localhost:8080
6 | serviceIdentifier: Feats
7 | license:
8 | name: CC0
9 | url: https://www.tldrlegal.com/license/creative-commons-cc0-1-0-universal
10 | ogcApi:
11 | features:
12 | datasources:
13 | defaultWGS84:
14 | geopackage:
15 | local:
16 | file: ./internal/ogc/features/datasources/geopackage/testdata/external-fid.gpkg
17 | fid: feature_id
18 | externalFid: external_fid
19 | collections:
20 | - id: ligplaatsen
21 | metadata:
22 | title: Ligplaatsen
23 | description: Ligplaatsen example data
24 | - id: standplaatsen
25 | metadata:
26 | title: Standplaatsen
27 | description: Standplaatsen example data
28 |
--------------------------------------------------------------------------------
/internal/ogc/features/testdata/config_features_geom_null_empty.yaml:
--------------------------------------------------------------------------------
1 | ---
2 | version: 1.0.2
3 | title: OGC API Features
4 | abstract: Contains a slimmed-down/example version of the BAG-dataset
5 | baseUrl: http://localhost:8080
6 | serviceIdentifier: Feats
7 | license:
8 | name: CC0
9 | url: https://www.tldrlegal.com/license/creative-commons-cc0-1-0-universal
10 | ogcApi:
11 | features:
12 | datasources:
13 | defaultWGS84:
14 | geopackage:
15 | local:
16 | file: ./internal/ogc/features/datasources/geopackage/testdata/null-empty-geoms.gpkg
17 | fid: feature_id
18 | queryTimeout: 15m # pretty high to allow debugging
19 | collections:
20 | - id: foo
21 | tableName: ligplaatsen
22 | filters:
23 | properties:
24 | - name: straatnaam
25 | - name: postcode
26 | metadata:
27 | title: Foooo
28 | description: Foooo
29 | - id: bar
30 | tableName: ligplaatsen
31 | metadata:
32 | title: Barrr
33 | description: Barrr
34 | tableName: ligplaatsen
35 | - id: baz
36 |
--------------------------------------------------------------------------------
/internal/ogc/features/testdata/config_features_multiple_collection_single_table.yaml:
--------------------------------------------------------------------------------
1 | ---
2 | version: 1.0.2
3 | title: OGC API Features
4 | abstract: Testdata
5 | baseUrl: http://localhost:8080
6 | serviceIdentifier: Feats
7 | license:
8 | name: CC0
9 | url: https://www.tldrlegal.com/license/creative-commons-cc0-1-0-universal
10 | ogcApi:
11 | features:
12 | datasources:
13 | defaultWGS84:
14 | geopackage:
15 | local:
16 | file: ./examples/resources/addresses-crs84.gpkg
17 | externalFid: external_fid
18 | collections:
19 | - id: dutch-addresses-first
20 | tableName: addresses # both use the same table, odd but allowed (with warning)
21 | - id: dutch-addresses
22 | tableName: addresses # both use the same table, odd but allowed (with warning)
23 | - id: dutch-addresses-third
24 | tableName: addresses # both use the same table, odd but allowed (with warning)
25 |
--------------------------------------------------------------------------------
/internal/ogc/features/testdata/config_features_multiple_gpkgs.yaml:
--------------------------------------------------------------------------------
1 | ---
2 | version: 1.0.2
3 | title: OGC API Features
4 | abstract: Contains a multiple geopackages in different projections
5 | baseUrl: http://localhost:8080
6 | serviceIdentifier: Feats
7 | license:
8 | name: CC0
9 | url: https://www.tldrlegal.com/license/creative-commons-cc0-1-0-universal
10 | ogcApi:
11 | features:
12 | datasources:
13 | defaultWGS84:
14 | geopackage:
15 | local:
16 | file: ./examples/resources/addresses-crs84.gpkg
17 | externalFid: external_fid
18 | additional:
19 | - srs: EPSG:28992
20 | geopackage:
21 | local:
22 | file: ./examples/resources/addresses-rd.gpkg
23 | externalFid: external_fid
24 | - srs: EPSG:3035
25 | geopackage:
26 | local:
27 | file: ./examples/resources/addresses-etrs89.gpkg
28 | externalFid: external_fid
29 | collections:
30 | - id: dutch-addresses
31 | tableName: addresses # name of the feature table (optional), when omitted collection ID is used.
32 | metadata:
33 | description: addresses
34 | temporalProperties:
35 | startDate: validfrom
36 | endDate: validto
37 | extent:
38 | srs: EPSG:4326
39 | interval: [ "\"1970-01-01T00:00:00Z\"", "null" ]
40 |
--------------------------------------------------------------------------------
/internal/ogc/features/testdata/config_features_multiple_gpkgs_multiple_levels.yaml:
--------------------------------------------------------------------------------
1 | ---
2 | version: 1.0.2
3 | title: OGC API Features
4 | abstract: Contains a multiple geopackages in different projections
5 | baseUrl: http://localhost:8080
6 | serviceIdentifier: Feats
7 | license:
8 | name: CC0
9 | url: https://www.tldrlegal.com/license/creative-commons-cc0-1-0-universal
10 | ogcApi:
11 | features:
12 | datasources:
13 | defaultWGS84:
14 | geopackage:
15 | local:
16 | file: ./examples/resources/addresses-crs84.gpkg
17 | externalFid: external_fid
18 | additional:
19 | - srs: EPSG:28992
20 | geopackage:
21 | local:
22 | file: ./examples/resources/addresses-rd.gpkg
23 | externalFid: external_fid
24 | collections:
25 | - id: dutch-addresses
26 | datasources:
27 | defaultWGS84:
28 | geopackage:
29 | local:
30 | file: ./examples/resources/addresses-crs84.gpkg
31 | externalFid: external_fid
32 | additional:
33 | - srs: EPSG:3035
34 | geopackage:
35 | local:
36 | file: ./examples/resources/addresses-etrs89.gpkg
37 | externalFid: external_fid
38 | tableName: addresses # name of the feature table (optional), when omitted collection ID is used.
39 | metadata:
40 | description: addresses
41 | temporalProperties:
42 | startDate: validfrom
43 | endDate: validto
44 | extent:
45 | srs: EPSG:4326
46 | interval: [ "\"1970-01-01T00:00:00Z\"", "null" ]
47 |
--------------------------------------------------------------------------------
/internal/ogc/features/testdata/config_features_properties_exclude.yaml:
--------------------------------------------------------------------------------
1 | ---
2 | version: 1.0.2
3 | title: OGC API Features
4 | abstract: Testdata
5 | baseUrl: http://localhost:8080
6 | serviceIdentifier: Feats
7 | license:
8 | name: CC0
9 | url: https://www.tldrlegal.com/license/creative-commons-cc0-1-0-universal
10 | ogcApi:
11 | features:
12 | datasources:
13 | defaultWGS84:
14 | geopackage:
15 | local:
16 | file: ./examples/resources/addresses-crs84.gpkg
17 | externalFid: external_fid
18 | collections:
19 | - id: dutch-addresses
20 | tableName: addresses
21 | propertiesExcludeUnknown: true
22 | properties:
23 | - alternativeidentifier
24 | - beginlifespanversion
25 | - building
26 | - validfrom
27 |
--------------------------------------------------------------------------------
/internal/ogc/features/testdata/config_features_properties_order.yaml:
--------------------------------------------------------------------------------
1 | ---
2 | version: 1.0.2
3 | title: OGC API Features
4 | abstract: Testdata
5 | baseUrl: http://localhost:8080
6 | serviceIdentifier: Feats
7 | license:
8 | name: CC0
9 | url: https://www.tldrlegal.com/license/creative-commons-cc0-1-0-universal
10 | ogcApi:
11 | features:
12 | datasources:
13 | defaultWGS84:
14 | geopackage:
15 | local:
16 | file: ./examples/resources/addresses-crs84.gpkg
17 | externalFid: external_fid
18 | collections:
19 | - id: dutch-addresses
20 | tableName: addresses
21 | propertiesInSpecificOrder: true
22 | properties:
23 | - building
24 | - alternativeidentifier
25 | - beginlifespanversion
26 | - endlifespanversion
27 | - validfrom
28 | - component_adminunitname_6
29 | - component_adminunitname_4
30 | - component_adminunitname_5
31 | - component_adminunitname_2
32 | - component_adminunitname_3
33 | - component_adminunitname_1
34 |
--------------------------------------------------------------------------------
/internal/ogc/features/testdata/config_features_properties_order_exclude.yaml:
--------------------------------------------------------------------------------
1 | ---
2 | version: 1.0.2
3 | title: OGC API Features
4 | abstract: Testdata
5 | baseUrl: http://localhost:8080
6 | serviceIdentifier: Feats
7 | license:
8 | name: CC0
9 | url: https://www.tldrlegal.com/license/creative-commons-cc0-1-0-universal
10 | ogcApi:
11 | features:
12 | datasources:
13 | defaultWGS84:
14 | geopackage:
15 | local:
16 | file: ./examples/resources/addresses-crs84.gpkg
17 | externalFid: external_fid
18 | collections:
19 | - id: dutch-addresses
20 | tableName: addresses
21 | propertiesExcludeUnknown: true
22 | propertiesInSpecificOrder: true
23 | properties:
24 | - building
25 | - alternativeidentifier
26 | - beginlifespanversion
27 | - endlifespanversion
28 | - validfrom
29 | - component_adminunitname_6
30 | - component_adminunitname_4
31 | - component_adminunitname_5
32 | - component_adminunitname_2
33 | - component_adminunitname_3
34 | - component_adminunitname_1
35 |
--------------------------------------------------------------------------------
/internal/ogc/features/testdata/config_features_roads.yaml:
--------------------------------------------------------------------------------
1 | ---
2 | version: 1.0.2
3 | title: OGC API Features
4 | abstract: Test to verify support for more complex geoms like polygons
5 | baseUrl: http://localhost:8080
6 | serviceIdentifier: Feats
7 | license:
8 | name: CC0
9 | url: https://www.tldrlegal.com/license/creative-commons-cc0-1-0-universal
10 | ogcApi:
11 | features:
12 | datasources:
13 | defaultWGS84:
14 | geopackage:
15 | local:
16 | file: ./internal/ogc/features/datasources/geopackage/testdata/roads.gpkg
17 | collections:
18 | - id: road
19 | metadata:
20 | title: Roads
21 | description: A few road parts in the Netherlands
22 |
--------------------------------------------------------------------------------
/internal/ogc/features/testdata/config_features_short_query_timeout.yaml:
--------------------------------------------------------------------------------
1 | ---
2 | version: 1.0.2
3 | title: OGC API Features
4 | abstract: Query's should fail since we use a very short (nanoseconds) query timeout
5 | baseUrl: http://localhost:8080
6 | serviceIdentifier: Feats
7 | license:
8 | name: CC0
9 | url: https://www.tldrlegal.com/license/creative-commons-cc0-1-0-universal
10 | ogcApi:
11 | features:
12 | datasources:
13 | defaultWGS84:
14 | geopackage:
15 | local:
16 | file: ./examples/resources/addresses-crs84.gpkg
17 | queryTimeout: 5ns
18 | additional:
19 | - srs: EPSG:28992
20 | geopackage:
21 | local:
22 | file: ./examples/resources/addresses-rd.gpkg
23 | queryTimeout: 5ns
24 | collections:
25 | - id: dutch-addresses
26 | tableName: addresses # name of the feature table (optional), when omitted collection ID is used.
27 | metadata:
28 | description: Query should fail since we use a very short (nanoseconds) query timeout
29 | extent:
30 | srs: EPSG:4326
31 | interval: [ "\"1970-01-01T00:00:00Z\"", "null" ]
32 |
33 |
--------------------------------------------------------------------------------
/internal/ogc/features/testdata/config_features_validation_disabled.yaml:
--------------------------------------------------------------------------------
1 | ---
2 | version: 1.0.2
3 | title: OGC API Features
4 | abstract: Testdata
5 | baseUrl: http://localhost:8080
6 | serviceIdentifier: Feats
7 | license:
8 | name: CC0
9 | url: https://www.tldrlegal.com/license/creative-commons-cc0-1-0-universal
10 | ogcApi:
11 | features:
12 | validateResponses: false
13 | datasources:
14 | defaultWGS84:
15 | geopackage:
16 | local:
17 | file: ./examples/resources/addresses-crs84.gpkg
18 | externalFid: external_fid
19 | collections:
20 | - id: dutch-addresses
21 | tableName: addresses
22 |
--------------------------------------------------------------------------------
/internal/ogc/features/testdata/config_features_webconfig.yaml:
--------------------------------------------------------------------------------
1 | ---
2 | version: 1.0.2
3 | title: OGC API Features
4 | abstract: Contains a slimmed-down/example version of the BAG-dataset
5 | baseUrl: http://localhost:8080
6 | serviceIdentifier: Feats
7 | license:
8 | name: CC0
9 | url: https://www.tldrlegal.com/license/creative-commons-cc0-1-0-universal
10 | ogcApi:
11 | features:
12 | datasources:
13 | defaultWGS84:
14 | geopackage:
15 | local:
16 | file: ./internal/ogc/features/datasources/geopackage/testdata/bag.gpkg
17 | fid: feature_id
18 | collections:
19 | - id: ligplaatsen
20 | metadata:
21 | title: Foo bar
22 | description: |
23 | Focus of this test is on this 'web' part in this configfile, and how it reflects in the HTML rendering
24 | web:
25 | featuresViewer:
26 | minScale: 3000
27 | maxScale: 40000
28 | featureViewer:
29 | minScale: 22
30 | urlAsHyperlink: true
31 |
--------------------------------------------------------------------------------
/internal/ogc/features/testdata/config_mapsheets.yaml:
--------------------------------------------------------------------------------
1 | ---
2 | version: 1.0.2
3 | title: OGC API Features
4 | abstract: Example config to test mapsheet
5 | baseUrl: http://localhost:8080
6 | serviceIdentifier: Feats
7 | license:
8 | name: CC0
9 | url: https://www.tldrlegal.com/license/creative-commons-cc0-1-0-universal
10 | ogcApi:
11 | features:
12 | datasources:
13 | defaultWGS84:
14 | geopackage:
15 | local:
16 | file: ./internal/ogc/features/datasources/geopackage/testdata/bag.gpkg
17 | fid: feature_id
18 | collections:
19 | - id: example_mapsheets
20 | tableName: ligplaatsen
21 | mapSheetDownloads:
22 | properties:
23 | # this gpgk doesn't actually contain mapsheets, we just (mis)use some columns
24 | # in order to test the mapsheet functionality
25 | assetUrl: rdf_seealso
26 | size: nummer_id
27 | mediaType: application/octet-stream
28 | mapSheetId: nummer_id
29 | metadata:
30 | title: Dummy mapsheets
31 | description: Map sheets test
32 | links:
33 | downloads:
34 | - name: Full download
35 | assetUrl: https://example.com/awesome.zip
36 | size: 123MB
37 | mediaType: application/zip
38 |
--------------------------------------------------------------------------------
/internal/ogc/features/testdata/expected_bar_collection_snippet.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
This description of collection Barrr is quite long, and as such would distract the user from the rest of the content on overview pages. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec accumsan lectus id ipsum condimentum pretium. Aenean cursus et diam aliquam vestibulum. Cras at est risus. Suspendisse venenatis dignissim aliquet. Maecenas rhoncus mi vulputate mi ullamcorper…
7 |
--------------------------------------------------------------------------------
/viewer/src/app/link.ts:
--------------------------------------------------------------------------------
1 | export type Link = {
2 | /**
3 | * Supplies the URI to a remote resource (or resource fragment).
4 | */
5 | href: string
6 | /**
7 | * A hint indicating what the language of the result of dereferencing the link should be.
8 | */
9 | hreflang?: string
10 | length?: number
11 | /**
12 | * The type or semantics of the relation.
13 | */
14 | rel: string
15 | /**
16 | * Use `true` if the `href` property contains a URI template with variables that needs to be substituted by values to get a URI
17 | */
18 | templated?: boolean
19 | /**
20 | * Used to label the destination of a link such that it can be used as a human-readable identifier.
21 | */
22 | title?: string
23 | /**
24 | * A hint indicating what the media type of the result of dereferencing the link should be.
25 | */
26 | type?: string
27 | /**
28 | * Without this parameter you should repeat a link for each media type the resource is offered.
29 | * Adding this parameter allows listing alternative media types that you can use for this resource. The value in the `type` parameter becomes the recommended media type.
30 | */
31 | types?: Array
32 | /**
33 | * A base path to retrieve semantic information about the variables used in URL template.
34 | */
35 | varBase?: string
36 | }
37 |
--------------------------------------------------------------------------------
/viewer/src/app/matrix-set.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@angular/core'
2 | import { HttpClient } from '@angular/common/http'
3 | import { Observable } from 'rxjs'
4 | import { Link } from './link'
5 |
6 | export interface MatrixSet {
7 | links: Link[]
8 | id: string
9 | title: string
10 | crs: string
11 | wellKnownScaleSet: string
12 | tileMatrices: TileMatrix[]
13 | orderedAxes: string[]
14 | }
15 |
16 | export interface TileMatrix {
17 | id: number
18 | tileWidth: number
19 | tileHeight: number
20 | matrixWidth: number
21 | matrixHeight: number
22 | scaleDenominator: number
23 | cellSize: number
24 | pointOfOrigin: number[]
25 | }
26 |
27 | export interface Matrix {
28 | title: string
29 | links: Link[]
30 | crs: string
31 | dataType: string
32 | tileMatrixSetId: string
33 | tileMatrixSetLimits: TileMatrixSetLimit[]
34 | }
35 |
36 | export interface TileMatrixSetLimit {
37 | tileMatrix: string
38 | minTileRow: number
39 | maxTileRow: number
40 | minTileCol: number
41 | maxTileCol: number
42 | }
43 |
44 | @Injectable({
45 | providedIn: 'root',
46 | })
47 | export class MatrixSetService {
48 | constructor(private http: HttpClient) {}
49 | getMatrixSet(url: string): Observable {
50 | return this.http.get(url)
51 | }
52 |
53 | getMatrix(url: string): Observable {
54 | return this.http.get(url)
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/viewer/src/app/object-info/object-info.component.css:
--------------------------------------------------------------------------------
1 | .objectinfo {
2 | margin-top: 0;
3 | margin-bottom: 0;
4 | margin-right: 0;
5 | background-color: white;
6 | overflow-y: auto;
7 | overflow-x: auto;
8 | max-height: 98%;
9 | }
10 |
11 | .featuretable {
12 | text-align: left;
13 | }
14 |
15 | .featuretablecaption {
16 | text-align: left;
17 | font-weight: bold;
18 | }
19 |
--------------------------------------------------------------------------------
/viewer/src/app/object-info/object-info.component.html:
--------------------------------------------------------------------------------
1 |