diff --git a/.github/workflows/e2e-test.yml b/.github/workflows/e2e-test.yml
index 569f5281..44c45e30 100644
--- a/.github/workflows/e2e-test.yml
+++ b/.github/workflows/e2e-test.yml
@@ -8,7 +8,7 @@ jobs:
steps:
- uses: actions/checkout@v3
- # Build a local test image for re-use across end-to-end tests
+ # Build a local test image for (potential) re-use across end-to-end tests
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
with:
@@ -19,13 +19,12 @@ jobs:
push: false
tags: gokoala:local
- # E2E Test
+ # E2E Test (Docker Compose V2 can't unfortunately re-use image, so build it again).
- name: E2E Test => OGC API Features with Azure GeoPackage
run: |
- docker-compose -f ./examples/docker-compose-features-azure.yaml pull \
- && \
- docker-compose -f ./examples/docker-compose-features-azure.yaml up \
- --no-build --exit-code-from smoketest
+ docker compose -f ./examples/docker-compose-features-azure.yaml build gokoala && \
+ docker compose -f ./examples/docker-compose-features-azure.yaml up \
+ --exit-code-from smoketest
- name: Start GoKoala test instance
run: |
diff --git a/config/config.go b/config/config.go
index 375f50ca..c053e9eb 100644
--- a/config/config.go
+++ b/config/config.go
@@ -440,9 +440,14 @@ type CollectionEntryFeatures struct {
// +optional
Filters FeatureFilters `yaml:"filters,omitempty" json:"filters,omitempty"`
- // Downloads available for this collection
+ // Downloads available for this collection through map sheets. Note that 'map sheets' refer to a map
+ // divided in rectangle areas that can be downloaded individually.
// +optional
MapSheetDownloads *MapSheetDownloads `yaml:"mapSheetDownloads,omitempty" json:"mapSheetDownloads,omitempty"`
+
+ // Configuration specifically related to HTML/Web representation
+ // +optional
+ Web *WebConfig `yaml:"web,omitempty" json:"web,omitempty"`
}
// +kubebuilder:object:generate=true
@@ -485,25 +490,50 @@ type DownloadLink struct {
// +kubebuilder:object:generate=true
type MapSheetDownloads struct {
- // Properties that provide the download details per map sheet
+ // Properties that provide the download details per map sheet. Note that 'map sheets' refer to a map
+ // divided in rectangle areas that can be downloaded individually.
Properties MapSheetDownloadProperties `yaml:"properties" json:"properties" validate:"required"`
}
// +kubebuilder:object:generate=true
type MapSheetDownloadProperties struct {
- // Property containing file download URL
+ // Property/column containing file download URL
AssetURL string `yaml:"assetUrl" json:"assetUrl" validate:"required"`
- // Property containing file size
+ // Property/column containing file size
Size string `yaml:"size" json:"size" validate:"required"`
- // Property containing file media type
+ // The actual media type (not a property/column) of the download, like application/zip.
MediaType MediaType `yaml:"mediaType" json:"mediaType" validate:"required"`
- // Property containing the map sheet identifier
+ // Property/column containing the map sheet identifier
MapSheetID string `yaml:"mapSheetId" json:"mapSheetId" validate:"required"`
}
+// +kubebuilder:object:generate=true
+type WebConfig struct {
+ // Viewer config for displaying multiple features on a map
+ // +optional
+ FeaturesViewer *FeaturesViewer `yaml:"featuresViewer,omitempty" json:"featuresViewer,omitempty"`
+
+ // Viewer config for displaying a single feature on a map
+ // +optional
+ FeatureViewer *FeaturesViewer `yaml:"featureViewer,omitempty" json:"featureViewer,omitempty"`
+}
+
+// +kubebuilder:object:generate=true
+type FeaturesViewer struct {
+ // Maximum initial zoom level of the viewer when rendering features, specified by scale denominator.
+ // Defaults to 1000 (= scale 1:1000).
+ // +optional
+ MinScale int `yaml:"minScale,omitempty" json:"minScale,omitempty" validate:"gt=0" default:"1000"`
+
+ // Minimal initial zoom level of the viewer when rendering features, specified by scale denominator
+ // (not set by default).
+ // +optional
+ MaxScale *int `yaml:"maxScale,omitempty" json:"maxScale,omitempty" validate:"omitempty,gt=0,gtefield=MinScale"`
+}
+
// +kubebuilder:object:generate=true
type OgcAPI3dGeoVolumes struct {
// Reference to the server (or object storage) hosting the 3D Tiles
@@ -612,24 +642,6 @@ func (oaf *OgcAPIFeatures) ProjectionsForCollection(collectionID string) []strin
return result
}
-func (oaf *OgcAPIFeatures) PropertyFiltersForCollection(collectionID string) []PropertyFilter {
- for _, coll := range oaf.Collections {
- if coll.ID == collectionID && coll.Features != nil && coll.Features.Filters.Properties != nil {
- return coll.Features.Filters.Properties
- }
- }
- return []PropertyFilter{}
-}
-
-func (oaf *OgcAPIFeatures) MapSheetPropertiesForCollection(collectionID string) *MapSheetDownloadProperties {
- for _, coll := range oaf.Collections {
- if coll.ID == collectionID && coll.Features != nil && coll.Features.MapSheetDownloads != nil {
- return &coll.Features.MapSheetDownloads.Properties
- }
- }
- return nil
-}
-
// +kubebuilder:object:generate=true
type OgcAPIProcesses struct {
// Enable to advertise dismiss operations on the conformance page
diff --git a/config/zz_generated.deepcopy.go b/config/zz_generated.deepcopy.go
index f26f5d85..633d9d15 100644
--- a/config/zz_generated.deepcopy.go
+++ b/config/zz_generated.deepcopy.go
@@ -80,6 +80,11 @@ func (in *CollectionEntryFeatures) DeepCopyInto(out *CollectionEntryFeatures) {
*out = new(MapSheetDownloads)
(*in).DeepCopyInto(*out)
}
+ if in.Web != nil {
+ in, out := &in.Web, &out.Web
+ *out = new(WebConfig)
+ (*in).DeepCopyInto(*out)
+ }
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CollectionEntryFeatures.
@@ -315,6 +320,26 @@ func (in *FeatureFilters) DeepCopy() *FeatureFilters {
return out
}
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *FeaturesViewer) DeepCopyInto(out *FeaturesViewer) {
+ *out = *in
+ if in.MaxScale != nil {
+ in, out := &in.MaxScale, &out.MaxScale
+ *out = new(int)
+ **out = **in
+ }
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FeaturesViewer.
+func (in *FeaturesViewer) DeepCopy() *FeaturesViewer {
+ if in == nil {
+ return nil
+ }
+ out := new(FeaturesViewer)
+ in.DeepCopyInto(out)
+ return out
+}
+
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *GeoPackage) DeepCopyInto(out *GeoPackage) {
*out = *in
@@ -944,6 +969,31 @@ func (in *TemporalProperties) DeepCopy() *TemporalProperties {
return out
}
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *WebConfig) DeepCopyInto(out *WebConfig) {
+ *out = *in
+ if in.FeaturesViewer != nil {
+ in, out := &in.FeaturesViewer, &out.FeaturesViewer
+ *out = new(FeaturesViewer)
+ (*in).DeepCopyInto(*out)
+ }
+ if in.FeatureViewer != nil {
+ in, out := &in.FeatureViewer, &out.FeatureViewer
+ *out = new(FeaturesViewer)
+ (*in).DeepCopyInto(*out)
+ }
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WebConfig.
+func (in *WebConfig) DeepCopy() *WebConfig {
+ if in == nil {
+ return nil
+ }
+ out := new(WebConfig)
+ in.DeepCopyInto(out)
+ return out
+}
+
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ZoomLevelRange) DeepCopyInto(out *ZoomLevelRange) {
*out = *in
diff --git a/examples/docker-compose-features-azure.yaml b/examples/docker-compose-features-azure.yaml
index 64c0f531..f5baa959 100644
--- a/examples/docker-compose-features-azure.yaml
+++ b/examples/docker-compose-features-azure.yaml
@@ -1,6 +1,4 @@
---
-version: "3"
-
services:
azurite:
image: mcr.microsoft.com/azure-storage/azurite:3.29.0
@@ -42,7 +40,6 @@ services:
retries: 30
gokoala:
- image: gokoala:local # run local image if available (used in CI)
build:
context: ../
dockerfile: Dockerfile
diff --git a/internal/ogc/features/html.go b/internal/ogc/features/html.go
index fa682a02..0c510137 100644
--- a/internal/ogc/features/html.go
+++ b/internal/ogc/features/html.go
@@ -51,6 +51,7 @@ type featureCollectionPage struct {
Limit int
ReferenceDate *time.Time
MapSheetProperties *config.MapSheetDownloadProperties
+ WebConfig *config.WebConfig
// Property filters as supplied by the user in the URL: filter name + value(s)
PropertyFilters map[string]string
@@ -66,19 +67,20 @@ type featurePage struct {
FeatureID string
Metadata *config.GeoSpatialCollectionMetadata
MapSheetProperties *config.MapSheetDownloadProperties
+ WebConfig *config.WebConfig
}
func (hf *htmlFeatures) features(w http.ResponseWriter, r *http.Request, collectionID string, cursor domain.Cursors,
- featuresURL featureCollectionURL, limit int, referenceDate *time.Time,
- propertyFilters map[string]string, configuredPropertyFilters datasources.PropertyFiltersWithAllowedValues,
- mapSheetProperties *config.MapSheetDownloadProperties, fc *domain.FeatureCollection) {
+ featuresURL featureCollectionURL, limit int, referenceDate *time.Time, propertyFilters map[string]string,
+ configuredPropertyFilters datasources.PropertyFiltersWithAllowedValues, configuredFC *config.CollectionEntryFeatures,
+ fc *domain.FeatureCollection) {
- collectionMetadata := collections[collectionID]
+ collection := configuredCollections[collectionID]
breadcrumbs := collectionsBreadcrumb
breadcrumbs = append(breadcrumbs, []engine.Breadcrumb{
{
- Name: getCollectionTitle(collectionID, collectionMetadata),
+ Name: getCollectionTitle(collectionID, collection.Metadata),
Path: collectionsCrumb + collectionID,
},
{
@@ -90,17 +92,26 @@ func (hf *htmlFeatures) features(w http.ResponseWriter, r *http.Request, collect
if referenceDate.IsZero() {
referenceDate = nil
}
+ var mapSheetProps *config.MapSheetDownloadProperties
+ var wc *config.WebConfig
+ if configuredFC != nil {
+ if configuredFC.MapSheetDownloads != nil {
+ mapSheetProps = &configuredFC.MapSheetDownloads.Properties
+ }
+ wc = configuredFC.Web
+ }
pageContent := &featureCollectionPage{
*fc,
collectionID,
- collectionMetadata,
+ collection.Metadata,
cursor,
featuresURL.toPrevNextURL(collectionID, cursor.Prev, engine.FormatHTML),
featuresURL.toPrevNextURL(collectionID, cursor.Next, engine.FormatHTML),
limit,
referenceDate,
- mapSheetProperties,
+ mapSheetProps,
+ wc,
propertyFilters,
configuredPropertyFilters,
}
@@ -110,13 +121,13 @@ func (hf *htmlFeatures) features(w http.ResponseWriter, r *http.Request, collect
}
func (hf *htmlFeatures) feature(w http.ResponseWriter, r *http.Request, collectionID string,
- mapSheetProperties *config.MapSheetDownloadProperties, feat *domain.Feature) {
- collectionMetadata := collections[collectionID]
+ configuredFC *config.CollectionEntryFeatures, feat *domain.Feature) {
+ collection := configuredCollections[collectionID]
breadcrumbs := collectionsBreadcrumb
breadcrumbs = append(breadcrumbs, []engine.Breadcrumb{
{
- Name: getCollectionTitle(collectionID, collectionMetadata),
+ Name: getCollectionTitle(collectionID, collection.Metadata),
Path: collectionsCrumb + collectionID,
},
{
@@ -129,12 +140,22 @@ func (hf *htmlFeatures) feature(w http.ResponseWriter, r *http.Request, collecti
},
}...)
+ var mapSheetProps *config.MapSheetDownloadProperties
+ var wc *config.WebConfig
+ if configuredFC != nil {
+ if configuredFC.MapSheetDownloads != nil {
+ mapSheetProps = &configuredFC.MapSheetDownloads.Properties
+ }
+ wc = configuredFC.Web
+ }
+
pageContent := &featurePage{
*feat,
collectionID,
feat.ID,
- collectionMetadata,
- mapSheetProperties,
+ collection.Metadata,
+ mapSheetProps,
+ wc,
}
lang := hf.engine.CN.NegotiateLanguage(w, r)
diff --git a/internal/ogc/features/json.go b/internal/ogc/features/json.go
index e21ad929..0c7a1e20 100644
--- a/internal/ogc/features/json.go
+++ b/internal/ogc/features/json.go
@@ -11,6 +11,7 @@ import (
"strconv"
"time"
+ "github.com/PDOK/gokoala/config"
"github.com/PDOK/gokoala/internal/engine"
"github.com/PDOK/gokoala/internal/ogc/features/domain"
perfjson "github.com/goccy/go-json"
@@ -38,13 +39,13 @@ func newJSONFeatures(e *engine.Engine) *jsonFeatures {
}
// GeoJSON
-func (jf *jsonFeatures) featuresAsGeoJSON(w http.ResponseWriter, r *http.Request, collectionID string,
- cursor domain.Cursors, featuresURL featureCollectionURL, fc *domain.FeatureCollection) {
+func (jf *jsonFeatures) featuresAsGeoJSON(w http.ResponseWriter, r *http.Request, collectionID string, cursor domain.Cursors,
+ featuresURL featureCollectionURL, configuredFC *config.CollectionEntryFeatures, fc *domain.FeatureCollection) {
fc.Timestamp = now().Format(time.RFC3339)
fc.Links = jf.createFeatureCollectionLinks(engine.FormatGeoJSON, collectionID, cursor, featuresURL)
- jf.createFeatureDownloadLinks(collectionID, fc)
+ jf.createFeatureDownloadLinks(configuredFC, fc)
if jf.validateResponse {
jf.serveAndValidateJSON(&fc, engine.MediaTypeGeoJSON, r, w)
@@ -55,10 +56,10 @@ func (jf *jsonFeatures) featuresAsGeoJSON(w http.ResponseWriter, r *http.Request
// GeoJSON
func (jf *jsonFeatures) featureAsGeoJSON(w http.ResponseWriter, r *http.Request, collectionID string,
- feat *domain.Feature, url featureURL) {
+ configuredFC *config.CollectionEntryFeatures, feat *domain.Feature, url featureURL) {
feat.Links = jf.createFeatureLinks(engine.FormatGeoJSON, url, collectionID, feat.ID)
- if mapSheetProperties := jf.engine.Config.OgcAPI.Features.MapSheetPropertiesForCollection(collectionID); mapSheetProperties != nil {
+ if mapSheetProperties := getMapSheetProperties(configuredFC); mapSheetProperties != nil {
feat.Links = append(feat.Links, domain.Link{
Rel: "enclosure",
Title: "Download feature",
@@ -75,8 +76,8 @@ 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 domain.ContentCrs) {
+func (jf *jsonFeatures) featuresAsJSONFG(w http.ResponseWriter, r *http.Request, collectionID string, cursor domain.Cursors,
+ featuresURL featureCollectionURL, configuredFC *config.CollectionEntryFeatures, fc *domain.FeatureCollection, crs domain.ContentCrs) {
fgFC := domain.JSONFGFeatureCollection{}
fgFC.ConformsTo = []string{domain.ConformanceJSONFGCore}
@@ -98,7 +99,7 @@ func (jf *jsonFeatures) featuresAsJSONFG(w http.ResponseWriter, r *http.Request,
fgFC.Timestamp = now().Format(time.RFC3339)
fgFC.Links = jf.createFeatureCollectionLinks(engine.FormatJSONFG, collectionID, cursor, featuresURL)
- jf.createJSONFGFeatureDownloadLinks(collectionID, &fgFC)
+ jf.createJSONFGFeatureDownloadLinks(configuredFC, &fgFC)
if jf.validateResponse {
jf.serveAndValidateJSON(&fgFC, engine.MediaTypeJSONFG, r, w)
@@ -109,7 +110,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 domain.ContentCrs) {
+ configuredFC *config.CollectionEntryFeatures, f *domain.Feature, url featureURL, crs domain.ContentCrs) {
fgF := domain.JSONFGFeature{
ID: f.ID,
@@ -120,7 +121,7 @@ func (jf *jsonFeatures) featureAsJSONFG(w http.ResponseWriter, r *http.Request,
}
setGeom(crs, &fgF, f)
fgF.Links = jf.createFeatureLinks(engine.FormatJSONFG, url, collectionID, fgF.ID)
- if mapSheetProperties := jf.engine.Config.OgcAPI.Features.MapSheetPropertiesForCollection(collectionID); mapSheetProperties != nil {
+ if mapSheetProperties := getMapSheetProperties(configuredFC); mapSheetProperties != nil {
fgF.Links = append(fgF.Links, domain.Link{
Rel: "enclosure",
Title: "Download feature",
@@ -263,8 +264,8 @@ func (jf *jsonFeatures) createFeatureLinks(currentFormat string, url featureURL,
return links
}
-func (jf *jsonFeatures) createFeatureDownloadLinks(collectionID string, fc *domain.FeatureCollection) {
- if mapSheetProperties := jf.engine.Config.OgcAPI.Features.MapSheetPropertiesForCollection(collectionID); mapSheetProperties != nil {
+func (jf *jsonFeatures) createFeatureDownloadLinks(configuredFC *config.CollectionEntryFeatures, fc *domain.FeatureCollection) {
+ if mapSheetProperties := getMapSheetProperties(configuredFC); mapSheetProperties != nil {
for _, feature := range fc.Features {
links := make([]domain.Link, 0)
links = append(links, domain.Link{
@@ -278,8 +279,8 @@ func (jf *jsonFeatures) createFeatureDownloadLinks(collectionID string, fc *doma
}
}
-func (jf *jsonFeatures) createJSONFGFeatureDownloadLinks(collectionID string, fc *domain.JSONFGFeatureCollection) {
- if mapSheetProperties := jf.engine.Config.OgcAPI.Features.MapSheetPropertiesForCollection(collectionID); mapSheetProperties != nil {
+func (jf *jsonFeatures) createJSONFGFeatureDownloadLinks(configuredFC *config.CollectionEntryFeatures, fc *domain.JSONFGFeatureCollection) {
+ if mapSheetProperties := getMapSheetProperties(configuredFC); mapSheetProperties != nil {
for _, feature := range fc.Features {
links := make([]domain.Link, 0)
links = append(links, domain.Link{
@@ -344,3 +345,10 @@ func setGeom(crs domain.ContentCrs, jsonfgFeature *domain.JSONFGFeature, feature
jsonfgFeature.Place = feature.Geometry
}
}
+
+func getMapSheetProperties(configuredFC *config.CollectionEntryFeatures) *config.MapSheetDownloadProperties {
+ if configuredFC != nil && configuredFC.MapSheetDownloads != nil {
+ return &configuredFC.MapSheetDownloads.Properties
+ }
+ return nil
+}
diff --git a/internal/ogc/features/main.go b/internal/ogc/features/main.go
index 933523ec..ad658885 100644
--- a/internal/ogc/features/main.go
+++ b/internal/ogc/features/main.go
@@ -28,7 +28,7 @@ const (
)
var (
- collections map[string]*config.GeoSpatialCollectionMetadata
+ configuredCollections map[string]config.GeoSpatialCollection
emptyFeatureCollection = &domain.FeatureCollection{Features: make([]*domain.Feature, 0)}
)
@@ -53,7 +53,7 @@ type Features struct {
}
func NewFeatures(e *engine.Engine) *Features {
- collections = cacheCollectionsMetadata(e)
+ configuredCollections = cacheConfiguredFeatureCollections(e)
datasources := createDatasources(e)
configuredPropertyFilters := configurePropertyFiltersWithAllowedValues(datasources)
@@ -63,7 +63,7 @@ func NewFeatures(e *engine.Engine) *Features {
engine: e,
datasources: datasources,
configuredPropertyFilters: configuredPropertyFilters,
- defaultProfile: domain.NewProfile(domain.RelAsLink, *e.Config.BaseURL.URL, util.Keys(collections)),
+ defaultProfile: domain.NewProfile(domain.RelAsLink, *e.Config.BaseURL.URL, util.Keys(configuredCollections)),
html: newHTMLFeatures(e),
json: newJSONFeatures(e),
}
@@ -88,13 +88,13 @@ func (f *Features) Features() http.HandlerFunc {
}
collectionID := chi.URLParam(r, "collectionId")
- collection, ok := collections[collectionID]
+ collection, ok := configuredCollections[collectionID]
if !ok {
handleCollectionNotFound(w, collectionID)
return
}
url := featureCollectionURL{*cfg.BaseURL.URL, r.URL.Query(), cfg.OgcAPI.Features.Limit,
- cfg.OgcAPI.Features.PropertyFiltersForCollection(collectionID), hasDateTime(collection)}
+ getConfiguredPropertyFilters(collection), hasDateTime(collection)}
encodedCursor, limit, inputSRID, outputSRID, contentCrs, bbox, referenceDate, propertyFilters, err := url.parse()
if err != nil {
engine.RenderProblem(engine.ProblemBadRequest, w, err.Error())
@@ -152,12 +152,11 @@ func (f *Features) Features() http.HandlerFunc {
switch format {
case engine.FormatHTML:
f.html.features(w, r, collectionID, newCursor, url, limit, &referenceDate,
- propertyFilters, f.configuredPropertyFilters[collectionID],
- cfg.OgcAPI.Features.MapSheetPropertiesForCollection(collectionID), fc)
+ propertyFilters, f.configuredPropertyFilters[collectionID], collection.Features, fc)
case engine.FormatGeoJSON, engine.FormatJSON:
- f.json.featuresAsGeoJSON(w, r, collectionID, newCursor, url, fc)
+ f.json.featuresAsGeoJSON(w, r, collectionID, newCursor, url, collection.Features, fc)
case engine.FormatJSONFG:
- f.json.featuresAsJSONFG(w, r, collectionID, newCursor, url, fc, contentCrs)
+ f.json.featuresAsJSONFG(w, r, collectionID, newCursor, url, collection.Features, fc, contentCrs)
default:
engine.RenderProblem(engine.ProblemNotAcceptable, w, fmt.Sprintf("format '%s' is not supported", format))
return
@@ -167,8 +166,6 @@ func (f *Features) Features() http.HandlerFunc {
// Feature serves a single Feature
func (f *Features) Feature() http.HandlerFunc {
- cfg := f.engine.Config
-
return func(w http.ResponseWriter, r *http.Request) {
if err := f.engine.OpenAPI.ValidateRequest(r); err != nil {
engine.RenderProblem(engine.ProblemBadRequest, w, err.Error())
@@ -176,7 +173,8 @@ func (f *Features) Feature() http.HandlerFunc {
}
collectionID := chi.URLParam(r, "collectionId")
- if _, ok := collections[collectionID]; !ok {
+ collection, ok := configuredCollections[collectionID]
+ if !ok {
handleCollectionNotFound(w, collectionID)
return
}
@@ -192,7 +190,6 @@ func (f *Features) Feature() http.HandlerFunc {
return
}
w.Header().Add(engine.HeaderContentCrs, contentCrs.ToLink())
- mapSheetProperties := cfg.OgcAPI.Features.MapSheetPropertiesForCollection(collectionID)
datasource := f.datasources[DatasourceKey{srid: outputSRID.GetOrDefault(), collectionID: collectionID}]
feat, err := datasource.GetFeature(r.Context(), collectionID, featureID, f.defaultProfile)
@@ -208,11 +205,11 @@ func (f *Features) Feature() http.HandlerFunc {
format := f.engine.CN.NegotiateFormat(r)
switch format {
case engine.FormatHTML:
- f.html.feature(w, r, collectionID, mapSheetProperties, feat)
+ f.html.feature(w, r, collectionID, collection.Features, feat)
case engine.FormatGeoJSON, engine.FormatJSON:
- f.json.featureAsGeoJSON(w, r, collectionID, feat, url)
+ f.json.featureAsGeoJSON(w, r, collectionID, collection.Features, feat, url)
case engine.FormatJSONFG:
- f.json.featureAsJSONFG(w, r, collectionID, feat, url, contentCrs)
+ f.json.featureAsJSONFG(w, r, collectionID, collection.Features, feat, url, contentCrs)
default:
engine.RenderProblem(engine.ProblemNotAcceptable, w, fmt.Sprintf("format '%s' is not supported", format))
return
@@ -233,10 +230,10 @@ func parseFeatureID(r *http.Request) (any, error) {
return featureID, nil
}
-func cacheCollectionsMetadata(e *engine.Engine) map[string]*config.GeoSpatialCollectionMetadata {
- result := make(map[string]*config.GeoSpatialCollectionMetadata)
+func cacheConfiguredFeatureCollections(e *engine.Engine) map[string]config.GeoSpatialCollection {
+ result := make(map[string]config.GeoSpatialCollection)
for _, collection := range e.Config.OgcAPI.Features.Collections {
- result[collection.ID] = collection.Metadata
+ result[collection.ID] = collection
}
return result
}
@@ -388,17 +385,24 @@ func querySingleDatasource(input domain.SRID, output domain.SRID, bbox *geom.Ext
(int(input) == domain.WGS84SRID && int(output) == domain.UndefinedSRID)
}
-func getTemporalCriteria(collection *config.GeoSpatialCollectionMetadata, referenceDate time.Time) ds.TemporalCriteria {
+func getConfiguredPropertyFilters(collection config.GeoSpatialCollection) []config.PropertyFilter {
+ if collection.Features != nil && collection.Features.Filters.Properties != nil {
+ return collection.Features.Filters.Properties
+ }
+ return []config.PropertyFilter{}
+}
+
+func getTemporalCriteria(collection config.GeoSpatialCollection, referenceDate time.Time) ds.TemporalCriteria {
var temporalCriteria ds.TemporalCriteria
if hasDateTime(collection) {
temporalCriteria = ds.TemporalCriteria{
ReferenceDate: referenceDate,
- StartDateProperty: collection.TemporalProperties.StartDate,
- EndDateProperty: collection.TemporalProperties.EndDate}
+ StartDateProperty: collection.Metadata.TemporalProperties.StartDate,
+ EndDateProperty: collection.Metadata.TemporalProperties.EndDate}
}
return temporalCriteria
}
-func hasDateTime(collection *config.GeoSpatialCollectionMetadata) bool {
- return collection != nil && collection.TemporalProperties != nil
+func hasDateTime(collection config.GeoSpatialCollection) bool {
+ return collection.Metadata != nil && collection.Metadata.TemporalProperties != nil
}
diff --git a/internal/ogc/features/main_test.go b/internal/ogc/features/main_test.go
index 51cca5af..4747fba7 100644
--- a/internal/ogc/features/main_test.go
+++ b/internal/ogc/features/main_test.go
@@ -559,6 +559,20 @@ func TestFeatures(t *testing.T) {
statusCode: http.StatusOK,
},
},
+ {
+ name: "Request features for collection with specific web/viewer configuration, to make sure this is reflected in the HTML output",
+ fields: fields{
+ configFile: "internal/ogc/features/testdata/config_features_webconfig.yaml",
+ url: "http://localhost:8080/collections/:collectionId/items?f=html",
+ collectionID: "ligplaatsen",
+ contentCrs: "<" + domain.WGS84CrsURI + ">",
+ format: "html",
+ },
+ want: want{
+ body: "internal/ogc/features/testdata/expected_features_webconfig_snippet.html",
+ statusCode: http.StatusOK,
+ },
+ },
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
@@ -897,6 +911,20 @@ func TestFeatures_Feature(t *testing.T) {
statusCode: http.StatusInternalServerError,
},
},
+ {
+ name: "Request feature with specific web/viewer configuration, and make sure this is reflected in the HTML output",
+ fields: fields{
+ configFile: "internal/ogc/features/testdata/config_features_webconfig.yaml",
+ url: "http://localhost:8080/collections/:collectionId/items/:featureId?f=html",
+ collectionID: "ligplaatsen",
+ featureID: "4030",
+ format: "html",
+ },
+ want: want{
+ body: "internal/ogc/features/testdata/expected_feature_webconfig_snippet.html",
+ statusCode: http.StatusOK,
+ },
+ },
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
@@ -979,7 +1007,10 @@ func normalize(s string) string {
}
func printActual(rr *httptest.ResponseRecorder) {
- log.Print("\n==> ACTUAL JSON RESPONSE (copy/paste and compare with response in file):")
+ log.Print("\n===========================\n")
+ log.Print("\n==> ACTUAL RESPONSE BELOW. Copy/paste and compare with response in file. " +
+ "Note that In the case of HTML output we only compare a relevant snippet instead of the whole file.")
+ log.Print("\n===========================\n")
log.Print(rr.Body.String()) // to ease debugging & updating expected results
- log.Print("\n=========\n")
+ log.Print("\n===========================\n")
}
diff --git a/internal/ogc/features/templates/feature.go.html b/internal/ogc/features/templates/feature.go.html
index 4a392406..988dfe04 100644
--- a/internal/ogc/features/templates/feature.go.html
+++ b/internal/ogc/features/templates/feature.go.html
@@ -3,6 +3,8 @@
{{ $cfg := .Config }}
{{ $baseUrl := $cfg.BaseURL }}
{{ $mapSheetProperties := .Params.MapSheetProperties }}
+{{ $webConfig := .Params.WebConfig }}
+
+
{{/* different viewer settings depending on whether features are map sheets or not */}}
{{ if $mapSheetProperties }}
-
+
+
{{ else }}
-
+
+
{{ end }}
+
-
+
{{end}}
diff --git a/internal/ogc/features/templates/features.go.html b/internal/ogc/features/templates/features.go.html
index d69e2357..3e5c7736 100644
--- a/internal/ogc/features/templates/features.go.html
+++ b/internal/ogc/features/templates/features.go.html
@@ -3,6 +3,7 @@
{{ $cfg := .Config }}
{{ $baseUrl := $cfg.BaseURL }}
{{ $mapSheetProperties := .Params.MapSheetProperties }}
+{{ $webConfig := .Params.WebConfig }}
+
{{/* different viewer settings depending on whether features are map sheets or not */}}
{{ if $mapSheetProperties }}
-
+
+
{{ else }}
-
+
+
{{ end }}
+