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 }} +