From e5beff95646106e20441a4eb1402dd1c754d363e Mon Sep 17 00:00:00 2001 From: Richard Kettelerij Date: Fri, 1 Dec 2023 10:27:37 +0100 Subject: [PATCH 01/26] Update readme + fix typo --- .github/workflows/cypress-ts.yml | 2 +- .github/workflows/lint-ts.yml | 2 +- README.md | 15 +++++++-------- 3 files changed, 9 insertions(+), 10 deletions(-) diff --git a/.github/workflows/cypress-ts.yml b/.github/workflows/cypress-ts.yml index 81c6f5ba..5a80400e 100644 --- a/.github/workflows/cypress-ts.yml +++ b/.github/workflows/cypress-ts.yml @@ -10,7 +10,7 @@ defaults: run: working-directory: ./viewer jobs: - cypres-ts: + cypress-ts: runs-on: ubuntu-latest strategy: diff --git a/.github/workflows/lint-ts.yml b/.github/workflows/lint-ts.yml index 04fe4bea..f9b1259b 100644 --- a/.github/workflows/lint-ts.yml +++ b/.github/workflows/lint-ts.yml @@ -1,5 +1,5 @@ --- -name: lint (web/typescript) +name: lint (web/ts) on: push: branches: diff --git a/README.md b/README.md index 5f4d50c0..e913cdb9 100644 --- a/README.md +++ b/README.md @@ -10,10 +10,9 @@ _Cloud Native OGC APIs server, written in Go._ [![Lint (go)](https://github.com/PDOK/gokoala/actions/workflows/lint-go.yml/badge.svg)](https://github.com/PDOK/gokoala/actions/workflows/lint-go.yml) [![Lint (ts)](https://github.com/PDOK/gokoala/actions/workflows/lint-ts.yml/badge.svg)](https://github.com/PDOK/gokoala/actions/workflows/lint-ts.yml) [![Go Report Card](https://goreportcard.com/badge/github.com/PDOK/gokoala)](https://goreportcard.com/report/github.com/PDOK/gokoala) -[![GitHub -license](https://img.shields.io/github/license/PDOK/gokoala)](https://github.com/PDOK/gokoala/blob/master/LICENSE) -[![Docker -Pulls](https://img.shields.io/docker/pulls/pdok/gokoala.svg)](https://hub.docker.com/r/pdok/gokoala) +[![Coverage (go)](https://github.com/PDOK/gokoala/wiki/coverage.svg)](https://raw.githack.com/wiki/PDOK/gokoala/coverage.html) +[![GitHub license](https://img.shields.io/github/license/PDOK/gokoala)](https://github.com/PDOK/gokoala/blob/master/LICENSE) +[![Docker Pulls](https://img.shields.io/docker/pulls/pdok/gokoala.svg)](https://hub.docker.com/r/pdok/gokoala) ## Description @@ -153,12 +152,12 @@ Design principles: Install [golangci-lint](https://golangci-lint.run/usage/install/) and run `golangci-lint run` from the root. -### Viewer Web Component +### Viewer GoKoala includes a [viewer](viewer) which is available -as a Web Component for embedding in HTML pages. To use the vector tile viewer locally when running -GoKoala outside Docker execute: `hack/build-local-viewer.sh`. This will build the viewer and add -it to the GoKoala assets. +as a [Web Component](https://developer.mozilla.org/en-US/docs/Web/API/Web_components) for embedding in HTML pages. +To use the viewer locally when running GoKoala outside Docker execute: `hack/build-local-viewer.sh`. This will +build the viewer and add it to the GoKoala assets. Note this is only required for local development. When running GoKoala as a container this is already being taken care of when building the Docker container image. From ea319fe345dcfba1872758222fee469140bde4d1 Mon Sep 17 00:00:00 2001 From: Richard Kettelerij Date: Fri, 1 Dec 2023 13:28:02 +0100 Subject: [PATCH 02/26] Add Content-Crs header --- ogc/features/main.go | 16 +++++++---- ogc/features/main_test.go | 12 ++++++++ ogc/features/url.go | 58 ++++++++++++++++++++++++--------------- ogc/features/url_test.go | 26 +++++++++--------- 4 files changed, 71 insertions(+), 41 deletions(-) diff --git a/ogc/features/main.go b/ogc/features/main.go index bdfe1223..8d08846b 100644 --- a/ogc/features/main.go +++ b/ogc/features/main.go @@ -18,10 +18,12 @@ import ( ) const ( - templatesDir = "ogc/features/templates/" - 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" - crsURLPrefix = "http://www.opengis.net/def/crs/" + templatesDir = "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 ) var ( @@ -63,7 +65,7 @@ func (f *Features) CollectionContent(_ ...any) http.HandlerFunc { collectionID := chi.URLParam(r, "collectionId") url := featureCollectionURL{*cfg.BaseURL.URL, r.URL.Query(), cfg.OgcAPI.Features.Limit} - encodedCursor, limit, inputSRID, outputSRID, bbox, err := url.parseParams() + encodedCursor, limit, inputSRID, outputSRID, contentCrs, bbox, err := url.parse() if err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return @@ -77,6 +79,7 @@ func (f *Features) CollectionContent(_ ...any) http.HandlerFunc { http.NotFound(w, r) return } + w.Header().Add("Content-Crs", contentCrs) var newCursor domain.Cursors var fc *domain.FeatureCollection @@ -149,7 +152,7 @@ func (f *Features) Feature() http.HandlerFunc { } url := featureURL{*f.engine.Config.BaseURL.URL, r.URL.Query()} - outputSRID, err := url.parseParams() + outputSRID, contentCrs, err := url.parse() if err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return @@ -163,6 +166,7 @@ func (f *Features) Feature() http.HandlerFunc { http.NotFound(w, r) return } + w.Header().Add("Content-Crs", contentCrs) datasource := f.datasources[DatasourceKey{srid: outputSRID.GetOrDefault(), collectionID: collectionID}] feat, err := datasource.GetFeature(r.Context(), collectionID, int64(featureID)) diff --git a/ogc/features/main_test.go b/ogc/features/main_test.go index afa2083d..52494e5a 100644 --- a/ogc/features/main_test.go +++ b/ogc/features/main_test.go @@ -31,6 +31,7 @@ func TestFeatures_CollectionContent(t *testing.T) { type fields struct { configFile string url string + contentCrs string collectionID string format string } @@ -49,6 +50,7 @@ func TestFeatures_CollectionContent(t *testing.T) { configFile: "ogc/features/testdata/config_features_bag.yaml", url: "http://localhost:8080/collections/:collectionId/items", collectionID: "foo", + contentCrs: "<" + wgs84CrsURI + ">", format: "json", }, want: want{ @@ -62,6 +64,7 @@ func TestFeatures_CollectionContent(t *testing.T) { configFile: "ogc/features/testdata/config_features_bag.yaml", url: "http://localhost:8080/collections/:collectionId/items?limit=2", collectionID: "foo", + contentCrs: "<" + wgs84CrsURI + ">", format: "json", }, want: want{ @@ -75,6 +78,7 @@ func TestFeatures_CollectionContent(t *testing.T) { configFile: "ogc/features/testdata/config_features_bag.yaml", url: "http://localhost:8080/collections/tunneldelen/items?f=json&cursor=Dv58Nwyr1Q%3D%3D&limit=2", collectionID: "foo", + contentCrs: "<" + wgs84CrsURI + ">", format: "json", }, want: want{ @@ -140,6 +144,7 @@ func TestFeatures_CollectionContent(t *testing.T) { configFile: "ogc/features/testdata/config_features_bag.yaml", url: "http://localhost:8080/collections/:collectionId/items?limit=1", collectionID: "foo", + contentCrs: "<" + wgs84CrsURI + ">", format: "html", }, want: want{ @@ -153,6 +158,7 @@ func TestFeatures_CollectionContent(t *testing.T) { configFile: "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 + ">", format: "json", }, want: want{ @@ -179,6 +185,7 @@ func TestFeatures_CollectionContent(t *testing.T) { configFile: "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=Wl989YRHSw%3D%3D&f=json&limit=10", collectionID: "dutch-addresses", + contentCrs: "<" + wgs84CrsURI + ">", format: "json", }, want: want{ @@ -192,6 +199,7 @@ func TestFeatures_CollectionContent(t *testing.T) { configFile: "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&crs=http%3A%2F%2Fwww.opengis.net%2Fdef%2Fcrs%2FEPSG%2F0%2F28992&f=json&limit=10", collectionID: "dutch-addresses", + contentCrs: "", format: "json", }, want: want{ @@ -205,6 +213,7 @@ func TestFeatures_CollectionContent(t *testing.T) { configFile: "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 + ">", format: "json", }, want: want{ @@ -218,6 +227,7 @@ func TestFeatures_CollectionContent(t *testing.T) { configFile: "ogc/features/testdata/config_features_multiple_gpkgs.yaml", url: "view-source: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&crs=http%3A%2F%2Fwww.opengis.net%2Fdef%2Fcrs%2FEPSG%2F0%2F28992&f=json&limit=10", collectionID: "dutch-addresses", + contentCrs: "", format: "json", }, want: want{ @@ -231,6 +241,7 @@ func TestFeatures_CollectionContent(t *testing.T) { configFile: "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 + ">", format: "json", }, want: want{ @@ -253,6 +264,7 @@ func TestFeatures_CollectionContent(t *testing.T) { handler := features.CollectionContent() handler.ServeHTTP(rr, req) + assert.Equal(t, tt.fields.contentCrs, rr.Header().Get("Content-Crs")) assert.Equal(t, tt.want.statusCode, rr.Code) if tt.want.body != "" { expectedBody, err := os.ReadFile(tt.want.body) diff --git a/ogc/features/url.go b/ogc/features/url.go index 642c7e9d..9f219303 100644 --- a/ogc/features/url.go +++ b/ogc/features/url.go @@ -25,24 +25,14 @@ const ( bboxCrsParam = "bbox-crs" filterParam = "filter" filterCrsParam = "filter-crs" - undefinedSRID = 0 ) var ( checksumExcludedParams = []string{engine.FormatParam, cursorParam} // don't include these in checksum ) -type URL interface { - validateNoUnknownParams() error -} - -// URL to a page in a collection of features -type featureCollectionURL struct { - baseURL url.URL - params url.Values - limit engine.Limit -} - +// 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 { @@ -59,12 +49,25 @@ func (s SRID) IsSameAs(other SRID) bool { (int(s) == wgs84SRID && int(other) == undefinedSRID) } -func (fc featureCollectionURL) parseParams() (encodedCursor domain.EncodedCursor, limit int, - inputSRID SRID, outputSRID SRID, bbox *geom.Extent, err error) { +type URL interface { + validateNoUnknownParams() error +} + +// URL to a page in a collection of features +type featureCollectionURL struct { + baseURL url.URL + params url.Values + limit engine.Limit +} + +// 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 string, bbox *geom.Extent, err error) { encodedCursor = domain.EncodedCursor(fc.params.Get(cursorParam)) limit, limitErr := parseLimit(fc.params, fc.limit) - outputSRID, outputSRIDErr := parseSRID(fc.params, crsParam) + outputSRID, outputSRIDErr := parseCrsToSRID(fc.params, crsParam) + contentCrs = parseCrsToContentCrs(fc.params) bbox, bboxSRID, bboxErr := parseBbox(fc.params) dateTimeErr := parseDateTime(fc.params) _, filterSRID, filterErr := parseFilter(fc.params) @@ -154,8 +157,11 @@ type featureURL struct { params url.Values } -func (f featureURL) parseParams() (srid SRID, err error) { - return parseSRID(f.params, crsParam) +// parse the given URL to values required to delivery a specific Feature +func (f featureURL) parse() (srid SRID, contentCrs string, err error) { + srid, err = parseCrsToSRID(f.params, crsParam) + contentCrs = parseCrsToContentCrs(f.params) + return } func (f featureURL) toSelfURL(collectionID string, featureID int64, format string) string { @@ -226,7 +232,7 @@ func parseLimit(params url.Values, limitCfg engine.Limit) (int, error) { } func parseBbox(params url.Values) (*geom.Extent, SRID, error) { - bboxSRID, err := parseSRID(params, bboxCrsParam) + bboxSRID, err := parseCrsToSRID(params, bboxCrsParam) if err != nil { return nil, undefinedSRID, err } @@ -251,14 +257,22 @@ func parseBbox(params url.Values) (*geom.Extent, SRID, error) { return &extent, bboxSRID, nil } -func parseSRID(params url.Values, paramName string) (SRID, error) { +func parseCrsToContentCrs(params url.Values) string { + param := params.Get(crsParam) + if param == "" { + return fmt.Sprintf("<%s>", wgs84CrsURI) + } + return fmt.Sprintf("<%s>", param) +} + +func parseCrsToSRID(params url.Values, paramName string) (SRID, error) { param := params.Get(paramName) if param == "" { return undefinedSRID, nil } param = strings.TrimSpace(param) - if !strings.HasPrefix(param, crsURLPrefix) { - return undefinedSRID, fmt.Errorf("%s param should start with %s, got: %s", paramName, crsURLPrefix, param) + if !strings.HasPrefix(param, crsURIPrefix) { + return undefinedSRID, fmt.Errorf("%s param should start with %s, got: %s", paramName, crsURIPrefix, param) } var srid SRID lastIndex := strings.LastIndex(param, "/") @@ -285,7 +299,7 @@ func parseDateTime(params url.Values) error { func parseFilter(params url.Values) (filter string, filterSRID SRID, err error) { filter = params.Get(filterParam) - filterSRID, _ = parseSRID(params, filterCrsParam) + filterSRID, _ = parseCrsToSRID(params, filterCrsParam) if filter != "" { return filter, filterSRID, fmt.Errorf("CQL filter param is currently not supported") diff --git a/ogc/features/url_test.go b/ogc/features/url_test.go index 4e9ffdb6..46e651a2 100644 --- a/ogc/features/url_test.go +++ b/ogc/features/url_test.go @@ -152,7 +152,7 @@ func Test_featureCollectionURL_parseParams(t *testing.T) { }, }, wantErr: func(t assert.TestingT, err error, i ...interface{}) bool { - assert.Equalf(t, "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", err.Error(), "parseParams()") + assert.Equalf(t, "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", err.Error(), "parse()") return false }, }, @@ -170,7 +170,7 @@ func Test_featureCollectionURL_parseParams(t *testing.T) { }, }, wantErr: func(t assert.TestingT, err error, i ...interface{}) bool { - assert.Equalf(t, "crs param should start with http://www.opengis.net/def/crs/, got: EPSG:28992", err.Error(), "parseParams()") + assert.Equalf(t, "crs param should start with http://www.opengis.net/def/crs/, got: EPSG:28992", err.Error(), "parse()") return false }, }, @@ -187,7 +187,7 @@ func Test_featureCollectionURL_parseParams(t *testing.T) { }, }, wantErr: func(t assert.TestingT, err error, i ...interface{}) bool { - assert.Equalf(t, "bbox should contain exactly 4 values separated by commas: minx,miny,maxx,maxy", err.Error(), "parseParams()") + assert.Equalf(t, "bbox should contain exactly 4 values separated by commas: minx,miny,maxx,maxy", err.Error(), "parse()") return false }, }, @@ -204,7 +204,7 @@ func Test_featureCollectionURL_parseParams(t *testing.T) { }, }, wantErr: func(t assert.TestingT, err error, i ...interface{}) bool { - assert.Equalf(t, "limit can't be negative", err.Error(), "parseParams()") + assert.Equalf(t, "limit can't be negative", err.Error(), "parse()") return false }, }, @@ -221,7 +221,7 @@ func Test_featureCollectionURL_parseParams(t *testing.T) { }, }, wantErr: func(t assert.TestingT, err error, i ...interface{}) bool { - assert.Equalf(t, "datetime param is currently not supported", err.Error(), "parseParams()") + assert.Equalf(t, "datetime param is currently not supported", err.Error(), "parse()") return false }, }, @@ -238,7 +238,7 @@ func Test_featureCollectionURL_parseParams(t *testing.T) { }, }, wantErr: func(t assert.TestingT, err error, i ...interface{}) bool { - assert.Equalf(t, "CQL filter param is currently not supported", err.Error(), "parseParams()") + assert.Equalf(t, "CQL filter param is currently not supported", err.Error(), "parse()") return false }, }, @@ -250,15 +250,15 @@ func Test_featureCollectionURL_parseParams(t *testing.T) { params: tt.fields.params, limit: tt.fields.limit, } - gotEncodedCursor, gotLimit, gotInputCrs, gotOutputCrs, gotBbox, err := fc.parseParams() - if !tt.wantErr(t, err, "parseParams()") { + gotEncodedCursor, gotLimit, gotInputCrs, gotOutputCrs, _, gotBbox, err := fc.parse() + if !tt.wantErr(t, err, "parse()") { return } - assert.Equalf(t, tt.wantEncodedCursor, gotEncodedCursor, "parseParams()") - assert.Equalf(t, tt.wantLimit, gotLimit, "parseParams()") - assert.Equalf(t, tt.wantOutputCrs, gotOutputCrs.GetOrDefault(), "parseParams()") - assert.Equalf(t, tt.wantBbox, gotBbox, "parseParams()") - assert.Equalf(t, tt.wantInputCrs, gotInputCrs.GetOrDefault(), "parseParams()") + assert.Equalf(t, tt.wantEncodedCursor, gotEncodedCursor, "parse()") + assert.Equalf(t, tt.wantLimit, gotLimit, "parse()") + assert.Equalf(t, tt.wantOutputCrs, gotOutputCrs.GetOrDefault(), "parse()") + assert.Equalf(t, tt.wantBbox, gotBbox, "parse()") + assert.Equalf(t, tt.wantInputCrs, gotInputCrs.GetOrDefault(), "parse()") }) } } From 776cfbdc7a6426e7a57b2fbb433cd5d0275fac69 Mon Sep 17 00:00:00 2001 From: Richard Kettelerij Date: Fri, 1 Dec 2023 13:38:21 +0100 Subject: [PATCH 03/26] Use constants for HTTP headers --- engine/contentnegotiation.go | 4 ++-- engine/contentnegotiation_test.go | 4 ++-- engine/engine.go | 19 +++++++++++++------ engine/engine_test.go | 2 +- engine/openapi.go | 2 +- ogc/common/core/main.go | 2 +- ogc/features/main.go | 4 ++-- ogc/features/main_test.go | 1 + 8 files changed, 23 insertions(+), 15 deletions(-) diff --git a/engine/contentnegotiation.go b/engine/contentnegotiation.go index 5f9bf83a..416a1845 100644 --- a/engine/contentnegotiation.go +++ b/engine/contentnegotiation.go @@ -203,8 +203,8 @@ func (cn *ContentNegotiation) getLanguageFromCookie(req *http.Request) language. func (cn *ContentNegotiation) getLanguageFromHeader(req *http.Request) language.Tag { var requestedLanguage = language.Und - if req.Header.Get("Accept-Language") != "" { - accepted, _, err := language.ParseAcceptLanguage(req.Header.Get("Accept-Language")) + if req.Header.Get(HeaderAcceptLanguage) != "" { + accepted, _, err := language.ParseAcceptLanguage(req.Header.Get(HeaderAcceptLanguage)) if err != nil { log.Printf("Failed to parse Accept-Language header: %v. Continuing\n", err) return requestedLanguage diff --git a/engine/contentnegotiation_test.go b/engine/contentnegotiation_test.go index 625dd04c..bc9415aa 100644 --- a/engine/contentnegotiation_test.go +++ b/engine/contentnegotiation_test.go @@ -33,7 +33,7 @@ func TestContentNegotiation_NegotiateFormat(t *testing.T) { func testFormat(t *testing.T, cn *ContentNegotiation, acceptHeader string, givenURL string, expectedFormat string) { req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, givenURL, nil) - req.Header.Set("Accept", acceptHeader) + req.Header.Set(HeaderAccept, acceptHeader) if err != nil { t.Fatal(err) } @@ -45,7 +45,7 @@ func testFormat(t *testing.T, cn *ContentNegotiation, acceptHeader string, given func testLanguage(t *testing.T, cn *ContentNegotiation, acceptLanguageHeader string, givenURL string, expectedLanguage language.Tag) { req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, givenURL, nil) - req.Header.Set("Accept-Language", acceptLanguageHeader) + req.Header.Set(HeaderAcceptLanguage, acceptLanguageHeader) if err != nil { t.Fatal(err) } diff --git a/engine/engine.go b/engine/engine.go index 1a9ba51d..0e3b4508 100644 --- a/engine/engine.go +++ b/engine/engine.go @@ -24,6 +24,13 @@ import ( const ( templatesDir = "engine/templates/" shutdownTimeout = 5 * time.Second + + HeaderAccept = "Accept" + HeaderAcceptLanguage = "Accept-Language" + HeaderContentType = "Content-Type" + HeaderContentLength = "Content-Length" + HeaderContentCrs = "Content-Crs" + HeaderBaseURL = "X-BaseUrl" ) // Engine encapsulates shared non-OGC API specific logic @@ -196,7 +203,7 @@ func (e *Engine) RenderAndServePage(w http.ResponseWriter, r *http.Request, key // return response output to client if contentType != "" { - w.Header().Set("Content-Type", contentType) + w.Header().Set(HeaderContentType, contentType) } SafeWrite(w.Write, output) } @@ -227,7 +234,7 @@ func (e *Engine) ServePage(w http.ResponseWriter, r *http.Request, templateKey T // return response output to client if contentType != "" { - w.Header().Set("Content-Type", contentType) + w.Header().Set(HeaderContentType, contentType) } SafeWrite(w.Write, output) } @@ -240,7 +247,7 @@ func (e *Engine) ReverseProxy(w http.ResponseWriter, r *http.Request, target *ur r.Out.URL = target r.Out.Host = "" // Don't pass Host header (similar to Traefik's passHostHeader=false) r.SetXForwarded() // Set X-Forwarded-* headers. - r.Out.Header.Set("X-BaseUrl", e.Config.BaseURL.String()) + r.Out.Header.Set(HeaderBaseURL, e.Config.BaseURL.String()) } modifyResponse := func(proxyRes *http.Response) error { @@ -253,7 +260,7 @@ func (e *Engine) ReverseProxy(w http.ResponseWriter, r *http.Request, target *ur removeBody(proxyRes) } if contentTypeOverwrite != "" { - proxyRes.Header.Set("Content-Type", contentTypeOverwrite) + proxyRes.Header.Set(HeaderContentType, contentTypeOverwrite) } } return nil @@ -266,8 +273,8 @@ func (e *Engine) ReverseProxy(w http.ResponseWriter, r *http.Request, target *ur func removeBody(proxyRes *http.Response) { buf := bytes.NewBuffer(make([]byte, 0)) proxyRes.Body = io.NopCloser(buf) - proxyRes.Header["Content-Length"] = []string{"0"} - proxyRes.Header["Content-Type"] = []string{} + proxyRes.Header[HeaderContentLength] = []string{"0"} + proxyRes.Header[HeaderContentType] = []string{} } func (e *Engine) validateStaticResponse(key TemplateKey, urlPath string) { diff --git a/engine/engine_test.go b/engine/engine_test.go index f2607feb..ca718c41 100644 --- a/engine/engine_test.go +++ b/engine/engine_test.go @@ -43,6 +43,6 @@ func TestEngine_ServePage_LandingPage(t *testing.T) { // then assert.Equal(t, http.StatusOK, recorder.Code) - assert.Equal(t, "application/json", recorder.Header().Get("Content-Type")) + assert.Equal(t, "application/json", recorder.Header().Get(HeaderContentType)) assert.Contains(t, recorder.Body.String(), "This is a minimal OGC API, offering only OGC API Common") } diff --git a/engine/openapi.go b/engine/openapi.go index 7713e62e..61256437 100644 --- a/engine/openapi.go +++ b/engine/openapi.go @@ -204,7 +204,7 @@ func (o *OpenAPI) validateRequest(r *http.Request) error { func (o *OpenAPI) validateResponse(contentType string, body []byte, r *http.Request) error { requestValidationInput, _ := o.getRequestValidationInput(r) if requestValidationInput != nil { - responseHeaders := http.Header{"Content-Type": []string{contentType}} + responseHeaders := http.Header{HeaderContentType: []string{contentType}} responseCode := 200 responseValidationInput := &openapi3filter.ResponseValidationInput{ diff --git a/ogc/common/core/main.go b/ogc/common/core/main.go index aff32e62..09abb65a 100644 --- a/ogc/common/core/main.go +++ b/ogc/common/core/main.go @@ -85,7 +85,7 @@ func (c *CommonCore) apiAsHTML(w http.ResponseWriter, r *http.Request) { } func (c *CommonCore) apiAsJSON(w http.ResponseWriter) { - w.Header().Set("Content-Type", engine.MediaTypeOpenAPI) + w.Header().Set(engine.HeaderContentType, engine.MediaTypeOpenAPI) engine.SafeWrite(w.Write, c.engine.OpenAPI.SpecJSON) } diff --git a/ogc/features/main.go b/ogc/features/main.go index 8d08846b..0f8c627b 100644 --- a/ogc/features/main.go +++ b/ogc/features/main.go @@ -79,7 +79,7 @@ func (f *Features) CollectionContent(_ ...any) http.HandlerFunc { http.NotFound(w, r) return } - w.Header().Add("Content-Crs", contentCrs) + w.Header().Add(engine.HeaderContentCrs, contentCrs) var newCursor domain.Cursors var fc *domain.FeatureCollection @@ -166,7 +166,7 @@ func (f *Features) Feature() http.HandlerFunc { http.NotFound(w, r) return } - w.Header().Add("Content-Crs", contentCrs) + w.Header().Add(engine.HeaderContentCrs, contentCrs) datasource := f.datasources[DatasourceKey{srid: outputSRID.GetOrDefault(), collectionID: collectionID}] feat, err := datasource.GetFeature(r.Context(), collectionID, int64(featureID)) diff --git a/ogc/features/main_test.go b/ogc/features/main_test.go index 52494e5a..de434d12 100644 --- a/ogc/features/main_test.go +++ b/ogc/features/main_test.go @@ -172,6 +172,7 @@ func TestFeatures_CollectionContent(t *testing.T) { configFile: "ogc/features/testdata/config_features_multiple_gpkgs.yaml", url: "http://localhost:8080/collections/:collectionId/items?crs=http%3A%2F%2Fwww.opengis.net%2Fdef%2Fcrs%2FEPSG%2F0%2F28992&limit=2", collectionID: "dutch-addresses", + contentCrs: "", format: "json", }, want: want{ From 0e94bb9f49a7f33d880937131eaf63f30886d28c Mon Sep 17 00:00:00 2001 From: Richard Kettelerij Date: Mon, 4 Dec 2023 15:25:39 +0100 Subject: [PATCH 04/26] Rename GH action --- ...ld-and-publish-viewer-on-github-pages.yml} | 68 +++++++++---------- 1 file changed, 34 insertions(+), 34 deletions(-) rename .github/workflows/{build-and-publish-webcomponent-on-github-pages.yml => build-and-publish-viewer-on-github-pages.yml} (92%) diff --git a/.github/workflows/build-and-publish-webcomponent-on-github-pages.yml b/.github/workflows/build-and-publish-viewer-on-github-pages.yml similarity index 92% rename from .github/workflows/build-and-publish-webcomponent-on-github-pages.yml rename to .github/workflows/build-and-publish-viewer-on-github-pages.yml index 57ae27c0..aec193ff 100644 --- a/.github/workflows/build-and-publish-webcomponent-on-github-pages.yml +++ b/.github/workflows/build-and-publish-viewer-on-github-pages.yml @@ -1,34 +1,34 @@ ---- -name: deploy web components -on: - push: - branches: ["master"] - pull_request: - branches: ["master"] - -defaults: - run: - working-directory: ./viewer - -jobs: - build: - runs-on: ubuntu-latest - - strategy: - matrix: - node-version: [18.x] - # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ - - steps: - - uses: actions/checkout@v3 - - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v3 - with: - node-version: ${{ matrix.node-version }} - cache: "npm" - cache-dependency-path: "./viewer/package-lock.json" - - run: npm ci - - name: Deploy - run: npm run deploy - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} +--- +name: deploy viewer +on: + push: + branches: ["master"] + pull_request: + branches: ["master"] + +defaults: + run: + working-directory: ./viewer + +jobs: + build: + runs-on: ubuntu-latest + + strategy: + matrix: + node-version: [18.x] + # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ + + steps: + - uses: actions/checkout@v3 + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v3 + with: + node-version: ${{ matrix.node-version }} + cache: "npm" + cache-dependency-path: "./viewer/package-lock.json" + - run: npm ci + - name: Deploy + run: npm run deploy + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} From c198d305de90c9c448336fd6ee8b21acab0de756 Mon Sep 17 00:00:00 2001 From: Richard Kettelerij Date: Mon, 4 Dec 2023 16:11:01 +0100 Subject: [PATCH 05/26] Remove unused extent --- engine/templates/openapi/tiles.go.json | 300 ------------------------- 1 file changed, 300 deletions(-) diff --git a/engine/templates/openapi/tiles.go.json b/engine/templates/openapi/tiles.go.json index 005ec8d7..4d02e327 100644 --- a/engine/templates/openapi/tiles.go.json +++ b/engine/templates/openapi/tiles.go.json @@ -299,306 +299,6 @@ } } }, - "extent": { - "description": "The extent of the data in the collection. In the Core only spatial and temporal\nextents are specified. Extensions may add additional members to represent other\nextents, for example, thermal or pressure ranges.\n\nThe first item in the array describes the overall extent of\nthe data. All subsequent items describe more precise extents,\ne.g., to identify clusters of data.\nClients only interested in the overall extent will only need to\naccess the first item in each array.", - "type": "object", - "properties": { - "spatial": { - "description": "The spatial extent of the data in the collection.", - "type": "object", - "properties": { - "bbox": { - "description": "One or more bounding boxes that describe the spatial extent of the dataset.\nIn the Core only a single bounding box is supported.\n\nExtensions may support additional areas.\nThe first bounding box describes the overall spatial\nextent of the data. All subsequent bounding boxes describe\nmore precise bounding boxes, e.g., to identify clusters of data.\nClients only interested in the overall spatial extent will\nonly need to access the first item in each array.", - "type": "array", - "minItems": 1, - "items": { - "description": "Each bounding box is provided as four or six numbers, depending on\nwhether the coordinate reference system includes a vertical axis\n(height or depth):\n\n* Lower left corner, coordinate axis 1\n* Lower left corner, coordinate axis 2\n* Minimum value, coordinate axis 3 (optional)\n* Upper right corner, coordinate axis 1\n* Upper right corner, coordinate axis 2\n* Maximum value, coordinate axis 3 (optional)\n\nIf the value consists of four numbers, the coordinate reference system is\nWGS 84 longitude/latitude (http://www.opengis.net/def/crs/OGC/1.3/CRS84)\nunless a different coordinate reference system is specified in a parameter `bbox-crs`.\n\nIf the value consists of six numbers, the coordinate reference system is WGS 84\nlongitude/latitude/ellipsoidal height (http://www.opengis.net/def/crs/OGC/0/CRS84h)\nunless a different coordinate reference system is specified in a parameter `bbox-crs`.\n\nFor WGS 84 longitude/latitude the values are in most cases the sequence of\nminimum longitude, minimum latitude, maximum longitude and maximum latitude.\nHowever, in cases where the box spans the antimeridian the first value\n(west-most box edge) is larger than the third value (east-most box edge).\n\nIf the vertical axis is included, the third and the sixth number are\nthe bottom and the top of the 3-dimensional bounding box.\n\nIf a feature has multiple spatial geometry properties, it is the decision of the\nserver whether only a single spatial geometry property is used to determine\nthe extent or all relevant geometries.", - "type": "array", - "oneOf": [ - { - "minItems": 4, - "maxItems": 4 - }, - { - "minItems": 6, - "maxItems": 6 - } - ], - "items": { - "type": "number" - }, - "example": [ - -180, - -90, - 180, - 90 - ] - } - }, - "crs": { - "description": "Coordinate reference system of the coordinates in the spatial extent\n(property `bbox`). The default reference system is WGS 84 longitude/latitude.\nIn the Core the only other supported coordinate reference system is\nWGS 84 longitude/latitude/ellipsoidal height for coordinates with height.\nExtensions may support additional coordinate reference systems and add\nadditional enum values.", - "type": "string", - "enum": [ - "http://www.opengis.net/def/crs/OGC/1.3/CRS84", - "http://www.opengis.net/def/crs/OGC/0/CRS84h" - ], - "default": "http://www.opengis.net/def/crs/OGC/1.3/CRS84" - }, - "grid": { - "description": "Provides information about the limited availability of data within the collection organized\nas a grid (regular or irregular) along each spatial dimension.", - "type": "array", - "minItems": 2, - "maxItems": 3, - "items": { - "type": "object", - "properties": { - "coordinates": { - "description": "List of coordinates along the dimension for which data organized as an irregular grid in the collection is available\n(e.g., 2, 10, 80, 100).", - "type": "array", - "minItems": 1, - "items": { - "oneOf": [ - { - "type": "string", - "nullable": true - }, - { - "type": "number" - } - ] - }, - "example": [ - 2, - 10, - 80, - 100 - ] - }, - "cellsCount": { - "description": "Number of samples available along the dimension for data organized as a regular grid.\nFor values representing the whole area of contiguous cells spanning _resolution_ units along the dimension, this will be (_upperBound_ - _lowerBound_) / _resolution_.\nFor values representing infinitely small point cells spaced by _resolution_ units along the dimension, this will be (_upperBound_ - _lowerBound_) / _resolution_ + 1.", - "type": "integer", - "example": 50 - }, - "resolution": { - "description": "Resolution of regularly gridded data along the dimension in the collection", - "oneOf": [ - { - "type": "string", - "nullable": true - }, - { - "type": "number" - } - ], - "example": 0.0006866455078 - } - } - } - } - } - }, - "temporal": { - "description": "The temporal extent of the features in the collection.", - "type": "object", - "properties": { - "interval": { - "description": "One or more time intervals that describe the temporal extent of the dataset.\nIn the Core only a single time interval is supported.\n\nExtensions may support multiple intervals.\nThe first time interval describes the overall\ntemporal extent of the data. All subsequent time intervals describe\nmore precise time intervals, e.g., to identify clusters of data.\nClients only interested in the overall extent will only need\nto access the first item in each array.", - "type": "array", - "minItems": 1, - "items": { - "description": "Begin and end times of the time interval. The timestamps are in the\ntemporal coordinate reference system specified in `trs`. By default\nthis is the Gregorian calendar.\n\nThe value `null` for start or end time is supported and indicates a half-bounded time interval.", - "type": "array", - "minItems": 2, - "maxItems": 2, - "items": { - "type": "string", - "format": "date-time", - "nullable": true - }, - "example": [ - "2011-11-11T12:22:11Z", - null - ] - } - }, - "trs": { - "description": "Coordinate reference system of the coordinates in the temporal extent\n(property `interval`). The default reference system is the Gregorian calendar.\nIn the Core this is the only supported temporal coordinate reference system.\nExtensions may support additional temporal coordinate reference systems and add\nadditional enum values.", - "type": "string", - "enum": [ - "http://www.opengis.net/def/uom/ISO-8601/0/Gregorian" - ], - "default": "http://www.opengis.net/def/uom/ISO-8601/0/Gregorian" - }, - "grid": { - "type": "object", - "description": "Provides information about the limited availability of data within the collection organized as a grid (regular or irregular) along the temporal dimension.", - "properties": { - "coordinates": { - "description": "List of coordinates along the temporal dimension for which data organized as an irregular grid in the collection is available\n(e.g., \"2017-11-14T09:00Z\",\"2017-11-14T12:00Z\",\"2017-11-14T15:00Z\",\"2017-11-14T18:00Z\",\"2017-11-14T21:00Z\").", - "type": "array", - "minItems": 1, - "items": { - "type": "string", - "nullable": true - }, - "example": [ - [ - "2020-11-12T12:15Z", - "2020-11-12T12:30Z", - "2020-11-12T12:45Z" - ] - ] - }, - "cellsCount": { - "description": "Number of samples available along the temporal dimension for data organized as a regular grid.\nFor values representing the whole area of contiguous cells spanning _resolution_ units along the dimension, this will be (_upperBound_ - _lowerBound_) / _resolution_.\nFor values representing infinitely small point cells spaced by _resolution_ units along the dimension, this will be (_upperBound_ - _lowerBound_) / _resolution_ + 1.", - "type": "integer", - "example": 50 - }, - "resolution": { - "description": "Resolution of regularly gridded data along the temporal dimension in the collection", - "oneOf": [ - { - "type": "string", - "nullable": true - }, - { - "type": "number" - } - ], - "example": "PT1H" - } - } - } - } - } - } - }, - "extent-uad": { - "title": "Extent with Uniform Additional Dimensions Schema", - "description": "The extent module only addresses spatial and temporal extents. This module extends extent by specifying how\nintervals and crs properties can be used to specify additional geometries.", - "allOf": [ - { - "$ref": "#/components/schemas/extent" - }, - { - "type": "object", - "additionalProperties": { - "description": "The domain intervals for any additional dimensions of the extent (envelope) beyond those described in temporal and spatial.", - "type": "object", - "oneOf": [ - { - "required": [ - "interval", - "crs" - ] - }, - { - "required": [ - "interval", - "trs" - ] - }, - { - "required": [ - "interval", - "vrs" - ] - } - ], - "properties": { - "interval": { - "description": "One or more intervals that describe the extent for this dimension of the dataset.\nThe value `null` is supported and indicates an unbounded or half-bounded interval.\nThe first interval describes the overall extent of the data for this dimension.\nAll subsequent intervals describe more precise intervals, e.g., to identify clusters of data.\nClients only interested in the overall extent will only need\nto access the first item (a pair of lower and upper bound values).", - "type": "array", - "minItems": 1, - "items": { - "description": "Lower and upper bound values of the interval. The values\nare in the coordinate reference system specified in `crs`, `trs` or `vrs`.", - "type": "array", - "minItems": 2, - "maxItems": 2, - "items": { - "oneOf": [ - { - "type": "string", - "nullable": true - }, - { - "type": "number" - } - ] - }, - "example": [ - "2011-11-11T12:22:11Z", - 32.5, - null - ] - } - }, - "crs": { - "type": "string", - "description": "generic coordinate reference system suitable for any type of dimensions" - }, - "trs": { - "type": "string", - "description": "temporal coordinate reference system (e.g. as defined by Features for 'temporal')" - }, - "vrs": { - "type": "string", - "description": "vertical coordinate reference system (e.g. as defined in EDR for 'vertical')" - }, - "grid": { - "type": "object", - "description": "Provides information about the limited availability of data within the collection organized as a grid (regular or irregular) along the dimension.", - "properties": { - "coordinates": { - "description": "List of coordinates along the temporal dimension for which data organized as an irregular grid in the collection is available\n(e.g., 2, 10, 80, 100).", - "type": "array", - "minItems": 1, - "items": { - "oneOf": [ - { - "type": "string", - "nullable": true - }, - { - "type": "number" - } - ] - }, - "example": [ - 2, - 10, - 80, - 100 - ] - }, - "cellsCount": { - "description": "Number of samples available along the dimension for data organized as a regular grid.\nFor values representing the whole area of contiguous cells spanning _resolution_ units along the dimension, this will be (_upperBound_ - _lowerBound_) / _resolution_.\nFor values representing infinitely small point cells spaced by _resolution_ units along the dimension, this will be (_upperBound_ - _lowerBound_) / _resolution_ + 1.", - "type": "integer", - "example": 50 - }, - "resolution": { - "description": "Resolution of regularly gridded data along the dimension in the collection", - "oneOf": [ - { - "type": "string", - "nullable": true - }, - { - "type": "number" - } - ], - "example": [ - "PT1H", - 0.0006866455078 - ] - } - } - } - } - } - } - ] - }, "crs": { "title": "CRS", "oneOf": [ From 6deaf910c4f47b02cc198e773545afa6fbb5e2e5 Mon Sep 17 00:00:00 2001 From: Richard Kettelerij Date: Mon, 4 Dec 2023 16:11:26 +0100 Subject: [PATCH 06/26] Remove unused components (overlaps with OGC API Features) --- engine/templates/openapi/3dgeovolumes.go.json | 1368 ----------------- 1 file changed, 1368 deletions(-) diff --git a/engine/templates/openapi/3dgeovolumes.go.json b/engine/templates/openapi/3dgeovolumes.go.json index 790a0379..68139727 100644 --- a/engine/templates/openapi/3dgeovolumes.go.json +++ b/engine/templates/openapi/3dgeovolumes.go.json @@ -154,874 +154,10 @@ }, "components" : { "schemas" : { - "Link" : { - "required" : [ "href" ], - "type" : "object", - "properties" : { - "href" : { - "type" : "string", - "format" : "uri-reference" - }, - "rel" : { - "type" : "string" - }, - "type" : { - "type" : "string" - }, - "title" : { - "type" : "string" - } - } - }, - "StringArray" : { - "type" : "array", - "items" : { - "type" : "string" - } - }, - "StringArrayArray" : { - "type" : "array", - "items" : { - "$ref" : "#/components/schemas/StringArray" - } - }, - "SpatialExtent" : { - "required" : [ "bbox", "crs" ], - "type" : "object", - "properties" : { - "crs" : { - "type" : "string" - }, - "bbox" : { - "$ref" : "#/components/schemas/doubleArrayArray" - } - } - }, - "doubleArrayArray" : { - "type" : "array", - "items" : { - "$ref" : "#/components/schemas/doubleArray" - } - }, - "Extent" : { - "type" : "object", - "properties" : { - "temporal" : { - "$ref" : "#/components/schemas/TemporalExtent" - }, - "spatial" : { - "$ref" : "#/components/schemas/SpatialExtent" - } - } - }, - "doubleArray" : { - "type" : "array", - "items" : { - "type" : "number" - } - }, - "TemporalExtent" : { - "required" : [ "interval", "trs" ], - "type" : "object", - "properties" : { - "interval" : { - "$ref" : "#/components/schemas/StringArrayArray" - }, - "trs" : { - "type" : "string" - } - } - }, - "ExternalDocumentation" : { - "required" : [ "url" ], - "type" : "object", - "properties" : { - "description" : { - "type" : "string" - }, - "url" : { - "type" : "string" - } - } - }, - "htmlSchema" : { - "type" : "string", - "example" : "..." - }, - "Collections" : { - "type" : "object", - "properties" : { - "crs" : { - "type" : "array", - "items" : { - "type" : "string" - } - }, - "collections" : { - "type" : "array", - "items" : { - "$ref" : "#/components/schemas/Collection" - } - }, - "description" : { - "type" : "string" - }, - "title" : { - "type" : "string" - }, - "links" : { - "type" : "array", - "items" : { - "$ref" : "#/components/schemas/Link" - } - } - } - }, - "Collection" : { - "required" : [ "id" ], - "type" : "object", - "properties" : { - "extent" : { - "$ref" : "#/components/schemas/Extent" - }, - "storageCrsCoordinateEpoch" : { - "type" : "number" - }, - "crs" : { - "type" : "array", - "items" : { - "type" : "string" - } - }, - "storageCrs" : { - "type" : "string" - }, - "itemType" : { - "type" : "string" - }, - "id" : { - "type" : "string" - }, - "description" : { - "type" : "string" - }, - "title" : { - "type" : "string" - }, - "links" : { - "type" : "array", - "items" : { - "$ref" : "#/components/schemas/Link" - } - } - } - }, - "featureCollectionGeoJson_building" : { - "required" : [ "type", "features" ], - "type" : "object", - "properties" : { - "type" : { - "type" : "string", - "enum" : [ "FeatureCollection" ] - }, - "features" : { - "type" : "array", - "items" : { - "$ref" : "#/components/schemas/featureGeoJson_building" - } - }, - "links" : { - "type" : "array", - "items" : { - "$ref" : "#/components/schemas/Link" - } - }, - "timeStamp" : { - "$ref" : "https://raw.githubusercontent.com/opengeospatial/ogcapi-features/master/core/openapi/ogcapi-features-1.yaml#/components/schemas/timeStamp" - }, - "numberMatched" : { - "$ref" : "https://raw.githubusercontent.com/opengeospatial/ogcapi-features/master/core/openapi/ogcapi-features-1.yaml#/components/schemas/numberMatched" - }, - "numberReturned" : { - "$ref" : "https://raw.githubusercontent.com/opengeospatial/ogcapi-features/master/core/openapi/ogcapi-features-1.yaml#/components/schemas/numberReturned" - } - } - }, "glTF" : { "type" : "string", "format" : "binary" }, - "CityJson_ignore" : { - "required" : [ "type", "version", "CityObjects", "vertices" ], - "type" : "object", - "properties" : { - "type" : { - "type" : "string", - "enum" : [ "CityJSON" ] - }, - "version" : { - "type" : "string", - "enum" : [ "1.0", "1.1" ] - }, - "transform" : { - "required" : [ "scale", "translate" ], - "type" : "object", - "properties" : { - "scale" : { - "maxItems" : 3, - "minItems" : 3, - "type" : "array", - "items" : { - "type" : "number" - } - }, - "translate" : { - "maxItems" : 3, - "minItems" : 3, - "type" : "array", - "items" : { - "type" : "number" - } - } - } - }, - "vertices" : { - "type" : "array", - "items" : { - "maxItems" : 3, - "minItems" : 3, - "type" : "array", - "items" : { - "type" : "number" - } - } - }, - "metadata" : { - "type" : "object", - "properties" : { - "title" : { - "type" : "string" - }, - "referenceSystem" : { - "pattern" : "^http://www.opengis.net/def/crs/(EPSG|OGC)/[^/]+/[^/]+$", - "type" : "string" - }, - "geographicalExtent" : { - "maxItems" : 6, - "minItems" : 6, - "type" : "array", - "items" : { - "type" : "number" - } - } - } - }, - "CityObjects" : { - "type" : "object", - "additionalProperties" : { - "required" : [ "type" ], - "type" : "object", - "properties" : { - "type" : { - "type" : "string", - "enum" : [ "Building", "BuildingPart" ] - }, - "parents" : { - "type" : "array", - "items" : { - "type" : "string" - } - }, - "children" : { - "type" : "array", - "items" : { - "type" : "string" - } - }, - "geometry" : { - "type" : "array", - "items" : { - "required" : [ "type", "lod", "boundaries" ], - "type" : "object", - "properties" : { - "type" : { - "type" : "string", - "enum" : [ "Solid" ] - }, - "lod" : { - "type" : "string", - "enum" : [ "1", "2" ] - }, - "boundaries" : { - "type" : "array", - "items" : { - "type" : "array", - "items" : { - "type" : "array", - "items" : { - "type" : "array", - "items" : { - "type" : "integer", - "format" : "int32" - } - } - } - } - }, - "semantics" : { - "type" : "object", - "properties" : { - "surfaces" : { - "type" : "array", - "items" : { - "type" : "object", - "properties" : { - "type" : { - "type" : "string", - "enum" : [ "CeilingSurface", "ClosureSurface", "Door", "FloorSurface", "GroundSurface", "InteriorWallSurface", "OuterCeilingSurface", "OuterFloorSurface", "RoofSurface", "WallSurface", "Window" ] - } - } - } - }, - "values" : { - "type" : "array", - "items" : { - "type" : "array", - "items" : { - "type" : "integer", - "format" : "int32" - } - } - } - } - } - } - } - }, - "address" : { - "type" : "array", - "items" : { - "type" : "object", - "properties" : { - "ThoroughfareName" : { - "type" : "string" - }, - "ThoroughfareNumber" : { - "type" : "string" - }, - "LocalityName" : { - "type" : "string" - }, - "CountryName" : { - "type" : "string" - }, - "location" : { - "required" : [ "type", "lod", "boundaries" ], - "type" : "object", - "properties" : { - "type" : { - "type" : "string", - "enum" : [ "MultiPoint" ] - }, - "lod" : { - "type" : "string", - "enum" : [ "1", "2" ] - }, - "boundaries" : { - "type" : "array", - "items" : { - "type" : "integer", - "format" : "int32" - } - } - } - } - } - } - }, - "attributes" : { - "type" : "object" - } - } - } - } - } - }, - "featureGeoJson_building" : { - "title" : "Buildings", - "required" : [ "type", "geometry", "properties" ], - "type" : "object", - "properties" : { - "type" : { - "type" : "string", - "enum" : [ "Feature" ] - }, - "links" : { - "type" : "array", - "items" : { - "$ref" : "#/components/schemas/Link" - } - }, - "id" : { - "title" : "ALKIS-ID", - "type" : "string" - }, - "geometry" : { - "$ref" : "https://raw.githubusercontent.com/opengeospatial/ogcapi-features/master/core/openapi/ogcapi-features-1.yaml#/components/schemas/polygonGeoJSON" - }, - "properties" : { - "type" : "object", - "properties" : { - "name" : { - "title" : "Name", - "type" : "string", - "description" : "Only set for proper building names." - }, - "measuredHeight" : { - "title" : "Gebäudehöhe [m]", - "type" : "number", - "description" : "Height of the building as the difference in meters between the highest reference point and the lowest reference point of the building." - }, - "storeysAboveGround" : { - "title" : "Geschosse", - "type" : "integer", - "format" : "int32" - }, - "roofType" : { - "title" : "Dachform", - "type" : "string", - "description" : "The meaning of the individual values results from the codelist RoofTypeTypeAdV.xml.", - "enum" : [ "1000", "2100", "2200", "3100", "3200", "3300", "3400", "3500", "3600", "3700", "3800", "3900", "4000", "5000", "9999" ] - }, - "creationDate" : { - "title" : "Erzeugungsdatum", - "type" : "string", - "format" : "date" - }, - "function" : { - "title" : "Gebäudefunktion", - "type" : "string", - "description" : "The meaning of the individual values results from the codelist BuildingFunctionTypeAdV.xml.", - "enum" : [ "31001_1000", "31001_1010", "31001_1020", "31001_1021", "31001_1022", "31001_1023", "31001_1024", "31001_1025", "31001_1100", "31001_1110", "31001_1120", "31001_1121", "31001_1122", "31001_1123", "31001_1130", "31001_1131", "31001_1210", "31001_1220", "31001_1221", "31001_1222", "31001_1223", "31001_1310", "31001_1311", "31001_1312", "31001_1313", "31001_2000", "31001_2010", "31001_2020", "31001_2030", "31001_2040", "31001_2050", "31001_2051", "31001_2052", "31001_2053", "31001_2054", "31001_2055", "31001_2056", "31001_2060", "31001_2070", "31001_2071", "31001_2072", "31001_2073", "31001_2074", "31001_2080", "31001_2081", "31001_2082", "31001_2083", "31001_2090", "31001_2091", "31001_2092", "31001_2093", "31001_2094", "31001_2100", "31001_2110", "31001_2111", "31001_2112", "31001_2113", "31001_2114", "31001_2120", "31001_2121", "31001_2130", "31001_2131", "31001_2140", "31001_2141", "31001_2142", "31001_2143", "31001_2150", "31001_2160", "31001_2170", "31001_2171", "31001_2172", "31001_2180", "31001_2200", "31001_2210", "31001_2211", "31001_2212", "31001_2213", "31001_2220", "31001_2310", "31001_2320", "31001_2400", "31001_2410", "31001_2411", "31001_2412", "31001_2420", "31001_2421", "31001_2422", "31001_2423", "31001_2424", "31001_2430", "31001_2431", "31001_2440", "31001_2441", "31001_2442", "31001_2443", "31001_2444", "31001_2450", "31001_2451", "31001_2460", "31001_2461", "31001_2462", "31001_2463", "31001_2464", "31001_2465", "31001_2500", "31001_2501", "31001_2510", "31001_2511", "31001_2512", "31001_2513", "31001_2520", "31001_2521", "31001_2522", "31001_2523", "31001_2527", "31001_2528", "31001_2529", "31001_2540", "31001_2560", "31001_2570", "31001_2571", "31001_2580", "31001_2590", "31001_2591", "31001_2600", "31001_2610", "31001_2611", "31001_2612", "31001_2620", "31001_2621", "31001_2622", "31001_2623", "31001_2700", "31001_2720", "31001_2721", "31001_2723", "31001_2724", "31001_2726", "31001_2727", "31001_2728", "31001_2729", "31001_2732", "31001_2735", "31001_2740", "31001_2741", "31001_2742", "31001_3000", "31001_3010", "31001_3011", "31001_3012", "31001_3013", "31001_3014", "31001_3015", "31001_3016", "31001_3017", "31001_3018", "31001_3019", "31001_3020", "31001_3021", "31001_3022", "31001_3023", "31001_3024", "31001_3030", "31001_3031", "31001_3032", "31001_3033", "31001_3034", "31001_3035", "31001_3036", "31001_3037", "31001_3038", "31001_3040", "31001_3041", "31001_3042", "31001_3043", "31001_3044", "31001_3045", "31001_3046", "31001_3047", "31001_3048", "31001_3050", "31001_3051", "31001_3052", "31001_3053", "31001_3060", "31001_3061", "31001_3062", "31001_3063", "31001_3064", "31001_3065", "31001_3066", "31001_3070", "31001_3071", "31001_3072", "31001_3073", "31001_3074", "31001_3075", "31001_3080", "31001_3081", "31001_3082", "31001_3090", "31001_3091", "31001_3092", "31001_3094", "31001_3095", "31001_3097", "31001_3098", "31001_3100", "31001_3200", "31001_3210", "31001_3211", "31001_3212", "31001_3220", "31001_3221", "31001_3222", "31001_3230", "31001_3240", "31001_3241", "31001_3242", "31001_3260", "31001_3261", "31001_3262", "31001_3263", "31001_3264", "31001_3270", "31001_3271", "31001_3272", "31001_3273", "31001_3280", "31001_3281", "31001_3290", "31001_9998", "51001_1001", "51001_1002", "51001_1003", "51001_1004", "51001_1005", "51001_1006", "51001_1007", "51001_1008", "51001_1009", "51001_1010", "51001_1011", "51001_1012", "51001_9998", "51001_9999", "51002_1215", "51002_1220", "51002_1230", "51002_1250", "51002_1260", "51002_1280", "51002_1290", "51002_1330", "51002_1331", "51002_1332", "51002_1333", "51002_1350", "51002_1400", "51002_9999", "51003_1201", "51003_1205", "51003_1206", "51003_9999", "51006_1430", "51006_1431", "51006_1432", "51006_1440", "51006_1470", "51006_1490", "51006_9999", "51007_1210", "51007_1400", "51007_1500", "51007_1510", "51007_1520", "51007_9999", "51009_1610", "51009_1611", "51009_1750", "51009_9999", "11_1001", "11_1002", "11_1003", "11_1004", "11_1005", "11_1006", "11_1036", "11_1101", "11_1111", "11_1112", "11_1113", "11_1114", "11_1115", "11_1116", "11_1118", "11_1121", "11_1122", "11_1123", "11_1124", "11_1128", "11_1131", "11_1132", "11_1133", "11_1134", "11_1135", "11_1136", "11_1137", "11_1138", "11_1141", "11_1142", "11_1143", "11_1144", "11_1145", "11_1148", "11_1151", "11_1152", "11_1158", "11_1161", "11_1162", "11_1163", "11_1164", "11_1165", "11_1168", "11_1171", "11_1172", "11_1173", "11_1174", "11_1175", "11_1178", "11_1181", "11_1182", "11_1188", "11_1191", "11_1192", "11_1194", "11_1195", "11_1196", "11_1197", "11_1198", "11_1211", "11_1221", "11_1231", "11_1301", "11_1311", "11_1321", "11_1331", "11_1341", "11_1361", "11_1371", "11_1372", "11_1373", "11_1374", "11_1375", "11_1378", "11_1381", "11_1399", "11_1401", "11_1411", "11_1421", "11_1431", "11_1441", "11_1442", "11_1443", "11_1444", "11_1445", "11_1448", "11_1451", "11_1461", "11_1462", "11_1463", "11_1468", "11_1471", "11_1472", "11_1473", "11_1474", "11_1478", "11_1481", "11_1482", "11_1483", "11_1484", "11_1488", "11_1701", "11_1711", "11_1721", "11_1731", "11_1741", "11_1742", "11_1743", "11_1748", "11_1751", "11_1761", "11_1771", "11_1772", "11_1773", "11_1774", "11_1778", "11_1781", "11_1911", "11_1913", "11_2101", "11_2121", "11_2131", "11_2141", "11_2301", "11_2311", "11_2312", "11_2313", "11_2318", "11_2321", "11_2322", "11_2323", "11_2324", "11_2328", "11_2332", "11_2338", "11_2341", "11_2342", "11_2343", "11_2344", "11_2348", "11_2351", "11_2358", "11_2361", "11_2362", "11_2363", "11_2364", "11_2365", "11_2366", "11_2367", "11_2368", "11_2501", "11_2511", "11_2512", "11_2514", "11_2515", "11_2518", "11_2521", "11_2522", "11_2523", "11_2528", "11_2541", "11_2548", "11_2551", "11_2561", "11_2571", "11_2572", "11_2581", "11_2591", "11_2601", "11_2611", "11_2612", "11_2619", "11_2621", "11_2622", "11_2623", "11_2628", "11_2701", "11_2711", "11_2721", "11_2723", "11_2724", "11_2725", "11_2726", "11_2727", "11_2728", "11_2731", "11_2736", "11_2737", "11_2738", "11_2741", "11_2742", "11_2748", "11_2801", "11_2811", "11_2812", "11_2818", "11_2821", "11_2822", "11_2828", "11_2831", "11_2841", "11_2842", "11_2848", "11_2851", "11_2861", "11_2862", "11_2863", "11_2868", "11_2871", "11_2872", "11_2873", "11_2874", "11_2878", "11_2881", "11_2882", "11_2883", "11_2888", "11_2891", "11_2894", "11_2921" ] - }, - "externalReferences" : { - "type" : "array", - "description" : "ALKIS-Objektidentifikator", - "items" : { - "title" : "Fachdatenverbindungen", - "required" : [ ], - "type" : "object", - "properties" : { - "name" : { - "title" : "Bezeichnung", - "type" : "string" - }, - "informationSystem" : { - "title" : "Informationssystem", - "type" : "string" - } - }, - "description" : "ALKIS-Objektidentifikator" - } - }, - "bezugspunktDach" : { - "title" : "Bezugspunkt Dach", - "type" : "string", - "enum" : [ "1000", "2000", "2100", "2200", "2300", "3000", "4000", "9998" ] - }, - "datenquelleBodenhoehe" : { - "title" : "Datenquelle Bodenhöhe", - "type" : "string", - "enum" : [ "1000", "1100", "1200", "1300", "1400", "1500", "1600", "1700", "1800", "2000", "3000", "4000" ] - }, - "datenquelleDachhoehe" : { - "title" : "Datenquelle Dachhöhe", - "type" : "string", - "enum" : [ "1000", "2000", "3000", "4000", "5000", "6000" ] - }, - "datenquelleLage" : { - "title" : "Datenquelle Lage", - "type" : "string", - "enum" : [ "1000", "1100", "1200", "1300", "2300", "2000", "3000" ] - }, - "gemeindeschluessel" : { - "title" : "Gemeindeschlüssel", - "type" : "string" - }, - "grundrissaktualitaet" : { - "title" : "Grundrissaktualität", - "type" : "string", - "format" : "date" - }, - "geometrietyp2dReferenz" : { - "title" : "Geometrietyp 2D Referenz", - "type" : "string" - }, - "address" : { - "type" : "array", - "items" : { - "title" : "Adresse", - "required" : [ ], - "type" : "object", - "properties" : { - "multiPoint" : { - "$ref" : "https://raw.githubusercontent.com/opengeospatial/ogcapi-features/master/core/openapi/ogcapi-features-1.yaml#/components/schemas/multipointGeoJSON" - }, - "ThoroughfareName" : { - "title" : "Straße", - "type" : "string" - }, - "ThoroughfareNumber" : { - "title" : "Hausnummer", - "type" : "string" - }, - "PostalCode" : { - "title" : "Postleitzahl", - "type" : "string" - }, - "LocalityName" : { - "title" : "Stadt", - "type" : "string" - }, - "AdministrativeArea" : { - "title" : "Verwaltungsbezirk", - "type" : "string" - }, - "CountryName" : { - "title" : "Land", - "type" : "string" - } - } - } - }, - "surfaces" : { - "type" : "array", - "items" : { - "title" : "Begrenzungsflächen (LoD 2)", - "required" : [ ], - "type" : "object", - "properties" : { - "gml_id" : { - "title" : "ID", - "type" : "string" - }, - "creationDate" : { - "title" : "Erzeugungsdatum", - "type" : "string", - "format" : "date" - }, - "surfaceType" : { - "title" : "Flächentyp", - "type" : "string" - }, - "lod2MultiSurface" : { - "$ref" : "https://raw.githubusercontent.com/opengeospatial/ogcapi-features/master/core/openapi/ogcapi-features-1.yaml#/components/schemas/multipolygonGeoJSON" - } - } - } - }, - "consistsOfBuildingPart" : { - "type" : "array", - "items" : { - "title" : "Gebäudeteile", - "required" : [ ], - "type" : "object", - "properties" : { - "id" : { - "title" : "Objekt-ID", - "type" : "integer", - "format" : "int32" - }, - "gml_id" : { - "title" : "GML-ID", - "type" : "string" - }, - "name" : { - "title" : "Name", - "type" : "string", - "description" : "Nur bei Gebäudeeigennamen gesetzt." - }, - "bbox" : { - "$ref" : "https://raw.githubusercontent.com/opengeospatial/ogcapi-features/master/core/openapi/ogcapi-features-1.yaml#/components/schemas/polygonGeoJSON" - }, - "measuredHeight" : { - "title" : "Gebäudehöhe [m]", - "type" : "number", - "description" : "Höhe des Gebäudes aus der Differenz in Metern zwischen dem höchsten Bezugspunkt und dem tiefsten Bezugspunkt des Gebäudes." - }, - "storeysAboveGround" : { - "title" : "Geschosse", - "type" : "integer", - "format" : "int32" - }, - "roofType" : { - "title" : "Dachform", - "type" : "string", - "description" : "Die Bedeutung der einzelnen Werte ergibt aus der Codelist RoofTypeTypeAdV.xml.", - "enum" : [ "1000", "2100", "2200", "3100", "3200", "3300", "3400", "3500", "3600", "3700", "3800", "3900", "4000", "5000", "9999" ] - }, - "creationDate" : { - "title" : "Erzeugungsdatum", - "type" : "string", - "format" : "date" - }, - "function" : { - "title" : "Gebäudefunktion", - "type" : "string", - "description" : "Die Bedeutung der einzelnen Werte ergibt aus der Codelist BuildingFunctionTypeAdV.xml.", - "enum" : [ "31001_1000", "31001_1010", "31001_1020", "31001_1021", "31001_1022", "31001_1023", "31001_1024", "31001_1025", "31001_1100", "31001_1110", "31001_1120", "31001_1121", "31001_1122", "31001_1123", "31001_1130", "31001_1131", "31001_1210", "31001_1220", "31001_1221", "31001_1222", "31001_1223", "31001_1310", "31001_1311", "31001_1312", "31001_1313", "31001_2000", "31001_2010", "31001_2020", "31001_2030", "31001_2040", "31001_2050", "31001_2051", "31001_2052", "31001_2053", "31001_2054", "31001_2055", "31001_2056", "31001_2060", "31001_2070", "31001_2071", "31001_2072", "31001_2073", "31001_2074", "31001_2080", "31001_2081", "31001_2082", "31001_2083", "31001_2090", "31001_2091", "31001_2092", "31001_2093", "31001_2094", "31001_2100", "31001_2110", "31001_2111", "31001_2112", "31001_2113", "31001_2114", "31001_2120", "31001_2121", "31001_2130", "31001_2131", "31001_2140", "31001_2141", "31001_2142", "31001_2143", "31001_2150", "31001_2160", "31001_2170", "31001_2171", "31001_2172", "31001_2180", "31001_2200", "31001_2210", "31001_2211", "31001_2212", "31001_2213", "31001_2220", "31001_2310", "31001_2320", "31001_2400", "31001_2410", "31001_2411", "31001_2412", "31001_2420", "31001_2421", "31001_2422", "31001_2423", "31001_2424", "31001_2430", "31001_2431", "31001_2440", "31001_2441", "31001_2442", "31001_2443", "31001_2444", "31001_2450", "31001_2451", "31001_2460", "31001_2461", "31001_2462", "31001_2463", "31001_2464", "31001_2465", "31001_2500", "31001_2501", "31001_2510", "31001_2511", "31001_2512", "31001_2513", "31001_2520", "31001_2521", "31001_2522", "31001_2523", "31001_2527", "31001_2528", "31001_2529", "31001_2540", "31001_2560", "31001_2570", "31001_2571", "31001_2580", "31001_2590", "31001_2591", "31001_2600", "31001_2610", "31001_2611", "31001_2612", "31001_2620", "31001_2621", "31001_2622", "31001_2623", "31001_2700", "31001_2720", "31001_2721", "31001_2723", "31001_2724", "31001_2726", "31001_2727", "31001_2728", "31001_2729", "31001_2732", "31001_2735", "31001_2740", "31001_2741", "31001_2742", "31001_3000", "31001_3010", "31001_3011", "31001_3012", "31001_3013", "31001_3014", "31001_3015", "31001_3016", "31001_3017", "31001_3018", "31001_3019", "31001_3020", "31001_3021", "31001_3022", "31001_3023", "31001_3024", "31001_3030", "31001_3031", "31001_3032", "31001_3033", "31001_3034", "31001_3035", "31001_3036", "31001_3037", "31001_3038", "31001_3040", "31001_3041", "31001_3042", "31001_3043", "31001_3044", "31001_3045", "31001_3046", "31001_3047", "31001_3048", "31001_3050", "31001_3051", "31001_3052", "31001_3053", "31001_3060", "31001_3061", "31001_3062", "31001_3063", "31001_3064", "31001_3065", "31001_3066", "31001_3070", "31001_3071", "31001_3072", "31001_3073", "31001_3074", "31001_3075", "31001_3080", "31001_3081", "31001_3082", "31001_3090", "31001_3091", "31001_3092", "31001_3094", "31001_3095", "31001_3097", "31001_3098", "31001_3100", "31001_3200", "31001_3210", "31001_3211", "31001_3212", "31001_3220", "31001_3221", "31001_3222", "31001_3230", "31001_3240", "31001_3241", "31001_3242", "31001_3260", "31001_3261", "31001_3262", "31001_3263", "31001_3264", "31001_3270", "31001_3271", "31001_3272", "31001_3273", "31001_3280", "31001_3281", "31001_3290", "31001_9998", "51001_1001", "51001_1002", "51001_1003", "51001_1004", "51001_1005", "51001_1006", "51001_1007", "51001_1008", "51001_1009", "51001_1010", "51001_1011", "51001_1012", "51001_9998", "51001_9999", "51002_1215", "51002_1220", "51002_1230", "51002_1250", "51002_1260", "51002_1280", "51002_1290", "51002_1330", "51002_1331", "51002_1332", "51002_1333", "51002_1350", "51002_1400", "51002_9999", "51003_1201", "51003_1205", "51003_1206", "51003_9999", "51006_1430", "51006_1431", "51006_1432", "51006_1440", "51006_1470", "51006_1490", "51006_9999", "51007_1210", "51007_1400", "51007_1500", "51007_1510", "51007_1520", "51007_9999", "51009_1610", "51009_1611", "51009_1750", "51009_9999", "11_1001", "11_1002", "11_1003", "11_1004", "11_1005", "11_1006", "11_1036", "11_1101", "11_1111", "11_1112", "11_1113", "11_1114", "11_1115", "11_1116", "11_1118", "11_1121", "11_1122", "11_1123", "11_1124", "11_1128", "11_1131", "11_1132", "11_1133", "11_1134", "11_1135", "11_1136", "11_1137", "11_1138", "11_1141", "11_1142", "11_1143", "11_1144", "11_1145", "11_1148", "11_1151", "11_1152", "11_1158", "11_1161", "11_1162", "11_1163", "11_1164", "11_1165", "11_1168", "11_1171", "11_1172", "11_1173", "11_1174", "11_1175", "11_1178", "11_1181", "11_1182", "11_1188", "11_1191", "11_1192", "11_1194", "11_1195", "11_1196", "11_1197", "11_1198", "11_1211", "11_1221", "11_1231", "11_1301", "11_1311", "11_1321", "11_1331", "11_1341", "11_1361", "11_1371", "11_1372", "11_1373", "11_1374", "11_1375", "11_1378", "11_1381", "11_1399", "11_1401", "11_1411", "11_1421", "11_1431", "11_1441", "11_1442", "11_1443", "11_1444", "11_1445", "11_1448", "11_1451", "11_1461", "11_1462", "11_1463", "11_1468", "11_1471", "11_1472", "11_1473", "11_1474", "11_1478", "11_1481", "11_1482", "11_1483", "11_1484", "11_1488", "11_1701", "11_1711", "11_1721", "11_1731", "11_1741", "11_1742", "11_1743", "11_1748", "11_1751", "11_1761", "11_1771", "11_1772", "11_1773", "11_1774", "11_1778", "11_1781", "11_1911", "11_1913", "11_2101", "11_2121", "11_2131", "11_2141", "11_2301", "11_2311", "11_2312", "11_2313", "11_2318", "11_2321", "11_2322", "11_2323", "11_2324", "11_2328", "11_2332", "11_2338", "11_2341", "11_2342", "11_2343", "11_2344", "11_2348", "11_2351", "11_2358", "11_2361", "11_2362", "11_2363", "11_2364", "11_2365", "11_2366", "11_2367", "11_2368", "11_2501", "11_2511", "11_2512", "11_2514", "11_2515", "11_2518", "11_2521", "11_2522", "11_2523", "11_2528", "11_2541", "11_2548", "11_2551", "11_2561", "11_2571", "11_2572", "11_2581", "11_2591", "11_2601", "11_2611", "11_2612", "11_2619", "11_2621", "11_2622", "11_2623", "11_2628", "11_2701", "11_2711", "11_2721", "11_2723", "11_2724", "11_2725", "11_2726", "11_2727", "11_2728", "11_2731", "11_2736", "11_2737", "11_2738", "11_2741", "11_2742", "11_2748", "11_2801", "11_2811", "11_2812", "11_2818", "11_2821", "11_2822", "11_2828", "11_2831", "11_2841", "11_2842", "11_2848", "11_2851", "11_2861", "11_2862", "11_2863", "11_2868", "11_2871", "11_2872", "11_2873", "11_2874", "11_2878", "11_2881", "11_2882", "11_2883", "11_2888", "11_2891", "11_2894", "11_2921" ] - }, - "externalReferences" : { - "type" : "array", - "description" : "ALKIS-Objektidentifikator", - "items" : { - "title" : "Fachdatenverbindungen", - "required" : [ ], - "type" : "object", - "properties" : { - "name" : { - "title" : "Bezeichnung", - "type" : "string" - }, - "informationSystem" : { - "title" : "Informationssystem", - "type" : "string" - } - }, - "description" : "ALKIS-Objektidentifikator" - } - }, - "bezugspunktDach" : { - "title" : "Bezugspunkt Dach", - "type" : "string", - "enum" : [ "1000", "2000", "2100", "2200", "2300", "3000", "4000", "9998" ] - }, - "datenquelleBodenhoehe" : { - "title" : "Datenquelle Bodenhöhe", - "type" : "string", - "enum" : [ "1000", "1100", "1200", "1300", "1400", "1500", "1600", "1700", "1800", "2000", "3000", "4000" ] - }, - "datenquelleDachhoehe" : { - "title" : "Datenquelle Dachhöhe", - "type" : "string", - "enum" : [ "1000", "2000", "3000", "4000", "5000", "6000" ] - }, - "datenquelleLage" : { - "title" : "Datenquelle Lage", - "type" : "string", - "enum" : [ "1000", "1100", "1200", "1300", "2300", "2000", "3000" ] - }, - "gemeindeschluessel" : { - "title" : "Gemeindeschlüssel", - "type" : "string" - }, - "grundrissaktualitaet" : { - "title" : "Grundrissaktualität", - "type" : "string", - "format" : "date" - }, - "geometrietyp2dReferenz" : { - "title" : "Geometrietyp 2D Referenz", - "type" : "string" - }, - "address" : { - "type" : "array", - "items" : { - "title" : "Adresse", - "required" : [ ], - "type" : "object", - "properties" : { - "multiPoint" : { - "$ref" : "https://raw.githubusercontent.com/opengeospatial/ogcapi-features/master/core/openapi/ogcapi-features-1.yaml#/components/schemas/multipointGeoJSON" - }, - "ThoroughfareName" : { - "title" : "Straße", - "type" : "string" - }, - "ThoroughfareNumber" : { - "title" : "Hausnummer", - "type" : "string" - }, - "PostalCode" : { - "title" : "Postleitzahl", - "type" : "string" - }, - "LocalityName" : { - "title" : "Stadt", - "type" : "string" - }, - "AdministrativeArea" : { - "title" : "Verwaltungsbezirk", - "type" : "string" - }, - "CountryName" : { - "title" : "Land", - "type" : "string" - } - } - } - }, - "lod2Solid" : { - "$ref" : "https://raw.githubusercontent.com/opengeospatial/ogcapi-features/master/core/openapi/ogcapi-features-1.yaml#/components/schemas/multipolygonGeoJSON" - }, - "surfaces" : { - "type" : "array", - "items" : { - "title" : "Begrenzungsflächen (LoD 2)", - "required" : [ ], - "type" : "object", - "properties" : { - "gml_id" : { - "title" : "ID", - "type" : "string" - }, - "creationDate" : { - "title" : "Erzeugungsdatum", - "type" : "string", - "format" : "date" - }, - "surfaceType" : { - "title" : "Flächentyp", - "type" : "string" - }, - "lod2MultiSurface" : { - "$ref" : "https://raw.githubusercontent.com/opengeospatial/ogcapi-features/master/core/openapi/ogcapi-features-1.yaml#/components/schemas/multipolygonGeoJSON" - } - } - } - } - } - } - } - } - } - } - }, - "anyObject" : { - "type" : "object" - }, - "Metadata3dSchema" : { - "required" : [ "id" ], - "type" : "object", - "properties" : { - "version" : { - "type" : "string" - }, - "name" : { - "type" : "string" - }, - "classes" : { - "type" : "object", - "additionalProperties" : { - "$ref" : "#/components/schemas/SchemaClass" - } - }, - "id" : { - "type" : "string" - }, - "enums" : { - "type" : "object", - "additionalProperties" : { - "$ref" : "#/components/schemas/SchemaEnum" - } - }, - "description" : { - "type" : "string" - } - } - }, - "SchemaProperty" : { - "required" : [ "check", "type" ], - "type" : "object", - "properties" : { - "componentType" : { - "$ref" : "#/components/schemas/ComponentType" - }, - "check" : { - "$ref" : "#/components/schemas/void" - }, - "type" : { - "$ref" : "#/components/schemas/Type" - }, - "noData" : { - "type" : "string" - }, - "enumType" : { - "type" : "string" - }, - "description" : { - "type" : "string" - }, - "required" : { - "type" : "boolean" - } - } - }, - "Type" : { - "type" : "string", - "enum" : [ "SCALAR", "VEC2", "VEC3", "VEC4", "MAT2", "MAT3", "MAT4", "STRING", "BOOLEAN", "ENUM" ] - }, - "SchemaEnum" : { - "required" : [ "check", "valueType" ], - "type" : "object", - "properties" : { - "name" : { - "type" : "string" - }, - "check" : { - "$ref" : "#/components/schemas/void" - }, - "valueType" : { - "$ref" : "#/components/schemas/ComponentType" - }, - "values" : { - "type" : "array", - "items" : { - "$ref" : "#/components/schemas/SchemaEnumValue" - } - }, - "description" : { - "type" : "string" - } - } - }, - "SchemaEnumValue" : { - "required" : [ "name", "value" ], - "type" : "object", - "properties" : { - "name" : { - "type" : "string" - }, - "value" : { - "type" : "integer", - "format" : "int32" - } - } - }, - "void" : { - "type" : "object" - }, - "SchemaClass" : { - "type" : "object", - "properties" : { - "name" : { - "type" : "string" - }, - "properties" : { - "type" : "object", - "additionalProperties" : { - "$ref" : "#/components/schemas/SchemaProperty" - } - }, - "description" : { - "type" : "string" - } - } - }, - "ComponentType" : { - "type" : "string", - "enum" : [ "INT8", "UINT8", "INT16", "UINT16", "INT32", "UINT32", "INT64", "UINT64", "FLOAT32", "FLOAT64" ] - }, "Tileset3dTiles" : { "required" : [ "asset", "root" ], "type" : "object", @@ -1135,510 +271,6 @@ } }, "parameters" : { - "fCommon" : { - "name" : "f", - "in" : "query", - "description" : "Select the output format of the response. If no value is provided, the standard HTTP rules apply, i.e., the accept header will be used to determine the format.", - "required" : false, - "style" : "form", - "explode" : false, - "schema" : { - "type" : "string", - "enum" : [ "html", "json" ] - } - }, - "fCollections" : { - "name" : "f", - "in" : "query", - "description" : "Select the output format of the response. If no value is provided, the standard HTTP rules apply, i.e., the accept header will be used to determine the format.", - "required" : false, - "style" : "form", - "explode" : false, - "schema" : { - "type" : "string", - "enum" : [ "html", "json" ] - } - }, - "fCollection" : { - "name" : "f", - "in" : "query", - "description" : "Select the output format of the response. If no value is provided, the standard HTTP rules apply, i.e., the accept header will be used to determine the format.", - "required" : false, - "style" : "form", - "explode" : false, - "schema" : { - "type" : "string", - "enum" : [ "html", "json" ] - } - }, - "bbox" : { - "name" : "bbox", - "in" : "query", - "description" : "Only features that have a geometry that intersects the bounding box are selected. The bounding box is provided as four numbers:\n\n* Lower left corner, coordinate axis 1 \n* Lower left corner, coordinate axis 2 \n* Upper right corner, coordinate axis 1 \n* Upper right corner, coordinate axis 2 \nThe coordinate reference system of the values is WGS 84 longitude/latitude (http://www.opengis.net/def/crs/OGC/1.3/CRS84) unless a different coordinate reference system is specified in the parameter `bbox-crs`. For WGS 84 longitude/latitude the values are in most cases the sequence of minimum longitude, minimum latitude, maximum longitude and maximum latitude. However, in cases where the box spans the antimeridian the first value (west-most box edge) is larger than the third value (east-most box edge).", - "required" : false, - "style" : "form", - "explode" : false, - "schema" : { - "maxItems" : 6, - "minItems" : 4, - "type" : "array", - "items" : { - "type" : "number", - "format" : "double" - } - } - }, - "bbox-crsFeatures_building" : { - "name" : "bbox-crs", - "in" : "query", - "description" : "The coordinate reference system of the `bbox` parameter. Default is WGS84 longitude/latitude.", - "required" : false, - "style" : "form", - "explode" : false, - "schema" : { - "type" : "string", - "default" : "http://www.opengis.net/def/crs/OGC/1.3/CRS84", - "enum" : [ "http://www.opengis.net/def/crs/OGC/1.3/CRS84", "http://www.opengis.net/def/crs/EPSG/0/5555", "http://www.opengis.net/def/crs/EPSG/0/3857", "http://www.opengis.net/def/crs/EPSG/0/4978", "http://www.opengis.net/def/crs/EPSG/0/4979", "http://www.opengis.net/def/crs/EPSG/0/25832" ] - } - }, - "clampToEllipsoid" : { - "name" : "clampToEllipsoid", - "in" : "query", - "description" : "If set to `true`, the z coordinates of each feature will be changed so that the bottom of the feature is on the WGS84 ellipsoid. This parameter only affects glTF models.", - "required" : false, - "style" : "form", - "explode" : false, - "schema" : { - "type" : "boolean" - } - }, - "crsFeatures_building" : { - "name" : "crs", - "in" : "query", - "description" : "The coordinate reference system of the response geometries. Default is WGS84 longitude/latitude (with or without height).", - "required" : false, - "style" : "form", - "explode" : false, - "schema" : { - "type" : "string", - "default" : "http://www.opengis.net/def/crs/OGC/0/CRS84h", - "enum" : [ "http://www.opengis.net/def/crs/OGC/0/CRS84h", "http://www.opengis.net/def/crs/EPSG/0/5555", "http://www.opengis.net/def/crs/EPSG/0/3857", "http://www.opengis.net/def/crs/EPSG/0/4978", "http://www.opengis.net/def/crs/EPSG/0/4979", "http://www.opengis.net/def/crs/EPSG/0/25832" ] - } - }, - "datetime" : { - "name" : "datetime", - "in" : "query", - "description" : "Either a date-time or an interval. Date and time expressions adhere to RFC 3339.\nIntervals may be bounded or half-bounded (double-dots at start or end).\nExamples:\n\n* A date-time: \"2018-02-12T23:20:50Z\"\n* A bounded interval: \"2018-02-12T00:00:00Z/2018-03-18T12:31:12Z\"\n* Half-bounded intervals: \"2018-02-12T00:00:00Z/..\" or \"../2018-03-18T12:31:12Z\"\n\nOnly features that have a temporal property that intersects the value of `datetime` are selected.", - "required" : false, - "style" : "form", - "explode" : false, - "schema" : { - "pattern" : "^(?:\\d{4})-(?:0[1-9]|1[0-2])-(?:0[1-9]|[12][0-9]|3[01])$|^(?:[nN][oO][wW])$|^(?:(?:\\d{4})-(?:0[1-9]|1[0-2])-(?:0[1-9]|[12][0-9]|3[01])|(?:\\.\\.)?|(?:[nN][oO][wW]))/(?:(?:\\d{4})-(?:0[1-9]|1[0-2])-(?:0[1-9]|[12][0-9]|3[01])|(?:\\.\\.)?|(?:[nN][oO][wW]))$|^(?:\\d{4})-(?:0[1-9]|1[0-2])-(?:0[1-9]|[12][0-9]|3[01])T(?:[01][0-9]|2[0-3]):(?:[0-5][0-9]):(?:[0-5][0-9]|60)(?:\\.[0-9]+)?(Z|(\\+|-)(?:[01][0-9]|2[0-3]):(?:[0-5][0-9]))$|^(?:(?:\\d{4})-(?:0[1-9]|1[0-2])-(?:0[1-9]|[12][0-9]|3[01])T(?:[01][0-9]|2[0-3]):(?:[0-5][0-9]):(?:[0-5][0-9]|60)(?:\\.[0-9]+)?(Z|(\\+|-)(?:[01][0-9]|2[0-3]):(?:[0-5][0-9]))|(?:\\.\\.)?|(?:[nN][oO][wW]))/(?:(?:\\d{4})-(?:0[1-9]|1[0-2])-(?:0[1-9]|[12][0-9]|3[01])T(?:[01][0-9]|2[0-3]):(?:[0-5][0-9]):(?:[0-5][0-9]|60)(?:\\.[0-9]+)?(Z|(\\+|-)(?:[01][0-9]|2[0-3]):(?:[0-5][0-9]))|(?:\\.\\.)?|(?:[nN][oO][wW]))$", - "type" : "string" - } - }, - "fFeatures" : { - "name" : "f", - "in" : "query", - "description" : "Select the output format of the response. If no value is provided, the standard HTTP rules apply, i.e., the accept header will be used to determine the format.", - "required" : false, - "style" : "form", - "explode" : false, - "schema" : { - "type" : "string", - "enum" : [ "cityjson", "cityjsonseq", "glb", "html", "jsonfg", "jsonfgc" ] - } - }, - "filter" : { - "name" : "filter", - "in" : "query", - "description" : "Filter features in the collection using the query expression in the parameter value. Filter expressions are written in the Common Query Language (CQL2), which is a candidate OGC standard. This API implements the draft version from February 2022, which is a release candidate. The language for this query parameter is CQL2 Text (`filter-lang=cql2-text`).\n\nCQL2 Text expressions are similar to SQL expressions and also support spatial, temporal and array predicates. All property references must be queryables of the collection and must be declared in the Queryables sub-resource of the collection.\n\nThe following are examples of CQL Text expressions:\n\n* Logical operators (`AND`, `OR`, `NOT`) are supported\n* Simple comparison predicates (`=`, `<>`, `<`, `>`, `<=`, `>=`):\n * `address.LocalityName = 'Bonn'`\n * `measuredHeight > 10`\n * `storeysAboveGround <= 4`\n * `creationDate > '2017-12-31'`\n * `creationDate < '2018-01-01'`\n * `creationDate >= '2018-01-01' AND creationDate <= '2018-12-31'`\n* Advanced comparison operators (`LIKE`, `BETWEEN`, `IN`, `IS NULL`):\n * `name LIKE '%Kirche%'`\n * `measuredHeight BETWEEN 10 AND 20`\n * `address.LocalityName IN ('Bonn', 'Köln', 'Düren')`\n * `address.LocalityName NOT IN ('Bonn', 'Köln', 'Düren')`\n * `name IS NULL`\n * `name IS NOT NULL`\n* Spatial operators (the standard Simple Feature operators, e.g., `S_INTERSECTS`, `S_WITHIN`):\n * `S_INTERSECTS(bbox, POLYGON((8 52, 9 52, 9 53, 8 53, 8 52)))`\n* Temporal operators (e.g., `T_AFTER`, `T_BEFORE`, `T_INTERSECTS`)\n * `T_AFTER(creationDate, DATE('2018-01-01'))`\n * `T_BEFORE(creationDate, DATE('2018-01-01'))`\n * `T_INTERSECTS(creationDate, INTERVAL('2018-01-01','2018-12-31'))`\n * `T_INTERSECTS(INTERVAL(begin,end), INTERVAL('2018-01-01','2018-12-31'))`\n\nWarning: The final version of the Common Query Language standard may include changes to the CQL2 Text language supported by this API.", - "required" : false, - "style" : "form", - "explode" : false, - "schema" : { - "type" : "string" - } - }, - "filter-crs" : { - "name" : "filter-crs", - "in" : "query", - "description" : "Specify which of the supported CRSs to use to encode geometric values in a filter expression (parameter 'filter'). Default is WGS84 longitude/latitude.", - "required" : false, - "style" : "form", - "explode" : false, - "schema" : { - "type" : "string", - "default" : "http://www.opengis.net/def/crs/OGC/0/CRS84h", - "enum" : [ "http://www.opengis.net/def/crs/OGC/0/CRS84h", "http://www.opengis.net/def/crs/EPSG/0/5555", "http://www.opengis.net/def/crs/EPSG/0/3857", "http://www.opengis.net/def/crs/EPSG/0/4978", "http://www.opengis.net/def/crs/EPSG/0/4979", "http://www.opengis.net/def/crs/EPSG/0/25832", "http://www.opengis.net/def/crs/OGC/1.3/CRS84" ] - } - }, - "filter-lang" : { - "name" : "filter-lang", - "in" : "query", - "description" : "Language of the query expression in the 'filter' parameter. Supported are 'cql2-text' (default) and 'cql2-json', specified in the OGC candidate standard 'Common Query Language (CQL2)'. 'cql2-text' is an SQL-like text encoding for filter expressions that also supports spatial, temporal and array predicates. 'cql2-json' is a JSON encoding of that grammar, suitable for use as part of a JSON object that represents a query. The use of 'cql2-text' is recommended for filter expressions in the 'filter' parameter.", - "required" : false, - "style" : "form", - "explode" : false, - "schema" : { - "type" : "string", - "default" : "cql2-text", - "enum" : [ "cql2-text", "cql2-json" ] - } - }, - "limitFeatures_building" : { - "name" : "limit", - "in" : "query", - "description" : "The optional limit parameter limits the number of items that are presented in the response document. Only items are counted that are on the first level of the collection in the response document. Nested objects contained within the explicitly requested items are not counted.", - "required" : false, - "style" : "form", - "explode" : false, - "schema" : { - "maximum" : 10000, - "minimum" : 1, - "type" : "integer", - "format" : "int32", - "default" : 10 - } - }, - "maxAllowableOffset" : { - "name" : "maxAllowableOffset", - "in" : "query", - "description" : "This option can be used to specify the maxAllowableOffset to be used for simplifying the geometries in the response. The maxAllowableOffset is in the units of the response coordinate reference system.", - "required" : false, - "style" : "form", - "explode" : false, - "schema" : { - "type" : "number", - "example" : 0.05, - "default" : 0 - } - }, - "offsetFeatures" : { - "name" : "offset", - "in" : "query", - "description" : "The optional offset parameter identifies the index of the first feature in the response in the overall result set.", - "required" : false, - "style" : "form", - "explode" : false, - "schema" : { - "minimum" : 0, - "type" : "integer", - "format" : "int32", - "default" : 0 - } - }, - "properties_building" : { - "name" : "properties", - "in" : "query", - "description" : "The properties that should be included for each feature. The parameter value is a comma-separated list of property names. By default, all feature properties with a value are returned.", - "required" : false, - "style" : "form", - "explode" : false, - "schema" : { - "type" : "array", - "items" : { - "type" : "string", - "enum" : [ "gml_id", "name", "measuredHeight", "storeysAboveGround", "roofType", "creationDate", "function", "externalReferences.name", "externalReferences.informationSystem", "bezugspunktDach", "datenquelleBodenhoehe", "datenquelleDachhoehe", "datenquelleLage", "gemeindeschluessel", "grundrissaktualitaet", "geometrietyp2dReferenz", "address.id", "address.multiPoint", "address.ThoroughfareName", "address.ThoroughfareNumber", "address.PostalCode", "address.LocalityName", "address.AdministrativeArea", "address.CountryName", "surfaces.gml_id", "surfaces.creationDate", "surfaces.surfaceType", "surfaces.lod2MultiSurface", "consistsOfBuildingPart.id", "consistsOfBuildingPart.gml_id", "consistsOfBuildingPart.name", "consistsOfBuildingPart.bbox", "consistsOfBuildingPart.measuredHeight", "consistsOfBuildingPart.storeysAboveGround", "consistsOfBuildingPart.roofType", "consistsOfBuildingPart.creationDate", "consistsOfBuildingPart.function", "consistsOfBuildingPart.externalReferences.name", "consistsOfBuildingPart.externalReferences.informationSystem", "consistsOfBuildingPart.bezugspunktDach", "consistsOfBuildingPart.datenquelleBodenhoehe", "consistsOfBuildingPart.datenquelleDachhoehe", "consistsOfBuildingPart.datenquelleLage", "consistsOfBuildingPart.gemeindeschluessel", "consistsOfBuildingPart.grundrissaktualitaet", "consistsOfBuildingPart.geometrietyp2dReferenz", "consistsOfBuildingPart.address.id", "consistsOfBuildingPart.address.multiPoint", "consistsOfBuildingPart.address.ThoroughfareName", "consistsOfBuildingPart.address.ThoroughfareNumber", "consistsOfBuildingPart.address.PostalCode", "consistsOfBuildingPart.address.LocalityName", "consistsOfBuildingPart.address.AdministrativeArea", "consistsOfBuildingPart.address.CountryName", "consistsOfBuildingPart.lod2Solid", "consistsOfBuildingPart.surfaces.gml_id", "consistsOfBuildingPart.surfaces.creationDate", "consistsOfBuildingPart.surfaces.surfaceType", "consistsOfBuildingPart.surfaces.lod2MultiSurface", "consistsOfBuildingPart.building_parent_id" ] - } - } - }, - "skipGeometry_building" : { - "name" : "skipGeometry", - "in" : "query", - "description" : "Use this option to exclude geometries from the response for each feature.", - "required" : false, - "style" : "form", - "explode" : false, - "schema" : { - "type" : "boolean", - "default" : false - } - }, - "sortby_building" : { - "name" : "sortby", - "in" : "query", - "description" : "Sort the results based on the properties identified by this parameter. The parameter value is a comma-separated list of property names that can be used to sort results (sortables), where each parameter name may be preceeded by a '+' (ascending, default) or '-' (descending).", - "required" : false, - "style" : "form", - "explode" : false, - "schema" : { - "type" : "array", - "items" : { - "type" : "string", - "enum" : [ "name", "+name", "-name", "measuredHeight", "+measuredHeight", "-measuredHeight", "storeysAboveGround", "+storeysAboveGround", "-storeysAboveGround" ] - } - } - }, - "gml_id_building" : { - "name" : "gml_id", - "in" : "query", - "description" : "Filter the collection by property 'gml_id' (ALKIS-ID).", - "required" : false, - "style" : "form", - "explode" : false, - "schema" : { - "title" : "ALKIS-ID", - "type" : "string" - } - }, - "name_building" : { - "name" : "name", - "in" : "query", - "description" : "Filter the collection by property 'name' (Name): Only set for proper building names.", - "required" : false, - "style" : "form", - "explode" : false, - "schema" : { - "title" : "Name", - "type" : "string", - "description" : "Only set for proper building names." - } - }, - "function_building" : { - "name" : "function", - "in" : "query", - "description" : "Filter the collection by property 'function' (Gebäudefunktion): The meaning of the individual values results from the codelist BuildingFunctionTypeAdV.xml.", - "required" : false, - "style" : "form", - "explode" : false, - "schema" : { - "title" : "Gebäudefunktion", - "type" : "string", - "description" : "The meaning of the individual values results from the codelist BuildingFunctionTypeAdV.xml.", - "enum" : [ "31001_1000", "31001_1010", "31001_1020", "31001_1021", "31001_1022", "31001_1023", "31001_1024", "31001_1025", "31001_1100", "31001_1110", "31001_1120", "31001_1121", "31001_1122", "31001_1123", "31001_1130", "31001_1131", "31001_1210", "31001_1220", "31001_1221", "31001_1222", "31001_1223", "31001_1310", "31001_1311", "31001_1312", "31001_1313", "31001_2000", "31001_2010", "31001_2020", "31001_2030", "31001_2040", "31001_2050", "31001_2051", "31001_2052", "31001_2053", "31001_2054", "31001_2055", "31001_2056", "31001_2060", "31001_2070", "31001_2071", "31001_2072", "31001_2073", "31001_2074", "31001_2080", "31001_2081", "31001_2082", "31001_2083", "31001_2090", "31001_2091", "31001_2092", "31001_2093", "31001_2094", "31001_2100", "31001_2110", "31001_2111", "31001_2112", "31001_2113", "31001_2114", "31001_2120", "31001_2121", "31001_2130", "31001_2131", "31001_2140", "31001_2141", "31001_2142", "31001_2143", "31001_2150", "31001_2160", "31001_2170", "31001_2171", "31001_2172", "31001_2180", "31001_2200", "31001_2210", "31001_2211", "31001_2212", "31001_2213", "31001_2220", "31001_2310", "31001_2320", "31001_2400", "31001_2410", "31001_2411", "31001_2412", "31001_2420", "31001_2421", "31001_2422", "31001_2423", "31001_2424", "31001_2430", "31001_2431", "31001_2440", "31001_2441", "31001_2442", "31001_2443", "31001_2444", "31001_2450", "31001_2451", "31001_2460", "31001_2461", "31001_2462", "31001_2463", "31001_2464", "31001_2465", "31001_2500", "31001_2501", "31001_2510", "31001_2511", "31001_2512", "31001_2513", "31001_2520", "31001_2521", "31001_2522", "31001_2523", "31001_2527", "31001_2528", "31001_2529", "31001_2540", "31001_2560", "31001_2570", "31001_2571", "31001_2580", "31001_2590", "31001_2591", "31001_2600", "31001_2610", "31001_2611", "31001_2612", "31001_2620", "31001_2621", "31001_2622", "31001_2623", "31001_2700", "31001_2720", "31001_2721", "31001_2723", "31001_2724", "31001_2726", "31001_2727", "31001_2728", "31001_2729", "31001_2732", "31001_2735", "31001_2740", "31001_2741", "31001_2742", "31001_3000", "31001_3010", "31001_3011", "31001_3012", "31001_3013", "31001_3014", "31001_3015", "31001_3016", "31001_3017", "31001_3018", "31001_3019", "31001_3020", "31001_3021", "31001_3022", "31001_3023", "31001_3024", "31001_3030", "31001_3031", "31001_3032", "31001_3033", "31001_3034", "31001_3035", "31001_3036", "31001_3037", "31001_3038", "31001_3040", "31001_3041", "31001_3042", "31001_3043", "31001_3044", "31001_3045", "31001_3046", "31001_3047", "31001_3048", "31001_3050", "31001_3051", "31001_3052", "31001_3053", "31001_3060", "31001_3061", "31001_3062", "31001_3063", "31001_3064", "31001_3065", "31001_3066", "31001_3070", "31001_3071", "31001_3072", "31001_3073", "31001_3074", "31001_3075", "31001_3080", "31001_3081", "31001_3082", "31001_3090", "31001_3091", "31001_3092", "31001_3094", "31001_3095", "31001_3097", "31001_3098", "31001_3100", "31001_3200", "31001_3210", "31001_3211", "31001_3212", "31001_3220", "31001_3221", "31001_3222", "31001_3230", "31001_3240", "31001_3241", "31001_3242", "31001_3260", "31001_3261", "31001_3262", "31001_3263", "31001_3264", "31001_3270", "31001_3271", "31001_3272", "31001_3273", "31001_3280", "31001_3281", "31001_3290", "31001_9998", "51001_1001", "51001_1002", "51001_1003", "51001_1004", "51001_1005", "51001_1006", "51001_1007", "51001_1008", "51001_1009", "51001_1010", "51001_1011", "51001_1012", "51001_9998", "51001_9999", "51002_1215", "51002_1220", "51002_1230", "51002_1250", "51002_1260", "51002_1280", "51002_1290", "51002_1330", "51002_1331", "51002_1332", "51002_1333", "51002_1350", "51002_1400", "51002_9999", "51003_1201", "51003_1205", "51003_1206", "51003_9999", "51006_1430", "51006_1431", "51006_1432", "51006_1440", "51006_1470", "51006_1490", "51006_9999", "51007_1210", "51007_1400", "51007_1500", "51007_1510", "51007_1520", "51007_9999", "51009_1610", "51009_1611", "51009_1750", "51009_9999", "11_1001", "11_1002", "11_1003", "11_1004", "11_1005", "11_1006", "11_1036", "11_1101", "11_1111", "11_1112", "11_1113", "11_1114", "11_1115", "11_1116", "11_1118", "11_1121", "11_1122", "11_1123", "11_1124", "11_1128", "11_1131", "11_1132", "11_1133", "11_1134", "11_1135", "11_1136", "11_1137", "11_1138", "11_1141", "11_1142", "11_1143", "11_1144", "11_1145", "11_1148", "11_1151", "11_1152", "11_1158", "11_1161", "11_1162", "11_1163", "11_1164", "11_1165", "11_1168", "11_1171", "11_1172", "11_1173", "11_1174", "11_1175", "11_1178", "11_1181", "11_1182", "11_1188", "11_1191", "11_1192", "11_1194", "11_1195", "11_1196", "11_1197", "11_1198", "11_1211", "11_1221", "11_1231", "11_1301", "11_1311", "11_1321", "11_1331", "11_1341", "11_1361", "11_1371", "11_1372", "11_1373", "11_1374", "11_1375", "11_1378", "11_1381", "11_1399", "11_1401", "11_1411", "11_1421", "11_1431", "11_1441", "11_1442", "11_1443", "11_1444", "11_1445", "11_1448", "11_1451", "11_1461", "11_1462", "11_1463", "11_1468", "11_1471", "11_1472", "11_1473", "11_1474", "11_1478", "11_1481", "11_1482", "11_1483", "11_1484", "11_1488", "11_1701", "11_1711", "11_1721", "11_1731", "11_1741", "11_1742", "11_1743", "11_1748", "11_1751", "11_1761", "11_1771", "11_1772", "11_1773", "11_1774", "11_1778", "11_1781", "11_1911", "11_1913", "11_2101", "11_2121", "11_2131", "11_2141", "11_2301", "11_2311", "11_2312", "11_2313", "11_2318", "11_2321", "11_2322", "11_2323", "11_2324", "11_2328", "11_2332", "11_2338", "11_2341", "11_2342", "11_2343", "11_2344", "11_2348", "11_2351", "11_2358", "11_2361", "11_2362", "11_2363", "11_2364", "11_2365", "11_2366", "11_2367", "11_2368", "11_2501", "11_2511", "11_2512", "11_2514", "11_2515", "11_2518", "11_2521", "11_2522", "11_2523", "11_2528", "11_2541", "11_2548", "11_2551", "11_2561", "11_2571", "11_2572", "11_2581", "11_2591", "11_2601", "11_2611", "11_2612", "11_2619", "11_2621", "11_2622", "11_2623", "11_2628", "11_2701", "11_2711", "11_2721", "11_2723", "11_2724", "11_2725", "11_2726", "11_2727", "11_2728", "11_2731", "11_2736", "11_2737", "11_2738", "11_2741", "11_2742", "11_2748", "11_2801", "11_2811", "11_2812", "11_2818", "11_2821", "11_2822", "11_2828", "11_2831", "11_2841", "11_2842", "11_2848", "11_2851", "11_2861", "11_2862", "11_2863", "11_2868", "11_2871", "11_2872", "11_2873", "11_2874", "11_2878", "11_2881", "11_2882", "11_2883", "11_2888", "11_2891", "11_2894", "11_2921" ] - } - }, - "roofType_building" : { - "name" : "roofType", - "in" : "query", - "description" : "Filter the collection by property 'roofType' (Dachform): The meaning of the individual values results from the codelist RoofTypeTypeAdV.xml.", - "required" : false, - "style" : "form", - "explode" : false, - "schema" : { - "title" : "Dachform", - "type" : "string", - "description" : "The meaning of the individual values results from the codelist RoofTypeTypeAdV.xml.", - "enum" : [ "1000", "2100", "2200", "3100", "3200", "3300", "3400", "3500", "3600", "3700", "3800", "3900", "4000", "5000", "9999" ] - } - }, - "measuredHeight_building" : { - "name" : "measuredHeight", - "in" : "query", - "description" : "Filter the collection by property 'measuredHeight' (Gebäudehöhe [m]): Height of the building as the difference in meters between the highest reference point and the lowest reference point of the building.", - "required" : false, - "style" : "form", - "explode" : false, - "schema" : { - "title" : "Gebäudehöhe [m]", - "type" : "number", - "description" : "Height of the building as the difference in meters between the highest reference point and the lowest reference point of the building." - } - }, - "storeysAboveGround_building" : { - "name" : "storeysAboveGround", - "in" : "query", - "description" : "Filter the collection by property 'storeysAboveGround' (Geschosse).", - "required" : false, - "style" : "form", - "explode" : false, - "schema" : { - "title" : "Geschosse", - "type" : "integer", - "format" : "int32" - } - }, - "address.ThoroughfareName_building" : { - "name" : "address.ThoroughfareName", - "in" : "query", - "description" : "Filter the collection by property 'address.ThoroughfareName' (Adresse > Straße).", - "required" : false, - "style" : "form", - "explode" : false, - "schema" : { - "title" : "Adresse > Straße", - "type" : "string" - } - }, - "address.ThoroughfareNumber_building" : { - "name" : "address.ThoroughfareNumber", - "in" : "query", - "description" : "Filter the collection by property 'address.ThoroughfareNumber' (Adresse > Hausnummer).", - "required" : false, - "style" : "form", - "explode" : false, - "schema" : { - "title" : "Adresse > Hausnummer", - "type" : "string" - } - }, - "address.PostalCode_building" : { - "name" : "address.PostalCode", - "in" : "query", - "description" : "Filter the collection by property 'address.PostalCode' (Adresse > Postleitzahl).", - "required" : false, - "style" : "form", - "explode" : false, - "schema" : { - "title" : "Adresse > Postleitzahl", - "type" : "string" - } - }, - "address.LocalityName_building" : { - "name" : "address.LocalityName", - "in" : "query", - "description" : "Filter the collection by property 'address.LocalityName' (Adresse > Stadt).", - "required" : false, - "style" : "form", - "explode" : false, - "schema" : { - "title" : "Adresse > Stadt", - "type" : "string" - } - }, - "consistsOfBuildingPart.roofType_building" : { - "name" : "consistsOfBuildingPart.roofType", - "in" : "query", - "description" : "Filter the collection by property 'consistsOfBuildingPart.roofType' (Gebäudeteile > Dachform): Die Bedeutung der einzelnen Werte ergibt aus der Codelist RoofTypeTypeAdV.xml.", - "required" : false, - "style" : "form", - "explode" : false, - "schema" : { - "title" : "Gebäudeteile > Dachform", - "type" : "string", - "description" : "Die Bedeutung der einzelnen Werte ergibt aus der Codelist RoofTypeTypeAdV.xml.", - "enum" : [ "1000", "2100", "2200", "3100", "3200", "3300", "3400", "3500", "3600", "3700", "3800", "3900", "4000", "5000", "9999" ] - } - }, - "consistsOfBuildingPart.measuredHeight_building" : { - "name" : "consistsOfBuildingPart.measuredHeight", - "in" : "query", - "description" : "Filter the collection by property 'consistsOfBuildingPart.measuredHeight' (Gebäudeteile > Gebäudehöhe [m]): Höhe des Gebäudes aus der Differenz in Metern zwischen dem höchsten Bezugspunkt und dem tiefsten Bezugspunkt des Gebäudes.", - "required" : false, - "style" : "form", - "explode" : false, - "schema" : { - "title" : "Gebäudeteile > Gebäudehöhe [m]", - "type" : "number", - "description" : "Höhe des Gebäudes aus der Differenz in Metern zwischen dem höchsten Bezugspunkt und dem tiefsten Bezugspunkt des Gebäudes." - } - }, - "consistsOfBuildingPart.storeysAboveGround_building" : { - "name" : "consistsOfBuildingPart.storeysAboveGround", - "in" : "query", - "description" : "Filter the collection by property 'consistsOfBuildingPart.storeysAboveGround' (Gebäudeteile > Geschosse).", - "required" : false, - "style" : "form", - "explode" : false, - "schema" : { - "title" : "Gebäudeteile > Geschosse", - "type" : "integer", - "format" : "int32" - } - }, - "featureId" : { - "name" : "featureId", - "in" : "path", - "description" : "The local identifier of a feature, unique within the feature collection.", - "required" : true, - "schema" : { - "pattern" : "[^/ ]+", - "type" : "string" - } - }, - "fQueryables" : { - "name" : "f", - "in" : "query", - "description" : "Select the output format of the response. If no value is provided, the standard HTTP rules apply, i.e., the accept header will be used to determine the format.", - "required" : false, - "style" : "form", - "explode" : false, - "schema" : { - "type" : "string", - "enum" : [ "html", "json" ] - } - }, - "fSortables" : { - "name" : "f", - "in" : "query", - "description" : "Select the output format of the response. If no value is provided, the standard HTTP rules apply, i.e., the accept header will be used to determine the format.", - "required" : false, - "style" : "form", - "explode" : false, - "schema" : { - "type" : "string", - "enum" : [ "html", "json" ] - } - }, - "fStyles" : { - "name" : "f", - "in" : "query", - "description" : "Select the output format of the response. If no value is provided, the standard HTTP rules apply, i.e., the accept header will be used to determine the format.", - "required" : false, - "style" : "form", - "explode" : false, - "schema" : { - "type" : "string", - "enum" : [ "html", "json" ] - } - }, - "styleId" : { - "name" : "styleId", - "in" : "path", - "description" : "The local identifier of a style, unique within the API.", - "required" : true, - "schema" : { - "pattern" : "[^/]+", - "type" : "string" - } - }, - "fStyle" : { - "name" : "f", - "in" : "query", - "description" : "Select the output format of the response. If no value is provided, the standard HTTP rules apply, i.e., the accept header will be used to determine the format.", - "required" : false, - "style" : "form", - "explode" : false, - "schema" : { - "type" : "string", - "enum" : [ "3dtiles", "html", "mbs" ] - } - }, - "collectionIdStyles" : { - "name" : "collectionId", - "in" : "path", - "description" : "The local identifier of a feature collection.", - "required" : true, - "schema" : { - "type" : "string", - "enum" : [ "building" ] - } - }, - "fGltfSchema" : { - "name" : "f", - "in" : "query", - "description" : "Select the output format of the response. If no value is provided, the standard HTTP rules apply, i.e., the accept header will be used to determine the format.", - "required" : false, - "style" : "form", - "explode" : false, - "schema" : { - "type" : "string", - "enum" : [ "json" ] - } - }, - "f3dTiles" : { - "name" : "f", - "in" : "query", - "description" : "Select the output format of the response. If no value is provided, the standard HTTP rules apply, i.e., the accept header will be used to determine the format.", - "required" : false, - "style" : "form", - "explode" : false, - "schema" : { - "type" : "string", - "enum" : [ "json" ] - } - }, "level" : { "name" : "level", "in" : "path", From 82772097e8eff69b85cf81f132b727d69987ad7e Mon Sep 17 00:00:00 2001 From: Richard Kettelerij Date: Tue, 5 Dec 2023 14:15:16 +0100 Subject: [PATCH 07/26] Add OGC API Features part 2 + fix multiple collections with same name support --- engine/config.go | 25 ++- engine/openapi.go | 1 - engine/templates/openapi/README.md | 8 +- .../openapi/common-collections.go.json | 94 +++------ engine/templates/openapi/features.go.json | 199 +++++++----------- ...g_multiple_ogc_apis_single_collection.yaml | 11 +- go.mod | 1 + go.sum | 2 + ogc/common/geospatial/main.go | 2 +- ogc/features/url.go | 6 - 10 files changed, 134 insertions(+), 215 deletions(-) diff --git a/engine/config.go b/engine/config.go index 2c656baa..c9a3a117 100644 --- a/engine/config.go +++ b/engine/config.go @@ -9,6 +9,7 @@ import ( "strings" "time" + "dario.cat/mergo" "github.com/PDOK/gokoala/engine/util" "github.com/creasty/defaults" "github.com/go-playground/validator/v10" @@ -182,8 +183,17 @@ func (g GeoSpatialCollections) ContainsID(id string) bool { func (g GeoSpatialCollections) toMap() map[string]GeoSpatialCollection { collectionsByID := make(map[string]GeoSpatialCollection) - for _, v := range g { - collectionsByID[v.ID] = v + for _, current := range g { + existing, ok := collectionsByID[current.ID] + if ok { + err := mergo.Merge(&existing, current) + if err != nil { + log.Fatalf("failed to merge 2 collections with the same name '%s': %v", current.ID, err) + } + collectionsByID[current.ID] = existing + } else { + collectionsByID[current.ID] = current + } } return collectionsByID } @@ -275,6 +285,15 @@ type OgcAPIFeatures struct { Collections GeoSpatialCollections `yaml:"collections" validate:"required"` } +func (oaf OgcAPIFeatures) ProjectionsForCollections() []string { + var result []string + for _, coll := range oaf.Collections { + projs := oaf.ProjectionsForCollection(coll.ID) + result = append(result, projs...) + } + return result +} + func (oaf OgcAPIFeatures) ProjectionsForCollection(collectionID string) []string { uniqueSRSs := make(map[string]struct{}) if oaf.Datasources != nil { @@ -283,7 +302,7 @@ func (oaf OgcAPIFeatures) ProjectionsForCollection(collectionID string) []string } } for _, coll := range oaf.Collections { - if coll.ID == collectionID && coll.Features.Datasources != nil { + if coll.ID == collectionID && coll.Features != nil && coll.Features.Datasources != nil { for _, a := range coll.Features.Datasources.Additional { uniqueSRSs[a.Srs] = struct{}{} } diff --git a/engine/openapi.go b/engine/openapi.go index 61256437..7eca6914 100644 --- a/engine/openapi.go +++ b/engine/openapi.go @@ -135,7 +135,6 @@ func mergeSpecs(ctx context.Context, config *Config, files []string) (*openapi3. for _, file := range files { specJSON := renderOpenAPITemplate(config, file) - _ = loadSpec(loader, specJSON) var mergedJSON []byte if resultSpecJSON == nil { mergedJSON = specJSON diff --git a/engine/templates/openapi/README.md b/engine/templates/openapi/README.md index 5b6f44fd..26c689f6 100644 --- a/engine/templates/openapi/README.md +++ b/engine/templates/openapi/README.md @@ -48,6 +48,7 @@ that are implemented and remove the references to the rest."_ source: OGC API Ti - Removal of generic OGC Collection types, already covered in `commons-collections.json` - Also removal of OGC Collection endpoints (we don't support these for Tiles at the moment), this may change in the future. + - Removed `extent` already, already covered in `commons-collections.json` - Removal of OGC Style endpoint (/styles), already - and better - covered by `styles.json` - Removal of GeoJSON as tiles format, only MapBox Vector Tiles are supported. - Removal of optional parameters for `/tiles` endpoint like datetime (temporal data) @@ -62,10 +63,11 @@ that are implemented and remove the references to the rest."_ source: OGC API Ti - Replaced "EuropeanETRS89_GRS80Quad_Draft" with "EuropeanETRS89_LAEAQuad" - Removed default contact details -### OGC Features (Part 1) +### OGC Features `features.go.json` is based on -[ogcapi-features-1.0.1](https://app.swaggerhub.com/apis/OGC/ogcapi-features-1-example-1/1.0.1) +[ogcapi-features-1.0.1](https://app.swaggerhub.com/apis/OGC/ogcapi-features-1-example-1/1.0.1) and +[ogcapi-features-2](https://schemas.opengis.net/ogcapi/features/part2/1.0/openapi/ogcapi-features-2.yaml) - Changes: - Removal of OGC Common endpoints (landing page, api, conformance), already @@ -79,6 +81,7 @@ that are implemented and remove the references to the rest."_ source: OGC API Ti - removed `offset` since we (will) use `cursor` for pagination - Added `cursor` query param to feature collection request. Vendor specific parameters need to be explicitly listed (the same holds true if you would use offset-based pagination, you would need to list the `offset` param). + - Added parameters/components/headers from part 2 schema (automatic merge wasn't possible). ### OGC 3D GeoVolumes @@ -90,6 +93,7 @@ and [cologne_lod2](https://demo.ldproxy.net/cologne_lod2/api/?f=json) - Removal of OGC Common endpoints (landing page, api, conformance), already covered by `common.json` - Removed most endpoints only included 3d tiles specific endpoints + - Removed all unused components - Removed default contact details ### OGC Styles diff --git a/engine/templates/openapi/common-collections.go.json b/engine/templates/openapi/common-collections.go.json index b77cdd70..fff08789 100644 --- a/engine/templates/openapi/common-collections.go.json +++ b/engine/templates/openapi/common-collections.go.json @@ -145,23 +145,6 @@ }, "components": { "parameters": { - "bbox": { - "name": "bbox", - "in": "query", - "description": "Only elements that have a geometry that intersects the bounding box are selected.\nThe bounding box is provided as four or six numbers, depending on whether the\ncoordinate reference system includes a vertical axis (elevation or depth):\n\n* Lower left corner, coordinate axis 1\n* Lower left corner, coordinate axis 2\n* Lower left corner, coordinate axis 3 (optional)\n* Upper right corner, coordinate axis 1\n* Upper right corner, coordinate axis 2\n* Upper right corner, coordinate axis 3 (optional)\n\nThe coordinate reference system of the values is WGS 84 longitude/latitude\n(http://www.opengis.net/def/crs/OGC/1.3/CRS84) unless a different coordinate\nreference system is specified by another parameter in the API (e.g 'bbox-crs').\n\nFor WGS 84 longitude/latitude the values are in most cases the sequence of\nminimum longitude, minimum latitude, maximum longitude and maximum latitude.\nHowever, in cases where the box spans the antimeridian the first value\n(west-most box edge) is larger than the third value (east-most box edge).\n\nIf an element has multiple spatial geometry properties, it is the decision of the\nserver whether only a single spatial geometry property is used to determine\nthe extent or all relevant geometries.", - "required": false, - "schema": { - "type": "array", - "minItems": 4, - "maxItems": 6, - "items": { - "type": "number", - "format": "double" - } - }, - "style": "form", - "explode": false - }, "collectionId": { "name": "collectionId", "in": "path", @@ -171,47 +154,6 @@ "type": "string" } }, - "datetime": { - "name": "datetime", - "in": "query", - "description": "Either a date-time or an interval, open or closed. Date and time expressions\nadhere to RFC 3339. Open intervals are expressed using double-dots.\n\nExamples:\n\n* A date-time: \"2018-02-12T23:20:50Z\"\n* A closed interval: \"2018-02-12T00:00:00Z/2018-03-18T12:31:12Z\"\n* Open intervals: \"2018-02-12T00:00:00Z/..\" or \"../2018-03-18T12:31:12Z\"\n\nOnly elemenets that have a temporal property that intersects the value of\n`datetime` are selected.\n\nIf a element has multiple temporal properties, it is the decision of the\nserver whether only a single temporal property is used to determine\nthe extent or all relevant temporal properties.", - "required": false, - "schema": { - "type": "string" - }, - "style": "form", - "explode": false - }, - "limit": { - "name": "limit", - "in": "query", - "description": "The optional limit parameter limits the number of items that are presented in the response document.\n\nOnly items are counted that are on the first level of the collection in the response document.\nNested objects contained within the explicitly requested items shall not be counted.\n\nMinimum = 1. Maximum = 10000. Default = 10.", - "required": false, - "schema": { - "type": "number", - "format": "integer", - "minimum": 0, - "maximum": 10000, - "default": 10 - }, - "style": "form", - "explode": false - }, - "offset": { - "name": "offset", - "in": "query", - "description": "The optional offset parameter indicates the index within the result set from which the server shall begin presenting results in the response document. The first element has an index of 0.\nIf offset is greater or equal to the number of items in the collection, the server should return an empty list.\n\nMinimum = 0. Default = 0.", - "required": false, - "style": "form", - "explode": false, - "schema": { - "type": "number", - "format": "integer", - "minimum": 0, - "default": 0 - }, - "example": 0 - }, "resultType": { "name": "resultType", "in": "query", @@ -312,6 +254,18 @@ "http://www.opengis.net/def/crs/EPSG/0/4326" ] } + {{- if and .Config.OgcAPI.Features .Config.OgcAPI.Features.Collections -}} + ,"storageCrs": { + "description": "the CRS identifier, from the list of supported CRS identifiers, that may be used to retrieve features from a collection without the need to apply a CRS transformation", + "type": "string", + "format": "uri" + }, + "storageCrsCoordinateEpoch": { + "description": "point in time at which coordinates in the spatial feature collection are referenced to the dynamic coordinate reference system in `storageCrs`, that may be used to retrieve features from a collection without the need to apply a change of coordinate epoch. It is expressed as a decimal year in the Gregorian calendar", + "type": "number", + "example": "2017-03-25 in the Gregorian calendar is epoch 2017.23" + } + {{- end -}} } }, "collection-link": { @@ -713,24 +667,24 @@ } } }, - "numberMatched": { - "description": "The number of elements that match the selection parameters like `bbox`.", - "type": "integer", - "minimum": 0, - "example": 127 - }, +{{/* "numberMatched": {*/}} +{{/* "description": "The number of elements that match the selection parameters like `bbox`.",*/}} +{{/* "type": "integer",*/}} +{{/* "minimum": 0,*/}} +{{/* "example": 127*/}} +{{/* },*/}} "numberReturned": { "description": "The number of elements in the collection.\n\nA server may omit this information in a response, if the information\nabout the number of elements is not known or difficult to compute.\n\nIf the value is provided, the value shall be identical to the number\nof items in the returned array.", "type": "integer", "minimum": 0, "example": 10 - }, - "timeStamp": { - "description": "This property indicates the time and date when the response was generated.", - "type": "string", - "format": "date-time", - "example": "2017-08-17T08:05:32Z" } +{{/* "timeStamp": {*/}} +{{/* "description": "This property indicates the time and date when the response was generated.",*/}} +{{/* "type": "string",*/}} +{{/* "format": "date-time",*/}} +{{/* "example": "2017-08-17T08:05:32Z"*/}} +{{/* }*/}} }, "responses": { "LandingPage": { diff --git a/engine/templates/openapi/features.go.json b/engine/templates/openapi/features.go.json index 50f9d3f4..66ae29df 100644 --- a/engine/templates/openapi/features.go.json +++ b/engine/templates/openapi/features.go.json @@ -22,59 +22,36 @@ "operationId": "{{ $type.ID }}.getFeatures", "parameters": [ { - "name": "limit", - "in": "query", - "description": "The optional limit parameter limits the number of items that are presented in the response document.\n\nOnly items are counted that are on the first level of the collection in the response document.\nNested objects contained within the explicitly requested items shall not be counted.\n\nMinimum = 1. Maximum = 1000. Default = 10.", - "required": false, - "style": "form", - "explode": false, - "schema": { - "maximum": {{ $cfg.OgcAPI.Features.Limit.Max }}, - "minimum": 1, - "type": "integer", - "default": {{ $cfg.OgcAPI.Features.Limit.Default }} - } + "$ref": "#/components/parameters/limit" }, { - "name": "cursor", - "in": "query", - "description": "The optional cursor parameter identifies the page in the features result set. You shouldn't specify the cursor value yourself, instead you should obtain the cursor value from the `next`-link in the features response.\n\nThis API uses \"_cursor-based pagination_\" which has the advantage of allowing users to pagination though all features regardless of the size of the dataset. This wouldn't be possible using traditional \"_offset-based pagination_\".", - "required": false, - "style": "form", - "explode": false, - "schema": { - "type": "string" - } + "$ref": "#/components/parameters/crs" }, { - "name": "bbox", - "in": "query", - "description": "Only features that have a geometry that intersects the bounding box are selected.\nThe bounding box is provided as four or six numbers, depending on whether the\ncoordinate reference system includes a vertical axis (height or depth):\n\n* Lower left corner, coordinate axis 1\n* Lower left corner, coordinate axis 2\n* Minimum value, coordinate axis 3 (optional)\n* Upper right corner, coordinate axis 1\n* Upper right corner, coordinate axis 2\n* Maximum value, coordinate axis 3 (optional)\n\nIf the value consists of four numbers, the coordinate reference system is\nWGS 84 longitude/latitude (http://www.opengis.net/def/crs/OGC/1.3/CRS84)\nunless a different coordinate reference system is specified in the parameter `bbox-crs`.\n\nIf the value consists of six numbers, the coordinate reference system is WGS 84\nlongitude/latitude/ellipsoidal height (http://www.opengis.net/def/crs/OGC/0/CRS84h)\nunless a different coordinate reference system is specified in the parameter `bbox-crs`.\n\nThe query parameter `bbox-crs` is specified in OGC API - Features - Part 2: Coordinate\nReference Systems by Reference.\n\nFor WGS 84 longitude/latitude the values are in most cases the sequence of\nminimum longitude, minimum latitude, maximum longitude and maximum latitude.\nHowever, in cases where the box spans the antimeridian the first value\n(west-most box edge) is larger than the third value (east-most box edge).\n\nIf the vertical axis is included, the third and the sixth number are\nthe bottom and the top of the 3-dimensional bounding box.\n\nIf a feature has multiple spatial geometry properties, it is the decision of the\nserver whether only a single spatial geometry property is used to determine\nthe extent or all relevant geometries.", - "required": false, - "style": "form", - "explode": false, - "schema": { - "type": "array", - "items": { - "type": "number" - } - } + "$ref": "#/components/parameters/bbox" }, { - "name": "datetime", - "in": "query", - "description": "Either a date-time or an interval. Date and time expressions adhere to RFC 3339.\nIntervals may be bounded or half-bounded (double-dots at start or end).\n\nExamples:\n\n* A date-time: \"2018-02-12T23:20:50Z\"\n* A bounded interval: \"2018-02-12T00:00:00Z/2018-03-18T12:31:12Z\"\n* Half-bounded intervals: \"2018-02-12T00:00:00Z/..\" or \"../2018-03-18T12:31:12Z\"\n\nOnly features that have a temporal property that intersects the value of\n`datetime` are selected.\n\nIf a feature has multiple temporal properties, it is the decision of the\nserver whether only a single temporal property is used to determine\nthe extent or all relevant temporal properties.", - "required": false, - "style": "form", - "explode": false, - "schema": { - "type": "string" - } + "$ref": "#/components/parameters/bbox-crs" + }, + { + "$ref": "#/components/parameters/datetime" + }, + { + "$ref": "#/components/parameters/cursor" } ], "responses": { "200": { "description": "The response is a document consisting of features in the collection.\nThe features included in the response are determined by the server\nbased on the query parameters of the request. To support access to\nlarger collections without overloading the client, the API supports\npaged access with links to the next page, if more features are selected\nthat the page size.\n\nThe `bbox` and `datetime` parameter can be used to select only a\nsubset of the features in the collection (the features that are in the\nbounding box or time interval). The `bbox` parameter matches all features\nin the collection that are not associated with a location, too. The\n`datetime` parameter matches all features in the collection that are\nnot associated with a time stamp or interval, too.\n\nThe `limit` parameter may be used to control the subset of the\nselected features that should be returned in the response, the page size.\nEach page may include information about the number of selected and\nreturned features (`numberMatched` and `numberReturned`) as well as\nlinks to support paging (link relation `next`).", + "headers": { + "Content-Crs": { + "description": "a URI, in angular brackets, identifying the coordinate reference system used in the content / payload", + "schema": { + "type": "string" + }, + "example": "" + } + }, "content": { "application/geo+json": { "schema": { @@ -199,11 +176,23 @@ "schema": { "type": "string" } + }, + { + "$ref": "#/components/parameters/crs" } ], "responses": { "200": { "description": "fetch the feature with id `featureId` in the feature collection\nwith id `collectionId`", + "headers": { + "Content-Crs": { + "description": "a URI, in angular brackets, identifying the coordinate reference system used in the content / payload", + "schema": { + "type": "string" + }, + "example": "" + } + }, "content": { "application/geo+json": { "schema": { @@ -282,90 +271,6 @@ }, "components": { "schemas": { - "collection": { - "required": [ - "id", - "links" - ], - "type": "object", - "properties": { - "id": { - "type": "string", - "description": "identifier of the collection used, for example, in URIs", - "example": "address" - }, - "title": { - "type": "string", - "description": "human readable title of the collection", - "example": "address" - }, - "description": { - "type": "string", - "description": "a description of the features in the collection", - "example": "An address." - }, - "links": { - "type": "array", - "example": [ - { - "href": "http://data.example.com/buildings", - "rel": "item" - }, - { - "href": "http://example.com/concepts/buildings.html", - "rel": "describedby", - "type": "text/html" - } - ], - "items": { - "$ref": "#/components/schemas/link" - } - }, - "extent": { - "$ref": "#/components/schemas/extent" - }, - "itemType": { - "type": "string", - "description": "indicator about the type of the items in the collection (the default value is 'feature').", - "default": "feature" - }, - "crs": { - "type": "array", - "description": "the list of coordinate reference systems supported by the service", - "example": [ - "http://www.opengis.net/def/crs/OGC/1.3/CRS84", - "http://www.opengis.net/def/crs/EPSG/0/4326" - ], - "items": { - "type": "string" - }, - "default": [ - "http://www.opengis.net/def/crs/OGC/1.3/CRS84" - ] - } - } - }, - "collections": { - "required": [ - "collections", - "links" - ], - "type": "object", - "properties": { - "links": { - "type": "array", - "items": { - "$ref": "#/components/schemas/link" - } - }, - "collections": { - "type": "array", - "items": { - "$ref": "#/components/schemas/collection" - } - } - } - }, "exception": { "required": [ "code" @@ -735,7 +640,7 @@ "description": "One or more bounding boxes that describe the spatial extent of the dataset.\nIn the Core only a single bounding box is supported. Extensions may support\nadditional areas. If multiple areas are provided, the union of the bounding\nboxes describes the spatial extent.", "items": { "type": "array", - "description": "Each bounding box is provided as four or six numbers, depending on\nwhether the coordinate reference system includes a vertical axis\n(height or depth):\n\n* Lower left corner, coordinate axis 1\n* Lower left corner, coordinate axis 2\n* Minimum value, coordinate axis 3 (optional)\n* Upper right corner, coordinate axis 1\n* Upper right corner, coordinate axis 2\n* Maximum value, coordinate axis 3 (optional)\n\nThe coordinate reference system of the values is WGS 84 longitude/latitude\n(http://www.opengis.net/def/crs/OGC/1.3/CRS84) unless a different coordinate\nreference system is specified in `crs`.\n\nFor WGS 84 longitude/latitude the values are in most cases the sequence of\nminimum longitude, minimum latitude, maximum longitude and maximum latitude.\nHowever, in cases where the box spans the antimeridian the first value\n(west-most box edge) is larger than the third value (east-most box edge).\n\nIf the vertical axis is included, the third and the sixth number are\nthe bottom and the top of the 3-dimensional bounding box.\n\nIf a feature has multiple spatial geometry properties, it is the decision of the\nserver whether only a single spatial geometry property is used to determine\nthe extent or all relevant geometries.", + "description": "Each bounding box is provided as four or six numbers, depending on\nwhether the coordinate reference system includes a vertical axis\n(height or depth):\n\n* Lower left corner, coordinate axis 1\n* Lower left corner, coordinate axis 2\n* Minimum value, coordinate axis 3 (optional)\n* Upper right corner, coordinate axis 1\n* Upper right corner, coordinate axis 2\n* Maximum value, coordinate axis 3 (optional)\n\nThe coordinate reference system of the values is WGS 84 longitude/latitude\n(http://www.opengis.net/def/crs/OGC/1.3/CRS84) unless a different coordinate\nreference system is specified in `crs`.\n\nFor WGS 84 longitude/latitude the values are in most cases the sequence of\nminimum longitude, minimum latitude, maximum longitude and maximum latitude.\nHowever, in cases where the box spans the antimeridian the first value\n(west-most box edge) is larger than the third value (east-most box edge).\n\nIf a feature has multiple spatial geometry properties, it is the decision of the\nserver whether only a single spatial geometry property is used to determine\nthe extent or all relevant geometries.", "example": [ -180, -90, @@ -1126,7 +1031,7 @@ "bbox": { "name": "bbox", "in": "query", - "description": "Only features that have a geometry that intersects the bounding box are selected.\nThe bounding box is provided as four or six numbers, depending on whether the\ncoordinate reference system includes a vertical axis (height or depth):\n\n* Lower left corner, coordinate axis 1\n* Lower left corner, coordinate axis 2\n* Minimum value, coordinate axis 3 (optional)\n* Upper right corner, coordinate axis 1\n* Upper right corner, coordinate axis 2\n* Maximum value, coordinate axis 3 (optional)\n\nIf the value consists of four numbers, the coordinate reference system is\nWGS 84 longitude/latitude (http://www.opengis.net/def/crs/OGC/1.3/CRS84)\nunless a different coordinate reference system is specified in the parameter `bbox-crs`.\n\nIf the value consists of six numbers, the coordinate reference system is WGS 84\nlongitude/latitude/ellipsoidal height (http://www.opengis.net/def/crs/OGC/0/CRS84h)\nunless a different coordinate reference system is specified in the parameter `bbox-crs`.\n\nThe query parameter `bbox-crs` is specified in OGC API - Features - Part 2: Coordinate\nReference Systems by Reference.\n\nFor WGS 84 longitude/latitude the values are in most cases the sequence of\nminimum longitude, minimum latitude, maximum longitude and maximum latitude.\nHowever, in cases where the box spans the antimeridian the first value\n(west-most box edge) is larger than the third value (east-most box edge).\n\nIf the vertical axis is included, the third and the sixth number are\nthe bottom and the top of the 3-dimensional bounding box.\n\nIf a feature has multiple spatial geometry properties, it is the decision of the\nserver whether only a single spatial geometry property is used to determine\nthe extent or all relevant geometries.", + "description": "Only features that have a geometry that intersects the bounding box are selected.\nThe bounding box is provided as four numbers\n* Lower left corner, coordinate axis 1\n* Lower left corner, coordinate axis 2\n* Upper right corner, coordinate axis 1\n* Upper right corner, coordinate axis 2\n\nThe coordinate reference system is\nWGS 84 longitude/latitude (http://www.opengis.net/def/crs/OGC/1.3/CRS84)\nunless a different coordinate reference system is specified in the parameter `bbox-crs`.\n\nThe query parameter `bbox-crs` is specified in OGC API - Features - Part 2: Coordinate\nReference Systems by Reference.\n\nFor WGS 84 longitude/latitude the values are in most cases the sequence of\nminimum longitude, minimum latitude, maximum longitude and maximum latitude.\nHowever, in cases where the box spans the antimeridian the first value\n(west-most box edge) is larger than the third value (east-most box edge).\n\nIf a feature has multiple spatial geometry properties, it is the decision of the\nserver whether only a single spatial geometry property is used to determine\nthe extent or all relevant geometries.", "required": false, "style": "form", "explode": false, @@ -1137,6 +1042,44 @@ } } }, + "bbox-crs": { + "name": "bbox-crs", + "in": "query", + "description": "The coordinate reference system of the `bbox` parameter. Default is WGS84 longitude/latitude.", + "required": false, + "schema": { + "type": "string", + "format": "uri", + "default": "http://www.opengis.net/def/crs/OGC/1.3/CRS84", + "enum": [ + "http://www.opengis.net/def/crs/OGC/1.3/CRS84" + {{ range $index, $srs := .Config.OgcAPI.Features.ProjectionsForCollections }} + ,"http://www.opengis.net/def/crs/EPSG/0/{{ trimPrefix "EPSG:" $srs }}" + {{ end }} + ] + }, + "style": "form", + "explode": false + }, + "crs": { + "name": "crs", + "in": "query", + "description": "The coordinate reference system of the geometries in the response. Default is WGS84 longitude/latitude", + "required": false, + "schema": { + "type": "string", + "format": "uri", + "default": "http://www.opengis.net/def/crs/OGC/1.3/CRS84", + "enum": [ + "http://www.opengis.net/def/crs/OGC/1.3/CRS84" + {{ range $index, $srs := .Config.OgcAPI.Features.ProjectionsForCollections }} + ,"http://www.opengis.net/def/crs/EPSG/0/{{ trimPrefix "EPSG:" $srs }}" + {{ end }} + ] + }, + "style": "form", + "explode": false + }, "collectionId": { "name": "collectionId", "in": "path", diff --git a/examples/config_multiple_ogc_apis_single_collection.yaml b/examples/config_multiple_ogc_apis_single_collection.yaml index 8a36f6b6..2cbec8ee 100644 --- a/examples/config_multiple_ogc_apis_single_collection.yaml +++ b/examples/config_multiple_ogc_apis_single_collection.yaml @@ -35,12 +35,15 @@ ogcApi: metadata: *collectionMetadata tileServerPath: "NewYork/3DTiles" uriTemplate3dTiles: "3DTiles/{level}/{x}/{y}.b3m" - 3dViewerUrl: "https://app.pdok.nl/viewer" - # this is just for demonstration purposes features: - datasource: - fakedb: true + datasources: + defaultWGS84: + geopackage: + local: + # Dutch addresses, but for example purposes let's present these are NYC addresses + file: ./examples/resources/addresses-crs84.gpkg collections: - id: NewYork + tableName: addresses # reference to common metadata metadata: *collectionMetadata diff --git a/go.mod b/go.mod index f3c16482..fef3144a 100644 --- a/go.mod +++ b/go.mod @@ -26,6 +26,7 @@ require ( ) require ( + dario.cat/mergo v1.0.0 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/gdey/errors v0.0.0-20190426172550-8ebd5bc891fb // indirect diff --git a/go.sum b/go.sum index 64cde397..c6172e7e 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= +dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= github.com/BurntSushi/toml v1.0.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8= github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= diff --git a/ogc/common/geospatial/main.go b/ogc/common/geospatial/main.go index 3230e94d..c73028ee 100644 --- a/ogc/common/geospatial/main.go +++ b/ogc/common/geospatial/main.go @@ -29,7 +29,7 @@ func NewCollections(e *engine.Engine, router *chi.Mux) *Collections { engine.NewTemplateKey(templatesDir+"collections.go.json"), engine.NewTemplateKey(templatesDir+"collections.go.html")) - for _, coll := range e.Config.AllCollections() { + for _, coll := range e.Config.AllCollections().Unique() { title := coll.ID if coll.Metadata != nil && coll.Metadata.Title != nil { title = *coll.Metadata.Title diff --git a/ogc/features/url.go b/ogc/features/url.go index 9f219303..966da688 100644 --- a/ogc/features/url.go +++ b/ogc/features/url.go @@ -43,12 +43,6 @@ func (s SRID) GetOrDefault() int { return val } -func (s SRID) IsSameAs(other SRID) bool { - return int(s) == int(other) || - (int(s) == undefinedSRID && int(other) == wgs84SRID) || - (int(s) == wgs84SRID && int(other) == undefinedSRID) -} - type URL interface { validateNoUnknownParams() error } From ff92c256ac1874aa5f66e9ed7de4fd7e5d20e335 Mon Sep 17 00:00:00 2001 From: Richard Kettelerij Date: Wed, 6 Dec 2023 09:46:31 +0100 Subject: [PATCH 08/26] Tidy + upgrade selected deps --- go.mod | 21 +++++++++---------- go.sum | 66 ++++++++++++++++------------------------------------------ 2 files changed, 28 insertions(+), 59 deletions(-) diff --git a/go.mod b/go.mod index fef3144a..7de4a891 100644 --- a/go.mod +++ b/go.mod @@ -3,35 +3,34 @@ module github.com/PDOK/gokoala go 1.21 require ( + dario.cat/mergo v1.0.0 github.com/BurntSushi/toml v1.3.2 github.com/PDOK/go-cloud-sqlite-vfs v0.2.4 github.com/creasty/defaults v1.7.0 github.com/elnormous/contenttype v1.0.4 - github.com/getkin/kin-openapi v0.116.0 - github.com/go-chi/chi/v5 v5.0.8 - github.com/go-chi/cors v1.2.1 + github.com/getkin/kin-openapi v0.122.0 + github.com/go-chi/chi/v5 v5.0.10 github.com/go-playground/validator/v10 v10.13.0 github.com/go-spatial/geom v0.0.0-20220918193402-3cd2f5a9a082 github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 - github.com/gomarkdown/markdown v0.0.0-20230322041520-c84983bdbf2a + github.com/gomarkdown/markdown v0.0.0-20231115200524-a660076da3fd github.com/jmoiron/sqlx v1.3.5 github.com/mattn/go-sqlite3 v1.14.18 - github.com/nicksnyder/go-i18n/v2 v2.2.1 + github.com/nicksnyder/go-i18n/v2 v2.3.0 github.com/qustavo/sqlhooks/v2 v2.1.0 - github.com/stretchr/testify v1.8.2 - github.com/urfave/cli/v2 v2.25.3 + github.com/stretchr/testify v1.8.4 + github.com/urfave/cli/v2 v2.26.0 github.com/writeas/go-strip-markdown/v2 v2.1.1 - golang.org/x/text v0.9.0 + golang.org/x/text v0.14.0 gopkg.in/yaml.v3 v3.0.1 ) require ( - dario.cat/mergo v1.0.0 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/gdey/errors v0.0.0-20190426172550-8ebd5bc891fb // indirect github.com/go-openapi/jsonpointer v0.19.6 // indirect - github.com/go-openapi/swag v0.22.3 // indirect + github.com/go-openapi/swag v0.22.4 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect github.com/gorilla/mux v1.8.0 // indirect @@ -40,7 +39,7 @@ require ( github.com/leodido/go-urn v1.2.3 // indirect github.com/mailru/easyjson v0.7.7 // indirect github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect - github.com/perimeterx/marshmallow v1.1.4 // indirect + github.com/perimeterx/marshmallow v1.1.5 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect diff --git a/go.sum b/go.sum index c6172e7e..a3055fa7 100644 --- a/go.sum +++ b/go.sum @@ -1,6 +1,5 @@ dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= -github.com/BurntSushi/toml v1.0.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8= github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= github.com/PDOK/go-cloud-sqlite-vfs v0.2.4 h1:OMUbfVBcue/qmfInQEwCD56pRJg0TqtXUGESGLuqxPM= @@ -18,18 +17,15 @@ github.com/elnormous/contenttype v1.0.4 h1:FjmVNkvQOGqSX70yvocph7keC8DtmJaLzTTq6 github.com/elnormous/contenttype v1.0.4/go.mod h1:5KTOW8m1kdX1dLMiUJeN9szzR2xkngiv2K+RVZwWBbI= github.com/gdey/errors v0.0.0-20190426172550-8ebd5bc891fb h1:FYO+lZtAUnakgSW9xYs7QvgawjCDM5wgHaXoDhYHNH4= github.com/gdey/errors v0.0.0-20190426172550-8ebd5bc891fb/go.mod h1:PFaV7MgSRe92Wo9O2H2i1CIm7urUk10AgdSHKyBfjmQ= -github.com/getkin/kin-openapi v0.116.0 h1:o986hwgMzR972JzOG5j6+WTwWqllZLs1EJKMKCivs2E= -github.com/getkin/kin-openapi v0.116.0/go.mod h1:l5e9PaFUo9fyLJCPGQeXI2ML8c3P8BHOEV2VaAVf/pc= -github.com/go-chi/chi/v5 v5.0.8 h1:lD+NLqFcAi1ovnVZpsnObHGW4xb4J8lNmoYVfECH1Y0= -github.com/go-chi/chi/v5 v5.0.8/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= -github.com/go-chi/cors v1.2.1 h1:xEC8UT3Rlp2QuWNEr4Fs/c2EAGVKBwy/1vHx3bppil4= -github.com/go-chi/cors v1.2.1/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58= -github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/getkin/kin-openapi v0.122.0 h1:WB9Jbl0Hp/T79/JF9xlSW5Kl9uYdk/AWD0yAd9HOM10= +github.com/getkin/kin-openapi v0.122.0/go.mod h1:PCWw/lfBrJY4HcdqE3jj+QFkaFK8ABoqo7PvqVhXXqw= +github.com/go-chi/chi/v5 v5.0.10 h1:rLz5avzKpjqxrYwXNfmjkrYYXOyLJd37pz53UFHC6vk= +github.com/go-chi/chi/v5 v5.0.10/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= github.com/go-openapi/jsonpointer v0.19.6 h1:eCs3fxoIi3Wh6vtgmLTOjdhSpiqphQ+DaPn38N2ZdrE= github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= -github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= -github.com/go-openapi/swag v0.22.3 h1:yMBqmnQ0gyZvEb/+KzuWZOXgllrXT4SADYbvDaXHv/g= github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= +github.com/go-openapi/swag v0.22.4 h1:QLMzNJnMGPRNDCbySlcj1x01tzU8/9LTTL9hZZZogBU= +github.com/go-openapi/swag v0.22.4/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= @@ -49,19 +45,17 @@ github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4 github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM= github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/gomarkdown/markdown v0.0.0-20230322041520-c84983bdbf2a h1:AWZzzFrqyjYlRloN6edwTLTUbKxf5flLXNuTBDm3Ews= -github.com/gomarkdown/markdown v0.0.0-20230322041520-c84983bdbf2a/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA= +github.com/gomarkdown/markdown v0.0.0-20231115200524-a660076da3fd h1:PppHBegd3uPZ3Y/Iax/2mlCFJm1w4Qf/zP1MdW4ju2o= +github.com/gomarkdown/markdown v0.0.0-20231115200524-a660076da3fd/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA= github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= -github.com/invopop/yaml v0.1.0/go.mod h1:2XuRLgs/ouIrW3XNzuNj7J3Nvu/Dig5MXvbCEdiBN3Q= github.com/invopop/yaml v0.2.0 h1:7zky/qH+O0DwAyoobXUqvVBwgBFRxKoQ/3FjcVpjTMY= github.com/invopop/yaml v0.2.0/go.mod h1:2XuRLgs/ouIrW3XNzuNj7J3Nvu/Dig5MXvbCEdiBN3Q= github.com/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g= github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= -github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= @@ -72,8 +66,6 @@ github.com/leodido/go-urn v1.2.3 h1:6BE2vPT0lqoz3fmOesHZiaiFh7889ssCo2GMvLCfiuA= github.com/leodido/go-urn v1.2.3/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4= github.com/lib/pq v1.2.0 h1:LXpIM/LZ5xGFhOpXAQUIMM1HdyqzVYM13zNdjCEEcA0= github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= -github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= -github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mattn/go-sqlite3 v1.10.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= @@ -84,12 +76,12 @@ github.com/mattn/go-sqlite3 v1.14.18/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S github.com/mattn/goveralls v0.0.3-0.20180319021929-1c14a4061c1c/go.mod h1:8d1ZMHsd7fW6IRPKQh46F2WRpyib5/X4FOpevwGNQEw= github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw= github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8= -github.com/nicksnyder/go-i18n/v2 v2.2.1 h1:aOzRCdwsJuoExfZhoiXHy4bjruwCMdt5otbYojM/PaA= -github.com/nicksnyder/go-i18n/v2 v2.2.1/go.mod h1:fF2++lPHlo+/kPaj3nB0uxtPwzlPm+BlgwGX7MkeGj0= +github.com/nicksnyder/go-i18n/v2 v2.3.0 h1:2NPsCsNFCVd7i+Su0xYsBrIhS3bE2XMv5gNTft2O+PQ= +github.com/nicksnyder/go-i18n/v2 v2.3.0/go.mod h1:nxYSZE9M0bf3Y70gPQjN9ha7XNHX7gMc814+6wVyEI4= github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= -github.com/perimeterx/marshmallow v1.1.4 h1:pZLDH9RjlLGGorbXhcaQLhfuV0pFMNfPO55FuFkxqLw= -github.com/perimeterx/marshmallow v1.1.4/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw= +github.com/perimeterx/marshmallow v1.1.5 h1:a2LALqQ1BlHM8PZblsDdidgv1mWi1DgC2UmX50IvK2s= +github.com/perimeterx/marshmallow v1.1.5/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/qustavo/sqlhooks/v2 v2.1.0 h1:54yBemHnGHp/7xgT+pxwmIlMSDNYKx5JW5dfRAiCZi0= @@ -99,61 +91,39 @@ github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQD github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= -github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/ugorji/go v1.2.7 h1:qYhyWUUd6WbiM+C6JZAUkIJt/1WrjzNHY9+KCIjVqTo= -github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6M= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/ugorji/go/codec v1.2.7 h1:YPXUKf7fYbp/y8xloBqZOw2qaVggbfwMlI8WM3wZUJ0= github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY= -github.com/urfave/cli/v2 v2.25.3 h1:VJkt6wvEBOoSjPFQvOkv6iWIrsJyCrKGtCtxXWwmGeY= -github.com/urfave/cli/v2 v2.25.3/go.mod h1:GHupkWPMM0M/sj1a2b4wUrWBPzazNrIjouW6fmdJLxc= +github.com/urfave/cli/v2 v2.26.0 h1:3f3AMg3HpThFNT4I++TKOejZO8yU55t3JnnSr4S4QEI= +github.com/urfave/cli/v2 v2.26.0/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ= github.com/writeas/go-strip-markdown/v2 v2.1.1 h1:hAxUM21Uhznf/FnbVGiJciqzska6iLei22Ijc3q2e28= github.com/writeas/go-strip-markdown/v2 v2.1.1/go.mod h1:UvvgPJgn1vvN8nWuE5e7v/+qmDu3BSVnKAB6Gl7hFzA= github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU= github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8= -github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g= golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0= -golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE= -golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/tools v0.0.0-20191114222411-4191b8cbba09/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= From a1759e37a7ef9a516ebc9b6afef8eb4f98fbe7e2 Mon Sep 17 00:00:00 2001 From: Richard Kettelerij Date: Wed, 6 Dec 2023 16:52:50 +0100 Subject: [PATCH 09/26] Turn multiple OGC APIs for a single collection example into a unit test, that's a better fit. --- ...g_multiple_ogc_apis_single_collection.yaml | 6 +- ...d_multiple_ogc_apis_single_collection.json | 64 +++++++++++++++++++ examples/README.md | 13 ---- main_test.go | 52 +++++++++++++++ 4 files changed, 117 insertions(+), 18 deletions(-) rename {examples => engine/testdata}/config_multiple_ogc_apis_single_collection.yaml (90%) create mode 100644 engine/testdata/expected_multiple_ogc_apis_single_collection.json create mode 100644 main_test.go diff --git a/examples/config_multiple_ogc_apis_single_collection.yaml b/engine/testdata/config_multiple_ogc_apis_single_collection.yaml similarity index 90% rename from examples/config_multiple_ogc_apis_single_collection.yaml rename to engine/testdata/config_multiple_ogc_apis_single_collection.yaml index 2cbec8ee..ec5701d5 100644 --- a/examples/config_multiple_ogc_apis_single_collection.yaml +++ b/engine/testdata/config_multiple_ogc_apis_single_collection.yaml @@ -18,14 +18,10 @@ title: New York serviceIdentifier: New York abstract: >- This is a description about the dataset in Markdown. -thumbnail: 3d.png -resources: - directory: ./examples/resources license: name: CC0 1.0 url: https://creativecommons.org/publicdomain/zero/1.0/deed.nl -datasetCatalogUrl: https://www.pdok.nl/datasets -baseUrl: http://localhost:8080 +baseUrl: http://localhost:8180 ogcApi: 3dgeovolumes: tileServer: https://maps.ecere.com/3DAPI/collections/ diff --git a/engine/testdata/expected_multiple_ogc_apis_single_collection.json b/engine/testdata/expected_multiple_ogc_apis_single_collection.json new file mode 100644 index 00000000..cd503f9c --- /dev/null +++ b/engine/testdata/expected_multiple_ogc_apis_single_collection.json @@ -0,0 +1,64 @@ +{ + "id": "NewYork", + "title": "NewYork", + "description": "This is a description about the NewYork collection in Markdown. We offer both 3D Tiles and Features for this collection.", + "collectionType": "3d-container", + "extent": { + "spatial": { + "bbox": [ + [ + -74.391538, + 40.435655, + -73.430235, + 41.030882 + ] + ], + "crs": "http://www.opengis.net/def/crs/EPSG/0/3857" + } + }, + "crs": [ + "http://www.opengis.net/def/crs/OGC/1.3/CRS84" + ], + "storageCrs": "http://www.opengis.net/def/crs/OGC/1.3/CRS84", + "links": [ + { + "rel": "self", + "type": "application/json", + "title": "This document as JSON", + "href": "http://localhost:8180/collections/NewYork?f=json" + }, + { + "rel": "alternate", + "type": "text/html", + "title": "This document as HTML", + "href": "http://localhost:8180/collections/NewYork?f=html" + }, + { + "rel": "items", + "type": "application/json+3dtiles", + "title": "Tileset definition of collection NewYork according to the OGC 3D Tiles specification", + "href": "http://localhost:8180/collections/NewYork/3dtiles?f=json" + }, + { + "rel": "items", + "type": "application/geo+json", + "title": "The JSON representation of the NewYork features served from this endpoint", + "href": "http://localhost:8180/collections/NewYork/items?f=json" + }, + { + "rel": "items", + "type": "text/html", + "title": "The HTML representation of the NewYork features served from this endpoint", + "href": "http://localhost:8180/collections/NewYork/items?f=html" + } + ], + "content": [ + { + "rel": "original", + "type": "application/json+3dtiles", + "title": "Tileset definition of collection NewYork according to the OGC 3D Tiles specification", + "href": "http://localhost:8180/collections/NewYork/3dtiles?f=json", + "collectionType": "3d-container" + } + ] +} diff --git a/examples/README.md b/examples/README.md index 8807e995..808fc872 100644 --- a/examples/README.md +++ b/examples/README.md @@ -33,16 +33,3 @@ This example uses 3D tiles of New York. and provide `config_3d.yaml` as the config file. - Open http://localhost:8080 to explore the landing page - Call http://localhost:8080/collections/NewYork/3dtiles/6/0/1.b3dm to download a specific 3D tile - -## Multiple OGC APIs for a single collection example - -This example demonstrates that you can have a collection (NewYork in this case) that offers -multiple OGC APIs: both OGC API 3D GeoVolumes and OGC API Features. - -To keep the config DRY we use YAML anchors+aliases to reference common metadata for a collection. - -- Start GoKoala as specified in the root [README](../README.md#run) - and provide `config_multiple_ogc_apis_single_collection.yaml` as the config file. -- Open http://localhost:8080 to explore the landing page -- Call http://localhost:8080/collections/NewYork/ to view the collection - diff --git a/main_test.go b/main_test.go new file mode 100644 index 00000000..b04029cd --- /dev/null +++ b/main_test.go @@ -0,0 +1,52 @@ +package main + +import ( + "log" + "net/http" + "net/http/httptest" + "os" + "testing" + + gokoalaEngine "github.com/PDOK/gokoala/engine" + "github.com/stretchr/testify/assert" +) + +func Test_newRouter(t *testing.T) { + tests := []struct { + name string + configFile string + apiCall string + wantBody string + }{ + { + name: "multiple_ogc_apis_single_collection", + configFile: "engine/testdata/config_multiple_ogc_apis_single_collection.yaml", + apiCall: "http://localhost:8180/collections/NewYork?f=json", + wantBody: "engine/testdata/expected_multiple_ogc_apis_single_collection.json", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // given + eng := gokoalaEngine.NewEngine(tt.configFile, "") + router := newRouter(eng, false) + + recorder := httptest.NewRecorder() + req, err := http.NewRequest(http.MethodGet, tt.apiCall, nil) + if err != nil { + t.Fatal(err) + } + + // when + router.ServeHTTP(recorder, req) + + // then + assert.Equal(t, http.StatusOK, recorder.Code) + expectedBody, err := os.ReadFile(tt.wantBody) + if err != nil { + log.Fatal(err) + } + assert.JSONEq(t, recorder.Body.String(), string(expectedBody)) + }) + } +} From 43f3a5ba72758c8f889bd6c5f48d6c091a22366c Mon Sep 17 00:00:00 2001 From: Richard Kettelerij Date: Wed, 6 Dec 2023 17:13:24 +0100 Subject: [PATCH 10/26] Add extra test --- engine/engine.go | 6 +++--- engine/engine_test.go | 35 +++++++++++++++++++++++++++++++++++ 2 files changed, 38 insertions(+), 3 deletions(-) diff --git a/engine/engine.go b/engine/engine.go index 0e3b4508..5d60d5a4 100644 --- a/engine/engine.go +++ b/engine/engine.go @@ -259,9 +259,9 @@ func (e *Engine) ReverseProxy(w http.ResponseWriter, r *http.Request, target *ur proxyRes.StatusCode = http.StatusNoContent removeBody(proxyRes) } - if contentTypeOverwrite != "" { - proxyRes.Header.Set(HeaderContentType, contentTypeOverwrite) - } + } + if contentTypeOverwrite != "" { + proxyRes.Header.Set(HeaderContentType, contentTypeOverwrite) } return nil } diff --git a/engine/engine_test.go b/engine/engine_test.go index ca718c41..9fcb40b2 100644 --- a/engine/engine_test.go +++ b/engine/engine_test.go @@ -1,8 +1,10 @@ package engine import ( + "fmt" "net/http" "net/http/httptest" + "net/url" "os" "path" "runtime" @@ -46,3 +48,36 @@ func TestEngine_ServePage_LandingPage(t *testing.T) { assert.Equal(t, "application/json", recorder.Header().Get(HeaderContentType)) assert.Contains(t, recorder.Body.String(), "This is a minimal OGC API, offering only OGC API Common") } + +func TestReverseProxy(t *testing.T) { + // given + mockTargetServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + _, err := w.Write([]byte(fmt.Sprintf("Mock response, received header %s", r.Header.Get(HeaderBaseURL)))) + if err != nil { + t.Fatal(err) + } + })) + defer mockTargetServer.Close() + + engine := &Engine{ + Config: &Config{ + BaseURL: YAMLURL{&url.URL{Scheme: "https", Host: "api.foobar.example", Path: "/"}}, + }, + } + targetURL, _ := url.Parse(mockTargetServer.URL) + + rec := httptest.NewRecorder() + req, err := http.NewRequest(http.MethodGet, mockTargetServer.URL+"/some/path", nil) + if err != nil { + t.Fatal(err) + } + + // when + engine.ReverseProxy(rec, req, targetURL, false, "image/png") + + // then + assert.Equal(t, http.StatusOK, rec.Code) + assert.Equal(t, "image/png", rec.Header().Get(HeaderContentType)) + assert.Equal(t, rec.Body.String(), "Mock response, received header https://api.foobar.example/") +} From 3566f82af17b74c71f2ac5db72ff4aebd7cb11fb Mon Sep 17 00:00:00 2001 From: Richard Kettelerij Date: Fri, 8 Dec 2023 21:10:28 +0100 Subject: [PATCH 11/26] Add OpenAPI validation to OGC API Features --- engine/contentnegotiation.go | 4 ++++ engine/engine.go | 29 ++++++++++++++++++++++-- engine/engine_test.go | 44 ++++++++++++++++++++++++++++-------- engine/openapi.go | 26 +++++++++++---------- ogc/common/core/main.go | 9 ++++---- ogc/features/json.go | 10 ++++---- ogc/features/main.go | 15 +++++++++--- 7 files changed, 102 insertions(+), 35 deletions(-) diff --git a/engine/contentnegotiation.go b/engine/contentnegotiation.go index 416a1845..1822e07d 100644 --- a/engine/contentnegotiation.go +++ b/engine/contentnegotiation.go @@ -36,6 +36,10 @@ const ( FormatJSONFG = "jsonfg" ) +var ( + MediaTypeJSONVariations = []string{MediaTypeTileJSON, MediaTypeMapboxStyle, MediaTypeGeoJSON, MediaTypeJSONFG} +) + type ContentNegotiation struct { availableMediaTypes []contenttype.MediaType availableLanguages []language.Tag diff --git a/engine/engine.go b/engine/engine.go index 5d60d5a4..0b9e6e05 100644 --- a/engine/engine.go +++ b/engine/engine.go @@ -170,7 +170,7 @@ func (e *Engine) RenderAndServePage(w http.ResponseWriter, r *http.Request, key params interface{}, breadcrumbs []Breadcrumb) { // validate request - if err := e.OpenAPI.validateRequest(r); err != nil { + if err := e.OpenAPI.ValidateRequest(r); err != nil { log.Printf("%v", err.Error()) http.Error(w, err.Error(), http.StatusBadRequest) return @@ -211,7 +211,7 @@ func (e *Engine) RenderAndServePage(w http.ResponseWriter, r *http.Request, key // ServePage validates incoming HTTP request against OpenAPI spec and serve a pre-rendered template as HTTP response func (e *Engine) ServePage(w http.ResponseWriter, r *http.Request, templateKey TemplateKey) { // validate request - if err := e.OpenAPI.validateRequest(r); err != nil { + if err := e.OpenAPI.ValidateRequest(r); err != nil { log.Printf("%v", err.Error()) http.Error(w, err.Error(), http.StatusBadRequest) return @@ -239,6 +239,31 @@ func (e *Engine) ServePage(w http.ResponseWriter, r *http.Request, templateKey T SafeWrite(w.Write, output) } +// ServeResponse validates incoming HTTP request against OpenAPI spec and serve the given response (bytes) +func (e *Engine) ServeResponse(w http.ResponseWriter, r *http.Request, validateRequest bool, contentType string, response []byte) { + // validate request + if validateRequest { + if err := e.OpenAPI.ValidateRequest(r); err != nil { + log.Printf("%v", err.Error()) + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + } + + // validate response + if err := e.OpenAPI.validateResponse(contentType, response, r); err != nil { + log.Printf("%v", err.Error()) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + // return response output to client + if contentType != "" { + w.Header().Set(HeaderContentType, contentType) + } + SafeWrite(w.Write, response) +} + // ReverseProxy forwards given HTTP request to given target server, and optionally tweaks response func (e *Engine) ReverseProxy(w http.ResponseWriter, r *http.Request, target *url.URL, prefer204 bool, contentTypeOverwrite string) { diff --git a/engine/engine_test.go b/engine/engine_test.go index 9fcb40b2..38b27993 100644 --- a/engine/engine_test.go +++ b/engine/engine_test.go @@ -49,7 +49,7 @@ func TestEngine_ServePage_LandingPage(t *testing.T) { assert.Contains(t, recorder.Body.String(), "This is a minimal OGC API, offering only OGC API Common") } -func TestReverseProxy(t *testing.T) { +func TestEngine_ReverseProxy(t *testing.T) { // given mockTargetServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) @@ -60,24 +60,50 @@ func TestReverseProxy(t *testing.T) { })) defer mockTargetServer.Close() + engine, targetURL := makeEngine(mockTargetServer) + rec, req := makeAPICall(t, mockTargetServer) + + // when + engine.ReverseProxy(rec, req, targetURL, false, "") + + // then + assert.Equal(t, http.StatusOK, rec.Code) + assert.Equal(t, rec.Body.String(), "Mock response, received header https://api.foobar.example/") +} + +func TestEngine_ReverseProxy_Status204(t *testing.T) { + // given + mockTargetServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusNotFound) + })) + defer mockTargetServer.Close() + + engine, targetURL := makeEngine(mockTargetServer) + rec, req := makeAPICall(t, mockTargetServer) + + // when + engine.ReverseProxy(rec, req, targetURL, true, "audio/wav") + + // then + assert.Equal(t, http.StatusNoContent, rec.Code) + assert.Equal(t, "audio/wav", rec.Header().Get(HeaderContentType)) +} + +func makeEngine(mockTargetServer *httptest.Server) (*Engine, *url.URL) { engine := &Engine{ Config: &Config{ BaseURL: YAMLURL{&url.URL{Scheme: "https", Host: "api.foobar.example", Path: "/"}}, }, } targetURL, _ := url.Parse(mockTargetServer.URL) + return engine, targetURL +} +func makeAPICall(t *testing.T, mockTargetServer *httptest.Server) (*httptest.ResponseRecorder, *http.Request) { rec := httptest.NewRecorder() req, err := http.NewRequest(http.MethodGet, mockTargetServer.URL+"/some/path", nil) if err != nil { t.Fatal(err) } - - // when - engine.ReverseProxy(rec, req, targetURL, false, "image/png") - - // then - assert.Equal(t, http.StatusOK, rec.Code) - assert.Equal(t, "image/png", rec.Header().Get(HeaderContentType)) - assert.Equal(t, rec.Body.String(), "Mock response, received header https://api.foobar.example/") + return rec, req } diff --git a/engine/openapi.go b/engine/openapi.go index 7eca6914..9876a38c 100644 --- a/engine/openapi.go +++ b/engine/openapi.go @@ -103,17 +103,19 @@ func setupRequestResponseValidation() { return string(data), nil }) - openapi3filter.RegisterBodyDecoder(MediaTypeTileJSON, - func(body io.Reader, header http.Header, schema *openapi3.SchemaRef, - fn openapi3filter.EncodingFn) (interface{}, error) { - var value interface{} - dec := json.NewDecoder(body) - dec.UseNumber() - if err := dec.Decode(&value); err != nil { - return nil, errors.New("response doesn't contain valid JSON") - } - return value, nil - }) + for _, mediaType := range MediaTypeJSONVariations { + openapi3filter.RegisterBodyDecoder(mediaType, + func(body io.Reader, header http.Header, schema *openapi3.SchemaRef, + fn openapi3filter.EncodingFn) (interface{}, error) { + var value interface{} + dec := json.NewDecoder(body) + dec.UseNumber() + if err := dec.Decode(&value); err != nil { + return nil, errors.New("response doesn't contain valid JSON") + } + return value, nil + }) + } } // mergeSpecs merges the given OpenAPI specs. @@ -189,7 +191,7 @@ func renderOpenAPITemplate(config *Config, fileName string) []byte { return rendered.Bytes() } -func (o *OpenAPI) validateRequest(r *http.Request) error { +func (o *OpenAPI) ValidateRequest(r *http.Request) error { requestValidationInput, _ := o.getRequestValidationInput(r) if requestValidationInput != nil { err := openapi3filter.ValidateRequest(context.Background(), requestValidationInput) diff --git a/ogc/common/core/main.go b/ogc/common/core/main.go index 09abb65a..acd91739 100644 --- a/ogc/common/core/main.go +++ b/ogc/common/core/main.go @@ -51,7 +51,7 @@ func NewCommonCore(e *engine.Engine, router *chi.Mux) *CommonCore { router.Get(rootPath, core.LandingPage()) router.Get(apiPath, core.API()) // implements https://gitdocumentatie.logius.nl/publicatie/api/adr/#api-17 - router.Get(alternativeAPIPath, func(w http.ResponseWriter, r *http.Request) { core.apiAsJSON(w) }) + router.Get(alternativeAPIPath, func(w http.ResponseWriter, r *http.Request) { core.apiAsJSON(w, r) }) router.Get(conformancePath, core.Conformance()) router.Handle("/*", http.FileServer(http.Dir("assets"))) @@ -72,7 +72,7 @@ func (c *CommonCore) API() http.HandlerFunc { c.apiAsHTML(w, r) return } else if format == engine.FormatJSON { - c.apiAsJSON(w) + c.apiAsJSON(w, r) return } http.NotFound(w, r) @@ -84,9 +84,8 @@ func (c *CommonCore) apiAsHTML(w http.ResponseWriter, r *http.Request) { c.engine.ServePage(w, r, key) } -func (c *CommonCore) apiAsJSON(w http.ResponseWriter) { - w.Header().Set(engine.HeaderContentType, engine.MediaTypeOpenAPI) - engine.SafeWrite(w.Write, c.engine.OpenAPI.SpecJSON) +func (c *CommonCore) apiAsJSON(w http.ResponseWriter, r *http.Request) { + c.engine.ServeResponse(w, r, true, engine.MediaTypeOpenAPI, c.engine.OpenAPI.SpecJSON) } func (c *CommonCore) Conformance() http.HandlerFunc { diff --git a/ogc/features/json.go b/ogc/features/json.go index efd50094..85c96a17 100644 --- a/ogc/features/json.go +++ b/ogc/features/json.go @@ -19,7 +19,7 @@ func newJSONFeatures(e *engine.Engine) *jsonFeatures { } } -func (jf *jsonFeatures) featuresAsGeoJSON(w http.ResponseWriter, collectionID string, +func (jf *jsonFeatures) featuresAsGeoJSON(w http.ResponseWriter, r *http.Request, collectionID string, cursor domain.Cursors, featuresURL featureCollectionURL, fc *domain.FeatureCollection) { fc.Links = jf.createFeatureCollectionLinks(collectionID, cursor, featuresURL) @@ -28,17 +28,19 @@ func (jf *jsonFeatures) featuresAsGeoJSON(w http.ResponseWriter, collectionID st http.Error(w, "Failed to marshal FeatureCollection to JSON", http.StatusInternalServerError) return } - engine.SafeWrite(w.Write, fcJSON) + jf.engine.ServeResponse(w, r, false /* performed earlier */, engine.MediaTypeGeoJSON, fcJSON) } -func (jf *jsonFeatures) featureAsGeoJSON(w http.ResponseWriter, collectionID string, feat *domain.Feature, url featureURL) { +func (jf *jsonFeatures) featureAsGeoJSON(w http.ResponseWriter, r *http.Request, collectionID string, + feat *domain.Feature, url featureURL) { + feat.Links = jf.createFeatureLinks(url, collectionID, feat.ID) featJSON, err := toJSON(feat) if err != nil { http.Error(w, "Failed to marshal Feature to JSON", http.StatusInternalServerError) return } - engine.SafeWrite(w.Write, featJSON) + jf.engine.ServeResponse(w, r, false /* performed earlier */, engine.MediaTypeGeoJSON, featJSON) } func (jf *jsonFeatures) featuresAsJSONFG() { diff --git a/ogc/features/main.go b/ogc/features/main.go index 0f8c627b..f5402559 100644 --- a/ogc/features/main.go +++ b/ogc/features/main.go @@ -62,8 +62,12 @@ func (f *Features) CollectionContent(_ ...any) http.HandlerFunc { cfg := f.engine.Config return func(w http.ResponseWriter, r *http.Request) { - collectionID := chi.URLParam(r, "collectionId") + if err := f.engine.OpenAPI.ValidateRequest(r); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + collectionID := chi.URLParam(r, "collectionId") url := featureCollectionURL{*cfg.BaseURL.URL, r.URL.Query(), cfg.OgcAPI.Features.Limit} encodedCursor, limit, inputSRID, outputSRID, contentCrs, bbox, err := url.parse() if err != nil { @@ -131,7 +135,7 @@ func (f *Features) CollectionContent(_ ...any) http.HandlerFunc { case engine.FormatHTML: f.html.features(w, r, collectionID, newCursor, url, limit, fc) case engine.FormatJSON: - f.json.featuresAsGeoJSON(w, collectionID, newCursor, url, fc) + f.json.featuresAsGeoJSON(w, r, collectionID, newCursor, url, fc) case engine.FormatJSONFG: f.json.featuresAsJSONFG() default: @@ -144,6 +148,11 @@ func (f *Features) CollectionContent(_ ...any) http.HandlerFunc { // Feature serves a single Feature func (f *Features) Feature() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { + if err := f.engine.OpenAPI.ValidateRequest(r); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + collectionID := chi.URLParam(r, "collectionId") featureID, err := strconv.Atoi(chi.URLParam(r, "featureId")) if err != nil { @@ -188,7 +197,7 @@ func (f *Features) Feature() http.HandlerFunc { case engine.FormatHTML: f.html.feature(w, r, collectionID, feat) case engine.FormatJSON: - f.json.featureAsGeoJSON(w, collectionID, feat, url) + f.json.featureAsGeoJSON(w, r, collectionID, feat, url) case engine.FormatJSONFG: f.json.featureAsJSONFG() default: From c82abd5546ea9c0931da7d7ceeab4f2f705a1d37 Mon Sep 17 00:00:00 2001 From: Richard Kettelerij Date: Tue, 12 Dec 2023 08:53:27 +0100 Subject: [PATCH 12/26] Add extra test --- engine/config_test.go | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/engine/config_test.go b/engine/config_test.go index ef0161e8..6dc4cdb5 100644 --- a/engine/config_test.go +++ b/engine/config_test.go @@ -156,6 +156,34 @@ func TestGeoSpatialCollections_ContainsID(t *testing.T) { } } +func TestProjectionsForCollections(t *testing.T) { + oaf := OgcAPIFeatures{ + Datasources: &Datasources{ + DefaultWGS84: Datasource{}, + Additional: []AdditionalDatasource{ + {Srs: "EPSG:4355"}, + }, + }, + Collections: GeoSpatialCollections{ + GeoSpatialCollection{ + ID: "coll1", + Features: &CollectionEntryFeatures{ + Datasources: &Datasources{ + DefaultWGS84: Datasource{}, + Additional: []AdditionalDatasource{ + {Srs: "EPSG:4326"}, + {Srs: "EPSG:3857"}, + }, + }, + }, + }, + }, + } + + expected := []string{"EPSG:4355", "EPSG:4326", "EPSG:3857"} + assert.Equal(t, expected, oaf.ProjectionsForCollections()) +} + func ptrTo[T any](val T) *T { return &val } From 7afed84b9ee07e2ad97b7b9d3a307620d4ad78d3 Mon Sep 17 00:00:00 2001 From: Richard Kettelerij Date: Tue, 12 Dec 2023 08:58:33 +0100 Subject: [PATCH 13/26] Tidy --- go.mod | 1 + go.sum | 2 ++ 2 files changed, 3 insertions(+) diff --git a/go.mod b/go.mod index 7de4a891..04a57cdd 100644 --- a/go.mod +++ b/go.mod @@ -10,6 +10,7 @@ require ( github.com/elnormous/contenttype v1.0.4 github.com/getkin/kin-openapi v0.122.0 github.com/go-chi/chi/v5 v5.0.10 + github.com/go-chi/cors v1.2.1 github.com/go-playground/validator/v10 v10.13.0 github.com/go-spatial/geom v0.0.0-20220918193402-3cd2f5a9a082 github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 diff --git a/go.sum b/go.sum index a3055fa7..c0e88928 100644 --- a/go.sum +++ b/go.sum @@ -21,6 +21,8 @@ github.com/getkin/kin-openapi v0.122.0 h1:WB9Jbl0Hp/T79/JF9xlSW5Kl9uYdk/AWD0yAd9 github.com/getkin/kin-openapi v0.122.0/go.mod h1:PCWw/lfBrJY4HcdqE3jj+QFkaFK8ABoqo7PvqVhXXqw= github.com/go-chi/chi/v5 v5.0.10 h1:rLz5avzKpjqxrYwXNfmjkrYYXOyLJd37pz53UFHC6vk= github.com/go-chi/chi/v5 v5.0.10/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= +github.com/go-chi/cors v1.2.1 h1:xEC8UT3Rlp2QuWNEr4Fs/c2EAGVKBwy/1vHx3bppil4= +github.com/go-chi/cors v1.2.1/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58= github.com/go-openapi/jsonpointer v0.19.6 h1:eCs3fxoIi3Wh6vtgmLTOjdhSpiqphQ+DaPn38N2ZdrE= github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= From e01d29b000b2dde7259d10776c838e6681335b1d Mon Sep 17 00:00:00 2001 From: Richard Kettelerij Date: Tue, 12 Dec 2023 09:16:51 +0100 Subject: [PATCH 14/26] Fix tests --- engine/openapi.go | 3 +++ main_test.go | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/engine/openapi.go b/engine/openapi.go index 9876a38c..54207028 100644 --- a/engine/openapi.go +++ b/engine/openapi.go @@ -233,6 +233,9 @@ func (o *OpenAPI) getRequestValidationInput(r *http.Request) (*openapi3filter.Re Request: r, PathParams: pathParams, Route: route, + Options: &openapi3filter.Options{ + SkipSettingDefaults: true, + }, }, nil } diff --git a/main_test.go b/main_test.go index b04029cd..1194a4af 100644 --- a/main_test.go +++ b/main_test.go @@ -29,7 +29,7 @@ func Test_newRouter(t *testing.T) { t.Run(tt.name, func(t *testing.T) { // given eng := gokoalaEngine.NewEngine(tt.configFile, "") - router := newRouter(eng, false) + router := newRouter(eng, false, false) recorder := httptest.NewRecorder() req, err := http.NewRequest(http.MethodGet, tt.apiCall, nil) From f02e6ba1d7766daf3146ea3ae2d619b0106b78e5 Mon Sep 17 00:00:00 2001 From: Richard Kettelerij Date: Tue, 12 Dec 2023 14:14:24 +0100 Subject: [PATCH 15/26] Add query params to JSON link, to switch to correct GeoJSON when filters are applied --- engine/engine.go | 2 +- engine/template.go | 26 ++++++++++++++++++++++---- engine/templates/layout.go.html | 18 +++++------------- 3 files changed, 28 insertions(+), 18 deletions(-) diff --git a/engine/engine.go b/engine/engine.go index 0b9e6e05..2b5cd2a1 100644 --- a/engine/engine.go +++ b/engine/engine.go @@ -187,7 +187,7 @@ func (e *Engine) RenderAndServePage(w http.ResponseWriter, r *http.Request, key var output []byte if key.Format == FormatHTML { htmlTmpl := parsedTemplate.(*htmltemplate.Template) - output = e.Templates.renderHTMLTemplate(htmlTmpl, params, breadcrumbs, "") + output = e.Templates.renderHTMLTemplate(htmlTmpl, r.URL.Query(), params, breadcrumbs, "") } else { jsonTmpl := parsedTemplate.(*texttemplate.Template) output = e.Templates.renderNonHTMLTemplate(jsonTmpl, params, key, "") diff --git a/engine/template.go b/engine/template.go index c8738713..d0c93250 100644 --- a/engine/template.go +++ b/engine/template.go @@ -9,6 +9,7 @@ import ( "io" "io/fs" "log" + "net/url" "os" "path/filepath" "strings" @@ -53,14 +54,28 @@ type TemplateKey struct { // TemplateData the data/variables passed as an argument into the template. type TemplateData struct { + // Config set during startup based on the given config file Config *Config - // Params optional parameters not part of GoKoala's configfile. You can use + // Params optional parameters not part of GoKoala's config file. You can use // this to provide extra data to a template at rendering time. Params interface{} - // Crumb path to the page, in key-value pairs of name, path + // Breadcrumb path to the page, in key-value pairs of name->path Breadcrumbs []Breadcrumb + + queryParams url.Values +} + +// QueryString return ?=foo=a&bar=b style query string of the current page +func (td *TemplateData) QueryString(format string) string { + if len(td.queryParams) > 0 { + if format != "" { + td.queryParams.Set(FormatParam, format) + } + return "?" + td.queryParams.Encode() + } + return fmt.Sprintf("?%s=%s", FormatParam, format) } type Breadcrumb struct { @@ -161,7 +176,7 @@ func (t *Templates) renderAndSaveTemplate(key TemplateKey, breadcrumbs []Breadcr var result []byte if key.Format == FormatHTML { file, parsed := t.parseHTMLTemplate(key, lang) - result = t.renderHTMLTemplate(parsed, params, breadcrumbs, file) + result = t.renderHTMLTemplate(parsed, nil, params, breadcrumbs, file) } else { file, parsed := t.parseNonHTMLTemplate(key, lang) result = t.renderNonHTMLTemplate(parsed, params, key, file) @@ -181,12 +196,15 @@ func (t *Templates) parseHTMLTemplate(key TemplateKey, lang language.Tag) (strin return file, parsed } -func (t *Templates) renderHTMLTemplate(parsed *htmltemplate.Template, params interface{}, breadcrumbs []Breadcrumb, file string) []byte { +func (t *Templates) renderHTMLTemplate(parsed *htmltemplate.Template, queryParams url.Values, + params interface{}, breadcrumbs []Breadcrumb, file string) []byte { + var rendered bytes.Buffer if err := parsed.Execute(&rendered, &TemplateData{ Config: t.config, Params: params, Breadcrumbs: breadcrumbs, + queryParams: queryParams, }); err != nil { log.Fatalf("failed to execute HTML template %s, error: %v", file, err) } diff --git a/engine/templates/layout.go.html b/engine/templates/layout.go.html index ba2577b7..3c8c66ff 100644 --- a/engine/templates/layout.go.html +++ b/engine/templates/layout.go.html @@ -64,21 +64,13 @@ From dbbae4f3784b900092366f4f0d1648219c320f0e Mon Sep 17 00:00:00 2001 From: Richard Kettelerij Date: Tue, 12 Dec 2023 16:00:02 +0100 Subject: [PATCH 16/26] Add support for multiple output formats to global navigation --- engine/engine.go | 2 +- engine/template.go | 24 +++++++++++++++++------- engine/templates/layout.go.html | 8 +++++--- 3 files changed, 23 insertions(+), 11 deletions(-) diff --git a/engine/engine.go b/engine/engine.go index 2b5cd2a1..16d1b260 100644 --- a/engine/engine.go +++ b/engine/engine.go @@ -187,7 +187,7 @@ func (e *Engine) RenderAndServePage(w http.ResponseWriter, r *http.Request, key var output []byte if key.Format == FormatHTML { htmlTmpl := parsedTemplate.(*htmltemplate.Template) - output = e.Templates.renderHTMLTemplate(htmlTmpl, r.URL.Query(), params, breadcrumbs, "") + output = e.Templates.renderHTMLTemplate(htmlTmpl, r.URL, params, breadcrumbs, "") } else { jsonTmpl := parsedTemplate.(*texttemplate.Template) output = e.Templates.renderNonHTMLTemplate(jsonTmpl, params, key, "") diff --git a/engine/template.go b/engine/template.go index d0c93250..f2fe8bcc 100644 --- a/engine/template.go +++ b/engine/template.go @@ -64,16 +64,26 @@ type TemplateData struct { // Breadcrumb path to the page, in key-value pairs of name->path Breadcrumbs []Breadcrumb - queryParams url.Values + url *url.URL } -// QueryString return ?=foo=a&bar=b style query string of the current page +// AvailableFormats returns the output formats available for the current page +func (td *TemplateData) AvailableFormats() map[string]string { + if td.url != nil && strings.Contains(td.url.Path, "/items") { + // For OGC API Features + return map[string]string{FormatJSON: "GeoJSON", FormatJSONFG: "JSON-FG"} + } + return map[string]string{FormatJSON: "JSON"} +} + +// QueryString returns ?=foo=a&bar=b style query string of the current page func (td *TemplateData) QueryString(format string) string { - if len(td.queryParams) > 0 { + if td.url != nil { + q := td.url.Query() if format != "" { - td.queryParams.Set(FormatParam, format) + q.Set(FormatParam, format) } - return "?" + td.queryParams.Encode() + return "?" + q.Encode() } return fmt.Sprintf("?%s=%s", FormatParam, format) } @@ -196,7 +206,7 @@ func (t *Templates) parseHTMLTemplate(key TemplateKey, lang language.Tag) (strin return file, parsed } -func (t *Templates) renderHTMLTemplate(parsed *htmltemplate.Template, queryParams url.Values, +func (t *Templates) renderHTMLTemplate(parsed *htmltemplate.Template, URL *url.URL, params interface{}, breadcrumbs []Breadcrumb, file string) []byte { var rendered bytes.Buffer @@ -204,7 +214,7 @@ func (t *Templates) renderHTMLTemplate(parsed *htmltemplate.Template, queryParam Config: t.config, Params: params, Breadcrumbs: breadcrumbs, - queryParams: queryParams, + url: URL, }); err != nil { log.Fatalf("failed to execute HTML template %s, error: %v", file, err) } diff --git a/engine/templates/layout.go.html b/engine/templates/layout.go.html index 3c8c66ff..d3155f1b 100644 --- a/engine/templates/layout.go.html +++ b/engine/templates/layout.go.html @@ -67,10 +67,12 @@ From 90b3cd7cfea927b11ff3f61873101704b28285f1 Mon Sep 17 00:00:00 2001 From: Richard Kettelerij Date: Tue, 12 Dec 2023 16:04:00 +0100 Subject: [PATCH 17/26] Use stable output --- engine/config.go | 5 ++++- engine/config_test.go | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/engine/config.go b/engine/config.go index c9a3a117..e31dbfc8 100644 --- a/engine/config.go +++ b/engine/config.go @@ -5,6 +5,7 @@ import ( "log" "net/url" "os" + "slices" "sort" "strings" "time" @@ -309,7 +310,9 @@ func (oaf OgcAPIFeatures) ProjectionsForCollection(collectionID string) []string break } } - return util.Keys(uniqueSRSs) + result := util.Keys(uniqueSRSs) + slices.Sort(result) + return result } type OgcAPIMaps struct { diff --git a/engine/config_test.go b/engine/config_test.go index 6dc4cdb5..4b3f5f9d 100644 --- a/engine/config_test.go +++ b/engine/config_test.go @@ -180,7 +180,7 @@ func TestProjectionsForCollections(t *testing.T) { }, } - expected := []string{"EPSG:4355", "EPSG:4326", "EPSG:3857"} + expected := []string{"EPSG:3857", "EPSG:4326", "EPSG:4355"} assert.Equal(t, expected, oaf.ProjectionsForCollections()) } From bea4f5794d7c5a90844eafdb25c4110ef3364696 Mon Sep 17 00:00:00 2001 From: Richard Kettelerij Date: Tue, 12 Dec 2023 17:01:26 +0100 Subject: [PATCH 18/26] Create working Features collection page, remove dummy text/links --- assets/i18n/active.en.toml | 2 +- assets/i18n/active.nl.toml | 2 +- engine/contentnegotiation.go | 4 +++- engine/openapi.go | 2 +- engine/template.go | 11 ++++++++--- ogc/common/geospatial/templates/collection.go.html | 14 ++++++++------ 6 files changed, 22 insertions(+), 13 deletions(-) diff --git a/assets/i18n/active.en.toml b/assets/i18n/active.en.toml index ed9603a6..72ae7576 100644 --- a/assets/i18n/active.en.toml +++ b/assets/i18n/active.en.toml @@ -94,7 +94,7 @@ Extent = "Geographic extent" GoTo = "Go to the" ViewIn = "View in the" Browse = "Browse through the" -FeaturesExplanation = "TODO Explain here GeoJSON vs JSON-FG" +FeaturesExplanation = "GeoJSON is an open and widely accepted format for feature data and the first choice for many (web) applications. However, it formally only supports the WGS84 projection. JSON-FG is an extension of GeoJSON and does offer official support for other projections. Since JSON-FG is a recent development, from a compatibility point of view GeoJSON is also offered in other projections for the time being." # Features page Geometry = "geometry" diff --git a/assets/i18n/active.nl.toml b/assets/i18n/active.nl.toml index fc87221a..74836b4b 100644 --- a/assets/i18n/active.nl.toml +++ b/assets/i18n/active.nl.toml @@ -99,7 +99,7 @@ Extent = "Geografische begrenzing" GoTo = "Ga naar de" ViewIn = "Bekijk in de" Browse = "Blader door de" -FeaturesExplanation = "Uitleg over welke JSON, wanneer kies je voor GeoJSON en wanneer voor JSON-FG. Verschil tussen projecties, etc." +FeaturesExplanation = "GeoJSON is een open en breed geaccepteerd formaat voor feature data en voor veel (web)toepassingen de eerste keus. Formeel ondersteund het echter alleen de WGS84 projectie. JSON-FG is een uitbreiding op GeoJSON en biedt wel officiële ondersteuning voor andere projecties. Aangezien JSON-FG een recente ontwikkeling is wordt er vanuit compatibiliteit oogpunt vooralsnog ook GeoJSON in andere projecties aangeboden." # Features page Geometry = "geometrie" diff --git a/engine/contentnegotiation.go b/engine/contentnegotiation.go index 1822e07d..a306669a 100644 --- a/engine/contentnegotiation.go +++ b/engine/contentnegotiation.go @@ -37,7 +37,9 @@ const ( ) var ( - MediaTypeJSONVariations = []string{MediaTypeTileJSON, MediaTypeMapboxStyle, MediaTypeGeoJSON, MediaTypeJSONFG} + MediaTypeJSONFamily = []string{MediaTypeTileJSON, MediaTypeMapboxStyle, MediaTypeGeoJSON, MediaTypeJSONFG} + OutputFormatDefault = map[string]string{FormatJSON: "JSON"} + OutputFormatFeatures = map[string]string{FormatJSON: "GeoJSON", FormatJSONFG: "JSON-FG"} ) type ContentNegotiation struct { diff --git a/engine/openapi.go b/engine/openapi.go index 54207028..56445495 100644 --- a/engine/openapi.go +++ b/engine/openapi.go @@ -103,7 +103,7 @@ func setupRequestResponseValidation() { return string(data), nil }) - for _, mediaType := range MediaTypeJSONVariations { + for _, mediaType := range MediaTypeJSONFamily { openapi3filter.RegisterBodyDecoder(mediaType, func(body io.Reader, header http.Header, schema *openapi3.SchemaRef, fn openapi3filter.EncodingFn) (interface{}, error) { diff --git a/engine/template.go b/engine/template.go index f2fe8bcc..d2c8ceb3 100644 --- a/engine/template.go +++ b/engine/template.go @@ -64,16 +64,21 @@ type TemplateData struct { // Breadcrumb path to the page, in key-value pairs of name->path Breadcrumbs []Breadcrumb + // Request URL url *url.URL } // AvailableFormats returns the output formats available for the current page func (td *TemplateData) AvailableFormats() map[string]string { if td.url != nil && strings.Contains(td.url.Path, "/items") { - // For OGC API Features - return map[string]string{FormatJSON: "GeoJSON", FormatJSONFG: "JSON-FG"} + return td.AvailableFormatsFeatures() } - return map[string]string{FormatJSON: "JSON"} + return OutputFormatDefault +} + +// AvailableFormatsFeatures convenience function +func (td *TemplateData) AvailableFormatsFeatures() map[string]string { + return OutputFormatFeatures } // QueryString returns ?=foo=a&bar=b style query string of the current page diff --git a/ogc/common/geospatial/templates/collection.go.html b/ogc/common/geospatial/templates/collection.go.html index 98d124a7..db4d1ab0 100644 --- a/ogc/common/geospatial/templates/collection.go.html +++ b/ogc/common/geospatial/templates/collection.go.html @@ -57,12 +57,14 @@
Tiles
  • Features
      -
    • {{ i18n "Browse" }} Features
    • -
    • {{ i18n "GoTo" }} Features {{ i18n "As" }} GeoJSON in WGS84
    • -
    • {{ i18n "GoTo" }} Features {{ i18n "As" }} JSON-FG in WGS84
    • -
    • {{ i18n "GoTo" }} Features {{ i18n "As" }} JSON-FG in RD
    • -
    • {{ i18n "GoTo" }} Features {{ i18n "As" }} JSON-FG in ETRS89
    • -
    • Download {{ i18n "As" }} GeoPackage
    • +
    • {{ i18n "Browse" }} Features
    • +
    • {{ i18n "GoTo" }} features in WGS84 {{ i18n "As" }} GeoJSON
    • + {{ range $index, $srs := .Config.OgcAPI.Features.ProjectionsForCollections }} + {{ range $formatKey, $formatName := $.AvailableFormatsFeatures }} +
    • {{ i18n "GoTo" }} features in {{ $srs }} {{ i18n "As" }} {{ $formatName }}
    • + {{ end }} + {{ end }} + {{/* TODO offer download link to GeoPackage, for example
    • Download {{ i18n "As" }} GeoPackage
    • */}}

  • +
    3D GeoVolumes
    + +
  • +
  • +
    Features
    +
      +
    • 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
    • +
    \ No newline at end of file diff --git a/engine/testdata/expected_multiple_ogc_apis_single_collection.json b/engine/testdata/expected_multiple_ogc_apis_single_collection.json index cd503f9c..2ee24e59 100644 --- a/engine/testdata/expected_multiple_ogc_apis_single_collection.json +++ b/engine/testdata/expected_multiple_ogc_apis_single_collection.json @@ -17,7 +17,8 @@ } }, "crs": [ - "http://www.opengis.net/def/crs/OGC/1.3/CRS84" + "http://www.opengis.net/def/crs/OGC/1.3/CRS84", + "http://www.opengis.net/def/crs/EPSG/0/28992" ], "storageCrs": "http://www.opengis.net/def/crs/OGC/1.3/CRS84", "links": [ diff --git a/main_test.go b/main_test.go index 1194a4af..3b5cc43a 100644 --- a/main_test.go +++ b/main_test.go @@ -5,6 +5,7 @@ import ( "net/http" "net/http/httptest" "os" + "strings" "testing" gokoalaEngine "github.com/PDOK/gokoala/engine" @@ -19,11 +20,17 @@ func Test_newRouter(t *testing.T) { wantBody string }{ { - name: "multiple_ogc_apis_single_collection", + name: "multiple_ogc_apis_single_collection_json", configFile: "engine/testdata/config_multiple_ogc_apis_single_collection.yaml", apiCall: "http://localhost:8180/collections/NewYork?f=json", wantBody: "engine/testdata/expected_multiple_ogc_apis_single_collection.json", }, + { + name: "multiple_ogc_apis_single_collection_html", + configFile: "engine/testdata/config_multiple_ogc_apis_single_collection.yaml", + apiCall: "http://localhost:8180/collections/NewYork?f=html", + wantBody: "engine/testdata/expected_multiple_ogc_apis_single_collection.html", + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -46,7 +53,19 @@ func Test_newRouter(t *testing.T) { if err != nil { log.Fatal(err) } - assert.JSONEq(t, recorder.Body.String(), string(expectedBody)) + log.Print(recorder.Body.String()) // to ease debugging + switch { + case strings.HasSuffix(tt.apiCall, "json"): + assert.JSONEq(t, recorder.Body.String(), string(expectedBody)) + case strings.HasSuffix(tt.apiCall, "html"): + assert.Contains(t, normalize(recorder.Body.String()), normalize(string(expectedBody))) + default: + log.Fatalf("implement support to test format: %s", tt.apiCall) + } }) } } + +func normalize(s string) string { + return strings.ToLower(strings.Join(strings.Fields(s), "")) +} From b64ff3a1632755448344fe4c01eda87c2530e18f Mon Sep 17 00:00:00 2001 From: Richard Kettelerij Date: Tue, 12 Dec 2023 20:19:07 +0100 Subject: [PATCH 22/26] Move more headers to constants --- engine/engine.go | 2 ++ main.go | 4 ++-- main_test.go | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/engine/engine.go b/engine/engine.go index 16d1b260..a4d756c7 100644 --- a/engine/engine.go +++ b/engine/engine.go @@ -25,12 +25,14 @@ const ( templatesDir = "engine/templates/" shutdownTimeout = 5 * time.Second + HeaderLink = "Link" HeaderAccept = "Accept" HeaderAcceptLanguage = "Accept-Language" HeaderContentType = "Content-Type" HeaderContentLength = "Content-Length" HeaderContentCrs = "Content-Crs" HeaderBaseURL = "X-BaseUrl" + HeaderRequestedWith = "X-Requested-With" ) // Engine encapsulates shared non-OGC API specific logic diff --git a/main.go b/main.go index a4668bb0..5976cf44 100644 --- a/main.go +++ b/main.go @@ -119,8 +119,8 @@ func newRouter(engine *gokoalaEngine.Engine, allowTrailingSlash bool, enableCORS router.Use(cors.Handler(cors.Options{ AllowedOrigins: []string{"*"}, AllowedMethods: []string{"GET", "HEAD", "OPTIONS"}, - AllowedHeaders: []string{"X-Requested-With"}, - ExposedHeaders: []string{"Content-Crs", "Link"}, + AllowedHeaders: []string{gokoalaEngine.HeaderRequestedWith}, + ExposedHeaders: []string{gokoalaEngine.HeaderContentCrs, gokoalaEngine.HeaderLink}, AllowCredentials: false, MaxAge: int((time.Hour * 24).Seconds()), })) diff --git a/main_test.go b/main_test.go index 3b5cc43a..62849601 100644 --- a/main_test.go +++ b/main_test.go @@ -36,7 +36,7 @@ func Test_newRouter(t *testing.T) { t.Run(tt.name, func(t *testing.T) { // given eng := gokoalaEngine.NewEngine(tt.configFile, "") - router := newRouter(eng, false, false) + router := newRouter(eng, false, true) recorder := httptest.NewRecorder() req, err := http.NewRequest(http.MethodGet, tt.apiCall, nil) From d52c7eaa951001a7873b20ac75fa2c7af88b5ecf Mon Sep 17 00:00:00 2001 From: Richard Kettelerij Date: Wed, 13 Dec 2023 10:59:40 +0100 Subject: [PATCH 23/26] Replace bbox from OAF spec 1.0.1 with 1.0.0 version due to validator, also conform to maxItems 6 even thought we don't support this (already covered in a separate error message) --- engine/templates/openapi/features.go.json | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/engine/templates/openapi/features.go.json b/engine/templates/openapi/features.go.json index 66ae29df..d567c032 100644 --- a/engine/templates/openapi/features.go.json +++ b/engine/templates/openapi/features.go.json @@ -1037,10 +1037,31 @@ "explode": false, "schema": { "type": "array", + "minItems": 4, + "maxItems": 6, "items": { "type": "number" } } +{{/* Replace schema with the following once https://github.com/opengeospatial/ets-ogcapi-features10/issues/223 is fixed*/}} +{{/* */}} +{{/* "schema": {*/}} +{{/* "type": "array",*/}} +{{/* "oneOf": [*/}} +{{/* {*/}} +{{/* "maxItems": 4,*/}} +{{/* "minItems": 4*/}} +{{/* },*/}} +{{/* {*/}} +{{/* "maxItems": 6,*/}} +{{/* "minItems": 6*/}} +{{/* }*/}} +{{/* ],*/}} +{{/* "items": {*/}} +{{/* "type": "number",*/}} +{{/* "format": "double"*/}} +{{/* }*/}} +{{/* }*/}} }, "bbox-crs": { "name": "bbox-crs", From ddc5f9faee3da8f0d25d3bad0d6302a3b7d81c29 Mon Sep 17 00:00:00 2001 From: Richard Kettelerij Date: Wed, 13 Dec 2023 13:04:59 +0100 Subject: [PATCH 24/26] Return valid empty response --- ogc/features/main.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/ogc/features/main.go b/ogc/features/main.go index f5402559..0a9bd52d 100644 --- a/ogc/features/main.go +++ b/ogc/features/main.go @@ -128,7 +128,9 @@ func (f *Features) CollectionContent(_ ...any) http.HandlerFunc { if fc == nil { log.Printf("no results found for collection '%s' with params: %s", collectionID, r.URL.Query().Encode()) - fc = &domain.FeatureCollection{} + fc = &domain.FeatureCollection{ + Features: make([]*domain.Feature, 0), + } } switch f.engine.CN.NegotiateFormat(r) { From 7b0a9c89fb060f708bacd104a6fc0321c3d2b084 Mon Sep 17 00:00:00 2001 From: Richard Kettelerij Date: Wed, 13 Dec 2023 13:22:25 +0100 Subject: [PATCH 25/26] Add new validation report --- README.md | 1 + docs/ogc-features-test-report/20231213.html | 1120 +++++++++++++++++ .../20231213_files/banner.jpg | Bin 0 -> 14010 bytes .../20231213_files/jquery-1.9.1.min.js | 5 + .../20231213_files/logo.png | Bin 0 -> 9229 bytes 5 files changed, 1126 insertions(+) create mode 100644 docs/ogc-features-test-report/20231213.html create mode 100644 docs/ogc-features-test-report/20231213_files/banner.jpg create mode 100644 docs/ogc-features-test-report/20231213_files/jquery-1.9.1.min.js create mode 100644 docs/ogc-features-test-report/20231213_files/logo.png diff --git a/README.md b/README.md index e913cdb9..80f37901 100644 --- a/README.md +++ b/README.md @@ -202,6 +202,7 @@ compliance when available. In the case of OGC API Features follow these steps: - Start a new test session in the TEAM Engine against http://localhost:8080 (or http://host.docker.internal:8080). - More details in the [features conformance test suite](https://opengeospatial.github.io/ets-ogcapi-features10/). - Publish test results HTML report in [docs](./docs/ogc-features-test-report) and list below. + - Test results on [13-12-2023](https://htmlpreview.github.io/?https://github.com/PDOK/gokoala/blob/master/docs/ogc-features-test-report/20231213.html) - Test results on [27-09-2023](https://htmlpreview.github.io/?https://github.com/PDOK/gokoala/blob/master/docs/ogc-features-test-report/20230927.html) ## Misc diff --git a/docs/ogc-features-test-report/20231213.html b/docs/ogc-features-test-report/20231213.html new file mode 100644 index 00000000..dbe26e1a --- /dev/null +++ b/docs/ogc-features-test-report/20231213.html @@ -0,0 +1,1120 @@ + + + + + + Test Session Results + + + +
    +
    + + + TEAM Engine Banner +
    +
    + + OGC Validator + + +
    +
    + User: ogctest
    + Logout +
    + +
    +
    +
    + +

    Results for session s0001

    + + + + + + + + + + + + + + +

    + + Test Name: + ogcapi-features-1.0
    + Test version: + 1.6
    + Time: + 2023-12-13T12:20:05.775Z

    + Test INPUT: + +
    noofcollections : 3
    +
    Tested Instance : http://host.docker.internal:8080

    + Result: +
    Yes
    +
    + Number of conformance classes tested: + 2 +
    + Number of conformance classes passed: + 2 +
    +
    + Number of conformance classes failed: + 0 +

    + + Core conformance classes (Pass = Green; Fail = Red; Skip = Grey): + +
    Core

    + + + + + + + +
    Color LegendPassFailSkip
    +
    +

    Core

    +
    +

    +

    + + + + + + +
    + Pass: + 76 + Fail: + 0 + Skip: + 24 + Total tests: + 100
    +

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    NameReason
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + + +

    org.testng.SkipException: Property numberMatched is not set in collection items 'dutch-addresses'

    +
    + +
    + +
    + +
    + +
    + + +

    org.testng.SkipException: Property numberMatched is not set in collection items 'dutch-addresses'

    +
    + + +

    org.testng.SkipException: Property numberMatched is not set in collection items 'dutch-addresses'

    +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + + +

    org.testng.SkipException: No server response timeStamp set for collection items request ( 'dutch-addresses').

    +
    + + +

    org.testng.SkipException: No server response timeStamp set for collection items request ( 'dutch-addresses').

    +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + + +

    org.testng.SkipException: No server response timeStamp set for collection items request ( 'dutch-addresses').

    +
    + + +

    org.testng.SkipException: No server response timeStamp set for collection items request ( 'dutch-addresses').

    +
    + + +

    org.testng.SkipException: No server response timeStamp set for collection items request ( 'dutch-addresses').

    +
    + + +

    org.testng.SkipException: No server response timeStamp set for collection items request ( 'dutch-addresses').

    +
    + + +

    org.testng.SkipException: No server response timeStamp set for collection items request ( 'dutch-addresses').

    +
    + + +

    org.testng.SkipException: No server response timeStamp set for collection items request ( 'dutch-addresses').

    +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + + +

    org.testng.SkipException: No features were returned for collection.

    +
    + + +

    org.testng.SkipException: No features were returned for collection.

    +
    + + +

    org.testng.SkipException: No features were returned for collection.

    +
    + + +

    org.testng.SkipException: No features were returned for collection.

    +
    + + +

    org.testng.SkipException: No features were returned for collection.

    +
    + + +

    org.testng.SkipException: No features were returned for collection.

    +
    + + +

    org.testng.SkipException: No server response timeStamp set for collection items request ( 'dutch-addresses').

    +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + + +

    org.testng.SkipException: Property numberMatched is not set in collection items 'dutch-addresses'

    +
    + + +

    org.testng.SkipException: Property numberMatched is not set in collection items 'dutch-addresses'

    +
    + + +

    org.testng.SkipException: Property numberMatched is not set in collection items 'dutch-addresses'

    +
    + + +

    org.testng.SkipException: Property numberMatched is not set in collection items 'dutch-addresses'

    +
    + + +

    org.testng.SkipException: Property numberMatched is not set in collection items 'dutch-addresses'

    +
    + + +

    org.testng.SkipException: Property numberMatched is not set in collection items 'dutch-addresses'

    +
    +

    Coordinate Reference Systems by Reference

    +

    +

    + + + + + + +
    + Pass: + 37 + Fail: + 0 + Skip: + 0 + Total tests: + 37
    +

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    NameReason
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + + + + + + + + + + +

    + See the detailed old test report. +

    + + +
    + + + + + + +
    +

    Sessions list

    + +
    + +
    +
    +

    TEAM Engine 5.4.1

    +
    +

    If you have any questions or suggestions, feel free to contact the + site administrator. +

    +
    +
    +
    + + + + \ No newline at end of file diff --git a/docs/ogc-features-test-report/20231213_files/banner.jpg b/docs/ogc-features-test-report/20231213_files/banner.jpg new file mode 100644 index 0000000000000000000000000000000000000000..341ce01cf63d3ad3f535c02c92d4da12d42ed949 GIT binary patch literal 14010 zcmbt)1yCGMx9`9LixUVW5ZnnCG&l<($U<;;4^D7*f&>B4#ogUEKp?ow zZ7{s_uRFcGszqBQ?M2p6(;_IQO^)Jd>4zN&!enNPy8(2OigfDM=5jj{pFL zG6U!U03ZPw8A!m7r?Xd2^iPuloPbY&9Y6)p0Pg`Sz#gyxJfE&y087C9>8Sd2W)2wt zOG5IAYw<6ce>iMUWcS4RM~4b9d$MB%xcy7V{a?D~fbEk_)_>dh51!nUhSQUkf9_j8 z*>(XO072l*Q+NMYX9r9G*Cz@4f7!Koy7!MATY&xPDgV)XLbV7;0O)9FU^G;8Fc^%1 zfsTnyh=YxVg-t^69FLIv6(t4vD>5=FI(9}X8dh2|GA6#)tQ?#?JUo<)0wVle!tC5U zT>ltB!oa}5#=<7X!6D|NCZp#1|89>R06sck2BHEX(E!N!NFaQq$8Lb~={2K1jln;| z@V^@pG6)3~4UCR~iS;B<_Y6Qr0)dcGK&YrFC{NP9PkaCcAC-WbQylHNvN4#(k&r7e z?i)JoyXsCNmGL7wZWE^<49piViAi43GcYo}X6E62%f~MuDDhrWN?HagtE#51p{b>< zV`^sp(ZbTo+S$d`&E3P(EBJFrXju4{i1>uWq_4>-scE@+`2~eV#U-UbYHI818ycIM ze|2^D^uqi42PP(`re|j7<`>pCHn+BS5Wjc#j!#a{&Mz*nu5bRqg#>{91J=Kh{a?87 zpKu|gpny=o|KLJGc7GxeJ_;%|CmMmcGT7MhISp4JI^nyxZ`GX`wA?C3L?%w-m@nvf z*6EM`f%Y$C|21Gi|1D(y2KIk&Edbacq^H3H;R6uh@`fol0Q0{9 zDTxfEUEA+)r``0nn;jcuEI_`vVxbYMrA8n_KHm-aGb=i9s8WaUza{ry*gPc+OZec} z_)JmQkLaBcCAJ(+NXYP?IG*?y&yZS>)TaYQ1aEw;m!qrZ@EO)7-Xt}p(-Bey-?&t$ z%1!CZEe=x4m{S)3X7wvhBBwfDcGIml@zTU|z0#JK{?92Qud-e@$LoS=ywi1R$|92O zaS>Rp-qDb*lhOyGlrA?dgb^z++Eld9cHvQ;=YV86NZA<nb-omq`F?MG2`lH-e7&3>4q4gf?WpiFuP&JVt_Op}cPSBg#)v7>tTebR@>k6jqsF3*vE2>mxLSv~r|@$Rsb+`3I;S(Y@V z;9?Ud7u0R-4QO(fDA8=HZ3AYB z?t&m0!D4I)kboHFt%gpTyG{7I&HGj&4u9p z(Q2HXeHC4bFcLa}YsZY)(JKAr18+6RC+XWuj2j5 zK1SPE_t=M3b!)J5UWOtS*#=~xK}EBY5I2w*`#P-cx&#GStX6Ftr+$w2tEuMWi%1I( z*1r{Wb*Y}+;j*WP+|q(r7$K898$PP9lHE1MSVnvuI8=tqt{W=-) zQ)15Rn_NcE%VIKDk+Y9Q1KqDQ7KIu!(F(;?L3&&NZg9z&Zm^IWj3s$en|+dEfr~cb z#QdraQdvqBvW&=j(*V+H%;M8)(r|)dCL2d>PQ8U7<2dVtR(j;}KLWMvN`#mxu@`z2 z&ii+JOm*Pe49O}Ur{4a$b|a)M)5(nNl3_Ew+Js`YZRN<#NywI;x;@lZHqtv?@ME#* z6gHn~OTh#_wfmB&k@L+K%v%Gk+_s;`Gmk2zAto1f)*{%+(w(uY9YBfX= z>MU_F{o2G9BHN-In?R%ahAA#wypb9JT{|5m`i_%zT)XDDSWJ@>KlW>w)XOk$3fK!sdY|q=0%|(1$nI=^k=>*7 zt1N2E>Y|k5Wb24l%*(&k8#8a+AJ6bATc=^tHkpl)b<2?ex6EehTqhpsMo>mq0Odx7UNb`kJ~epapVu;b;k2id!##- zq0MU&5?!zWxbYDXv6=kyMpbPb)Z{yX%1slXJxUeejg5eWSiO*=Z)n6~8o})Ehnr@f zUe7F7Y0$g1`~hPwp(>=_oa(`BHRxeKhqBqF9XQ1As&8iHKzHME4xec$Q>3o?I#HpB zi~wt$8Wcfjid03Pwm6t%J$#R4q}AB$YjeYbiGI~m(A+_*L)Xm%@2>ktIp6UCrQzvs zFmqGYJ^!`?y`qS33Ca`ZZmbiFnY)VsHvU!2wt~={1rFiT))ux&;gWFuR&ch$G?eH^ zI<7me$llQVX9Cn=Id(WA;+b_ zXlpvUdgoq`VbOPeS=tK3_FJYA&nS(QC5glcl_i1yjEHt9UkY>BADGORPrk8l*Y6Xs{($b*?s;1~q#P>Ad~ zCC-&71Xs`MCTfy3c|y-NPBST->+P6`g5$7-U<8(mIsHpFC!M68hCAwu1s5;vx~vt~ zcq>vzPpwcDPcpZ%@46pPet35BKluW}cKLG7g>*CH7;jrh!nf6>m`RTELm$JW0<-U#D;RAvn-w! z?hPrVi+WO+*>WG^o%ir}N@}CtqvDFmFRv)V?{9Lu1kFxG6bYe5bImdriXnlc>#U5~7}RC@5esdZ z!dN+rHG(?i%MNDr;y3DjcOr!yjk+>5v-CEZ8$@Rk(lNSLaqKxUPC!(EX|{93?}Gy> zIj8ilp!qwNPrc0_tjIIQdP|{}bAhu|_;*oGe13K?+KTO7Y2IuZOxJ#3wBGC=qltPja7hU&;DZT$h{0X!08;Cs7zbcKwO(NQGS{EsL#IQ zW7eynxUF4IhqH8iK}R4?6}`4mclMKbf5E(44)?qB_E#XU{v1ADqwN%ps_NhgZ~J7{ z?KRxcQ?97c@&1yWva4Xn)a!g6a4Z|K7<*1a8+=z%cp(&fjI}TrX_L@LySnq6*1C&ssVyu%9D{t<3H3}LQiJv1 zCa+_iiJEuFxNT;3WW+k_xz)W%FVz!O80&r`5(YXeRcT^-^@;?iV3o{w*QE1or;-nL-&gMCGWO2JKLd3J&JG}&ptc6H91Z9S2@OWft$sGVrlS`d?+$O!bCqhCB#FK zgPvRVO097ycx+wvmHwbi#P0(K@2fh?QP}y~F8CFQRF58J6mJ%fNdLC_9)IhBlD2T{ zS~2*$VwOr4vzk#jI{O=ZUGG|;^S7^6+aaB(9~n8E&RIQl%+LXGrC7u(oZ^vAyJH}J zUV3%EMuI|-7;xCibKsuqX=-(%cf?OSDLNgz@!eH7hR5CZ0mb5avX$H6$Z!y|s{1%2 z#!<)X{Kx1=%0?CNw8FZ=-Y*?X2fUMN+CpmxS5d0R2!ClxK9?~w#W|HZ?O(i+QVJ`8 z5Xt)!IrG7+q=|q78KrDDN^mFt1%E~8#RgwP&B9A3DWYCMUrRAHy{{U5V6ech0_TCl zTj!Q?ju6IeVyqq+m`^uqAggqPM1%=lmO1oe?3t=sXq_vGDu&De4EbgDOeA8NfNIm3 zJ9}IEiYL?(UcL;U%Dd&7{&@whphKr?2pm{5JKWu!sVX_HG2~ZqWySDH#IY{7dDe?Ql_jGXh4Wn(wZ@|2GN1X9D@DzT^!qo8nJ-bo zFlA#!ddzv77BhU>>&_+3mpq)Mn!rPoY#_3`eaz6^=*G*x3SFZ1lXs9}8=c67-I*OL zb-pzYV!ch_Y@NM>2lBnQ<Fkdja_1U6AW*&Tv?WP zwC3xvc@{OJ+%DyYDkBVl3tR-FQw#H3VB>TJq3XSk5B~kdbX7GCfqhN2V3ha{^Oz>a zh;0wIADzeSaq79yuyD3u+lm}aYmKLUc?A2)Y(E@8MSgR28E*J;0v>IF_b zD5jf=J_O5aB?Ts2{-`4NK$7wYY)~Km@_Ne(WQi%Fz1=)2^~My+f_rl5daOz?QYjkk zwIf0wlG-XP>ldm+t+vo-;yuT`>sKHK{+I);KTgl_@b&=Eww4U{BE2g3+3pT+AbN+`qIPzt4W?Qi<3wP;3 zucC-e+p(JW9ZLx^=a08k(cRB%Mnx&!FNA$DFoC#RkoY`h5>}ZW>c-_8d*)cvr_TA* zWlSXjCIApYbutjHe|}|e>yPVrO}Vdcg8(g5w@TWwy9lN9cV6y3cuk$I$Y_WZ_*Lh-(nN|oQU!zqF9CxCiaSk<_FH+B2T=- zc+E|PXg{aDPIC{>rCHY6vP})RAyQ$+6j@pi(ov&IxUhk1LV38!N$vrhonPMoEdm{>+4UBUZn|DuNj^a7Pc=y^^R%Rr&>#w_{%ls z70v``w=|z)Q1bS&U?~(A{F{W`?4iRYyND8C&%5?VHP#i!!pb`B!W) zJTvhuLf?J47)b4CuOER`duXY$5V1Rf#MZ_6e$44g_H)w6-9xia9QFF66`l{SC_Cy7 zRfxOua*aTfy1wht9AM{%y3b%)LTqov$_UiPuL5s0%O8{H_M>N)?P#3}eHzX43#HO| zz~Y#E|63@OE{I_+hXpkeH~ro(ta69BEi{$(Tm=mmjU~5g(kHgM)sUKs3qb<9duaNm zFr*+znnsaH_MOC}{8jua2UQ6pZFc3XXj1jtNHWs{UGfF$%y2QVz2AlRi~T=&6E)N| z(KE|$s2WrGkb-b2sm_TvMW+jVH*|%3QP)o`tO$v>zf7Tx@Y7vV++SC0oe3 z(xS5#=&rt!M5cXOFT_t?&^f`! zjEd?wNcmf`r$&*%X{+ zrG)03aHH%{t(PO5W~Cf$-47xcJTFdh5jj>tk*9bWp{crEawrewSnQP)RX0X{4LHvp z<)GsN%wG5^zvB9t^X|vcI2Upfs9pyXg)vJB;D(X= zrQGc;g^11!@nc_uio1-%_OoZIcywcG4Xo3gdP3XXlVT4pOx7D%kH%dfM#w8U;I7bC`ave~@i1&MBm!a?m;bZw&TwHJi zA$m>GQ2k9*Z2<=;U`{03rYiOQh05zk;Hz!X(a7YDN#lpirqRiL{T^7B-;9CJm4mds z^woq>13%Qbd!lQU1ir5Q>Evgf%c}s+y@H5MR)|hdGfbNMjdGfiW)B075TO##q^Z%t zFCt>2;PY3--6)!Sp+OOB>Kdyssq1#|f+<0#!=k z2?^On$SGPy#SUYp51v~Obko9kvP~sG&3o=gfHl;gTdgw(SX05XmKbIdRsyg7XBm_K z7as+g!%H3dC-19dT;PfzhwyYW6DY0G()KX_VJytY-jNX2-WF5NHu|}?TuM=JV`RFE z35EF?u?1BI{%D)_F@3@IbQ#o!Kqlmv3gRhk@-ybN>^CoIIU$x@Yy)~8lX{b(n0101 z^XwF&RW8>`&>+s|iy501!!*7`lKJSnt4)aBY?jqayo+=*bw_5%SM|pArxYLNS*EjQ zvRWPUyrqO00B}~3S*(torXgHdFwAD}5K2egd@pIgBXvCd40*0tf^Ut;nL~O-$1Ex{ z#zd(ANiz14WPEt$D`wH`|NCA2*)_-vQD@?DKA{M*CJPu+!@lKpDoz|l5)RhkYt^pD zxQ>HJXx~*Y0N-^!2mgU_dCdJBsoxU~m2=tG z8B04a^XwP29!VwhQ}QL$>;{QH0%O+0dxh2OSaY%G4e{I$N2p=@5!-iot~^;dQFKw= zHr_mz-40R`&+w{E^+RK6!{yPJB#2@UFRv9Jf>(QcGXKSKU=hXf1x-@dt7N07u-E~#~3kUJ<+6_ z_on;c5g@EDo!hsgrd?@z1VYo?=Up%khs+I5oSgidTk)nEwsl3>2p}Q6=yqeRlf5)i z(i$I)AmvutI?_eE7Z;8&I(&3^JNOYOd*G=&45IT(l2#UH$aV<3%Kh|Er{^4}eu(y4 z-&ZYWBA?`x{>bvL#jjY6cer2$>sXRx5h)zY{mxn4pC2+j5kD&-BQ?3y3f%EpJ1t*( zr4C9MyX5=sr#BjQkRC!S_jBZnWY%0QSC5&xZZHQXR|Q`bem=8Y+?~>B&wT`rSu<0` zHy8kHe-E`p3lB!D4K)YFv^egb;2=D5k(;#%k6t9a@IyTy*LY_Q8_=z!KrkMc3CUg8 z9>|goy50YIU)8sUfcbK!?Yinh>mnl;cbI=jV#+tvlQ!)32(^5n#ofGIU~X|zalGbw z1R`cjeRzyko&`|2Ee)VQ0)d#P^LEHr6uL=%K3AQh%be#G`|Hk#Uq9GAb!YF@6|ZJo zUBhoOB?}v?tj0t(gp%Cf4G3Io2aK+v8ejd`KBtS=fHU-Sk<6JA;KPhAd&NpfalYx9 zHxM6gt0)p*@7_q&73wa^7wmC@YnrI4g^7$rPqL=o4SBOaW0jNQhavfjd!smjA0j+!Bwvr zk(Rg)u+da}r*}^7z4jY}fhoYmNw?kFPN?hMYbo*bU47Tm`$9S&r!Gj8ZuIz!-T^I` zGQ_FEX08qU)|ZnoFaRjAXM1l(`K#{hlD%Db@GKGel=XfmYQSzknnn#ScS#PM$G|#4*lBN z>!ud#L)KH2b?91Jr0D7e_!&_yMroR-T$&P+YPp3WYsEQHu57X|IJ#Q~;-WfS0Cl_< zSJ8Aa0})HV=9c<~GXopdK}CuKQSLnp(M2wd%UhjT4|}{%361A_{v#}-t{7Q6CL^n_ z`}+l!|6W~EOlKg*COGR0u=-bQV)z{Ux9kOalw!Zy3t|3P)Pu2*VLQuPY6pH)ZJ7B% zKG0!5DuQMO6X#!$A!i;~I^5Y1Q39mo>?wB3p8-TnxtQi~J4+U)z? z{V@_f)Cl_VvLJ3yEU!|;9}_A?9w@RqVQeg4L>HK_+rvT1d{|F*KRwE;Bgz={bI}l? zQZ1@1unBu`>ABr~IC^kF`ey1fVhu$u&KbNC@v4e6wv6nz7}y1VL^Tq zzZuF(%!2HVcmyhSt-H;tZ~I91RW4uSMhTDA_nEG86Ai|;cs?AjxmQAtiJIzX=94^< zBr?3tAm#2{L@T$VI|?gz-Z!%>8^2{_ER>w$iTlYMx*YDE72f152>i%_eT*BkC4-sL zWR$etH8GOpcY5?~7Fvf>tM@NXQY6j%mfx{Ds+Tfh&A(?!eCLjhE54;EnFD?J;!bMp z90Z;TV7kg$>hXaJP$k_-^Nw0GagxSt=*=2$q;xIz@RP7cQj1fn3x0REW#z ztLC%QWZyPi$}`|_;!=9}+F1WVMM857)Cwo#*lgUW|Af01wI2r!Vh)?966j@y{-6{f z7IB=&HF0ttW@O+J4Dgyn77=fddr3}1Ia9JzZgBck5OtJ-V3tGxOhrfsGK6FQz5Vx} zJ();rPNz0{2Zi+!@YpQZ&JExnwh?>5xwIn9FL}F%-gl|G2N@+AwDO9oSvm;6{zQHK z4FPjC7wI@VFsNaL^2n1#kPsUM8`x}QqsSS%EYwF*9wuDIixnNNA!v^gcc)N8alWaA z`UG1~xX%O8pwIi(lRLrswU`yTmc4&gd5wz%SDgFiVbYj1+9r432{6@c>eVBlar?2D zEmtTWWscb0bjmNsSMuYg@zT12jwH!i9kdvlZhk4l z)=k_8nR9XrrF5x?QD{998@4R9@svyk%*>bKL5G=|w-E2=y{pQncw5s^8!c?& zW@cr(sNpG&25HO^mYqo$z z-M!BfEEuvE;$O?2Xp5Ac+MGI@D_SN_H>n*DeS+NM^=eDleBHa;V=j_+>6n`?`+T(N zj%DOct9Y?h^ZoWkrpHBvnK>%n3J5?Jq(D_<6jp}LXI(p>%)Y3)fC^tw9v(qd@YXe; zzn&ugt|RLe=z1hen`Gu+_Az-Dh=G?cf9IWzN1@uRP_RN@p;&LO^OP5bm3~WB9fhA^ zB`=MrAJi@%xTxI8D&= z$wjs=PTlIwKmILmvsVTe1Ajx$@xJ?2efCRZR#gPzBy>A-QQLI1W{V%5ymPt4bSN4I zkX>hn;(49nVT`1;=yy?LQ8&C_Tsl~S-g{|gp>~;_;PKYv>$kof`w&m$%F?$9%YwWr ztoR&V+5OeQ@_`^yAta^Wm3c_*44`N{+toG1On;!)Nfbla4OHE!J_6;+a^DJzyx7g- zrIrJOyiA{R$q;Nhtm1vQH6wFkygZI47w?zsxMf~$7N5S5Zg>E)flL>pFhTjf%`k8J zRQwtoJD`U`oL2arW%_W9bz#VxOJBX?oF06xY(VY)xpAIyC@(k~32p$vEVbUW@7ue3 zef(Vc=zK^?kE3}~Di^OA%)kbrk<&Aoq`AoS< zjqDl514H>@GHcIN)KI1~oG9Ia#2kImZHoitI4g`IV<+t0vg}3jJn^b&kT5o@^B)Feu)!6tkrD^QsG{qLYJ?>_?_$br{mqwTJif%N%RK_+coox@1Ig_}T7u9$v4RnM1 zQ5yVM8jmKieqzZ2bc=Gqbv%q`+iVt=Wer=evC~4;o4=%S$YxE(N>Ig|1q8SS$Zs(xpedB(~Q4(*BqaB@c}SPH~8tRc}y`Z_8u^a`#btj$3|hs8u4XuO?X*^VwJmjJ{(x zKx}O$pCn!O?Msxb^MZVWOe*UygI)j+5Aie>C%jZaUziPrq<8? z^JgceR_Qb2yJ5! z6AAo;>q+OoDzbjgfy(Lx0#5p32!@6?gfzWE$TqmF*ZmE}v_v&N+nLU*#rd6t3$bL+ zWctnu=ni_Qc7s2l*Ae`*OQia>W$epenz=WV`(S=H;@xgt=>-@#covYrbrk0EhXLPX zNkS#0{{ewtZct+Ku!!d6rV@PI(e*eEHMj*=Hf+yP3bq}cA@jxRfLJY6c3vYbca9th zQ8@VW^Zw-xEBCO+Q89KUjIwjfqcFj1>+FCGE;;T-$cvD$xrzG4$VFq!hj$pZWWBqc z<^Ha~n3U-urhwO=xE*o!6My&et+Pnx(rY9LONuHGyc*RXX>3R&=|$oIM<-pq`f;C@E|*aAuxuM|-8l&Y9>h1WU1$usz#S=Dl|UkxqP z((5rTU6g0F_bExr*IUbY<`(z&L%w%(UM18Srxt<28uKbt$;(dkN_cXkxprzJxi8L4 zX&Afl;d!6&y-ngVWBA#+O~Kpv(yC_RTTQGvQxVV2vkJ{$E3|^5rSjdM7xc+%GR_$3 z4Ub0mwPjCqar?QK%Gl|BnHh1uwC2M6pF(6mGtDT=hZjw=KE@Q;G!66JS+-B%=7+%k zyDQMXL0y6TsT?tN(!fAIS#V2mOt-~xxd>SDmCtN40V>zX)dyECxa3{`JqypZD)XST zB~EEX3TCMq6B*L(6}IXWwo%s9%qGf7>l9ZtK#{spAOs?H3KrpTR@Zif=X^&~>T&V} zmlV_6W^c>qpW~lqM{zx{Oabtl5l1b%6N+xTv+~O4qJN33iQzXJEHoy{QXE*w5ZqQj z-d4BwTq`N7zjfkvQ^d%;XVuFY@088^Hn%{hPc=VH#-bOuM2C3T;PZ++!^Ag%%6O`D z9`I^`LQRPg4_pN zU9)lY{nlQ5p@Vt7)FaStNI9h$Vm+4bE9Eh;X;amC)?ew<1Y<9;R`&2H`0yZoqY3l) z)qij&?Y@IXVai=B8gOJ;Sf0b3LLE8*ULoFzt>D~~mlem#T8`pWtQ1BZmtzS1{LIzr zClc_QN~E>a_lkIQ61nYC)kW8xD(E%{dv+unXXixN*?ferES$qT5wdP@E&O0!qPZ9( zj$HU5Kk_BjuUs`-7ACmJv3!)fZu}e_tOnO#7OEZ&<3IsO1rF4BaXzvkcO7uOpV$tU zR1KO3-+)x7e6iRgN;{CDBmBGCn)vuH?lJ(3avRb?Uqkumi|LpI<}Hf#K&s%sJ(8>* zlUthtCg_J~R{TL60-89mauyfgLA7)#sizY_`SM0>#Z2dkwwp0#$o(Zm*tLrsysJ% zm`nvwDm61XHqhFO<&KM&K+n>dl-1wcvHO|EG(Akyar9ueh6EbJ$wNM@Fdyy3R{gG| z6*3}W=!HHcW%EVZnZ~kZg_+`mam@@lZf{gof+$&|csbb7h^iKS6%nd5qdPwJ6Lyr2 z1l$Sd{AHR~AtL$gY_vZboZ$8}pzS|gxGY?^8*i)3VEx6O){1?!fW^taTo&c*9f!97 zd8y96({nN%mvB-6Rsc+1BmdApZM^ix-mZL8I4n(_?4D)V;`s2J(1BpVRCF03dNvTa z3gV4p47baS9Ho_`B35#e&A6Ss_*5XD@Rfs3 z*_pGE zl1RYTD$9^6)_ZPydNsA+6Gn`{7J&OfZl>nNqYsE3NHGm+X*oCA?F%Z;MLPf#y%~o#dDv1I@Xf@5EVgVa%)z>*Gn8ebWU4SVEnpW{U*y^B~D0XpSYj{^`<$p9KGEz)FsJRM1#f{ls zm1()qZ?M*J*xCya?>8s^2}+Shc$;$lDalb7si+aN-EF>5j6q>6rk_Dmk#r0%XJ6mG ztb@n?UnoiUF8mk|@c~m4B|TN~R{DBMD*N>i%vWm?2Vk%)$j`xRx6FNtbN`3beQX}p zwi^0(#WE=i%gX#qIEfUdB4v_CUB#7IgIV0k8iDhYNd)s|HG!bbpk;Zvt$nT%#irl! z9aW`+$RlvCRiWFf!y7e|`M~GeLH|)lT`57(6VsbH+`kS_#G$?_zBN`FT}K`G8>n4p1O(-sY+!;Dln)p-&uCsgGoyHi$kP1xA|*dcebLCEj#*O z=F0V9yZy4&qv!?fcSihFKzHd{>f)#W8bw^T)8s+S+x8AF5~No2!%n>-s~Cr7`np zzvEFarULTLKMo0waO>y@=KiQJByqGnNfPv;yoKJNi zl4!f#F$!Bhw?ZyY&;xUR;jmABtoJpJ9k%=F}pk72uKp>jeQ zBAC&fD5%9RE+|*tk0P{B8wOv60Jb9B!-Sge-RKWBgg;Xv(o&c_BHt)[^>]*|#([\w-]*))$/,C=/^<(\w+)\s*\/?>(?:<\/\1>|)$/,k=/^[\],:{}\s]*$/,E=/(?:^|:|,)(?:\s*\[)+/g,S=/\\(?:["\\\/bfnrt]|u[\da-fA-F]{4})/g,A=/"[^"\\\r\n]*"|true|false|null|-?(?:\d+\.|)\d+(?:[eE][+-]?\d+|)/g,j=/^-ms-/,D=/-([\da-z])/gi,L=function(e,t){return t.toUpperCase()},H=function(e){(o.addEventListener||"load"===e.type||"complete"===o.readyState)&&(q(),b.ready())},q=function(){o.addEventListener?(o.removeEventListener("DOMContentLoaded",H,!1),e.removeEventListener("load",H,!1)):(o.detachEvent("onreadystatechange",H),e.detachEvent("onload",H))};b.fn=b.prototype={jquery:p,constructor:b,init:function(e,n,r){var i,a;if(!e)return this;if("string"==typeof e){if(i="<"===e.charAt(0)&&">"===e.charAt(e.length-1)&&e.length>=3?[null,e,null]:N.exec(e),!i||!i[1]&&n)return!n||n.jquery?(n||r).find(e):this.constructor(n).find(e);if(i[1]){if(n=n instanceof b?n[0]:n,b.merge(this,b.parseHTML(i[1],n&&n.nodeType?n.ownerDocument||n:o,!0)),C.test(i[1])&&b.isPlainObject(n))for(i in n)b.isFunction(this[i])?this[i](n[i]):this.attr(i,n[i]);return this}if(a=o.getElementById(i[2]),a&&a.parentNode){if(a.id!==i[2])return r.find(e);this.length=1,this[0]=a}return this.context=o,this.selector=e,this}return e.nodeType?(this.context=this[0]=e,this.length=1,this):b.isFunction(e)?r.ready(e):(e.selector!==t&&(this.selector=e.selector,this.context=e.context),b.makeArray(e,this))},selector:"",length:0,size:function(){return this.length},toArray:function(){return h.call(this)},get:function(e){return null==e?this.toArray():0>e?this[this.length+e]:this[e]},pushStack:function(e){var t=b.merge(this.constructor(),e);return t.prevObject=this,t.context=this.context,t},each:function(e,t){return b.each(this,e,t)},ready:function(e){return b.ready.promise().done(e),this},slice:function(){return this.pushStack(h.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},eq:function(e){var t=this.length,n=+e+(0>e?t:0);return this.pushStack(n>=0&&t>n?[this[n]]:[])},map:function(e){return this.pushStack(b.map(this,function(t,n){return e.call(t,n,t)}))},end:function(){return this.prevObject||this.constructor(null)},push:d,sort:[].sort,splice:[].splice},b.fn.init.prototype=b.fn,b.extend=b.fn.extend=function(){var e,n,r,i,o,a,s=arguments[0]||{},u=1,l=arguments.length,c=!1;for("boolean"==typeof s&&(c=s,s=arguments[1]||{},u=2),"object"==typeof s||b.isFunction(s)||(s={}),l===u&&(s=this,--u);l>u;u++)if(null!=(o=arguments[u]))for(i in o)e=s[i],r=o[i],s!==r&&(c&&r&&(b.isPlainObject(r)||(n=b.isArray(r)))?(n?(n=!1,a=e&&b.isArray(e)?e:[]):a=e&&b.isPlainObject(e)?e:{},s[i]=b.extend(c,a,r)):r!==t&&(s[i]=r));return s},b.extend({noConflict:function(t){return e.$===b&&(e.$=u),t&&e.jQuery===b&&(e.jQuery=s),b},isReady:!1,readyWait:1,holdReady:function(e){e?b.readyWait++:b.ready(!0)},ready:function(e){if(e===!0?!--b.readyWait:!b.isReady){if(!o.body)return setTimeout(b.ready);b.isReady=!0,e!==!0&&--b.readyWait>0||(n.resolveWith(o,[b]),b.fn.trigger&&b(o).trigger("ready").off("ready"))}},isFunction:function(e){return"function"===b.type(e)},isArray:Array.isArray||function(e){return"array"===b.type(e)},isWindow:function(e){return null!=e&&e==e.window},isNumeric:function(e){return!isNaN(parseFloat(e))&&isFinite(e)},type:function(e){return null==e?e+"":"object"==typeof e||"function"==typeof e?l[m.call(e)]||"object":typeof e},isPlainObject:function(e){if(!e||"object"!==b.type(e)||e.nodeType||b.isWindow(e))return!1;try{if(e.constructor&&!y.call(e,"constructor")&&!y.call(e.constructor.prototype,"isPrototypeOf"))return!1}catch(n){return!1}var r;for(r in e);return r===t||y.call(e,r)},isEmptyObject:function(e){var t;for(t in e)return!1;return!0},error:function(e){throw Error(e)},parseHTML:function(e,t,n){if(!e||"string"!=typeof e)return null;"boolean"==typeof t&&(n=t,t=!1),t=t||o;var r=C.exec(e),i=!n&&[];return r?[t.createElement(r[1])]:(r=b.buildFragment([e],t,i),i&&b(i).remove(),b.merge([],r.childNodes))},parseJSON:function(n){return e.JSON&&e.JSON.parse?e.JSON.parse(n):null===n?n:"string"==typeof n&&(n=b.trim(n),n&&k.test(n.replace(S,"@").replace(A,"]").replace(E,"")))?Function("return "+n)():(b.error("Invalid JSON: "+n),t)},parseXML:function(n){var r,i;if(!n||"string"!=typeof n)return null;try{e.DOMParser?(i=new DOMParser,r=i.parseFromString(n,"text/xml")):(r=new ActiveXObject("Microsoft.XMLDOM"),r.async="false",r.loadXML(n))}catch(o){r=t}return r&&r.documentElement&&!r.getElementsByTagName("parsererror").length||b.error("Invalid XML: "+n),r},noop:function(){},globalEval:function(t){t&&b.trim(t)&&(e.execScript||function(t){e.eval.call(e,t)})(t)},camelCase:function(e){return e.replace(j,"ms-").replace(D,L)},nodeName:function(e,t){return e.nodeName&&e.nodeName.toLowerCase()===t.toLowerCase()},each:function(e,t,n){var r,i=0,o=e.length,a=M(e);if(n){if(a){for(;o>i;i++)if(r=t.apply(e[i],n),r===!1)break}else for(i in e)if(r=t.apply(e[i],n),r===!1)break}else if(a){for(;o>i;i++)if(r=t.call(e[i],i,e[i]),r===!1)break}else for(i in e)if(r=t.call(e[i],i,e[i]),r===!1)break;return e},trim:v&&!v.call("\ufeff\u00a0")?function(e){return null==e?"":v.call(e)}:function(e){return null==e?"":(e+"").replace(T,"")},makeArray:function(e,t){var n=t||[];return null!=e&&(M(Object(e))?b.merge(n,"string"==typeof e?[e]:e):d.call(n,e)),n},inArray:function(e,t,n){var r;if(t){if(g)return g.call(t,e,n);for(r=t.length,n=n?0>n?Math.max(0,r+n):n:0;r>n;n++)if(n in t&&t[n]===e)return n}return-1},merge:function(e,n){var r=n.length,i=e.length,o=0;if("number"==typeof r)for(;r>o;o++)e[i++]=n[o];else while(n[o]!==t)e[i++]=n[o++];return e.length=i,e},grep:function(e,t,n){var r,i=[],o=0,a=e.length;for(n=!!n;a>o;o++)r=!!t(e[o],o),n!==r&&i.push(e[o]);return i},map:function(e,t,n){var r,i=0,o=e.length,a=M(e),s=[];if(a)for(;o>i;i++)r=t(e[i],i,n),null!=r&&(s[s.length]=r);else for(i in e)r=t(e[i],i,n),null!=r&&(s[s.length]=r);return f.apply([],s)},guid:1,proxy:function(e,n){var r,i,o;return"string"==typeof n&&(o=e[n],n=e,e=o),b.isFunction(e)?(r=h.call(arguments,2),i=function(){return e.apply(n||this,r.concat(h.call(arguments)))},i.guid=e.guid=e.guid||b.guid++,i):t},access:function(e,n,r,i,o,a,s){var u=0,l=e.length,c=null==r;if("object"===b.type(r)){o=!0;for(u in r)b.access(e,n,u,r[u],!0,a,s)}else if(i!==t&&(o=!0,b.isFunction(i)||(s=!0),c&&(s?(n.call(e,i),n=null):(c=n,n=function(e,t,n){return c.call(b(e),n)})),n))for(;l>u;u++)n(e[u],r,s?i:i.call(e[u],u,n(e[u],r)));return o?e:c?n.call(e):l?n(e[0],r):a},now:function(){return(new Date).getTime()}}),b.ready.promise=function(t){if(!n)if(n=b.Deferred(),"complete"===o.readyState)setTimeout(b.ready);else if(o.addEventListener)o.addEventListener("DOMContentLoaded",H,!1),e.addEventListener("load",H,!1);else{o.attachEvent("onreadystatechange",H),e.attachEvent("onload",H);var r=!1;try{r=null==e.frameElement&&o.documentElement}catch(i){}r&&r.doScroll&&function a(){if(!b.isReady){try{r.doScroll("left")}catch(e){return setTimeout(a,50)}q(),b.ready()}}()}return n.promise(t)},b.each("Boolean Number String Function Array Date RegExp Object Error".split(" "),function(e,t){l["[object "+t+"]"]=t.toLowerCase()});function M(e){var t=e.length,n=b.type(e);return b.isWindow(e)?!1:1===e.nodeType&&t?!0:"array"===n||"function"!==n&&(0===t||"number"==typeof t&&t>0&&t-1 in e)}r=b(o);var _={};function F(e){var t=_[e]={};return b.each(e.match(w)||[],function(e,n){t[n]=!0}),t}b.Callbacks=function(e){e="string"==typeof e?_[e]||F(e):b.extend({},e);var n,r,i,o,a,s,u=[],l=!e.once&&[],c=function(t){for(r=e.memory&&t,i=!0,a=s||0,s=0,o=u.length,n=!0;u&&o>a;a++)if(u[a].apply(t[0],t[1])===!1&&e.stopOnFalse){r=!1;break}n=!1,u&&(l?l.length&&c(l.shift()):r?u=[]:p.disable())},p={add:function(){if(u){var t=u.length;(function i(t){b.each(t,function(t,n){var r=b.type(n);"function"===r?e.unique&&p.has(n)||u.push(n):n&&n.length&&"string"!==r&&i(n)})})(arguments),n?o=u.length:r&&(s=t,c(r))}return this},remove:function(){return u&&b.each(arguments,function(e,t){var r;while((r=b.inArray(t,u,r))>-1)u.splice(r,1),n&&(o>=r&&o--,a>=r&&a--)}),this},has:function(e){return e?b.inArray(e,u)>-1:!(!u||!u.length)},empty:function(){return u=[],this},disable:function(){return u=l=r=t,this},disabled:function(){return!u},lock:function(){return l=t,r||p.disable(),this},locked:function(){return!l},fireWith:function(e,t){return t=t||[],t=[e,t.slice?t.slice():t],!u||i&&!l||(n?l.push(t):c(t)),this},fire:function(){return p.fireWith(this,arguments),this},fired:function(){return!!i}};return p},b.extend({Deferred:function(e){var t=[["resolve","done",b.Callbacks("once memory"),"resolved"],["reject","fail",b.Callbacks("once memory"),"rejected"],["notify","progress",b.Callbacks("memory")]],n="pending",r={state:function(){return n},always:function(){return i.done(arguments).fail(arguments),this},then:function(){var e=arguments;return b.Deferred(function(n){b.each(t,function(t,o){var a=o[0],s=b.isFunction(e[t])&&e[t];i[o[1]](function(){var e=s&&s.apply(this,arguments);e&&b.isFunction(e.promise)?e.promise().done(n.resolve).fail(n.reject).progress(n.notify):n[a+"With"](this===r?n.promise():this,s?[e]:arguments)})}),e=null}).promise()},promise:function(e){return null!=e?b.extend(e,r):r}},i={};return r.pipe=r.then,b.each(t,function(e,o){var a=o[2],s=o[3];r[o[1]]=a.add,s&&a.add(function(){n=s},t[1^e][2].disable,t[2][2].lock),i[o[0]]=function(){return i[o[0]+"With"](this===i?r:this,arguments),this},i[o[0]+"With"]=a.fireWith}),r.promise(i),e&&e.call(i,i),i},when:function(e){var t=0,n=h.call(arguments),r=n.length,i=1!==r||e&&b.isFunction(e.promise)?r:0,o=1===i?e:b.Deferred(),a=function(e,t,n){return function(r){t[e]=this,n[e]=arguments.length>1?h.call(arguments):r,n===s?o.notifyWith(t,n):--i||o.resolveWith(t,n)}},s,u,l;if(r>1)for(s=Array(r),u=Array(r),l=Array(r);r>t;t++)n[t]&&b.isFunction(n[t].promise)?n[t].promise().done(a(t,l,n)).fail(o.reject).progress(a(t,u,s)):--i;return i||o.resolveWith(l,n),o.promise()}}),b.support=function(){var t,n,r,a,s,u,l,c,p,f,d=o.createElement("div");if(d.setAttribute("className","t"),d.innerHTML="
    a",n=d.getElementsByTagName("*"),r=d.getElementsByTagName("a")[0],!n||!r||!n.length)return{};s=o.createElement("select"),l=s.appendChild(o.createElement("option")),a=d.getElementsByTagName("input")[0],r.style.cssText="top:1px;float:left;opacity:.5",t={getSetAttribute:"t"!==d.className,leadingWhitespace:3===d.firstChild.nodeType,tbody:!d.getElementsByTagName("tbody").length,htmlSerialize:!!d.getElementsByTagName("link").length,style:/top/.test(r.getAttribute("style")),hrefNormalized:"/a"===r.getAttribute("href"),opacity:/^0.5/.test(r.style.opacity),cssFloat:!!r.style.cssFloat,checkOn:!!a.value,optSelected:l.selected,enctype:!!o.createElement("form").enctype,html5Clone:"<:nav>"!==o.createElement("nav").cloneNode(!0).outerHTML,boxModel:"CSS1Compat"===o.compatMode,deleteExpando:!0,noCloneEvent:!0,inlineBlockNeedsLayout:!1,shrinkWrapBlocks:!1,reliableMarginRight:!0,boxSizingReliable:!0,pixelPosition:!1},a.checked=!0,t.noCloneChecked=a.cloneNode(!0).checked,s.disabled=!0,t.optDisabled=!l.disabled;try{delete d.test}catch(h){t.deleteExpando=!1}a=o.createElement("input"),a.setAttribute("value",""),t.input=""===a.getAttribute("value"),a.value="t",a.setAttribute("type","radio"),t.radioValue="t"===a.value,a.setAttribute("checked","t"),a.setAttribute("name","t"),u=o.createDocumentFragment(),u.appendChild(a),t.appendChecked=a.checked,t.checkClone=u.cloneNode(!0).cloneNode(!0).lastChild.checked,d.attachEvent&&(d.attachEvent("onclick",function(){t.noCloneEvent=!1}),d.cloneNode(!0).click());for(f in{submit:!0,change:!0,focusin:!0})d.setAttribute(c="on"+f,"t"),t[f+"Bubbles"]=c in e||d.attributes[c].expando===!1;return d.style.backgroundClip="content-box",d.cloneNode(!0).style.backgroundClip="",t.clearCloneStyle="content-box"===d.style.backgroundClip,b(function(){var n,r,a,s="padding:0;margin:0;border:0;display:block;box-sizing:content-box;-moz-box-sizing:content-box;-webkit-box-sizing:content-box;",u=o.getElementsByTagName("body")[0];u&&(n=o.createElement("div"),n.style.cssText="border:0;width:0;height:0;position:absolute;top:0;left:-9999px;margin-top:1px",u.appendChild(n).appendChild(d),d.innerHTML="
    t
    ",a=d.getElementsByTagName("td"),a[0].style.cssText="padding:0;margin:0;border:0;display:none",p=0===a[0].offsetHeight,a[0].style.display="",a[1].style.display="none",t.reliableHiddenOffsets=p&&0===a[0].offsetHeight,d.innerHTML="",d.style.cssText="box-sizing:border-box;-moz-box-sizing:border-box;-webkit-box-sizing:border-box;padding:1px;border:1px;display:block;width:4px;margin-top:1%;position:absolute;top:1%;",t.boxSizing=4===d.offsetWidth,t.doesNotIncludeMarginInBodyOffset=1!==u.offsetTop,e.getComputedStyle&&(t.pixelPosition="1%"!==(e.getComputedStyle(d,null)||{}).top,t.boxSizingReliable="4px"===(e.getComputedStyle(d,null)||{width:"4px"}).width,r=d.appendChild(o.createElement("div")),r.style.cssText=d.style.cssText=s,r.style.marginRight=r.style.width="0",d.style.width="1px",t.reliableMarginRight=!parseFloat((e.getComputedStyle(r,null)||{}).marginRight)),typeof d.style.zoom!==i&&(d.innerHTML="",d.style.cssText=s+"width:1px;padding:1px;display:inline;zoom:1",t.inlineBlockNeedsLayout=3===d.offsetWidth,d.style.display="block",d.innerHTML="
    ",d.firstChild.style.width="5px",t.shrinkWrapBlocks=3!==d.offsetWidth,t.inlineBlockNeedsLayout&&(u.style.zoom=1)),u.removeChild(n),n=d=a=r=null)}),n=s=u=l=r=a=null,t}();var O=/(?:\{[\s\S]*\}|\[[\s\S]*\])$/,B=/([A-Z])/g;function P(e,n,r,i){if(b.acceptData(e)){var o,a,s=b.expando,u="string"==typeof n,l=e.nodeType,p=l?b.cache:e,f=l?e[s]:e[s]&&s;if(f&&p[f]&&(i||p[f].data)||!u||r!==t)return f||(l?e[s]=f=c.pop()||b.guid++:f=s),p[f]||(p[f]={},l||(p[f].toJSON=b.noop)),("object"==typeof n||"function"==typeof n)&&(i?p[f]=b.extend(p[f],n):p[f].data=b.extend(p[f].data,n)),o=p[f],i||(o.data||(o.data={}),o=o.data),r!==t&&(o[b.camelCase(n)]=r),u?(a=o[n],null==a&&(a=o[b.camelCase(n)])):a=o,a}}function R(e,t,n){if(b.acceptData(e)){var r,i,o,a=e.nodeType,s=a?b.cache:e,u=a?e[b.expando]:b.expando;if(s[u]){if(t&&(o=n?s[u]:s[u].data)){b.isArray(t)?t=t.concat(b.map(t,b.camelCase)):t in o?t=[t]:(t=b.camelCase(t),t=t in o?[t]:t.split(" "));for(r=0,i=t.length;i>r;r++)delete o[t[r]];if(!(n?$:b.isEmptyObject)(o))return}(n||(delete s[u].data,$(s[u])))&&(a?b.cleanData([e],!0):b.support.deleteExpando||s!=s.window?delete s[u]:s[u]=null)}}}b.extend({cache:{},expando:"jQuery"+(p+Math.random()).replace(/\D/g,""),noData:{embed:!0,object:"clsid:D27CDB6E-AE6D-11cf-96B8-444553540000",applet:!0},hasData:function(e){return e=e.nodeType?b.cache[e[b.expando]]:e[b.expando],!!e&&!$(e)},data:function(e,t,n){return P(e,t,n)},removeData:function(e,t){return R(e,t)},_data:function(e,t,n){return P(e,t,n,!0)},_removeData:function(e,t){return R(e,t,!0)},acceptData:function(e){if(e.nodeType&&1!==e.nodeType&&9!==e.nodeType)return!1;var t=e.nodeName&&b.noData[e.nodeName.toLowerCase()];return!t||t!==!0&&e.getAttribute("classid")===t}}),b.fn.extend({data:function(e,n){var r,i,o=this[0],a=0,s=null;if(e===t){if(this.length&&(s=b.data(o),1===o.nodeType&&!b._data(o,"parsedAttrs"))){for(r=o.attributes;r.length>a;a++)i=r[a].name,i.indexOf("data-")||(i=b.camelCase(i.slice(5)),W(o,i,s[i]));b._data(o,"parsedAttrs",!0)}return s}return"object"==typeof e?this.each(function(){b.data(this,e)}):b.access(this,function(n){return n===t?o?W(o,e,b.data(o,e)):null:(this.each(function(){b.data(this,e,n)}),t)},null,n,arguments.length>1,null,!0)},removeData:function(e){return this.each(function(){b.removeData(this,e)})}});function W(e,n,r){if(r===t&&1===e.nodeType){var i="data-"+n.replace(B,"-$1").toLowerCase();if(r=e.getAttribute(i),"string"==typeof r){try{r="true"===r?!0:"false"===r?!1:"null"===r?null:+r+""===r?+r:O.test(r)?b.parseJSON(r):r}catch(o){}b.data(e,n,r)}else r=t}return r}function $(e){var t;for(t in e)if(("data"!==t||!b.isEmptyObject(e[t]))&&"toJSON"!==t)return!1;return!0}b.extend({queue:function(e,n,r){var i;return e?(n=(n||"fx")+"queue",i=b._data(e,n),r&&(!i||b.isArray(r)?i=b._data(e,n,b.makeArray(r)):i.push(r)),i||[]):t},dequeue:function(e,t){t=t||"fx";var n=b.queue(e,t),r=n.length,i=n.shift(),o=b._queueHooks(e,t),a=function(){b.dequeue(e,t)};"inprogress"===i&&(i=n.shift(),r--),o.cur=i,i&&("fx"===t&&n.unshift("inprogress"),delete o.stop,i.call(e,a,o)),!r&&o&&o.empty.fire()},_queueHooks:function(e,t){var n=t+"queueHooks";return b._data(e,n)||b._data(e,n,{empty:b.Callbacks("once memory").add(function(){b._removeData(e,t+"queue"),b._removeData(e,n)})})}}),b.fn.extend({queue:function(e,n){var r=2;return"string"!=typeof e&&(n=e,e="fx",r--),r>arguments.length?b.queue(this[0],e):n===t?this:this.each(function(){var t=b.queue(this,e,n);b._queueHooks(this,e),"fx"===e&&"inprogress"!==t[0]&&b.dequeue(this,e)})},dequeue:function(e){return this.each(function(){b.dequeue(this,e)})},delay:function(e,t){return e=b.fx?b.fx.speeds[e]||e:e,t=t||"fx",this.queue(t,function(t,n){var r=setTimeout(t,e);n.stop=function(){clearTimeout(r)}})},clearQueue:function(e){return this.queue(e||"fx",[])},promise:function(e,n){var r,i=1,o=b.Deferred(),a=this,s=this.length,u=function(){--i||o.resolveWith(a,[a])};"string"!=typeof e&&(n=e,e=t),e=e||"fx";while(s--)r=b._data(a[s],e+"queueHooks"),r&&r.empty&&(i++,r.empty.add(u));return u(),o.promise(n)}});var I,z,X=/[\t\r\n]/g,U=/\r/g,V=/^(?:input|select|textarea|button|object)$/i,Y=/^(?:a|area)$/i,J=/^(?:checked|selected|autofocus|autoplay|async|controls|defer|disabled|hidden|loop|multiple|open|readonly|required|scoped)$/i,G=/^(?:checked|selected)$/i,Q=b.support.getSetAttribute,K=b.support.input;b.fn.extend({attr:function(e,t){return b.access(this,b.attr,e,t,arguments.length>1)},removeAttr:function(e){return this.each(function(){b.removeAttr(this,e)})},prop:function(e,t){return b.access(this,b.prop,e,t,arguments.length>1)},removeProp:function(e){return e=b.propFix[e]||e,this.each(function(){try{this[e]=t,delete this[e]}catch(n){}})},addClass:function(e){var t,n,r,i,o,a=0,s=this.length,u="string"==typeof e&&e;if(b.isFunction(e))return this.each(function(t){b(this).addClass(e.call(this,t,this.className))});if(u)for(t=(e||"").match(w)||[];s>a;a++)if(n=this[a],r=1===n.nodeType&&(n.className?(" "+n.className+" ").replace(X," "):" ")){o=0;while(i=t[o++])0>r.indexOf(" "+i+" ")&&(r+=i+" ");n.className=b.trim(r)}return this},removeClass:function(e){var t,n,r,i,o,a=0,s=this.length,u=0===arguments.length||"string"==typeof e&&e;if(b.isFunction(e))return this.each(function(t){b(this).removeClass(e.call(this,t,this.className))});if(u)for(t=(e||"").match(w)||[];s>a;a++)if(n=this[a],r=1===n.nodeType&&(n.className?(" "+n.className+" ").replace(X," "):"")){o=0;while(i=t[o++])while(r.indexOf(" "+i+" ")>=0)r=r.replace(" "+i+" "," ");n.className=e?b.trim(r):""}return this},toggleClass:function(e,t){var n=typeof e,r="boolean"==typeof t;return b.isFunction(e)?this.each(function(n){b(this).toggleClass(e.call(this,n,this.className,t),t)}):this.each(function(){if("string"===n){var o,a=0,s=b(this),u=t,l=e.match(w)||[];while(o=l[a++])u=r?u:!s.hasClass(o),s[u?"addClass":"removeClass"](o)}else(n===i||"boolean"===n)&&(this.className&&b._data(this,"__className__",this.className),this.className=this.className||e===!1?"":b._data(this,"__className__")||"")})},hasClass:function(e){var t=" "+e+" ",n=0,r=this.length;for(;r>n;n++)if(1===this[n].nodeType&&(" "+this[n].className+" ").replace(X," ").indexOf(t)>=0)return!0;return!1},val:function(e){var n,r,i,o=this[0];{if(arguments.length)return i=b.isFunction(e),this.each(function(n){var o,a=b(this);1===this.nodeType&&(o=i?e.call(this,n,a.val()):e,null==o?o="":"number"==typeof o?o+="":b.isArray(o)&&(o=b.map(o,function(e){return null==e?"":e+""})),r=b.valHooks[this.type]||b.valHooks[this.nodeName.toLowerCase()],r&&"set"in r&&r.set(this,o,"value")!==t||(this.value=o))});if(o)return r=b.valHooks[o.type]||b.valHooks[o.nodeName.toLowerCase()],r&&"get"in r&&(n=r.get(o,"value"))!==t?n:(n=o.value,"string"==typeof n?n.replace(U,""):null==n?"":n)}}}),b.extend({valHooks:{option:{get:function(e){var t=e.attributes.value;return!t||t.specified?e.value:e.text}},select:{get:function(e){var t,n,r=e.options,i=e.selectedIndex,o="select-one"===e.type||0>i,a=o?null:[],s=o?i+1:r.length,u=0>i?s:o?i:0;for(;s>u;u++)if(n=r[u],!(!n.selected&&u!==i||(b.support.optDisabled?n.disabled:null!==n.getAttribute("disabled"))||n.parentNode.disabled&&b.nodeName(n.parentNode,"optgroup"))){if(t=b(n).val(),o)return t;a.push(t)}return a},set:function(e,t){var n=b.makeArray(t);return b(e).find("option").each(function(){this.selected=b.inArray(b(this).val(),n)>=0}),n.length||(e.selectedIndex=-1),n}}},attr:function(e,n,r){var o,a,s,u=e.nodeType;if(e&&3!==u&&8!==u&&2!==u)return typeof e.getAttribute===i?b.prop(e,n,r):(a=1!==u||!b.isXMLDoc(e),a&&(n=n.toLowerCase(),o=b.attrHooks[n]||(J.test(n)?z:I)),r===t?o&&a&&"get"in o&&null!==(s=o.get(e,n))?s:(typeof e.getAttribute!==i&&(s=e.getAttribute(n)),null==s?t:s):null!==r?o&&a&&"set"in o&&(s=o.set(e,r,n))!==t?s:(e.setAttribute(n,r+""),r):(b.removeAttr(e,n),t))},removeAttr:function(e,t){var n,r,i=0,o=t&&t.match(w);if(o&&1===e.nodeType)while(n=o[i++])r=b.propFix[n]||n,J.test(n)?!Q&&G.test(n)?e[b.camelCase("default-"+n)]=e[r]=!1:e[r]=!1:b.attr(e,n,""),e.removeAttribute(Q?n:r)},attrHooks:{type:{set:function(e,t){if(!b.support.radioValue&&"radio"===t&&b.nodeName(e,"input")){var n=e.value;return e.setAttribute("type",t),n&&(e.value=n),t}}}},propFix:{tabindex:"tabIndex",readonly:"readOnly","for":"htmlFor","class":"className",maxlength:"maxLength",cellspacing:"cellSpacing",cellpadding:"cellPadding",rowspan:"rowSpan",colspan:"colSpan",usemap:"useMap",frameborder:"frameBorder",contenteditable:"contentEditable"},prop:function(e,n,r){var i,o,a,s=e.nodeType;if(e&&3!==s&&8!==s&&2!==s)return a=1!==s||!b.isXMLDoc(e),a&&(n=b.propFix[n]||n,o=b.propHooks[n]),r!==t?o&&"set"in o&&(i=o.set(e,r,n))!==t?i:e[n]=r:o&&"get"in o&&null!==(i=o.get(e,n))?i:e[n]},propHooks:{tabIndex:{get:function(e){var n=e.getAttributeNode("tabindex");return n&&n.specified?parseInt(n.value,10):V.test(e.nodeName)||Y.test(e.nodeName)&&e.href?0:t}}}}),z={get:function(e,n){var r=b.prop(e,n),i="boolean"==typeof r&&e.getAttribute(n),o="boolean"==typeof r?K&&Q?null!=i:G.test(n)?e[b.camelCase("default-"+n)]:!!i:e.getAttributeNode(n);return o&&o.value!==!1?n.toLowerCase():t},set:function(e,t,n){return t===!1?b.removeAttr(e,n):K&&Q||!G.test(n)?e.setAttribute(!Q&&b.propFix[n]||n,n):e[b.camelCase("default-"+n)]=e[n]=!0,n}},K&&Q||(b.attrHooks.value={get:function(e,n){var r=e.getAttributeNode(n);return b.nodeName(e,"input")?e.defaultValue:r&&r.specified?r.value:t},set:function(e,n,r){return b.nodeName(e,"input")?(e.defaultValue=n,t):I&&I.set(e,n,r)}}),Q||(I=b.valHooks.button={get:function(e,n){var r=e.getAttributeNode(n);return r&&("id"===n||"name"===n||"coords"===n?""!==r.value:r.specified)?r.value:t},set:function(e,n,r){var i=e.getAttributeNode(r);return i||e.setAttributeNode(i=e.ownerDocument.createAttribute(r)),i.value=n+="","value"===r||n===e.getAttribute(r)?n:t}},b.attrHooks.contenteditable={get:I.get,set:function(e,t,n){I.set(e,""===t?!1:t,n)}},b.each(["width","height"],function(e,n){b.attrHooks[n]=b.extend(b.attrHooks[n],{set:function(e,r){return""===r?(e.setAttribute(n,"auto"),r):t}})})),b.support.hrefNormalized||(b.each(["href","src","width","height"],function(e,n){b.attrHooks[n]=b.extend(b.attrHooks[n],{get:function(e){var r=e.getAttribute(n,2);return null==r?t:r}})}),b.each(["href","src"],function(e,t){b.propHooks[t]={get:function(e){return e.getAttribute(t,4)}}})),b.support.style||(b.attrHooks.style={get:function(e){return e.style.cssText||t},set:function(e,t){return e.style.cssText=t+""}}),b.support.optSelected||(b.propHooks.selected=b.extend(b.propHooks.selected,{get:function(e){var t=e.parentNode;return t&&(t.selectedIndex,t.parentNode&&t.parentNode.selectedIndex),null}})),b.support.enctype||(b.propFix.enctype="encoding"),b.support.checkOn||b.each(["radio","checkbox"],function(){b.valHooks[this]={get:function(e){return null===e.getAttribute("value")?"on":e.value}}}),b.each(["radio","checkbox"],function(){b.valHooks[this]=b.extend(b.valHooks[this],{set:function(e,n){return b.isArray(n)?e.checked=b.inArray(b(e).val(),n)>=0:t}})});var Z=/^(?:input|select|textarea)$/i,et=/^key/,tt=/^(?:mouse|contextmenu)|click/,nt=/^(?:focusinfocus|focusoutblur)$/,rt=/^([^.]*)(?:\.(.+)|)$/;function it(){return!0}function ot(){return!1}b.event={global:{},add:function(e,n,r,o,a){var s,u,l,c,p,f,d,h,g,m,y,v=b._data(e);if(v){r.handler&&(c=r,r=c.handler,a=c.selector),r.guid||(r.guid=b.guid++),(u=v.events)||(u=v.events={}),(f=v.handle)||(f=v.handle=function(e){return typeof b===i||e&&b.event.triggered===e.type?t:b.event.dispatch.apply(f.elem,arguments)},f.elem=e),n=(n||"").match(w)||[""],l=n.length;while(l--)s=rt.exec(n[l])||[],g=y=s[1],m=(s[2]||"").split(".").sort(),p=b.event.special[g]||{},g=(a?p.delegateType:p.bindType)||g,p=b.event.special[g]||{},d=b.extend({type:g,origType:y,data:o,handler:r,guid:r.guid,selector:a,needsContext:a&&b.expr.match.needsContext.test(a),namespace:m.join(".")},c),(h=u[g])||(h=u[g]=[],h.delegateCount=0,p.setup&&p.setup.call(e,o,m,f)!==!1||(e.addEventListener?e.addEventListener(g,f,!1):e.attachEvent&&e.attachEvent("on"+g,f))),p.add&&(p.add.call(e,d),d.handler.guid||(d.handler.guid=r.guid)),a?h.splice(h.delegateCount++,0,d):h.push(d),b.event.global[g]=!0;e=null}},remove:function(e,t,n,r,i){var o,a,s,u,l,c,p,f,d,h,g,m=b.hasData(e)&&b._data(e);if(m&&(c=m.events)){t=(t||"").match(w)||[""],l=t.length;while(l--)if(s=rt.exec(t[l])||[],d=g=s[1],h=(s[2]||"").split(".").sort(),d){p=b.event.special[d]||{},d=(r?p.delegateType:p.bindType)||d,f=c[d]||[],s=s[2]&&RegExp("(^|\\.)"+h.join("\\.(?:.*\\.|)")+"(\\.|$)"),u=o=f.length;while(o--)a=f[o],!i&&g!==a.origType||n&&n.guid!==a.guid||s&&!s.test(a.namespace)||r&&r!==a.selector&&("**"!==r||!a.selector)||(f.splice(o,1),a.selector&&f.delegateCount--,p.remove&&p.remove.call(e,a));u&&!f.length&&(p.teardown&&p.teardown.call(e,h,m.handle)!==!1||b.removeEvent(e,d,m.handle),delete c[d])}else for(d in c)b.event.remove(e,d+t[l],n,r,!0);b.isEmptyObject(c)&&(delete m.handle,b._removeData(e,"events"))}},trigger:function(n,r,i,a){var s,u,l,c,p,f,d,h=[i||o],g=y.call(n,"type")?n.type:n,m=y.call(n,"namespace")?n.namespace.split("."):[];if(l=f=i=i||o,3!==i.nodeType&&8!==i.nodeType&&!nt.test(g+b.event.triggered)&&(g.indexOf(".")>=0&&(m=g.split("."),g=m.shift(),m.sort()),u=0>g.indexOf(":")&&"on"+g,n=n[b.expando]?n:new b.Event(g,"object"==typeof n&&n),n.isTrigger=!0,n.namespace=m.join("."),n.namespace_re=n.namespace?RegExp("(^|\\.)"+m.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,n.result=t,n.target||(n.target=i),r=null==r?[n]:b.makeArray(r,[n]),p=b.event.special[g]||{},a||!p.trigger||p.trigger.apply(i,r)!==!1)){if(!a&&!p.noBubble&&!b.isWindow(i)){for(c=p.delegateType||g,nt.test(c+g)||(l=l.parentNode);l;l=l.parentNode)h.push(l),f=l;f===(i.ownerDocument||o)&&h.push(f.defaultView||f.parentWindow||e)}d=0;while((l=h[d++])&&!n.isPropagationStopped())n.type=d>1?c:p.bindType||g,s=(b._data(l,"events")||{})[n.type]&&b._data(l,"handle"),s&&s.apply(l,r),s=u&&l[u],s&&b.acceptData(l)&&s.apply&&s.apply(l,r)===!1&&n.preventDefault();if(n.type=g,!(a||n.isDefaultPrevented()||p._default&&p._default.apply(i.ownerDocument,r)!==!1||"click"===g&&b.nodeName(i,"a")||!b.acceptData(i)||!u||!i[g]||b.isWindow(i))){f=i[u],f&&(i[u]=null),b.event.triggered=g;try{i[g]()}catch(v){}b.event.triggered=t,f&&(i[u]=f)}return n.result}},dispatch:function(e){e=b.event.fix(e);var n,r,i,o,a,s=[],u=h.call(arguments),l=(b._data(this,"events")||{})[e.type]||[],c=b.event.special[e.type]||{};if(u[0]=e,e.delegateTarget=this,!c.preDispatch||c.preDispatch.call(this,e)!==!1){s=b.event.handlers.call(this,e,l),n=0;while((o=s[n++])&&!e.isPropagationStopped()){e.currentTarget=o.elem,a=0;while((i=o.handlers[a++])&&!e.isImmediatePropagationStopped())(!e.namespace_re||e.namespace_re.test(i.namespace))&&(e.handleObj=i,e.data=i.data,r=((b.event.special[i.origType]||{}).handle||i.handler).apply(o.elem,u),r!==t&&(e.result=r)===!1&&(e.preventDefault(),e.stopPropagation()))}return c.postDispatch&&c.postDispatch.call(this,e),e.result}},handlers:function(e,n){var r,i,o,a,s=[],u=n.delegateCount,l=e.target;if(u&&l.nodeType&&(!e.button||"click"!==e.type))for(;l!=this;l=l.parentNode||this)if(1===l.nodeType&&(l.disabled!==!0||"click"!==e.type)){for(o=[],a=0;u>a;a++)i=n[a],r=i.selector+" ",o[r]===t&&(o[r]=i.needsContext?b(r,this).index(l)>=0:b.find(r,this,null,[l]).length),o[r]&&o.push(i);o.length&&s.push({elem:l,handlers:o})}return n.length>u&&s.push({elem:this,handlers:n.slice(u)}),s},fix:function(e){if(e[b.expando])return e;var t,n,r,i=e.type,a=e,s=this.fixHooks[i];s||(this.fixHooks[i]=s=tt.test(i)?this.mouseHooks:et.test(i)?this.keyHooks:{}),r=s.props?this.props.concat(s.props):this.props,e=new b.Event(a),t=r.length;while(t--)n=r[t],e[n]=a[n];return e.target||(e.target=a.srcElement||o),3===e.target.nodeType&&(e.target=e.target.parentNode),e.metaKey=!!e.metaKey,s.filter?s.filter(e,a):e},props:"altKey bubbles cancelable ctrlKey currentTarget eventPhase metaKey relatedTarget shiftKey target timeStamp view which".split(" "),fixHooks:{},keyHooks:{props:"char charCode key keyCode".split(" "),filter:function(e,t){return null==e.which&&(e.which=null!=t.charCode?t.charCode:t.keyCode),e}},mouseHooks:{props:"button buttons clientX clientY fromElement offsetX offsetY pageX pageY screenX screenY toElement".split(" "),filter:function(e,n){var r,i,a,s=n.button,u=n.fromElement;return null==e.pageX&&null!=n.clientX&&(i=e.target.ownerDocument||o,a=i.documentElement,r=i.body,e.pageX=n.clientX+(a&&a.scrollLeft||r&&r.scrollLeft||0)-(a&&a.clientLeft||r&&r.clientLeft||0),e.pageY=n.clientY+(a&&a.scrollTop||r&&r.scrollTop||0)-(a&&a.clientTop||r&&r.clientTop||0)),!e.relatedTarget&&u&&(e.relatedTarget=u===e.target?n.toElement:u),e.which||s===t||(e.which=1&s?1:2&s?3:4&s?2:0),e}},special:{load:{noBubble:!0},click:{trigger:function(){return b.nodeName(this,"input")&&"checkbox"===this.type&&this.click?(this.click(),!1):t}},focus:{trigger:function(){if(this!==o.activeElement&&this.focus)try{return this.focus(),!1}catch(e){}},delegateType:"focusin"},blur:{trigger:function(){return this===o.activeElement&&this.blur?(this.blur(),!1):t},delegateType:"focusout"},beforeunload:{postDispatch:function(e){e.result!==t&&(e.originalEvent.returnValue=e.result)}}},simulate:function(e,t,n,r){var i=b.extend(new b.Event,n,{type:e,isSimulated:!0,originalEvent:{}});r?b.event.trigger(i,null,t):b.event.dispatch.call(t,i),i.isDefaultPrevented()&&n.preventDefault()}},b.removeEvent=o.removeEventListener?function(e,t,n){e.removeEventListener&&e.removeEventListener(t,n,!1)}:function(e,t,n){var r="on"+t;e.detachEvent&&(typeof e[r]===i&&(e[r]=null),e.detachEvent(r,n))},b.Event=function(e,n){return this instanceof b.Event?(e&&e.type?(this.originalEvent=e,this.type=e.type,this.isDefaultPrevented=e.defaultPrevented||e.returnValue===!1||e.getPreventDefault&&e.getPreventDefault()?it:ot):this.type=e,n&&b.extend(this,n),this.timeStamp=e&&e.timeStamp||b.now(),this[b.expando]=!0,t):new b.Event(e,n)},b.Event.prototype={isDefaultPrevented:ot,isPropagationStopped:ot,isImmediatePropagationStopped:ot,preventDefault:function(){var e=this.originalEvent;this.isDefaultPrevented=it,e&&(e.preventDefault?e.preventDefault():e.returnValue=!1)},stopPropagation:function(){var e=this.originalEvent;this.isPropagationStopped=it,e&&(e.stopPropagation&&e.stopPropagation(),e.cancelBubble=!0)},stopImmediatePropagation:function(){this.isImmediatePropagationStopped=it,this.stopPropagation()}},b.each({mouseenter:"mouseover",mouseleave:"mouseout"},function(e,t){b.event.special[e]={delegateType:t,bindType:t,handle:function(e){var n,r=this,i=e.relatedTarget,o=e.handleObj; +return(!i||i!==r&&!b.contains(r,i))&&(e.type=o.origType,n=o.handler.apply(this,arguments),e.type=t),n}}}),b.support.submitBubbles||(b.event.special.submit={setup:function(){return b.nodeName(this,"form")?!1:(b.event.add(this,"click._submit keypress._submit",function(e){var n=e.target,r=b.nodeName(n,"input")||b.nodeName(n,"button")?n.form:t;r&&!b._data(r,"submitBubbles")&&(b.event.add(r,"submit._submit",function(e){e._submit_bubble=!0}),b._data(r,"submitBubbles",!0))}),t)},postDispatch:function(e){e._submit_bubble&&(delete e._submit_bubble,this.parentNode&&!e.isTrigger&&b.event.simulate("submit",this.parentNode,e,!0))},teardown:function(){return b.nodeName(this,"form")?!1:(b.event.remove(this,"._submit"),t)}}),b.support.changeBubbles||(b.event.special.change={setup:function(){return Z.test(this.nodeName)?(("checkbox"===this.type||"radio"===this.type)&&(b.event.add(this,"propertychange._change",function(e){"checked"===e.originalEvent.propertyName&&(this._just_changed=!0)}),b.event.add(this,"click._change",function(e){this._just_changed&&!e.isTrigger&&(this._just_changed=!1),b.event.simulate("change",this,e,!0)})),!1):(b.event.add(this,"beforeactivate._change",function(e){var t=e.target;Z.test(t.nodeName)&&!b._data(t,"changeBubbles")&&(b.event.add(t,"change._change",function(e){!this.parentNode||e.isSimulated||e.isTrigger||b.event.simulate("change",this.parentNode,e,!0)}),b._data(t,"changeBubbles",!0))}),t)},handle:function(e){var n=e.target;return this!==n||e.isSimulated||e.isTrigger||"radio"!==n.type&&"checkbox"!==n.type?e.handleObj.handler.apply(this,arguments):t},teardown:function(){return b.event.remove(this,"._change"),!Z.test(this.nodeName)}}),b.support.focusinBubbles||b.each({focus:"focusin",blur:"focusout"},function(e,t){var n=0,r=function(e){b.event.simulate(t,e.target,b.event.fix(e),!0)};b.event.special[t]={setup:function(){0===n++&&o.addEventListener(e,r,!0)},teardown:function(){0===--n&&o.removeEventListener(e,r,!0)}}}),b.fn.extend({on:function(e,n,r,i,o){var a,s;if("object"==typeof e){"string"!=typeof n&&(r=r||n,n=t);for(a in e)this.on(a,n,r,e[a],o);return this}if(null==r&&null==i?(i=n,r=n=t):null==i&&("string"==typeof n?(i=r,r=t):(i=r,r=n,n=t)),i===!1)i=ot;else if(!i)return this;return 1===o&&(s=i,i=function(e){return b().off(e),s.apply(this,arguments)},i.guid=s.guid||(s.guid=b.guid++)),this.each(function(){b.event.add(this,e,i,r,n)})},one:function(e,t,n,r){return this.on(e,t,n,r,1)},off:function(e,n,r){var i,o;if(e&&e.preventDefault&&e.handleObj)return i=e.handleObj,b(e.delegateTarget).off(i.namespace?i.origType+"."+i.namespace:i.origType,i.selector,i.handler),this;if("object"==typeof e){for(o in e)this.off(o,n,e[o]);return this}return(n===!1||"function"==typeof n)&&(r=n,n=t),r===!1&&(r=ot),this.each(function(){b.event.remove(this,e,r,n)})},bind:function(e,t,n){return this.on(e,null,t,n)},unbind:function(e,t){return this.off(e,null,t)},delegate:function(e,t,n,r){return this.on(t,e,n,r)},undelegate:function(e,t,n){return 1===arguments.length?this.off(e,"**"):this.off(t,e||"**",n)},trigger:function(e,t){return this.each(function(){b.event.trigger(e,t,this)})},triggerHandler:function(e,n){var r=this[0];return r?b.event.trigger(e,n,r,!0):t}}),function(e,t){var n,r,i,o,a,s,u,l,c,p,f,d,h,g,m,y,v,x="sizzle"+-new Date,w=e.document,T={},N=0,C=0,k=it(),E=it(),S=it(),A=typeof t,j=1<<31,D=[],L=D.pop,H=D.push,q=D.slice,M=D.indexOf||function(e){var t=0,n=this.length;for(;n>t;t++)if(this[t]===e)return t;return-1},_="[\\x20\\t\\r\\n\\f]",F="(?:\\\\.|[\\w-]|[^\\x00-\\xa0])+",O=F.replace("w","w#"),B="([*^$|!~]?=)",P="\\["+_+"*("+F+")"+_+"*(?:"+B+_+"*(?:(['\"])((?:\\\\.|[^\\\\])*?)\\3|("+O+")|)|)"+_+"*\\]",R=":("+F+")(?:\\(((['\"])((?:\\\\.|[^\\\\])*?)\\3|((?:\\\\.|[^\\\\()[\\]]|"+P.replace(3,8)+")*)|.*)\\)|)",W=RegExp("^"+_+"+|((?:^|[^\\\\])(?:\\\\.)*)"+_+"+$","g"),$=RegExp("^"+_+"*,"+_+"*"),I=RegExp("^"+_+"*([\\x20\\t\\r\\n\\f>+~])"+_+"*"),z=RegExp(R),X=RegExp("^"+O+"$"),U={ID:RegExp("^#("+F+")"),CLASS:RegExp("^\\.("+F+")"),NAME:RegExp("^\\[name=['\"]?("+F+")['\"]?\\]"),TAG:RegExp("^("+F.replace("w","w*")+")"),ATTR:RegExp("^"+P),PSEUDO:RegExp("^"+R),CHILD:RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+_+"*(even|odd|(([+-]|)(\\d*)n|)"+_+"*(?:([+-]|)"+_+"*(\\d+)|))"+_+"*\\)|)","i"),needsContext:RegExp("^"+_+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+_+"*((?:-\\d)?\\d*)"+_+"*\\)|)(?=[^-]|$)","i")},V=/[\x20\t\r\n\f]*[+~]/,Y=/^[^{]+\{\s*\[native code/,J=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,G=/^(?:input|select|textarea|button)$/i,Q=/^h\d$/i,K=/'|\\/g,Z=/\=[\x20\t\r\n\f]*([^'"\]]*)[\x20\t\r\n\f]*\]/g,et=/\\([\da-fA-F]{1,6}[\x20\t\r\n\f]?|.)/g,tt=function(e,t){var n="0x"+t-65536;return n!==n?t:0>n?String.fromCharCode(n+65536):String.fromCharCode(55296|n>>10,56320|1023&n)};try{q.call(w.documentElement.childNodes,0)[0].nodeType}catch(nt){q=function(e){var t,n=[];while(t=this[e++])n.push(t);return n}}function rt(e){return Y.test(e+"")}function it(){var e,t=[];return e=function(n,r){return t.push(n+=" ")>i.cacheLength&&delete e[t.shift()],e[n]=r}}function ot(e){return e[x]=!0,e}function at(e){var t=p.createElement("div");try{return e(t)}catch(n){return!1}finally{t=null}}function st(e,t,n,r){var i,o,a,s,u,l,f,g,m,v;if((t?t.ownerDocument||t:w)!==p&&c(t),t=t||p,n=n||[],!e||"string"!=typeof e)return n;if(1!==(s=t.nodeType)&&9!==s)return[];if(!d&&!r){if(i=J.exec(e))if(a=i[1]){if(9===s){if(o=t.getElementById(a),!o||!o.parentNode)return n;if(o.id===a)return n.push(o),n}else if(t.ownerDocument&&(o=t.ownerDocument.getElementById(a))&&y(t,o)&&o.id===a)return n.push(o),n}else{if(i[2])return H.apply(n,q.call(t.getElementsByTagName(e),0)),n;if((a=i[3])&&T.getByClassName&&t.getElementsByClassName)return H.apply(n,q.call(t.getElementsByClassName(a),0)),n}if(T.qsa&&!h.test(e)){if(f=!0,g=x,m=t,v=9===s&&e,1===s&&"object"!==t.nodeName.toLowerCase()){l=ft(e),(f=t.getAttribute("id"))?g=f.replace(K,"\\$&"):t.setAttribute("id",g),g="[id='"+g+"'] ",u=l.length;while(u--)l[u]=g+dt(l[u]);m=V.test(e)&&t.parentNode||t,v=l.join(",")}if(v)try{return H.apply(n,q.call(m.querySelectorAll(v),0)),n}catch(b){}finally{f||t.removeAttribute("id")}}}return wt(e.replace(W,"$1"),t,n,r)}a=st.isXML=function(e){var t=e&&(e.ownerDocument||e).documentElement;return t?"HTML"!==t.nodeName:!1},c=st.setDocument=function(e){var n=e?e.ownerDocument||e:w;return n!==p&&9===n.nodeType&&n.documentElement?(p=n,f=n.documentElement,d=a(n),T.tagNameNoComments=at(function(e){return e.appendChild(n.createComment("")),!e.getElementsByTagName("*").length}),T.attributes=at(function(e){e.innerHTML="";var t=typeof e.lastChild.getAttribute("multiple");return"boolean"!==t&&"string"!==t}),T.getByClassName=at(function(e){return e.innerHTML="",e.getElementsByClassName&&e.getElementsByClassName("e").length?(e.lastChild.className="e",2===e.getElementsByClassName("e").length):!1}),T.getByName=at(function(e){e.id=x+0,e.innerHTML="
    ",f.insertBefore(e,f.firstChild);var t=n.getElementsByName&&n.getElementsByName(x).length===2+n.getElementsByName(x+0).length;return T.getIdNotName=!n.getElementById(x),f.removeChild(e),t}),i.attrHandle=at(function(e){return e.innerHTML="",e.firstChild&&typeof e.firstChild.getAttribute!==A&&"#"===e.firstChild.getAttribute("href")})?{}:{href:function(e){return e.getAttribute("href",2)},type:function(e){return e.getAttribute("type")}},T.getIdNotName?(i.find.ID=function(e,t){if(typeof t.getElementById!==A&&!d){var n=t.getElementById(e);return n&&n.parentNode?[n]:[]}},i.filter.ID=function(e){var t=e.replace(et,tt);return function(e){return e.getAttribute("id")===t}}):(i.find.ID=function(e,n){if(typeof n.getElementById!==A&&!d){var r=n.getElementById(e);return r?r.id===e||typeof r.getAttributeNode!==A&&r.getAttributeNode("id").value===e?[r]:t:[]}},i.filter.ID=function(e){var t=e.replace(et,tt);return function(e){var n=typeof e.getAttributeNode!==A&&e.getAttributeNode("id");return n&&n.value===t}}),i.find.TAG=T.tagNameNoComments?function(e,n){return typeof n.getElementsByTagName!==A?n.getElementsByTagName(e):t}:function(e,t){var n,r=[],i=0,o=t.getElementsByTagName(e);if("*"===e){while(n=o[i++])1===n.nodeType&&r.push(n);return r}return o},i.find.NAME=T.getByName&&function(e,n){return typeof n.getElementsByName!==A?n.getElementsByName(name):t},i.find.CLASS=T.getByClassName&&function(e,n){return typeof n.getElementsByClassName===A||d?t:n.getElementsByClassName(e)},g=[],h=[":focus"],(T.qsa=rt(n.querySelectorAll))&&(at(function(e){e.innerHTML="",e.querySelectorAll("[selected]").length||h.push("\\["+_+"*(?:checked|disabled|ismap|multiple|readonly|selected|value)"),e.querySelectorAll(":checked").length||h.push(":checked")}),at(function(e){e.innerHTML="",e.querySelectorAll("[i^='']").length&&h.push("[*^$]="+_+"*(?:\"\"|'')"),e.querySelectorAll(":enabled").length||h.push(":enabled",":disabled"),e.querySelectorAll("*,:x"),h.push(",.*:")})),(T.matchesSelector=rt(m=f.matchesSelector||f.mozMatchesSelector||f.webkitMatchesSelector||f.oMatchesSelector||f.msMatchesSelector))&&at(function(e){T.disconnectedMatch=m.call(e,"div"),m.call(e,"[s!='']:x"),g.push("!=",R)}),h=RegExp(h.join("|")),g=RegExp(g.join("|")),y=rt(f.contains)||f.compareDocumentPosition?function(e,t){var n=9===e.nodeType?e.documentElement:e,r=t&&t.parentNode;return e===r||!(!r||1!==r.nodeType||!(n.contains?n.contains(r):e.compareDocumentPosition&&16&e.compareDocumentPosition(r)))}:function(e,t){if(t)while(t=t.parentNode)if(t===e)return!0;return!1},v=f.compareDocumentPosition?function(e,t){var r;return e===t?(u=!0,0):(r=t.compareDocumentPosition&&e.compareDocumentPosition&&e.compareDocumentPosition(t))?1&r||e.parentNode&&11===e.parentNode.nodeType?e===n||y(w,e)?-1:t===n||y(w,t)?1:0:4&r?-1:1:e.compareDocumentPosition?-1:1}:function(e,t){var r,i=0,o=e.parentNode,a=t.parentNode,s=[e],l=[t];if(e===t)return u=!0,0;if(!o||!a)return e===n?-1:t===n?1:o?-1:a?1:0;if(o===a)return ut(e,t);r=e;while(r=r.parentNode)s.unshift(r);r=t;while(r=r.parentNode)l.unshift(r);while(s[i]===l[i])i++;return i?ut(s[i],l[i]):s[i]===w?-1:l[i]===w?1:0},u=!1,[0,0].sort(v),T.detectDuplicates=u,p):p},st.matches=function(e,t){return st(e,null,null,t)},st.matchesSelector=function(e,t){if((e.ownerDocument||e)!==p&&c(e),t=t.replace(Z,"='$1']"),!(!T.matchesSelector||d||g&&g.test(t)||h.test(t)))try{var n=m.call(e,t);if(n||T.disconnectedMatch||e.document&&11!==e.document.nodeType)return n}catch(r){}return st(t,p,null,[e]).length>0},st.contains=function(e,t){return(e.ownerDocument||e)!==p&&c(e),y(e,t)},st.attr=function(e,t){var n;return(e.ownerDocument||e)!==p&&c(e),d||(t=t.toLowerCase()),(n=i.attrHandle[t])?n(e):d||T.attributes?e.getAttribute(t):((n=e.getAttributeNode(t))||e.getAttribute(t))&&e[t]===!0?t:n&&n.specified?n.value:null},st.error=function(e){throw Error("Syntax error, unrecognized expression: "+e)},st.uniqueSort=function(e){var t,n=[],r=1,i=0;if(u=!T.detectDuplicates,e.sort(v),u){for(;t=e[r];r++)t===e[r-1]&&(i=n.push(r));while(i--)e.splice(n[i],1)}return e};function ut(e,t){var n=t&&e,r=n&&(~t.sourceIndex||j)-(~e.sourceIndex||j);if(r)return r;if(n)while(n=n.nextSibling)if(n===t)return-1;return e?1:-1}function lt(e){return function(t){var n=t.nodeName.toLowerCase();return"input"===n&&t.type===e}}function ct(e){return function(t){var n=t.nodeName.toLowerCase();return("input"===n||"button"===n)&&t.type===e}}function pt(e){return ot(function(t){return t=+t,ot(function(n,r){var i,o=e([],n.length,t),a=o.length;while(a--)n[i=o[a]]&&(n[i]=!(r[i]=n[i]))})})}o=st.getText=function(e){var t,n="",r=0,i=e.nodeType;if(i){if(1===i||9===i||11===i){if("string"==typeof e.textContent)return e.textContent;for(e=e.firstChild;e;e=e.nextSibling)n+=o(e)}else if(3===i||4===i)return e.nodeValue}else for(;t=e[r];r++)n+=o(t);return n},i=st.selectors={cacheLength:50,createPseudo:ot,match:U,find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(et,tt),e[3]=(e[4]||e[5]||"").replace(et,tt),"~="===e[2]&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),"nth"===e[1].slice(0,3)?(e[3]||st.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*("even"===e[3]||"odd"===e[3])),e[5]=+(e[7]+e[8]||"odd"===e[3])):e[3]&&st.error(e[0]),e},PSEUDO:function(e){var t,n=!e[5]&&e[2];return U.CHILD.test(e[0])?null:(e[4]?e[2]=e[4]:n&&z.test(n)&&(t=ft(n,!0))&&(t=n.indexOf(")",n.length-t)-n.length)&&(e[0]=e[0].slice(0,t),e[2]=n.slice(0,t)),e.slice(0,3))}},filter:{TAG:function(e){return"*"===e?function(){return!0}:(e=e.replace(et,tt).toLowerCase(),function(t){return t.nodeName&&t.nodeName.toLowerCase()===e})},CLASS:function(e){var t=k[e+" "];return t||(t=RegExp("(^|"+_+")"+e+"("+_+"|$)"))&&k(e,function(e){return t.test(e.className||typeof e.getAttribute!==A&&e.getAttribute("class")||"")})},ATTR:function(e,t,n){return function(r){var i=st.attr(r,e);return null==i?"!="===t:t?(i+="","="===t?i===n:"!="===t?i!==n:"^="===t?n&&0===i.indexOf(n):"*="===t?n&&i.indexOf(n)>-1:"$="===t?n&&i.slice(-n.length)===n:"~="===t?(" "+i+" ").indexOf(n)>-1:"|="===t?i===n||i.slice(0,n.length+1)===n+"-":!1):!0}},CHILD:function(e,t,n,r,i){var o="nth"!==e.slice(0,3),a="last"!==e.slice(-4),s="of-type"===t;return 1===r&&0===i?function(e){return!!e.parentNode}:function(t,n,u){var l,c,p,f,d,h,g=o!==a?"nextSibling":"previousSibling",m=t.parentNode,y=s&&t.nodeName.toLowerCase(),v=!u&&!s;if(m){if(o){while(g){p=t;while(p=p[g])if(s?p.nodeName.toLowerCase()===y:1===p.nodeType)return!1;h=g="only"===e&&!h&&"nextSibling"}return!0}if(h=[a?m.firstChild:m.lastChild],a&&v){c=m[x]||(m[x]={}),l=c[e]||[],d=l[0]===N&&l[1],f=l[0]===N&&l[2],p=d&&m.childNodes[d];while(p=++d&&p&&p[g]||(f=d=0)||h.pop())if(1===p.nodeType&&++f&&p===t){c[e]=[N,d,f];break}}else if(v&&(l=(t[x]||(t[x]={}))[e])&&l[0]===N)f=l[1];else while(p=++d&&p&&p[g]||(f=d=0)||h.pop())if((s?p.nodeName.toLowerCase()===y:1===p.nodeType)&&++f&&(v&&((p[x]||(p[x]={}))[e]=[N,f]),p===t))break;return f-=i,f===r||0===f%r&&f/r>=0}}},PSEUDO:function(e,t){var n,r=i.pseudos[e]||i.setFilters[e.toLowerCase()]||st.error("unsupported pseudo: "+e);return r[x]?r(t):r.length>1?(n=[e,e,"",t],i.setFilters.hasOwnProperty(e.toLowerCase())?ot(function(e,n){var i,o=r(e,t),a=o.length;while(a--)i=M.call(e,o[a]),e[i]=!(n[i]=o[a])}):function(e){return r(e,0,n)}):r}},pseudos:{not:ot(function(e){var t=[],n=[],r=s(e.replace(W,"$1"));return r[x]?ot(function(e,t,n,i){var o,a=r(e,null,i,[]),s=e.length;while(s--)(o=a[s])&&(e[s]=!(t[s]=o))}):function(e,i,o){return t[0]=e,r(t,null,o,n),!n.pop()}}),has:ot(function(e){return function(t){return st(e,t).length>0}}),contains:ot(function(e){return function(t){return(t.textContent||t.innerText||o(t)).indexOf(e)>-1}}),lang:ot(function(e){return X.test(e||"")||st.error("unsupported lang: "+e),e=e.replace(et,tt).toLowerCase(),function(t){var n;do if(n=d?t.getAttribute("xml:lang")||t.getAttribute("lang"):t.lang)return n=n.toLowerCase(),n===e||0===n.indexOf(e+"-");while((t=t.parentNode)&&1===t.nodeType);return!1}}),target:function(t){var n=e.location&&e.location.hash;return n&&n.slice(1)===t.id},root:function(e){return e===f},focus:function(e){return e===p.activeElement&&(!p.hasFocus||p.hasFocus())&&!!(e.type||e.href||~e.tabIndex)},enabled:function(e){return e.disabled===!1},disabled:function(e){return e.disabled===!0},checked:function(e){var t=e.nodeName.toLowerCase();return"input"===t&&!!e.checked||"option"===t&&!!e.selected},selected:function(e){return e.parentNode&&e.parentNode.selectedIndex,e.selected===!0},empty:function(e){for(e=e.firstChild;e;e=e.nextSibling)if(e.nodeName>"@"||3===e.nodeType||4===e.nodeType)return!1;return!0},parent:function(e){return!i.pseudos.empty(e)},header:function(e){return Q.test(e.nodeName)},input:function(e){return G.test(e.nodeName)},button:function(e){var t=e.nodeName.toLowerCase();return"input"===t&&"button"===e.type||"button"===t},text:function(e){var t;return"input"===e.nodeName.toLowerCase()&&"text"===e.type&&(null==(t=e.getAttribute("type"))||t.toLowerCase()===e.type)},first:pt(function(){return[0]}),last:pt(function(e,t){return[t-1]}),eq:pt(function(e,t,n){return[0>n?n+t:n]}),even:pt(function(e,t){var n=0;for(;t>n;n+=2)e.push(n);return e}),odd:pt(function(e,t){var n=1;for(;t>n;n+=2)e.push(n);return e}),lt:pt(function(e,t,n){var r=0>n?n+t:n;for(;--r>=0;)e.push(r);return e}),gt:pt(function(e,t,n){var r=0>n?n+t:n;for(;t>++r;)e.push(r);return e})}};for(n in{radio:!0,checkbox:!0,file:!0,password:!0,image:!0})i.pseudos[n]=lt(n);for(n in{submit:!0,reset:!0})i.pseudos[n]=ct(n);function ft(e,t){var n,r,o,a,s,u,l,c=E[e+" "];if(c)return t?0:c.slice(0);s=e,u=[],l=i.preFilter;while(s){(!n||(r=$.exec(s)))&&(r&&(s=s.slice(r[0].length)||s),u.push(o=[])),n=!1,(r=I.exec(s))&&(n=r.shift(),o.push({value:n,type:r[0].replace(W," ")}),s=s.slice(n.length));for(a in i.filter)!(r=U[a].exec(s))||l[a]&&!(r=l[a](r))||(n=r.shift(),o.push({value:n,type:a,matches:r}),s=s.slice(n.length));if(!n)break}return t?s.length:s?st.error(e):E(e,u).slice(0)}function dt(e){var t=0,n=e.length,r="";for(;n>t;t++)r+=e[t].value;return r}function ht(e,t,n){var i=t.dir,o=n&&"parentNode"===i,a=C++;return t.first?function(t,n,r){while(t=t[i])if(1===t.nodeType||o)return e(t,n,r)}:function(t,n,s){var u,l,c,p=N+" "+a;if(s){while(t=t[i])if((1===t.nodeType||o)&&e(t,n,s))return!0}else while(t=t[i])if(1===t.nodeType||o)if(c=t[x]||(t[x]={}),(l=c[i])&&l[0]===p){if((u=l[1])===!0||u===r)return u===!0}else if(l=c[i]=[p],l[1]=e(t,n,s)||r,l[1]===!0)return!0}}function gt(e){return e.length>1?function(t,n,r){var i=e.length;while(i--)if(!e[i](t,n,r))return!1;return!0}:e[0]}function mt(e,t,n,r,i){var o,a=[],s=0,u=e.length,l=null!=t;for(;u>s;s++)(o=e[s])&&(!n||n(o,r,i))&&(a.push(o),l&&t.push(s));return a}function yt(e,t,n,r,i,o){return r&&!r[x]&&(r=yt(r)),i&&!i[x]&&(i=yt(i,o)),ot(function(o,a,s,u){var l,c,p,f=[],d=[],h=a.length,g=o||xt(t||"*",s.nodeType?[s]:s,[]),m=!e||!o&&t?g:mt(g,f,e,s,u),y=n?i||(o?e:h||r)?[]:a:m;if(n&&n(m,y,s,u),r){l=mt(y,d),r(l,[],s,u),c=l.length;while(c--)(p=l[c])&&(y[d[c]]=!(m[d[c]]=p))}if(o){if(i||e){if(i){l=[],c=y.length;while(c--)(p=y[c])&&l.push(m[c]=p);i(null,y=[],l,u)}c=y.length;while(c--)(p=y[c])&&(l=i?M.call(o,p):f[c])>-1&&(o[l]=!(a[l]=p))}}else y=mt(y===a?y.splice(h,y.length):y),i?i(null,a,y,u):H.apply(a,y)})}function vt(e){var t,n,r,o=e.length,a=i.relative[e[0].type],s=a||i.relative[" "],u=a?1:0,c=ht(function(e){return e===t},s,!0),p=ht(function(e){return M.call(t,e)>-1},s,!0),f=[function(e,n,r){return!a&&(r||n!==l)||((t=n).nodeType?c(e,n,r):p(e,n,r))}];for(;o>u;u++)if(n=i.relative[e[u].type])f=[ht(gt(f),n)];else{if(n=i.filter[e[u].type].apply(null,e[u].matches),n[x]){for(r=++u;o>r;r++)if(i.relative[e[r].type])break;return yt(u>1&>(f),u>1&&dt(e.slice(0,u-1)).replace(W,"$1"),n,r>u&&vt(e.slice(u,r)),o>r&&vt(e=e.slice(r)),o>r&&dt(e))}f.push(n)}return gt(f)}function bt(e,t){var n=0,o=t.length>0,a=e.length>0,s=function(s,u,c,f,d){var h,g,m,y=[],v=0,b="0",x=s&&[],w=null!=d,T=l,C=s||a&&i.find.TAG("*",d&&u.parentNode||u),k=N+=null==T?1:Math.random()||.1;for(w&&(l=u!==p&&u,r=n);null!=(h=C[b]);b++){if(a&&h){g=0;while(m=e[g++])if(m(h,u,c)){f.push(h);break}w&&(N=k,r=++n)}o&&((h=!m&&h)&&v--,s&&x.push(h))}if(v+=b,o&&b!==v){g=0;while(m=t[g++])m(x,y,u,c);if(s){if(v>0)while(b--)x[b]||y[b]||(y[b]=L.call(f));y=mt(y)}H.apply(f,y),w&&!s&&y.length>0&&v+t.length>1&&st.uniqueSort(f)}return w&&(N=k,l=T),x};return o?ot(s):s}s=st.compile=function(e,t){var n,r=[],i=[],o=S[e+" "];if(!o){t||(t=ft(e)),n=t.length;while(n--)o=vt(t[n]),o[x]?r.push(o):i.push(o);o=S(e,bt(i,r))}return o};function xt(e,t,n){var r=0,i=t.length;for(;i>r;r++)st(e,t[r],n);return n}function wt(e,t,n,r){var o,a,u,l,c,p=ft(e);if(!r&&1===p.length){if(a=p[0]=p[0].slice(0),a.length>2&&"ID"===(u=a[0]).type&&9===t.nodeType&&!d&&i.relative[a[1].type]){if(t=i.find.ID(u.matches[0].replace(et,tt),t)[0],!t)return n;e=e.slice(a.shift().value.length)}o=U.needsContext.test(e)?0:a.length;while(o--){if(u=a[o],i.relative[l=u.type])break;if((c=i.find[l])&&(r=c(u.matches[0].replace(et,tt),V.test(a[0].type)&&t.parentNode||t))){if(a.splice(o,1),e=r.length&&dt(a),!e)return H.apply(n,q.call(r,0)),n;break}}}return s(e,p)(r,t,d,n,V.test(e)),n}i.pseudos.nth=i.pseudos.eq;function Tt(){}i.filters=Tt.prototype=i.pseudos,i.setFilters=new Tt,c(),st.attr=b.attr,b.find=st,b.expr=st.selectors,b.expr[":"]=b.expr.pseudos,b.unique=st.uniqueSort,b.text=st.getText,b.isXMLDoc=st.isXML,b.contains=st.contains}(e);var at=/Until$/,st=/^(?:parents|prev(?:Until|All))/,ut=/^.[^:#\[\.,]*$/,lt=b.expr.match.needsContext,ct={children:!0,contents:!0,next:!0,prev:!0};b.fn.extend({find:function(e){var t,n,r,i=this.length;if("string"!=typeof e)return r=this,this.pushStack(b(e).filter(function(){for(t=0;i>t;t++)if(b.contains(r[t],this))return!0}));for(n=[],t=0;i>t;t++)b.find(e,this[t],n);return n=this.pushStack(i>1?b.unique(n):n),n.selector=(this.selector?this.selector+" ":"")+e,n},has:function(e){var t,n=b(e,this),r=n.length;return this.filter(function(){for(t=0;r>t;t++)if(b.contains(this,n[t]))return!0})},not:function(e){return this.pushStack(ft(this,e,!1))},filter:function(e){return this.pushStack(ft(this,e,!0))},is:function(e){return!!e&&("string"==typeof e?lt.test(e)?b(e,this.context).index(this[0])>=0:b.filter(e,this).length>0:this.filter(e).length>0)},closest:function(e,t){var n,r=0,i=this.length,o=[],a=lt.test(e)||"string"!=typeof e?b(e,t||this.context):0;for(;i>r;r++){n=this[r];while(n&&n.ownerDocument&&n!==t&&11!==n.nodeType){if(a?a.index(n)>-1:b.find.matchesSelector(n,e)){o.push(n);break}n=n.parentNode}}return this.pushStack(o.length>1?b.unique(o):o)},index:function(e){return e?"string"==typeof e?b.inArray(this[0],b(e)):b.inArray(e.jquery?e[0]:e,this):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(e,t){var n="string"==typeof e?b(e,t):b.makeArray(e&&e.nodeType?[e]:e),r=b.merge(this.get(),n);return this.pushStack(b.unique(r))},addBack:function(e){return this.add(null==e?this.prevObject:this.prevObject.filter(e))}}),b.fn.andSelf=b.fn.addBack;function pt(e,t){do e=e[t];while(e&&1!==e.nodeType);return e}b.each({parent:function(e){var t=e.parentNode;return t&&11!==t.nodeType?t:null},parents:function(e){return b.dir(e,"parentNode")},parentsUntil:function(e,t,n){return b.dir(e,"parentNode",n)},next:function(e){return pt(e,"nextSibling")},prev:function(e){return pt(e,"previousSibling")},nextAll:function(e){return b.dir(e,"nextSibling")},prevAll:function(e){return b.dir(e,"previousSibling")},nextUntil:function(e,t,n){return b.dir(e,"nextSibling",n)},prevUntil:function(e,t,n){return b.dir(e,"previousSibling",n)},siblings:function(e){return b.sibling((e.parentNode||{}).firstChild,e)},children:function(e){return b.sibling(e.firstChild)},contents:function(e){return b.nodeName(e,"iframe")?e.contentDocument||e.contentWindow.document:b.merge([],e.childNodes)}},function(e,t){b.fn[e]=function(n,r){var i=b.map(this,t,n);return at.test(e)||(r=n),r&&"string"==typeof r&&(i=b.filter(r,i)),i=this.length>1&&!ct[e]?b.unique(i):i,this.length>1&&st.test(e)&&(i=i.reverse()),this.pushStack(i)}}),b.extend({filter:function(e,t,n){return n&&(e=":not("+e+")"),1===t.length?b.find.matchesSelector(t[0],e)?[t[0]]:[]:b.find.matches(e,t)},dir:function(e,n,r){var i=[],o=e[n];while(o&&9!==o.nodeType&&(r===t||1!==o.nodeType||!b(o).is(r)))1===o.nodeType&&i.push(o),o=o[n];return i},sibling:function(e,t){var n=[];for(;e;e=e.nextSibling)1===e.nodeType&&e!==t&&n.push(e);return n}});function ft(e,t,n){if(t=t||0,b.isFunction(t))return b.grep(e,function(e,r){var i=!!t.call(e,r,e);return i===n});if(t.nodeType)return b.grep(e,function(e){return e===t===n});if("string"==typeof t){var r=b.grep(e,function(e){return 1===e.nodeType});if(ut.test(t))return b.filter(t,r,!n);t=b.filter(t,r)}return b.grep(e,function(e){return b.inArray(e,t)>=0===n})}function dt(e){var t=ht.split("|"),n=e.createDocumentFragment();if(n.createElement)while(t.length)n.createElement(t.pop());return n}var ht="abbr|article|aside|audio|bdi|canvas|data|datalist|details|figcaption|figure|footer|header|hgroup|mark|meter|nav|output|progress|section|summary|time|video",gt=/ jQuery\d+="(?:null|\d+)"/g,mt=RegExp("<(?:"+ht+")[\\s/>]","i"),yt=/^\s+/,vt=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/gi,bt=/<([\w:]+)/,xt=/\s*$/g,At={option:[1,""],legend:[1,"
    ","
    "],area:[1,"",""],param:[1,"",""],thead:[1,"","
    "],tr:[2,"","
    "],col:[2,"","
    "],td:[3,"","
    "],_default:b.support.htmlSerialize?[0,"",""]:[1,"X
    ","
    "]},jt=dt(o),Dt=jt.appendChild(o.createElement("div"));At.optgroup=At.option,At.tbody=At.tfoot=At.colgroup=At.caption=At.thead,At.th=At.td,b.fn.extend({text:function(e){return b.access(this,function(e){return e===t?b.text(this):this.empty().append((this[0]&&this[0].ownerDocument||o).createTextNode(e))},null,e,arguments.length)},wrapAll:function(e){if(b.isFunction(e))return this.each(function(t){b(this).wrapAll(e.call(this,t))});if(this[0]){var t=b(e,this[0].ownerDocument).eq(0).clone(!0);this[0].parentNode&&t.insertBefore(this[0]),t.map(function(){var e=this;while(e.firstChild&&1===e.firstChild.nodeType)e=e.firstChild;return e}).append(this)}return this},wrapInner:function(e){return b.isFunction(e)?this.each(function(t){b(this).wrapInner(e.call(this,t))}):this.each(function(){var t=b(this),n=t.contents();n.length?n.wrapAll(e):t.append(e)})},wrap:function(e){var t=b.isFunction(e);return this.each(function(n){b(this).wrapAll(t?e.call(this,n):e)})},unwrap:function(){return this.parent().each(function(){b.nodeName(this,"body")||b(this).replaceWith(this.childNodes)}).end()},append:function(){return this.domManip(arguments,!0,function(e){(1===this.nodeType||11===this.nodeType||9===this.nodeType)&&this.appendChild(e)})},prepend:function(){return this.domManip(arguments,!0,function(e){(1===this.nodeType||11===this.nodeType||9===this.nodeType)&&this.insertBefore(e,this.firstChild)})},before:function(){return this.domManip(arguments,!1,function(e){this.parentNode&&this.parentNode.insertBefore(e,this)})},after:function(){return this.domManip(arguments,!1,function(e){this.parentNode&&this.parentNode.insertBefore(e,this.nextSibling)})},remove:function(e,t){var n,r=0;for(;null!=(n=this[r]);r++)(!e||b.filter(e,[n]).length>0)&&(t||1!==n.nodeType||b.cleanData(Ot(n)),n.parentNode&&(t&&b.contains(n.ownerDocument,n)&&Mt(Ot(n,"script")),n.parentNode.removeChild(n)));return this},empty:function(){var e,t=0;for(;null!=(e=this[t]);t++){1===e.nodeType&&b.cleanData(Ot(e,!1));while(e.firstChild)e.removeChild(e.firstChild);e.options&&b.nodeName(e,"select")&&(e.options.length=0)}return this},clone:function(e,t){return e=null==e?!1:e,t=null==t?e:t,this.map(function(){return b.clone(this,e,t)})},html:function(e){return b.access(this,function(e){var n=this[0]||{},r=0,i=this.length;if(e===t)return 1===n.nodeType?n.innerHTML.replace(gt,""):t;if(!("string"!=typeof e||Tt.test(e)||!b.support.htmlSerialize&&mt.test(e)||!b.support.leadingWhitespace&&yt.test(e)||At[(bt.exec(e)||["",""])[1].toLowerCase()])){e=e.replace(vt,"<$1>");try{for(;i>r;r++)n=this[r]||{},1===n.nodeType&&(b.cleanData(Ot(n,!1)),n.innerHTML=e);n=0}catch(o){}}n&&this.empty().append(e)},null,e,arguments.length)},replaceWith:function(e){var t=b.isFunction(e);return t||"string"==typeof e||(e=b(e).not(this).detach()),this.domManip([e],!0,function(e){var t=this.nextSibling,n=this.parentNode;n&&(b(this).remove(),n.insertBefore(e,t))})},detach:function(e){return this.remove(e,!0)},domManip:function(e,n,r){e=f.apply([],e);var i,o,a,s,u,l,c=0,p=this.length,d=this,h=p-1,g=e[0],m=b.isFunction(g);if(m||!(1>=p||"string"!=typeof g||b.support.checkClone)&&Ct.test(g))return this.each(function(i){var o=d.eq(i);m&&(e[0]=g.call(this,i,n?o.html():t)),o.domManip(e,n,r)});if(p&&(l=b.buildFragment(e,this[0].ownerDocument,!1,this),i=l.firstChild,1===l.childNodes.length&&(l=i),i)){for(n=n&&b.nodeName(i,"tr"),s=b.map(Ot(l,"script"),Ht),a=s.length;p>c;c++)o=l,c!==h&&(o=b.clone(o,!0,!0),a&&b.merge(s,Ot(o,"script"))),r.call(n&&b.nodeName(this[c],"table")?Lt(this[c],"tbody"):this[c],o,c);if(a)for(u=s[s.length-1].ownerDocument,b.map(s,qt),c=0;a>c;c++)o=s[c],kt.test(o.type||"")&&!b._data(o,"globalEval")&&b.contains(u,o)&&(o.src?b.ajax({url:o.src,type:"GET",dataType:"script",async:!1,global:!1,"throws":!0}):b.globalEval((o.text||o.textContent||o.innerHTML||"").replace(St,"")));l=i=null}return this}});function Lt(e,t){return e.getElementsByTagName(t)[0]||e.appendChild(e.ownerDocument.createElement(t))}function Ht(e){var t=e.getAttributeNode("type");return e.type=(t&&t.specified)+"/"+e.type,e}function qt(e){var t=Et.exec(e.type);return t?e.type=t[1]:e.removeAttribute("type"),e}function Mt(e,t){var n,r=0;for(;null!=(n=e[r]);r++)b._data(n,"globalEval",!t||b._data(t[r],"globalEval"))}function _t(e,t){if(1===t.nodeType&&b.hasData(e)){var n,r,i,o=b._data(e),a=b._data(t,o),s=o.events;if(s){delete a.handle,a.events={};for(n in s)for(r=0,i=s[n].length;i>r;r++)b.event.add(t,n,s[n][r])}a.data&&(a.data=b.extend({},a.data))}}function Ft(e,t){var n,r,i;if(1===t.nodeType){if(n=t.nodeName.toLowerCase(),!b.support.noCloneEvent&&t[b.expando]){i=b._data(t);for(r in i.events)b.removeEvent(t,r,i.handle);t.removeAttribute(b.expando)}"script"===n&&t.text!==e.text?(Ht(t).text=e.text,qt(t)):"object"===n?(t.parentNode&&(t.outerHTML=e.outerHTML),b.support.html5Clone&&e.innerHTML&&!b.trim(t.innerHTML)&&(t.innerHTML=e.innerHTML)):"input"===n&&Nt.test(e.type)?(t.defaultChecked=t.checked=e.checked,t.value!==e.value&&(t.value=e.value)):"option"===n?t.defaultSelected=t.selected=e.defaultSelected:("input"===n||"textarea"===n)&&(t.defaultValue=e.defaultValue)}}b.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(e,t){b.fn[e]=function(e){var n,r=0,i=[],o=b(e),a=o.length-1;for(;a>=r;r++)n=r===a?this:this.clone(!0),b(o[r])[t](n),d.apply(i,n.get());return this.pushStack(i)}});function Ot(e,n){var r,o,a=0,s=typeof e.getElementsByTagName!==i?e.getElementsByTagName(n||"*"):typeof e.querySelectorAll!==i?e.querySelectorAll(n||"*"):t;if(!s)for(s=[],r=e.childNodes||e;null!=(o=r[a]);a++)!n||b.nodeName(o,n)?s.push(o):b.merge(s,Ot(o,n));return n===t||n&&b.nodeName(e,n)?b.merge([e],s):s}function Bt(e){Nt.test(e.type)&&(e.defaultChecked=e.checked)}b.extend({clone:function(e,t,n){var r,i,o,a,s,u=b.contains(e.ownerDocument,e);if(b.support.html5Clone||b.isXMLDoc(e)||!mt.test("<"+e.nodeName+">")?o=e.cloneNode(!0):(Dt.innerHTML=e.outerHTML,Dt.removeChild(o=Dt.firstChild)),!(b.support.noCloneEvent&&b.support.noCloneChecked||1!==e.nodeType&&11!==e.nodeType||b.isXMLDoc(e)))for(r=Ot(o),s=Ot(e),a=0;null!=(i=s[a]);++a)r[a]&&Ft(i,r[a]);if(t)if(n)for(s=s||Ot(e),r=r||Ot(o),a=0;null!=(i=s[a]);a++)_t(i,r[a]);else _t(e,o);return r=Ot(o,"script"),r.length>0&&Mt(r,!u&&Ot(e,"script")),r=s=i=null,o},buildFragment:function(e,t,n,r){var i,o,a,s,u,l,c,p=e.length,f=dt(t),d=[],h=0;for(;p>h;h++)if(o=e[h],o||0===o)if("object"===b.type(o))b.merge(d,o.nodeType?[o]:o);else if(wt.test(o)){s=s||f.appendChild(t.createElement("div")),u=(bt.exec(o)||["",""])[1].toLowerCase(),c=At[u]||At._default,s.innerHTML=c[1]+o.replace(vt,"<$1>")+c[2],i=c[0];while(i--)s=s.lastChild;if(!b.support.leadingWhitespace&&yt.test(o)&&d.push(t.createTextNode(yt.exec(o)[0])),!b.support.tbody){o="table"!==u||xt.test(o)?""!==c[1]||xt.test(o)?0:s:s.firstChild,i=o&&o.childNodes.length;while(i--)b.nodeName(l=o.childNodes[i],"tbody")&&!l.childNodes.length&&o.removeChild(l) +}b.merge(d,s.childNodes),s.textContent="";while(s.firstChild)s.removeChild(s.firstChild);s=f.lastChild}else d.push(t.createTextNode(o));s&&f.removeChild(s),b.support.appendChecked||b.grep(Ot(d,"input"),Bt),h=0;while(o=d[h++])if((!r||-1===b.inArray(o,r))&&(a=b.contains(o.ownerDocument,o),s=Ot(f.appendChild(o),"script"),a&&Mt(s),n)){i=0;while(o=s[i++])kt.test(o.type||"")&&n.push(o)}return s=null,f},cleanData:function(e,t){var n,r,o,a,s=0,u=b.expando,l=b.cache,p=b.support.deleteExpando,f=b.event.special;for(;null!=(n=e[s]);s++)if((t||b.acceptData(n))&&(o=n[u],a=o&&l[o])){if(a.events)for(r in a.events)f[r]?b.event.remove(n,r):b.removeEvent(n,r,a.handle);l[o]&&(delete l[o],p?delete n[u]:typeof n.removeAttribute!==i?n.removeAttribute(u):n[u]=null,c.push(o))}}});var Pt,Rt,Wt,$t=/alpha\([^)]*\)/i,It=/opacity\s*=\s*([^)]*)/,zt=/^(top|right|bottom|left)$/,Xt=/^(none|table(?!-c[ea]).+)/,Ut=/^margin/,Vt=RegExp("^("+x+")(.*)$","i"),Yt=RegExp("^("+x+")(?!px)[a-z%]+$","i"),Jt=RegExp("^([+-])=("+x+")","i"),Gt={BODY:"block"},Qt={position:"absolute",visibility:"hidden",display:"block"},Kt={letterSpacing:0,fontWeight:400},Zt=["Top","Right","Bottom","Left"],en=["Webkit","O","Moz","ms"];function tn(e,t){if(t in e)return t;var n=t.charAt(0).toUpperCase()+t.slice(1),r=t,i=en.length;while(i--)if(t=en[i]+n,t in e)return t;return r}function nn(e,t){return e=t||e,"none"===b.css(e,"display")||!b.contains(e.ownerDocument,e)}function rn(e,t){var n,r,i,o=[],a=0,s=e.length;for(;s>a;a++)r=e[a],r.style&&(o[a]=b._data(r,"olddisplay"),n=r.style.display,t?(o[a]||"none"!==n||(r.style.display=""),""===r.style.display&&nn(r)&&(o[a]=b._data(r,"olddisplay",un(r.nodeName)))):o[a]||(i=nn(r),(n&&"none"!==n||!i)&&b._data(r,"olddisplay",i?n:b.css(r,"display"))));for(a=0;s>a;a++)r=e[a],r.style&&(t&&"none"!==r.style.display&&""!==r.style.display||(r.style.display=t?o[a]||"":"none"));return e}b.fn.extend({css:function(e,n){return b.access(this,function(e,n,r){var i,o,a={},s=0;if(b.isArray(n)){for(o=Rt(e),i=n.length;i>s;s++)a[n[s]]=b.css(e,n[s],!1,o);return a}return r!==t?b.style(e,n,r):b.css(e,n)},e,n,arguments.length>1)},show:function(){return rn(this,!0)},hide:function(){return rn(this)},toggle:function(e){var t="boolean"==typeof e;return this.each(function(){(t?e:nn(this))?b(this).show():b(this).hide()})}}),b.extend({cssHooks:{opacity:{get:function(e,t){if(t){var n=Wt(e,"opacity");return""===n?"1":n}}}},cssNumber:{columnCount:!0,fillOpacity:!0,fontWeight:!0,lineHeight:!0,opacity:!0,orphans:!0,widows:!0,zIndex:!0,zoom:!0},cssProps:{"float":b.support.cssFloat?"cssFloat":"styleFloat"},style:function(e,n,r,i){if(e&&3!==e.nodeType&&8!==e.nodeType&&e.style){var o,a,s,u=b.camelCase(n),l=e.style;if(n=b.cssProps[u]||(b.cssProps[u]=tn(l,u)),s=b.cssHooks[n]||b.cssHooks[u],r===t)return s&&"get"in s&&(o=s.get(e,!1,i))!==t?o:l[n];if(a=typeof r,"string"===a&&(o=Jt.exec(r))&&(r=(o[1]+1)*o[2]+parseFloat(b.css(e,n)),a="number"),!(null==r||"number"===a&&isNaN(r)||("number"!==a||b.cssNumber[u]||(r+="px"),b.support.clearCloneStyle||""!==r||0!==n.indexOf("background")||(l[n]="inherit"),s&&"set"in s&&(r=s.set(e,r,i))===t)))try{l[n]=r}catch(c){}}},css:function(e,n,r,i){var o,a,s,u=b.camelCase(n);return n=b.cssProps[u]||(b.cssProps[u]=tn(e.style,u)),s=b.cssHooks[n]||b.cssHooks[u],s&&"get"in s&&(a=s.get(e,!0,r)),a===t&&(a=Wt(e,n,i)),"normal"===a&&n in Kt&&(a=Kt[n]),""===r||r?(o=parseFloat(a),r===!0||b.isNumeric(o)?o||0:a):a},swap:function(e,t,n,r){var i,o,a={};for(o in t)a[o]=e.style[o],e.style[o]=t[o];i=n.apply(e,r||[]);for(o in t)e.style[o]=a[o];return i}}),e.getComputedStyle?(Rt=function(t){return e.getComputedStyle(t,null)},Wt=function(e,n,r){var i,o,a,s=r||Rt(e),u=s?s.getPropertyValue(n)||s[n]:t,l=e.style;return s&&(""!==u||b.contains(e.ownerDocument,e)||(u=b.style(e,n)),Yt.test(u)&&Ut.test(n)&&(i=l.width,o=l.minWidth,a=l.maxWidth,l.minWidth=l.maxWidth=l.width=u,u=s.width,l.width=i,l.minWidth=o,l.maxWidth=a)),u}):o.documentElement.currentStyle&&(Rt=function(e){return e.currentStyle},Wt=function(e,n,r){var i,o,a,s=r||Rt(e),u=s?s[n]:t,l=e.style;return null==u&&l&&l[n]&&(u=l[n]),Yt.test(u)&&!zt.test(n)&&(i=l.left,o=e.runtimeStyle,a=o&&o.left,a&&(o.left=e.currentStyle.left),l.left="fontSize"===n?"1em":u,u=l.pixelLeft+"px",l.left=i,a&&(o.left=a)),""===u?"auto":u});function on(e,t,n){var r=Vt.exec(t);return r?Math.max(0,r[1]-(n||0))+(r[2]||"px"):t}function an(e,t,n,r,i){var o=n===(r?"border":"content")?4:"width"===t?1:0,a=0;for(;4>o;o+=2)"margin"===n&&(a+=b.css(e,n+Zt[o],!0,i)),r?("content"===n&&(a-=b.css(e,"padding"+Zt[o],!0,i)),"margin"!==n&&(a-=b.css(e,"border"+Zt[o]+"Width",!0,i))):(a+=b.css(e,"padding"+Zt[o],!0,i),"padding"!==n&&(a+=b.css(e,"border"+Zt[o]+"Width",!0,i)));return a}function sn(e,t,n){var r=!0,i="width"===t?e.offsetWidth:e.offsetHeight,o=Rt(e),a=b.support.boxSizing&&"border-box"===b.css(e,"boxSizing",!1,o);if(0>=i||null==i){if(i=Wt(e,t,o),(0>i||null==i)&&(i=e.style[t]),Yt.test(i))return i;r=a&&(b.support.boxSizingReliable||i===e.style[t]),i=parseFloat(i)||0}return i+an(e,t,n||(a?"border":"content"),r,o)+"px"}function un(e){var t=o,n=Gt[e];return n||(n=ln(e,t),"none"!==n&&n||(Pt=(Pt||b("