diff --git a/assets/i18n/en.yaml b/assets/i18n/en.yaml index 8bb48953..a653519f 100644 --- a/assets/i18n/en.yaml +++ b/assets/i18n/en.yaml @@ -105,3 +105,6 @@ Next: Next Items: items ReferenceDate: Date Size: size +ApplyFilters: Apply filters +ResetFilter: Reset filter +BrowseSuffix: or go straight to the features in diff --git a/assets/i18n/nl.yaml b/assets/i18n/nl.yaml index 2b08cff8..f07b9b3f 100644 --- a/assets/i18n/nl.yaml +++ b/assets/i18n/nl.yaml @@ -107,3 +107,6 @@ Next: Volgende Items: items ReferenceDate: Peildatum Size: omvang +ApplyFilters: Filters toepassen +ResetFilter: Filters wissen +BrowseSuffix: of ga direct naar de features in \ No newline at end of file diff --git a/go.mod b/go.mod index 40ceb3d4..6c7430f6 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/PDOK/gokoala -go 1.22.4 +go 1.22.5 require ( dario.cat/mergo v1.0.0 diff --git a/hack/crd/go.mod b/hack/crd/go.mod index 2384c315..5a51f04b 100644 --- a/hack/crd/go.mod +++ b/hack/crd/go.mod @@ -1,6 +1,6 @@ module crd -go 1.22.4 +go 1.22.5 require ( github.com/PDOK/gokoala v0.0.0 diff --git a/internal/engine/testdata/expected_multiple_ogc_apis_single_collection.html b/internal/engine/testdata/expected_multiple_ogc_apis_single_collection.html index d592a4f6..b8515d0d 100644 --- a/internal/engine/testdata/expected_multiple_ogc_apis_single_collection.html +++ b/internal/engine/testdata/expected_multiple_ogc_apis_single_collection.html @@ -1,14 +1,42 @@ -
  • -

    3D GeoVolumes

    +
    +
    +

    +3D GeoVolumes +

    +
    -
  • -
  • -

    Features

    + + + + +
    +
    +

    +Features +

    +
    +

    +Blader door de Features of ga direct naar de features in: +

    +

      -
    • Blader door de Features
    • -
    • Ga naar de features in WGS84 als GeoJSON
    • -
    • Ga naar de features in EPSG:28992 als GeoJSON
    • -
    • Ga naar de features in EPSG:28992 als JSON-FG
    • +
    • + +als +GeoJSON. +
    • +
    • + +als +JSON-FG. +
    +

    diff --git a/internal/ogc/common/geospatial/templates/collection.go.html b/internal/ogc/common/geospatial/templates/collection.go.html index 7583ebc9..8103762a 100644 --- a/internal/ogc/common/geospatial/templates/collection.go.html +++ b/internal/ogc/common/geospatial/templates/collection.go.html @@ -1,31 +1,111 @@ {{- /*gotype: github.com/PDOK/gokoala/internal/engine.TemplateData*/ -}} {{define "content"}} + + +

    {{ .Config.Title }} - {{ if and .Params.Metadata .Params.Metadata.Title }}{{ .Params.Metadata.Title }}{{ else }}{{ .Params.ID }}{{ end }}

    -
    -
    -
    -

    - {{ if and .Params.Metadata .Params.Metadata.Title }} - {{ .Params.Metadata.Title }} - {{ else }} - {{ .Params.ID }} +
    + {{ if and .Params.Metadata .Params.Metadata.Thumbnail }} +
    + {{ else }} +
    + {{ end }} + {{ if and .Params.Metadata .Params.Metadata.Description }} + {{ markdown .Params.Metadata.Description }} + {{ end }} + + + + {{ if and .Params.Metadata .Params.Metadata.Keywords }} + + + + {{ end }} - -
    - {{ if and .Params.Metadata .Params.Metadata.Description }} - {{ markdown .Params.Metadata.Description }} + {{ if and .Params.Metadata .Params.Metadata.LastUpdated }} +
    + + + {{ end }} - + {{ if and .Params.Metadata .Params.Metadata.Extent }} + + + + + {{ end }} + {{ if and .Params.Metadata .Params.Metadata.Extent .Params.Metadata.Extent.Interval }} + + + + + {{ end }} + +
    Collection details
    + {{ i18n "Keywords" }}: + + {{ .Params.Metadata.Keywords | join ", " }} +
    + {{ i18n "LastUpdated" }}: + + {{ toDate "2006-01-02T15:04:05Z07:00" .Params.Metadata.LastUpdated | date "2006-01-02" }} +
    + {{ i18n "GeographicExtent" }} + {{ if .Params.Metadata.Extent.Srs }} + ({{ .Params.Metadata.Extent.Srs }}): + {{ else }} + (CRS84): + {{ end }} + + {{ .Params.Metadata.Extent.Bbox | join ", " }} +
    + {{ i18n "TemporalExtent" }} (ISO-8601): + + {{ toDate "2006-01-02T15:04:05Z" ((first .Params.Metadata.Extent.Interval) | replace "\"" "") | date "2006-01-02" }} / + {{ if not (contains "null" (last .Params.Metadata.Extent.Interval)) }}{{ toDate "2006-01-02T15:04:05Z" ((last .Params.Metadata.Extent.Interval) | replace "\"" "") | date "2006-01-02" }}{{ else }}..{{ end }} +
    +
    + {{ if and .Params.Metadata .Params.Metadata.Thumbnail }} +
    + {{ .Params.ID }} Thumbnail +
    + {{ end }} +
    + + +
    - -
      - {{ if and .Config.OgcAPI.GeoVolumes .Config.OgcAPI.GeoVolumes.Collections }} - {{ if .Config.OgcAPI.GeoVolumes.Collections.ContainsID .Params.ID }} -
    • -

      3D GeoVolumes

      + {{ if and .Config.OgcAPI.GeoVolumes .Config.OgcAPI.GeoVolumes.Collections }} + {{ if .Config.OgcAPI.GeoVolumes.Collections.ContainsID .Params.ID }} +
      +
      +

      + 3D GeoVolumes +

      +
        {{ if and .Params.GeoVolumes .Params.GeoVolumes.Has3DTiles }}
      • {{ i18n "GoTo" }} 3D Tiles
      • @@ -34,96 +114,86 @@

        3D GeoVolumes

      • {{ i18n "GoTo" }} Quantized Mesh DTM
      • {{ end }} {{ if and .Params.GeoVolumes .Params.GeoVolumes.URL3DViewer }} -
      • {{ i18n "ViewIn" }} 3D Viewer
      • +
      • {{ i18n "ViewIn" }} 3D Viewer
      • {{ end }}
      -
    • - {{ end }} - {{ end }} +
    +
    +

    + {{ end }} + {{ end }} - {{ if and .Config.OgcAPI.Tiles .Config.OgcAPI.Tiles.Collections }} - {{ if .Config.OgcAPI.Tiles.Collections.ContainsID .Params.ID }} -
  • -

    Tiles

    -
      -
    • TODO (placeholder)
    • -
    -
  • - {{ end }} - {{ end }} + {{ if and .Config.OgcAPI.Tiles .Config.OgcAPI.Tiles.Collections }} + {{ if .Config.OgcAPI.Tiles.Collections.ContainsID .Params.ID }} +
    +
    +

    + Tiles +

    +
    +

    + TODO (placeholder) +

    +
    +
    +
    + {{ end }} + {{ end }} - {{ if and .Config.OgcAPI.Features .Config.OgcAPI.Features.Collections }} - {{ if .Config.OgcAPI.Features.Collections.ContainsID .Params.ID }} -
  • -

    Features

    -
      -
    • {{ i18n "Browse" }} Features
    • -
    • {{ i18n "GoTo" }} features in WGS84 {{ i18n "As" }} GeoJSON
    • - {{ range $index, $srs := .Config.OgcAPI.Features.ProjectionsForCollection .Params.ID }} - {{ range $formatKey, $formatName := $.AvailableFormatsFeatures }} + {{ if and .Config.OgcAPI.Features .Config.OgcAPI.Features.Collections }} + {{ if .Config.OgcAPI.Features.Collections.ContainsID .Params.ID }} +
      +
      +

      + Features +

      +
      +

      + {{ i18n "Browse" }} Features {{ i18n "BrowseSuffix" }}: +

      +

      +

        + {{ range $formatKey, $formatName := $.AvailableFormatsFeatures }} +
      • + + {{ i18n "As" }} + {{ $formatName }}. +
      • + {{ end }}
      -
      +

      - - {{ end }} - {{ end }} +
      +
      +
      + {{ end }} + {{ end }} - {{ if and .Params.Links .Params.Links.Downloads }} -
    • -

      Downloads

      - - - -
        - {{ if and .Params.Metadata .Params.Metadata.Keywords }} -
      • - {{ i18n "Keywords" }}: {{ .Params.Metadata.Keywords | join ", " }} -
      • - {{ end }} - {{/*
      • Schema: TODO link to collection schema
      • */}} - {{ if and .Params.Metadata .Params.Metadata.LastUpdated }} -
      • - {{ i18n "LastUpdated" }}: - {{ toDate "2006-01-02T15:04:05Z07:00" .Params.Metadata.LastUpdated | date "2006-01-02" }} -
      • - {{ end }} - {{ if and .Params.Metadata .Params.Metadata.Extent }} -
      • - {{ i18n "GeographicExtent" }} - {{ if .Params.Metadata.Extent.Srs }} - ({{ .Params.Metadata.Extent.Srs }}): - {{ else }} - (CRS84): - {{ end }} - {{ .Params.Metadata.Extent.Bbox | join ", " }} -
      • - {{ end }} - {{ if and .Params.Metadata .Params.Metadata.Extent .Params.Metadata.Extent.Interval }} -
      • - {{ i18n "TemporalExtent" }} - (ISO-8601): - {{ toDate "2006-01-02T15:04:05Z" ((first .Params.Metadata.Extent.Interval) | replace "\"" "") | date "2006-01-02" }} / - {{ if not (contains "null" (last .Params.Metadata.Extent.Interval)) }}{{ toDate "2006-01-02T15:04:05Z" ((last .Params.Metadata.Extent.Interval) | replace "\"" "") | date "2006-01-02" }}{{ else }}..{{ end }} -
      • - {{ end }} -
      +
    +
  • +
    -
    - -{{end}} + {{ end }} + + + +{{end}} \ No newline at end of file diff --git a/internal/ogc/common/geospatial/templates/collections.go.json b/internal/ogc/common/geospatial/templates/collections.go.json index 0c3fe840..7d308e2f 100644 --- a/internal/ogc/common/geospatial/templates/collections.go.json +++ b/internal/ogc/common/geospatial/templates/collections.go.json @@ -83,7 +83,7 @@ "rel" : "alternate", "type" : "text/html", "title" : "Information about the {{ $coll.ID }} collection as HTML", - "href" : "{{ $baseUrl }}/collections/{{ $coll.ID }}?f=json" + "href" : "{{ $baseUrl }}/collections/{{ $coll.ID }}?f=html" } {{ if and $cfg.OgcAPI.GeoVolumes $cfg.OgcAPI.GeoVolumes.Collections }} {{ if $cfg.OgcAPI.GeoVolumes.Collections.ContainsID $coll.ID }} diff --git a/internal/ogc/features/domain/spatialref.go b/internal/ogc/features/domain/spatialref.go new file mode 100644 index 00000000..96766f90 --- /dev/null +++ b/internal/ogc/features/domain/spatialref.go @@ -0,0 +1,52 @@ +package domain + +import ( + "fmt" + "strconv" + "strings" +) + +const ( + CrsURIPrefix = "http://www.opengis.net/def/crs/" + UndefinedSRID = 0 + 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. + WGS84CodeOGC = "CRS84" + WGS84CrsURI = CrsURIPrefix + "OGC/1.3/" + WGS84CodeOGC +) + +// SRID Spatial Reference System Identifier: a unique value to unambiguously identify a spatial coordinate system. +// For example '28992' in https://www.opengis.net/def/crs/EPSG/0/28992 +type SRID int + +func (s SRID) GetOrDefault() int { + val := int(s) + if val <= 0 { + return WGS84SRID + } + return val +} + +func EpsgToSrid(srs string) (SRID, error) { + prefix := "EPSG:" + srsCode, found := strings.CutPrefix(srs, prefix) + if !found { + return -1, fmt.Errorf("expected SRS to start with '%s', got %s", prefix, srs) + } + srid, err := strconv.Atoi(srsCode) + if err != nil { + return -1, fmt.Errorf("expected EPSG code to have numeric value, got %s", srsCode) + } + return SRID(srid), nil +} + +// ContentCrs the coordinate reference system (represented as a URI) of the content/output to return. +type ContentCrs string + +// ToLink returns link target conforming to RFC 8288 +func (c ContentCrs) ToLink() string { + return fmt.Sprintf("<%s>", c) +} + +func (c ContentCrs) IsWGS84() bool { + return string(c) == WGS84CrsURI +} diff --git a/internal/ogc/features/domain/spatialref_test.go b/internal/ogc/features/domain/spatialref_test.go new file mode 100644 index 00000000..3dfbcda0 --- /dev/null +++ b/internal/ogc/features/domain/spatialref_test.go @@ -0,0 +1,49 @@ +package domain + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestGetOrDefault(t *testing.T) { + tests := []struct { + name string + srid SRID + expected int + }{ + {"Positive SRID", SRID(28992), 28992}, + {"Zero SRID", SRID(0), WGS84SRID}, + {"Negative SRID", SRID(-1), WGS84SRID}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.expected, tt.srid.GetOrDefault()) + }) + } +} + +func TestEpsgToSrid(t *testing.T) { + tests := []struct { + name string + srs string + expected SRID + expectError bool + }{ + {"Valid EPSG", "EPSG:28992", SRID(28992), false}, + {"Invalid prefix", "INVALID:28992", SRID(-1), true}, + {"Non-numeric EPSG code", "EPSG:ABC", SRID(-1), true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result, err := EpsgToSrid(tt.srs) + if tt.expectError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + assert.Equal(t, tt.expected, result) + } + }) + } +} diff --git a/internal/ogc/features/json.go b/internal/ogc/features/json.go index a4446766..4cd859d8 100644 --- a/internal/ogc/features/json.go +++ b/internal/ogc/features/json.go @@ -76,7 +76,7 @@ func (jf *jsonFeatures) featureAsGeoJSON(w http.ResponseWriter, r *http.Request, // JSON-FG func (jf *jsonFeatures) featuresAsJSONFG(w http.ResponseWriter, r *http.Request, collectionID string, - cursor domain.Cursors, featuresURL featureCollectionURL, fc *domain.FeatureCollection, crs ContentCrs) { + cursor domain.Cursors, featuresURL featureCollectionURL, fc *domain.FeatureCollection, crs domain.ContentCrs) { fgFC := domain.JSONFGFeatureCollection{} fgFC.ConformsTo = []string{domain.ConformanceJSONFGCore} @@ -109,7 +109,7 @@ func (jf *jsonFeatures) featuresAsJSONFG(w http.ResponseWriter, r *http.Request, // JSON-FG func (jf *jsonFeatures) featureAsJSONFG(w http.ResponseWriter, r *http.Request, collectionID string, - f *domain.Feature, url featureURL, crs ContentCrs) { + f *domain.Feature, url featureURL, crs domain.ContentCrs) { fgF := domain.JSONFGFeature{ ID: f.ID, @@ -338,7 +338,7 @@ func handleJSONEncodingFailure(err error, w http.ResponseWriter) { engine.RenderProblem(engine.ProblemServerError, w, "Failed to write JSON response") } -func setGeom(crs ContentCrs, jsonfgFeature *domain.JSONFGFeature, feature *domain.Feature) { +func setGeom(crs domain.ContentCrs, jsonfgFeature *domain.JSONFGFeature, feature *domain.Feature) { if crs.IsWGS84() { jsonfgFeature.Geometry = feature.Geometry } else { diff --git a/internal/ogc/features/main.go b/internal/ogc/features/main.go index 6874711d..1391b7eb 100644 --- a/internal/ogc/features/main.go +++ b/internal/ogc/features/main.go @@ -7,7 +7,6 @@ import ( "log" "net/http" "strconv" - "strings" "time" "github.com/PDOK/gokoala/config" @@ -24,12 +23,7 @@ import ( ) const ( - templatesDir = "internal/ogc/features/templates/" - crsURIPrefix = "http://www.opengis.net/def/crs/" - undefinedSRID = 0 - 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. - wgs84CodeOGC = "CRS84" - wgs84CrsURI = crsURIPrefix + "OGC/1.3/" + wgs84CodeOGC + templatesDir = "internal/ogc/features/templates/" ) var ( @@ -183,7 +177,7 @@ func (f *Features) Feature() http.HandlerFunc { handleCollectionNotFound(w, collectionID) return } - featureID, err := getFeatureID(r) + featureID, err := parseFeatureID(r) if err != nil { engine.RenderProblem(engine.ProblemBadRequest, w, err.Error()) return @@ -223,7 +217,7 @@ func (f *Features) Feature() http.HandlerFunc { } } -func getFeatureID(r *http.Request) (any, error) { +func parseFeatureID(r *http.Request) (any, error) { var featureID any featureID, err := uuid.Parse(chi.URLParam(r, "featureId")) if err != nil { @@ -296,7 +290,7 @@ func configureTopLevelDatasources(e *engine.Engine, result map[DatasourceKey]*Da } var defaultDS *DatasourceConfig for _, coll := range cfg.Collections { - key := DatasourceKey{srid: wgs84SRID, collectionID: coll.ID} + key := DatasourceKey{srid: domain.WGS84SRID, collectionID: coll.ID} if result[key] == nil { if defaultDS == nil { defaultDS = &DatasourceConfig{cfg.Collections, cfg.Datasources.DefaultWGS84} @@ -307,11 +301,11 @@ func configureTopLevelDatasources(e *engine.Engine, result map[DatasourceKey]*Da for _, additional := range cfg.Datasources.Additional { for _, coll := range cfg.Collections { - srid, err := epsgToSrid(additional.Srs) + srid, err := domain.EpsgToSrid(additional.Srs) if err != nil { log.Fatal(err) } - key := DatasourceKey{srid: srid, collectionID: coll.ID} + key := DatasourceKey{srid: srid.GetOrDefault(), collectionID: coll.ID} if result[key] == nil { result[key] = &DatasourceConfig{cfg.Collections, additional.Datasource} } @@ -326,15 +320,15 @@ func configureCollectionDatasources(e *engine.Engine, result map[DatasourceKey]* continue } defaultDS := &DatasourceConfig{cfg.Collections, coll.Features.Datasources.DefaultWGS84} - result[DatasourceKey{srid: wgs84SRID, collectionID: coll.ID}] = defaultDS + result[DatasourceKey{srid: domain.WGS84SRID, collectionID: coll.ID}] = defaultDS for _, additional := range coll.Features.Datasources.Additional { - srid, err := epsgToSrid(additional.Srs) + srid, err := domain.EpsgToSrid(additional.Srs) if err != nil { log.Fatal(err) } additionalDS := &DatasourceConfig{cfg.Collections, additional.Datasource} - result[DatasourceKey{srid: srid, collectionID: coll.ID}] = additionalDS + result[DatasourceKey{srid: srid.GetOrDefault(), collectionID: coll.ID}] = additionalDS } } } @@ -350,19 +344,6 @@ func newDatasource(e *engine.Engine, coll config.GeoSpatialCollections, dsConfig return datasource } -func epsgToSrid(srs string) (int, error) { - prefix := "EPSG:" - srsCode, found := strings.CutPrefix(srs, prefix) - if !found { - return -1, fmt.Errorf("expected configured SRS to start with '%s', got %s", prefix, srs) - } - srid, err := strconv.Atoi(srsCode) - if err != nil { - return -1, fmt.Errorf("expected EPSG code to have numeric value, got %s", srsCode) - } - return srid, nil -} - func handleCollectionNotFound(w http.ResponseWriter, collectionID string) { msg := fmt.Sprintf("collection %s doesn't exist in this features service", collectionID) log.Println(msg) @@ -397,11 +378,11 @@ func handleFeatureQueryError(w http.ResponseWriter, collectionID string, feature engine.RenderProblem(engine.ProblemServerError, w, msg) // don't include sensitive information in details msg } -func querySingleDatasource(input SRID, output SRID, bbox *geom.Extent) bool { +func querySingleDatasource(input domain.SRID, output domain.SRID, bbox *geom.Extent) bool { return bbox == nil || int(input) == int(output) || - (int(input) == undefinedSRID && int(output) == wgs84SRID) || - (int(input) == wgs84SRID && int(output) == undefinedSRID) + (int(input) == domain.UndefinedSRID && int(output) == domain.WGS84SRID) || + (int(input) == domain.WGS84SRID && int(output) == domain.UndefinedSRID) } func getTemporalCriteria(collection *config.GeoSpatialCollectionMetadata, referenceDate time.Time) ds.TemporalCriteria { diff --git a/internal/ogc/features/main_test.go b/internal/ogc/features/main_test.go index 66b07db7..1530494c 100644 --- a/internal/ogc/features/main_test.go +++ b/internal/ogc/features/main_test.go @@ -14,6 +14,7 @@ import ( "time" "github.com/PDOK/gokoala/internal/engine" + "github.com/PDOK/gokoala/internal/ogc/features/domain" "github.com/go-chi/chi/v5" "github.com/stretchr/testify/assert" ) @@ -51,7 +52,7 @@ func TestFeatures_CollectionContent(t *testing.T) { configFile: "internal/ogc/features/testdata/config_features_bag.yaml", url: "http://localhost:8080/collections/:collectionId/items", collectionID: "foo", - contentCrs: "<" + wgs84CrsURI + ">", + contentCrs: "<" + domain.WGS84CrsURI + ">", format: "json", }, want: want{ @@ -65,7 +66,7 @@ func TestFeatures_CollectionContent(t *testing.T) { configFile: "internal/ogc/features/testdata/config_features_bag.yaml", url: "http://localhost:8080/collections/:collectionId/items?limit=2", collectionID: "foo", - contentCrs: "<" + wgs84CrsURI + ">", + contentCrs: "<" + domain.WGS84CrsURI + ">", format: "json", }, want: want{ @@ -79,7 +80,7 @@ func TestFeatures_CollectionContent(t *testing.T) { configFile: "internal/ogc/features/testdata/config_features_bag.yaml", url: "http://localhost:8080/collections/tunneldelen/items?f=json&cursor=Dv4%7CNwyr1Q&limit=2", collectionID: "foo", - contentCrs: "<" + wgs84CrsURI + ">", + contentCrs: "<" + domain.WGS84CrsURI + ">", format: "json", }, want: want{ @@ -105,7 +106,7 @@ func TestFeatures_CollectionContent(t *testing.T) { fields: fields{ configFile: "internal/ogc/features/testdata/config_features_bag.yaml", url: "http://localhost:8080/collections/:collectionId/items", - contentCrs: "<" + wgs84CrsURI + ">", + contentCrs: "<" + domain.WGS84CrsURI + ">", collectionID: "foo", format: "docx", }, @@ -159,7 +160,7 @@ func TestFeatures_CollectionContent(t *testing.T) { configFile: "internal/ogc/features/testdata/config_features_bag.yaml", url: "http://localhost:8080/collections/:collectionId/items?limit=1", collectionID: "foo", - contentCrs: "<" + wgs84CrsURI + ">", + contentCrs: "<" + domain.WGS84CrsURI + ">", format: "html", }, want: want{ @@ -173,7 +174,7 @@ func TestFeatures_CollectionContent(t *testing.T) { configFile: "internal/ogc/features/testdata/config_features_bag.yaml", url: "http://localhost:8080/collections/:collectionId/items?straatnaam=Silodam", collectionID: "foo", - contentCrs: "<" + wgs84CrsURI + ">", + contentCrs: "<" + domain.WGS84CrsURI + ">", format: "json", }, want: want{ @@ -187,7 +188,7 @@ func TestFeatures_CollectionContent(t *testing.T) { configFile: "internal/ogc/features/testdata/config_features_bag.yaml", url: "http://localhost:8080/collections/:collectionId/items?straatnaam=Silodam", collectionID: "foo", - contentCrs: "<" + wgs84CrsURI + ">", + contentCrs: "<" + domain.WGS84CrsURI + ">", format: "html", }, want: want{ @@ -201,7 +202,7 @@ func TestFeatures_CollectionContent(t *testing.T) { configFile: "internal/ogc/features/testdata/config_features_bag.yaml", url: "http://localhost:8080/collections/:collectionId/items?straatnaam=Zandhoek&postcode=1104MM", collectionID: "foo", - contentCrs: "<" + wgs84CrsURI + ">", + contentCrs: "<" + domain.WGS84CrsURI + ">", format: "json", }, want: want{ @@ -215,7 +216,7 @@ func TestFeatures_CollectionContent(t *testing.T) { configFile: "internal/ogc/features/testdata/config_features_bag.yaml", url: "http://localhost:8080/collections/:collectionId/items?straatnaam=doesnotexist", collectionID: "foo", - contentCrs: "<" + wgs84CrsURI + ">", + contentCrs: "<" + domain.WGS84CrsURI + ">", format: "json", }, want: want{ @@ -229,7 +230,7 @@ func TestFeatures_CollectionContent(t *testing.T) { configFile: "internal/ogc/features/testdata/config_features_bag_allowed_values.yaml", url: "http://localhost:8080/collections/:collectionId/items?straatnaam=Silodam", collectionID: "foo", - contentCrs: "<" + wgs84CrsURI + ">", + contentCrs: "<" + domain.WGS84CrsURI + ">", format: "json", }, want: want{ @@ -256,7 +257,7 @@ func TestFeatures_CollectionContent(t *testing.T) { configFile: "internal/ogc/features/testdata/config_features_bag_allowed_values.yaml", url: "http://localhost:8080/collections/:collectionId/items?type=Ligplaats&straatnaam=Westerdok&limit=3", collectionID: "foo", - contentCrs: "<" + wgs84CrsURI + ">", + contentCrs: "<" + domain.WGS84CrsURI + ">", format: "json", }, want: want{ @@ -270,7 +271,7 @@ func TestFeatures_CollectionContent(t *testing.T) { configFile: "internal/ogc/features/testdata/config_features_multiple_gpkgs.yaml", url: "http://localhost:8080/collections/:collectionId/items?crs=http://www.opengis.net/def/crs/OGC/1.3/CRS84&limit=2", collectionID: "dutch-addresses", - contentCrs: "<" + wgs84CrsURI + ">", + contentCrs: "<" + domain.WGS84CrsURI + ">", format: "json", }, want: want{ @@ -298,7 +299,7 @@ func TestFeatures_CollectionContent(t *testing.T) { configFile: "internal/ogc/features/testdata/config_features_multiple_gpkgs.yaml", url: "http://localhost:8080/collections/dutch-addresses/items?bbox=4.86958187578342017%2C53.07965667574639212%2C4.88167082216529113%2C53.09197323827352477&cursor=Wl8%7C9YRHSw&f=json&limit=10", collectionID: "dutch-addresses", - contentCrs: "<" + wgs84CrsURI + ">", + contentCrs: "<" + domain.WGS84CrsURI + ">", format: "json", }, want: want{ @@ -312,7 +313,7 @@ func TestFeatures_CollectionContent(t *testing.T) { configFile: "internal/ogc/features/testdata/config_features_multiple_gpkgs.yaml", url: "http://localhost:8080/collections/dutch-addresses/items?bbox=4.86958187578342017%2C53.07965667574639212%2C4.88167082216529113%2C53.09197323827352477&cursor=Wl8%7C9YRHSw&f=jsonfg&limit=10", collectionID: "dutch-addresses", - contentCrs: "<" + wgs84CrsURI + ">", + contentCrs: "<" + domain.WGS84CrsURI + ">", format: "json", }, want: want{ @@ -340,7 +341,7 @@ func TestFeatures_CollectionContent(t *testing.T) { configFile: "internal/ogc/features/testdata/config_features_multiple_gpkgs.yaml", url: "http://localhost:8080/collections/dutch-addresses/items?bbox=120379.69%2C566718.72%2C120396.30%2C566734.62&bbox-crs=http%3A%2F%2Fwww.opengis.net%2Fdef%2Fcrs%2FEPSG%2F0%2F28992&f=json&limit=10", collectionID: "dutch-addresses", - contentCrs: "<" + wgs84CrsURI + ">", + contentCrs: "<" + domain.WGS84CrsURI + ">", format: "json", }, want: want{ @@ -354,7 +355,7 @@ func TestFeatures_CollectionContent(t *testing.T) { configFile: "internal/ogc/features/testdata/config_features_multiple_gpkgs_multiple_levels.yaml", url: "http://localhost:8080/collections/dutch-addresses/items?bbox=120379.69%2C566718.72%2C120396.30%2C566734.62&bbox-crs=http%3A%2F%2Fwww.opengis.net%2Fdef%2Fcrs%2FEPSG%2F0%2F28992&f=json&limit=10", collectionID: "dutch-addresses", - contentCrs: "<" + wgs84CrsURI + ">", + contentCrs: "<" + domain.WGS84CrsURI + ">", format: "json", }, want: want{ @@ -368,7 +369,7 @@ func TestFeatures_CollectionContent(t *testing.T) { configFile: "internal/ogc/features/testdata/config_features_multiple_gpkgs.yaml", url: "http://localhost:8080/collections/dutch-addresses/items?bbox=120379.69%2C566718.72%2C120396.30%2C566734.62&bbox-crs=http%3A%2F%2Fwww.opengis.net%2Fdef%2Fcrs%2FEPSG%2F0%2F28992&f=jsonfg&limit=10", collectionID: "dutch-addresses", - contentCrs: "<" + wgs84CrsURI + ">", + contentCrs: "<" + domain.WGS84CrsURI + ">", format: "json", }, want: want{ @@ -410,7 +411,7 @@ func TestFeatures_CollectionContent(t *testing.T) { configFile: "internal/ogc/features/testdata/config_features_multiple_gpkgs.yaml", url: "http://localhost:8080/collections/dutch-addresses/items?bbox=4.86%2C53.07%2C4.88%2C53.09&bbox-crs=http://www.opengis.net/def/crs/OGC/1.3/CRS84&f=json&limit=10", collectionID: "dutch-addresses", - contentCrs: "<" + wgs84CrsURI + ">", + contentCrs: "<" + domain.WGS84CrsURI + ">", format: "json", }, want: want{ @@ -424,7 +425,7 @@ func TestFeatures_CollectionContent(t *testing.T) { configFile: "internal/ogc/features/testdata/config_features_validation_disabled.yaml", url: "http://localhost:8080/collections/dutch-addresses/items?bbox=4.86%2C53.07%2C4.88%2C53.09&bbox-crs=http://www.opengis.net/def/crs/OGC/1.3/CRS84&f=json&limit=10", collectionID: "dutch-addresses", - contentCrs: "<" + wgs84CrsURI + ">", + contentCrs: "<" + domain.WGS84CrsURI + ">", format: "json", }, want: want{ @@ -438,7 +439,7 @@ func TestFeatures_CollectionContent(t *testing.T) { configFile: "internal/ogc/features/testdata/config_features_multiple_collection_single_table.yaml", url: "http://localhost:8080/collections/dutch-addresses/items?bbox=4.86%2C53.07%2C4.88%2C53.09&bbox-crs=http://www.opengis.net/def/crs/OGC/1.3/CRS84&f=json&limit=10", collectionID: "dutch-addresses", - contentCrs: "<" + wgs84CrsURI + ">", + contentCrs: "<" + domain.WGS84CrsURI + ">", format: "json", }, want: want{ @@ -452,7 +453,7 @@ func TestFeatures_CollectionContent(t *testing.T) { configFile: "internal/ogc/features/testdata/config_features_bag_temporal.yaml", url: "http://localhost:8080/collections/standplaatsen/items?datetime=2020-05-20T00:00:00Z&limit=10", collectionID: "standplaatsen", - contentCrs: "<" + wgs84CrsURI + ">", + contentCrs: "<" + domain.WGS84CrsURI + ">", format: "json", }, want: want{ @@ -466,7 +467,7 @@ func TestFeatures_CollectionContent(t *testing.T) { configFile: "internal/ogc/features/testdata/config_features_short_query_timeout.yaml", url: "http://localhost:8080/collections/:collectionId/items", collectionID: "dutch-addresses", - contentCrs: "<" + wgs84CrsURI + ">", + contentCrs: "<" + domain.WGS84CrsURI + ">", format: "json", }, want: want{ diff --git a/internal/ogc/features/templates/features.go.html b/internal/ogc/features/templates/features.go.html index 7d9504c8..af51b536 100644 --- a/internal/ogc/features/templates/features.go.html +++ b/internal/ogc/features/templates/features.go.html @@ -6,8 +6,8 @@
    @@ -51,12 +86,12 @@

    -
    +
    - @@ -71,9 +106,8 @@

    - +
    {{ end }} @@ -81,8 +115,7 @@

    - {{/* see Limit.Max in config.go, can't be smaller than 100 */}} @@ -100,8 +133,7 @@

    title="{{ $pfName | title }}">{{ $pfName | title }}
    {{ if and $pf $pf.AllowedValues }} - {{ range $value := $pf.AllowedValues }} @@ -109,15 +141,16 @@

    {{ end }} {{ else }} - + value="{{ index $.Params.PropertyFilters $pfName }}"> {{ end }}

    {{ end }} + +
    @@ -150,11 +183,13 @@

    {{ if $mapSheetProperties }} viewer.addEventListener('box', selectBox => { - let newUrl = new URL(updateQueryString('bbox', selectBox.detail, true)) - newUrl.searchParams.set('f', 'json') + let newUrl = new URL(updateQueryString('bbox', selectBox.detail)) + callUrl(newUrl.toString(), true) + // when moving the map to load additional sheets we don't want to do a full page reload (like we // do when one draws a bbox). Therefor we update the browser URL + link references (like GeoJSON/JSON-FG) // on the page manually. + newUrl.searchParams.set('f', 'json') viewer.setAttribute('items-url', newUrl.toString()) {{ range $formatKey, $formatName := .AvailableFormats }} if (document.getElementById("format-{{ $formatKey }}")) { @@ -164,11 +199,11 @@

    {{ end }} }) viewer.addEventListener('activeFeature', activeFeature => { - updateQueryString("{{ $mapSheetProperties.MapSheetID }}", activeFeature.detail.get('{{ $mapSheetProperties.MapSheetID }}')) + callUrl(updateQueryString("{{ $mapSheetProperties.MapSheetID }}", activeFeature.detail.get('{{ $mapSheetProperties.MapSheetID }}'))) }) {{ else }} viewer.addEventListener('box', selectBox => { - updateQueryString('bbox', selectBox.detail) + callUrl(updateQueryString('bbox', selectBox.detail)) }) {{ end }} diff --git a/internal/ogc/features/testdata/expected_multiple_feature_tables_single_geopackage.json b/internal/ogc/features/testdata/expected_multiple_feature_tables_single_geopackage.json index 4011a339..2310e7e4 100644 --- a/internal/ogc/features/testdata/expected_multiple_feature_tables_single_geopackage.json +++ b/internal/ogc/features/testdata/expected_multiple_feature_tables_single_geopackage.json @@ -38,7 +38,7 @@ "rel": "alternate", "type": "text/html", "title": "Information about the ligplaatsen collection as HTML", - "href": "http://localhost:8080/collections/ligplaatsen?f=json" + "href": "http://localhost:8080/collections/ligplaatsen?f=html" }, { "rel": "items", @@ -79,7 +79,7 @@ "rel": "alternate", "type": "text/html", "title": "Information about the standplaatsen collection as HTML", - "href": "http://localhost:8080/collections/standplaatsen?f=json" + "href": "http://localhost:8080/collections/standplaatsen?f=html" }, { "rel": "items", diff --git a/internal/ogc/features/testdata/expected_straatnaam_silodam.html b/internal/ogc/features/testdata/expected_straatnaam_silodam.html index 0e329427..f73b4cc8 100644 --- a/internal/ogc/features/testdata/expected_straatnaam_silodam.html +++ b/internal/ogc/features/testdata/expected_straatnaam_silodam.html @@ -1,22 +1,18 @@
    - +
    - +
    - +
    - +
    \ No newline at end of file diff --git a/internal/ogc/features/url.go b/internal/ogc/features/url.go index 4072cd15..62079db8 100644 --- a/internal/ogc/features/url.go +++ b/internal/ogc/features/url.go @@ -13,9 +13,8 @@ import ( "time" "github.com/PDOK/gokoala/config" - "github.com/PDOK/gokoala/internal/engine" - "github.com/PDOK/gokoala/internal/ogc/features/domain" + d "github.com/PDOK/gokoala/internal/ogc/features/domain" "github.com/go-spatial/geom" ) @@ -37,30 +36,6 @@ var ( checksumExcludedParams = []string{engine.FormatParam, cursorParam} // don't include these in checksum ) -// SRID Spatial Reference System Identifier: a unique value to unambiguously identify a spatial coordinate system. -// For example '28992' in https://www.opengis.net/def/crs/EPSG/0/28992 -type SRID int - -func (s SRID) GetOrDefault() int { - val := int(s) - if val <= 0 { - return wgs84SRID - } - return val -} - -// ContentCrs the coordinate reference system (represented as a URI) of the content/output to return. -type ContentCrs string - -// ToLink returns link target conforming to RFC 8288 -func (c ContentCrs) ToLink() string { - return fmt.Sprintf("<%s>", c) -} - -func (c ContentCrs) IsWGS84() bool { - return string(c) == wgs84CrsURI -} - // URL to a page in a collection of features type featureCollectionURL struct { baseURL url.URL @@ -71,14 +46,14 @@ type featureCollectionURL struct { } // parse the given URL to values required to delivery a set of Features -func (fc featureCollectionURL) parse() (encodedCursor domain.EncodedCursor, limit int, inputSRID SRID, outputSRID SRID, - contentCrs ContentCrs, bbox *geom.Extent, referenceDate time.Time, propertyFilters map[string]string, err error) { +func (fc featureCollectionURL) parse() (encodedCursor d.EncodedCursor, limit int, inputSRID d.SRID, outputSRID d.SRID, + contentCrs d.ContentCrs, bbox *geom.Extent, referenceDate time.Time, propertyFilters map[string]string, err error) { err = fc.validateNoUnknownParams() if err != nil { return } - encodedCursor = domain.EncodedCursor(fc.params.Get(cursorParam)) + encodedCursor = d.EncodedCursor(fc.params.Get(cursorParam)) limit, limitErr := parseLimit(fc.params, fc.limit) outputSRID, outputSRIDErr := parseCrsToSRID(fc.params, crsParam) contentCrs = parseCrsToContentCrs(fc.params) @@ -137,7 +112,7 @@ func (fc featureCollectionURL) toSelfURL(collectionID string, format string) str return result.String() } -func (fc featureCollectionURL) toPrevNextURL(collectionID string, cursor domain.EncodedCursor, format string) string { +func (fc featureCollectionURL) toPrevNextURL(collectionID string, cursor d.EncodedCursor, format string) string { copyParams := clone(fc.params) copyParams.Set(engine.FormatParam, format) copyParams.Set(cursorParam, cursor.String()) @@ -175,7 +150,7 @@ type featureURL struct { } // parse the given URL to values required to delivery a specific Feature -func (f featureURL) parse() (srid SRID, contentCrs ContentCrs, err error) { +func (f featureURL) parse() (srid d.SRID, contentCrs d.ContentCrs, err error) { err = f.validateNoUnknownParams() if err != nil { return @@ -223,12 +198,12 @@ func clone(params url.Values) url.Values { return copyParams } -func consolidateSRIDs(bboxSRID SRID, filterSRID SRID) (inputSRID SRID, err error) { - if bboxSRID != undefinedSRID && filterSRID != undefinedSRID && bboxSRID != filterSRID { +func consolidateSRIDs(bboxSRID d.SRID, filterSRID d.SRID) (inputSRID d.SRID, err error) { + if bboxSRID != d.UndefinedSRID && filterSRID != d.UndefinedSRID && bboxSRID != filterSRID { return 0, errors.New("bbox-crs and filter-crs need to be equal. " + "Can't use more than one CRS as input, but input and output CRS may differ") } - if bboxSRID != undefinedSRID || filterSRID != undefinedSRID { + if bboxSRID != d.UndefinedSRID || filterSRID != d.UndefinedSRID { inputSRID = bboxSRID // or filterCrs, both the same } return inputSRID, err @@ -254,14 +229,14 @@ func parseLimit(params url.Values, limitCfg config.Limit) (int, error) { return limit, err } -func parseBbox(params url.Values) (*geom.Extent, SRID, error) { +func parseBbox(params url.Values) (*geom.Extent, d.SRID, error) { bboxSRID, err := parseCrsToSRID(params, bboxCrsParam) if err != nil { - return nil, undefinedSRID, err + return nil, d.UndefinedSRID, err } if params.Get(bboxParam) == "" { - return nil, undefinedSRID, nil + return nil, d.UndefinedSRID, nil } bboxValues := strings.Split(params.Get(bboxParam), ",") if len(bboxValues) != 4 { @@ -280,35 +255,35 @@ func parseBbox(params url.Values) (*geom.Extent, SRID, error) { return &extent, bboxSRID, nil } -func parseCrsToContentCrs(params url.Values) ContentCrs { +func parseCrsToContentCrs(params url.Values) d.ContentCrs { param := params.Get(crsParam) if param == "" { - return wgs84CrsURI + return d.WGS84CrsURI } - return ContentCrs(param) + return d.ContentCrs(param) } -func parseCrsToSRID(params url.Values, paramName string) (SRID, error) { +func parseCrsToSRID(params url.Values, paramName string) (d.SRID, error) { param := params.Get(paramName) if param == "" { - return undefinedSRID, nil + return d.UndefinedSRID, nil } param = strings.TrimSpace(param) - if !strings.HasPrefix(param, crsURIPrefix) { - return undefinedSRID, fmt.Errorf("%s param should start with %s, got: %s", paramName, crsURIPrefix, param) + if !strings.HasPrefix(param, d.CrsURIPrefix) { + return d.UndefinedSRID, fmt.Errorf("%s param should start with %s, got: %s", paramName, d.CrsURIPrefix, param) } - var srid SRID + var srid d.SRID lastIndex := strings.LastIndex(param, "/") if lastIndex != -1 { crsCode := param[lastIndex+1:] - if crsCode == wgs84CodeOGC { - return wgs84SRID, nil // CRS84 is WGS84, just like EPSG:4326 (only axis order differs but SRID is the same) + if crsCode == d.WGS84CodeOGC { + return d.WGS84SRID, nil // CRS84 is WGS84, just like EPSG:4326 (only axis order differs but SRID is the same) } val, err := strconv.Atoi(crsCode) if err != nil { return 0, fmt.Errorf("expected numerical CRS code, received: %s", crsCode) } - srid = SRID(val) + srid = d.SRID(val) } return srid, nil } @@ -350,7 +325,7 @@ func parseDateTime(params url.Values, datetimeSupported bool) (time.Time, error) return time.Time{}, nil } -func parseFilter(params url.Values) (filter string, filterSRID SRID, err error) { +func parseFilter(params url.Values) (filter string, filterSRID d.SRID, err error) { filter = params.Get(filterParam) filterSRID, _ = parseCrsToSRID(params, filterCrsParam)