├── .github └── workflows │ ├── build-and-publish-image.yml │ ├── lint-codeql.yml │ ├── lint-docker.yml │ ├── lint-go.yml │ ├── lint-ts.yml │ ├── test-e2e.yml │ ├── test-go.yml │ └── test-ts.yml ├── .gitignore ├── .golangci.yaml ├── Dockerfile ├── LICENSE ├── README.md ├── assets ├── css │ ├── bootstrap.min.css │ ├── gokoala.css │ ├── swagger-ui-pdok.css │ └── swagger-ui.min.css ├── favicon.ico ├── i18n │ ├── en.yaml │ └── nl.yaml ├── img │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ ├── logo-footer.png │ ├── logo-header.svg │ └── logo-opengraph.png └── js │ ├── swagger-ui-bundle.js │ └── swagger-ui-standalone-preset.js ├── cmd ├── main.go └── main_test.go ├── config ├── README.md ├── collections.go ├── config.go ├── config_test.go ├── duration.go ├── duration_test.go ├── language.go ├── language_test.go ├── lowercase_id.go ├── mediatype.go ├── mediatype_test.go ├── ogcapi_3dgeovolumes.go ├── ogcapi_features.go ├── ogcapi_processes.go ├── ogcapi_styles.go ├── ogcapi_tiles.go ├── url.go ├── url_test.go └── zz_generated.deepcopy.go ├── docs └── gopher-koala.png ├── examples ├── README.md ├── config_3d.yaml ├── config_all.yaml ├── config_features_azure.yaml ├── config_features_local.yaml ├── config_vectortiles.yaml ├── docker-compose-features-azure.yaml └── resources │ ├── 3d.png │ ├── addresses-cloudbacked-gpkg │ ├── .gitattributes │ ├── 2D3C6C305DC57881A64984EB34ABD75A.bcv │ ├── 40EE500D382D45156F565C7642343DC2.bcv │ ├── 9CB36603436610FC3EA30DF696DE6E53.bcv │ ├── DC8B8FCFB25550D9E39E7F863856CE42.bcv │ ├── F6A73F9621F18A7E7E68E24E4F377E4E.bcv │ ├── bcv_kv.bcv │ └── manifest.bcv │ ├── addresses-crs84.gpkg │ ├── addresses-etrs89.gpkg │ ├── addresses-rd.gpkg │ ├── bgt.png │ ├── dummy-style.json │ └── old.png ├── go.mod ├── go.sum ├── hack ├── build-controller-gen.sh ├── build-local-viewer.sh ├── crd │ ├── Dockerfile │ ├── README.md │ ├── go.mod │ ├── go.sum │ └── gokoala_types.go ├── generate-crd.sh ├── generate-deepcopy.sh └── setup-jetbrains-gotemplates.sh ├── internal ├── README.md ├── engine │ ├── contentnegotiation.go │ ├── contentnegotiation_test.go │ ├── downloader.go │ ├── downloader_test.go │ ├── engine.go │ ├── engine_test.go │ ├── health.go │ ├── i18n.go │ ├── openapi.go │ ├── openapi_test.go │ ├── problems.go │ ├── resources.go │ ├── resources_test.go │ ├── router.go │ ├── router_test.go │ ├── sitemap.go │ ├── template.go │ ├── template_test.go │ ├── templatefuncs.go │ ├── templatefuncs_test.go │ ├── templates │ │ ├── layout.go.html │ │ ├── openapi │ │ │ ├── 3dgeovolumes.go.json │ │ │ ├── README.md │ │ │ ├── common-collections.go.json │ │ │ ├── common.go.json │ │ │ ├── features.go.json │ │ │ ├── preamble.go.json │ │ │ ├── problems.go.json │ │ │ ├── styles.go.json │ │ │ └── tiles.go.json │ │ ├── robots.go.txt │ │ └── sitemap.go.xml │ ├── testdata │ │ ├── config_collections_order_alphabetic.yaml │ │ ├── config_collections_order_alphabetic_titles.yaml │ │ ├── config_collections_order_literal.yaml │ │ ├── config_collections_unique.yaml │ │ ├── config_invalid.yaml │ │ ├── config_invalid_collection_ids.yaml │ │ ├── config_invalid_tiles_projection.yaml │ │ ├── config_minimal.yaml │ │ ├── config_multiple_ogc_apis_single_collection.yaml │ │ ├── config_processes.yaml │ │ ├── config_resources_dir.yaml │ │ ├── config_valid_collection_ids.yaml │ │ ├── expected_conformance.html │ │ ├── expected_conformance.json │ │ ├── expected_dataset_collection.json │ │ ├── expected_dataset_landingpage.json │ │ ├── expected_multiple_ogc_apis_single_collection.html │ │ ├── expected_multiple_ogc_apis_single_collection.json │ │ ├── expected_multiple_ogc_apis_single_collection_json_ld.html │ │ ├── expected_processes_conformance.html │ │ ├── expected_sitemap.xml │ │ ├── ogcapi-features-1.resolved.json │ │ ├── ogcapi-merged.json │ │ ├── ogcapi-tiles-1.bundled.json │ │ ├── ogcapi-tiles-1.modified.json │ │ ├── readfile-gzipped.txt.gz │ │ ├── readfile-plain.txt │ │ └── test-resources-dir │ │ │ ├── foo.txt │ │ │ └── thumbnail.jpg │ └── util │ │ ├── filereader.go │ │ ├── filereader_test.go │ │ ├── json.go │ │ ├── json_test.go │ │ └── maps.go └── ogc │ ├── README.md │ ├── common │ ├── core │ │ ├── main.go │ │ ├── main_test.go │ │ └── templates │ │ │ ├── api.go.html │ │ │ ├── conformance.go.html │ │ │ ├── conformance.go.json │ │ │ ├── landing-page.go.html │ │ │ └── landing-page.go.json │ └── geospatial │ │ ├── README.md │ │ ├── main.go │ │ ├── main_test.go │ │ └── templates │ │ ├── collection.go.html │ │ ├── collection.go.json │ │ ├── collections.go.html │ │ └── collections.go.json │ ├── features │ ├── datasources │ │ ├── datasource.go │ │ ├── geopackage │ │ │ ├── asserts.go │ │ │ ├── backend_cloud.go │ │ │ ├── backend_cloud_darwin.go │ │ │ ├── backend_cloud_windows.go │ │ │ ├── backend_local.go │ │ │ ├── encoding │ │ │ │ ├── geopackage.go │ │ │ │ └── geopackage_test.go │ │ │ ├── geopackage.go │ │ │ ├── geopackage_test.go │ │ │ ├── metadata.go │ │ │ ├── stmtcache.go │ │ │ ├── stmtcache_test.go │ │ │ ├── testdata │ │ │ │ ├── 3d-geoms.gpkg │ │ │ │ ├── bag-temporal.gpkg │ │ │ │ ├── bag.gpkg │ │ │ │ ├── external-fid.gpkg │ │ │ │ ├── null-empty-geoms.gpkg │ │ │ │ └── roads.gpkg │ │ │ └── warmup.go │ │ ├── postgis │ │ │ ├── postgis.go │ │ │ └── postgis_test.go │ │ ├── sqllog.go │ │ └── sqllog_test.go │ ├── domain │ │ ├── cursor.go │ │ ├── cursor_test.go │ │ ├── geojson.go │ │ ├── jsonfg.go │ │ ├── mapper.go │ │ ├── mapper_test.go │ │ ├── profile.go │ │ ├── profile_test.go │ │ ├── props.go │ │ ├── props_test.go │ │ ├── schema.go │ │ ├── schema_test.go │ │ ├── spatialref.go │ │ └── spatialref_test.go │ ├── feature.go │ ├── feature_test.go │ ├── features.go │ ├── features_bench_test.go │ ├── features_test.go │ ├── html.go │ ├── json.go │ ├── main.go │ ├── openapi.go │ ├── openapi_test.go │ ├── schema.go │ ├── schema_test.go │ ├── templates │ │ ├── feature.go.html │ │ ├── features.go.html │ │ ├── schema.go.html │ │ └── schema.go.json │ ├── testdata │ │ ├── config_benchmark.yaml │ │ ├── config_features_3d_geoms.yaml │ │ ├── config_features_bag.yaml │ │ ├── config_features_bag_allowed_values.yaml │ │ ├── config_features_bag_invalid_filters.yaml │ │ ├── config_features_bag_long_description.yaml │ │ ├── config_features_bag_multiple_feature_tables.yaml │ │ ├── config_features_bag_temporal.yaml │ │ ├── config_features_external_fid.yaml │ │ ├── config_features_geom_null_empty.yaml │ │ ├── config_features_multiple_collection_single_table.yaml │ │ ├── config_features_multiple_gpkgs.yaml │ │ ├── config_features_multiple_gpkgs_multiple_levels.yaml │ │ ├── config_features_properties_exclude.yaml │ │ ├── config_features_properties_order.yaml │ │ ├── config_features_properties_order_exclude.yaml │ │ ├── config_features_roads.yaml │ │ ├── config_features_short_query_timeout.yaml │ │ ├── config_features_validation_disabled.yaml │ │ ├── config_features_webconfig.yaml │ │ ├── config_mapsheets.yaml │ │ ├── expected_bar_collection_snippet.html │ │ ├── expected_empty_feature_collection.json │ │ ├── expected_feature_4030.html │ │ ├── expected_feature_4030.json │ │ ├── expected_feature_4030_jsonfg.json │ │ ├── expected_feature_404.json │ │ ├── expected_feature_geom_empty_point.json │ │ ├── expected_feature_geom_empty_point_jsonfg.json │ │ ├── expected_feature_geom_null.json │ │ ├── expected_feature_geom_null_jsonfg.json │ │ ├── expected_feature_webconfig_snippet.html │ │ ├── expected_features_3d_geoms.json │ │ ├── expected_features_3d_geoms_jsonfg.json │ │ ├── expected_features_3d_geoms_multipoint.json │ │ ├── expected_features_3d_geoms_multipoint_jsonfg.json │ │ ├── expected_features_properties_exclude.json │ │ ├── expected_features_properties_order.json │ │ ├── expected_features_properties_order_exclude.json │ │ ├── expected_features_roads.json │ │ ├── expected_features_roads_jsonfg.json │ │ ├── expected_features_webconfig_snippet.html │ │ ├── expected_features_with_rel_as_link.json │ │ ├── expected_features_with_rel_as_link_snippet.html │ │ ├── expected_foo_collection.json │ │ ├── expected_foo_collection_snippet.html │ │ ├── expected_foo_collection_with_cursor.json │ │ ├── expected_foo_collection_with_limit.json │ │ ├── expected_mapsheets.html │ │ ├── expected_mapsheets.json │ │ ├── expected_mapsheets_jsonfg.json │ │ ├── expected_multiple_feature_tables_single_geopackage.json │ │ ├── expected_multiple_gpkgs_bbox_explicit_wgs84.json │ │ ├── expected_multiple_gpkgs_bbox_rd.json │ │ ├── expected_multiple_gpkgs_bbox_rd_jsonfg.json │ │ ├── expected_multiple_gpkgs_bbox_rd_output_also_rd.json │ │ ├── expected_multiple_gpkgs_bbox_rd_output_also_rd_jsonfg.json │ │ ├── expected_multiple_gpkgs_bbox_wgs84.json │ │ ├── expected_multiple_gpkgs_bbox_wgs84_jsonfg.json │ │ ├── expected_multiple_gpkgs_bbox_wgs84_output_rd.json │ │ ├── expected_multiple_gpkgs_feature_b29c12b1_rd.json │ │ ├── expected_multiple_gpkgs_feature_b29c12b1_wgs84.json │ │ ├── expected_multiple_gpkgs_rd.json │ │ ├── expected_multiple_gpkgs_wgs84.json │ │ ├── expected_query_timeout_feature.json │ │ ├── expected_query_timeout_features.json │ │ ├── expected_schema.json │ │ ├── expected_schema_3d_geoms.json │ │ ├── expected_schema_external_fid.json │ │ ├── expected_schema_external_fid_snippet.html │ │ ├── expected_schema_snippet.html │ │ ├── expected_schema_temporal.json │ │ ├── expected_schema_temporal_snippet.html │ │ ├── expected_straatnaam_and_postcode.json │ │ ├── expected_straatnaam_not_allowed_value.json │ │ ├── expected_straatnaam_silodam.html │ │ ├── expected_straatnaam_silodam.json │ │ ├── expected_temporal.json │ │ ├── expected_type_ligplaats.json │ │ └── expected_webconfig_hyperlink_snippet.html │ ├── url.go │ └── url_test.go │ ├── geovolumes │ ├── main.go │ ├── main_test.go │ └── testdata │ │ ├── config_dtm.yaml │ │ └── config_minimal_3d.yaml │ ├── processes │ └── main.go │ ├── setup.go │ ├── styles │ ├── main.go │ ├── main_test.go │ ├── templates │ │ ├── style.go.html │ │ ├── styleMetadata.go.html │ │ ├── styleMetadata.go.json │ │ ├── styles.go.html │ │ └── styles.go.json │ └── testdata │ │ ├── config_legend.yaml │ │ ├── config_minimal_styles.yaml │ │ └── resources │ │ ├── alternative.json │ │ ├── default.json │ │ └── legend.png │ └── tiles │ ├── main.go │ ├── main_test.go │ ├── templates │ ├── tileMatrixSets.go.html │ ├── tileMatrixSets.go.json │ ├── tileMatrixSets │ │ ├── EuropeanETRS89_LAEAQuad.go.html │ │ ├── EuropeanETRS89_LAEAQuad.go.json │ │ ├── NetherlandsRDNewQuad.go.html │ │ ├── NetherlandsRDNewQuad.go.json │ │ ├── WebMercatorQuad.go.html │ │ └── WebMercatorQuad.go.json │ ├── tiles.go.html │ ├── tiles.go.json │ └── tiles │ │ ├── EuropeanETRS89_LAEAQuad.go.html │ │ ├── EuropeanETRS89_LAEAQuad.go.json │ │ ├── EuropeanETRS89_LAEAQuad.go.tilejson │ │ ├── NetherlandsRDNewQuad.go.html │ │ ├── NetherlandsRDNewQuad.go.json │ │ ├── NetherlandsRDNewQuad.go.tilejson │ │ ├── WebMercatorQuad.go.html │ │ ├── WebMercatorQuad.go.json │ │ └── WebMercatorQuad.go.tilejson │ ├── testdata │ ├── config_tiles_collectionlevel.yaml │ ├── config_tiles_toplevel.yaml │ ├── config_tiles_toplevel_and_collectionlevel.yaml │ ├── config_tiles_urltemplate.yaml │ ├── expected_collection_level_tiles.json │ └── expected_top_level_tiles.json │ └── tileMatrixSetLimits │ ├── EuropeanETRS89_LAEAQuad.yaml │ ├── NetherlandsRDNewQuad.yaml │ └── WebMercatorQuad.yaml ├── tests ├── .gitignore ├── README.md ├── cypress.config.ts ├── cypress │ ├── e2e │ │ ├── common.cy.ts │ │ ├── features.cy.ts │ │ ├── styles.cy.ts │ │ └── tiles.cy.ts │ ├── fixtures │ │ └── .keep │ ├── support │ │ ├── commands.ts │ │ └── e2e.ts │ └── tsconfig.json ├── package-lock.json └── package.json └── viewer ├── .editorconfig ├── .eslintrc.json ├── .gitignore ├── .prettierignore ├── .prettierrc.json ├── README.md ├── angular.json ├── cypress.config.ts ├── cypress ├── README.md ├── feature-view-test-auto-mode.cy.ts ├── feature-view-test.cy.ts ├── fixtures │ ├── 172300.png │ ├── amsterdam-epgs28992.json │ ├── amsterdam-epgs3035.json │ ├── amsterdam-epsg4258.json │ ├── amsterdam-wgs84.json │ ├── amsterdam.json │ ├── backgroundstub.png │ ├── collectionresponse.json │ ├── grid-amsterdam-epgs28992.json │ ├── grid-amsterdam-epgs3035.json │ ├── grid-amsterdam-epsg4258.json │ ├── grid-amsterdam-wgs84.json │ ├── pdokwegdelen.json │ ├── teststyle-complex.json │ ├── teststyle-filter.json │ ├── teststyle-fonts.json │ ├── teststyle-id.json │ ├── teststyle.json │ └── wegdelen-epsg3035.json ├── legend-view-test.cy.ts ├── shared.ts ├── support │ ├── commands.ts │ ├── component-index.html │ ├── component.ts │ ├── e2e.ts │ └── index.d.ts ├── tsconfig.json └── vectortile-view-test.cy.ts ├── examples ├── legend.html └── tile-samples.html ├── package-lock.json ├── package.json ├── src ├── app │ ├── app.module.ts │ ├── feature-view │ │ ├── boxcontrol.ts │ │ ├── feature-view.component.css │ │ ├── feature-view.component.html │ │ ├── feature-view.component.ts │ │ └── fullboxcontrol.ts │ ├── feature.service.ts │ ├── global-error-handler.service.ts │ ├── legend-view │ │ ├── legend-item │ │ │ ├── legend-item.component.css │ │ │ ├── legend-item.component.html │ │ │ └── legend-item.component.ts │ │ ├── legend-view.component.css │ │ ├── legend-view.component.html │ │ └── legend-view.component.ts │ ├── link.ts │ ├── map-projection.ts │ ├── mapbox-style.service.ts │ ├── matrix-set.service.ts │ ├── object-info │ │ ├── object-info.component.css │ │ ├── object-info.component.html │ │ └── object-info.component.ts │ └── vectortile-view │ │ ├── vectortile-view.component.css │ │ ├── vectortile-view.component.html │ │ └── vectortile-view.component.ts ├── assets │ └── .gitkeep ├── environments │ ├── environment.development.ts │ └── environment.ts ├── favicon.ico ├── index.html ├── main.ts └── styles.css ├── tsconfig.app.json ├── tsconfig.json └── tsconfig.spec.json /.github/workflows/lint-docker.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: lint (docker) 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | jobs: 9 | lint: 10 | name: lint 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v4 14 | with: 15 | sparse-checkout: | 16 | Dockerfile 17 | sparse-checkout-cone-mode: false 18 | 19 | - uses: hadolint/hadolint-action@v3.1.0 20 | with: 21 | dockerfile: Dockerfile 22 | -------------------------------------------------------------------------------- /.github/workflows/lint-ts.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: lint (ts) 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | 9 | defaults: 10 | run: 11 | working-directory: ./viewer 12 | 13 | jobs: 14 | lint-ts: 15 | runs-on: ubuntu-latest 16 | 17 | strategy: 18 | matrix: 19 | node-version: [20.x] 20 | # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ 21 | 22 | steps: 23 | - uses: actions/checkout@v4 24 | - name: Use Node.js ${{ matrix.node-version }} 25 | uses: actions/setup-node@v3 26 | with: 27 | node-version: ${{ matrix.node-version }} 28 | cache: "npm" 29 | cache-dependency-path: "./viewer/package-lock.json" 30 | 31 | - name: Install 32 | run: npm ci 33 | 34 | - name: Formatting 35 | run: npm run format 36 | 37 | - name: Linting 38 | run: npm run lint 39 | -------------------------------------------------------------------------------- /.github/workflows/test-ts.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: test (ts) 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | 9 | defaults: 10 | run: 11 | working-directory: ./viewer 12 | jobs: 13 | cypress-ts: 14 | runs-on: ubuntu-latest 15 | 16 | strategy: 17 | matrix: 18 | node-version: [20.x] 19 | 20 | steps: 21 | - uses: actions/checkout@v4 22 | 23 | - name: Use Node.js ${{ matrix.node-version }} 24 | uses: actions/setup-node@v3 25 | with: 26 | node-version: ${{ matrix.node-version }} 27 | cache: "npm" 28 | cache-dependency-path: "./viewer/package-lock.json" 29 | 30 | - name: Install 31 | run: npm ci 32 | 33 | - name: Cypress run 34 | uses: cypress-io/github-action@v6 35 | with: 36 | component: true 37 | working-directory: ./viewer 38 | browser: chrome 39 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | *.iml 3 | .vscode/ 4 | gokoala 5 | assets/view-component/ 6 | viewer/.nx 7 | viewer/cypress/screenshots 8 | viewer/.angular 9 | viewer/dist 10 | viewer/node_modules 11 | viewer/cypress.env.json 12 | hack/tmp 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Publieke Dienstverlening op de Kaart 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /assets/css/gokoala.css: -------------------------------------------------------------------------------- 1 | @charset "UTF-8"; 2 | 3 | /* see https://getbootstrap.com/docs/5.2/customize/css-variables/#root-variables */ 4 | :root { 5 | --bs-blue: #1a1e4f; 6 | --bs-blue-30: #676b9c; /* --bs-blue, 30% lighter */ 7 | --lightblue: #add8e6; 8 | --bs-body-color: var(--bs-blue); 9 | } 10 | 11 | /* navbar */ 12 | .navbar { 13 | background-color: var(--bs-blue); 14 | } 15 | .navbar .navbar-outputs { 16 | --bs-breadcrumb-divider: '|' 17 | } 18 | .navbar .breadcrumb .breadcrumb-item, 19 | .navbar .breadcrumb .breadcrumb-item a { 20 | color: var(--bs-white); 21 | } 22 | .navbar .breadcrumb .breadcrumb-item a:hover { 23 | color: var(--lightblue); 24 | } 25 | 26 | /* cards */ 27 | .card-header { 28 | color: var(--bs-white); 29 | background-color: var(--bs-blue); 30 | } 31 | .card-header a { 32 | color: var(--bs-white); 33 | } 34 | .card-header a:hover { 35 | color: var(--lightblue); 36 | } 37 | 38 | hgroup { 39 | margin-bottom: 0.5rem; 40 | } 41 | 42 | footer { 43 | background-color: var(--bs-blue); 44 | } 45 | 46 | /* tables */ 47 | td { 48 | overflow-wrap: break-word; 49 | word-wrap: break-word; 50 | word-break: break-all; 51 | } 52 | .table-sm td p { 53 | margin: 0; 54 | padding: 0; 55 | } 56 | .table-sm>:not(caption)>*>* { 57 | padding: 0.10rem; 58 | } 59 | .table-striped>thead>tr>th { 60 | color: var(--bs-white); 61 | background-color: var(--bs-blue); 62 | } 63 | .table-striped>thead>tr>th a { 64 | color: var(--bs-white); 65 | } 66 | .table-striped>thead>tr>th a:hover { 67 | color: var(--lightblue); 68 | } 69 | 70 | .btn-primary { 71 | --bs-btn-bg: var(--bs-blue); 72 | --bs-btn-color: var(--bs-white); 73 | --bs-btn-border-color: var(--bs-blue); 74 | --bs-btn-hover-bg: var(--bs-blue-30); 75 | --bs-btn-hover-border-color: var(--bs-blue-30); 76 | } 77 | 78 | .vectortile-view { 79 | flex-grow: 1; 80 | } 81 | -------------------------------------------------------------------------------- /assets/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PDOK/gokoala/7e31a0320491af419fb89300d02e154ce5e597f8/assets/favicon.ico -------------------------------------------------------------------------------- /assets/img/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PDOK/gokoala/7e31a0320491af419fb89300d02e154ce5e597f8/assets/img/favicon-16x16.png -------------------------------------------------------------------------------- /assets/img/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PDOK/gokoala/7e31a0320491af419fb89300d02e154ce5e597f8/assets/img/favicon-32x32.png -------------------------------------------------------------------------------- /assets/img/logo-footer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PDOK/gokoala/7e31a0320491af419fb89300d02e154ce5e597f8/assets/img/logo-footer.png -------------------------------------------------------------------------------- /assets/img/logo-opengraph.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PDOK/gokoala/7e31a0320491af419fb89300d02e154ce5e597f8/assets/img/logo-opengraph.png -------------------------------------------------------------------------------- /config/README.md: -------------------------------------------------------------------------------- 1 | # Config 2 | 3 | This config package is used to validate and unmarshal a GoKoala YAML config file to Go structs. 4 | 5 | In addition, this package is imported as a _library_ in the PDOK [OGCAPI operator](https://github.com/PDOK/ogcapi-operator) 6 | to validate the `OGCAPI` Custom Resource (CR) in order to orchestrate GoKoala in Kubernetes. 7 | For this reason the structs in the package are annotated with [Kubebuilder markers](https://book.kubebuilder.io/reference/markers). -------------------------------------------------------------------------------- /config/duration.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "encoding/json" 5 | "time" 6 | 7 | "gopkg.in/yaml.v3" 8 | ) 9 | 10 | // Duration Custom time.Duration compatible with YAML and JSON (un)marshalling and kubebuilder. 11 | // (Already supported in yaml/v3 but not encoding/json.) 12 | // 13 | // +kubebuilder:validation:Type=string 14 | // +kubebuilder:validation:Format=duration 15 | type Duration struct { 16 | time.Duration 17 | } 18 | 19 | // MarshalJSON turn duration tag into JSON 20 | // Value instead of pointer receiver because only that way it can be used for both. 21 | func (d Duration) MarshalJSON() ([]byte, error) { 22 | return json.Marshal(d.Duration.String()) 23 | } 24 | 25 | func (d *Duration) UnmarshalJSON(b []byte) error { 26 | return yaml.Unmarshal(b, &d.Duration) 27 | } 28 | 29 | // MarshalYAML turn duration tag into YAML 30 | // Value instead of pointer receiver because only that way it can be used for both. 31 | func (d Duration) MarshalYAML() (interface{}, error) { 32 | return d.Duration, nil 33 | } 34 | 35 | func (d *Duration) UnmarshalYAML(unmarshal func(any) error) error { 36 | return unmarshal(&d.Duration) 37 | } 38 | 39 | // DeepCopyInto copy the receiver, write into out. in must be non-nil. 40 | func (d *Duration) DeepCopyInto(out *Duration) { 41 | if out != nil { 42 | *out = *d 43 | } 44 | } 45 | 46 | // DeepCopy copy the receiver, create a new Duration. 47 | func (d *Duration) DeepCopy() *Duration { 48 | if d == nil { 49 | return nil 50 | } 51 | out := &Duration{} 52 | d.DeepCopyInto(out) 53 | return out 54 | } 55 | -------------------------------------------------------------------------------- /config/language.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | "golang.org/x/text/language" 7 | ) 8 | 9 | // Language represents a BCP 47 language tag. 10 | // +kubebuilder:validation:Type=string 11 | type Language struct { 12 | language.Tag 13 | } 14 | 15 | // MarshalJSON turn language tag into JSON 16 | // Value instead of pointer receiver because only that way it can be used for both. 17 | func (l Language) MarshalJSON() ([]byte, error) { 18 | return json.Marshal(l.Tag.String()) 19 | } 20 | 21 | // UnmarshalJSON turn JSON into Language 22 | func (l *Language) UnmarshalJSON(b []byte) error { 23 | var s string 24 | if err := json.Unmarshal(b, &s); err != nil { 25 | return err 26 | } 27 | *l = Language{language.Make(s)} 28 | return nil 29 | } 30 | 31 | // DeepCopyInto copy the receiver, write into out. in must be non-nil. 32 | func (l *Language) DeepCopyInto(out *Language) { 33 | *out = *l 34 | } 35 | 36 | // DeepCopy copy the receiver, create a new Language. 37 | func (l *Language) DeepCopy() *Language { 38 | if l == nil { 39 | return nil 40 | } 41 | out := &Language{} 42 | l.DeepCopyInto(out) 43 | return out 44 | } 45 | -------------------------------------------------------------------------------- /config/lowercase_id.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "log" 5 | "regexp" 6 | 7 | "github.com/go-playground/validator/v10" 8 | ) 9 | 10 | var ( 11 | lowercaseIDRegexp = regexp.MustCompile("^[a-z0-9\"]([a-z0-9_-]*[a-z0-9\"]+|)$") 12 | ) 13 | 14 | const ( 15 | lowercaseID = "lowercase_id" 16 | ) 17 | 18 | // LowercaseID is the validation function for validating if the current field 19 | // is not empty and contains only lowercase chars, numbers, hyphens or underscores. 20 | // It's similar to RFC 1035 DNS label but not the same. 21 | func LowercaseID(fl validator.FieldLevel) bool { 22 | valAsString := fl.Field().String() 23 | valid := lowercaseIDRegexp.MatchString(valAsString) 24 | if !valid { 25 | log.Printf("Invalid ID %s", valAsString) 26 | } 27 | return valid 28 | } 29 | -------------------------------------------------------------------------------- /config/mediatype.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | "github.com/elnormous/contenttype" 7 | ) 8 | 9 | // MediaType represents a IANA media type as described in RFC 6838. Media types were formerly known as MIME types. 10 | // +kubebuilder:validation:Type=string 11 | type MediaType struct { 12 | contenttype.MediaType 13 | } 14 | 15 | // MarshalJSON turn MediaType into JSON 16 | // Value instead of pointer receiver because only that way it can be used for both. 17 | func (m MediaType) MarshalJSON() ([]byte, error) { 18 | return json.Marshal(m.String()) 19 | } 20 | 21 | // UnmarshalJSON turn JSON into MediaType 22 | func (m *MediaType) UnmarshalJSON(b []byte) error { 23 | var s string 24 | if err := json.Unmarshal(b, &s); err != nil { 25 | return err 26 | } 27 | mt, err := contenttype.ParseMediaType(s) 28 | if err != nil { 29 | return err 30 | } 31 | m.MediaType = mt 32 | return nil 33 | } 34 | 35 | // MarshalYAML turns MediaType into YAML. 36 | // Value instead of pointer receiver because only that way it can be used for both. 37 | func (m MediaType) MarshalYAML() (interface{}, error) { 38 | return m.MediaType.String(), nil 39 | } 40 | 41 | // UnmarshalYAML parses a string to MediaType 42 | func (m *MediaType) UnmarshalYAML(unmarshal func(any) error) error { 43 | var s string 44 | if err := unmarshal(&s); err != nil { 45 | return err 46 | } 47 | mt, err := contenttype.ParseMediaType(s) 48 | if err != nil { 49 | return err 50 | } 51 | m.MediaType = mt 52 | return nil 53 | } 54 | 55 | // DeepCopyInto copy the receiver, write into out. in must be non-nil. 56 | func (m *MediaType) DeepCopyInto(out *MediaType) { 57 | *out = *m 58 | } 59 | 60 | // DeepCopy copy the receiver, create a new MediaType. 61 | func (m *MediaType) DeepCopy() *MediaType { 62 | if m == nil { 63 | return nil 64 | } 65 | out := &MediaType{} 66 | m.DeepCopyInto(out) 67 | return out 68 | } 69 | -------------------------------------------------------------------------------- /config/ogcapi_processes.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | // +kubebuilder:object:generate=true 4 | type OgcAPIProcesses struct { 5 | // Enable to advertise dismiss operations on the conformance page 6 | SupportsDismiss bool `yaml:"supportsDismiss" json:"supportsDismiss"` 7 | 8 | // Enable to advertise callback operations on the conformance page 9 | SupportsCallback bool `yaml:"supportsCallback" json:"supportsCallback"` 10 | 11 | // Reference to an external service implementing the process API. GoKoala acts only as a proxy for OGC API Processes. 12 | ProcessesServer URL `yaml:"processesServer" json:"processesServer" validate:"required"` 13 | } 14 | -------------------------------------------------------------------------------- /docs/gopher-koala.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PDOK/gokoala/7e31a0320491af419fb89300d02e154ce5e597f8/docs/gopher-koala.png -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | # Examples 2 | 3 | Checkout the examples below to see how GoKoala works. 4 | 5 | ## OGC API Tiles example 6 | 7 | This example uses vector tiles from the [PDOK BGT dataset](https://www.pdok.nl/introductie/-/article/basisregistratie-grootschalige-topografie-bgt-) (a small subset, just for demo purposes). 8 | 9 | - Start GoKoala as specified in the root [README](../README.md#run) 10 | and provide `config_vectortiles.yaml` as the config file. 11 | - Open http://localhost:8080 to explore the landing page 12 | - Call http://localhost:8080/tiles/NetherlandsRDNewQuad/12/2235/2031.pbf to download a specific tile 13 | 14 | ## OGC API Features example 15 | 16 | There are 2 examples configurations: 17 | - `config_features_local.yaml` - use local addresses geopackages in WGS84, RD and ETRS89 projections. 18 | - `config_features_azure.yaml` - use addresses geopackage (just one in WGS84) hosted in Azure Blob as a [Cloud-Backed SQLite/Geopackage](https://sqlite.org/cloudsqlite/doc/trunk/www/index.wiki). 19 | 20 | For the local version just start GoKoala as specified in the root [README](../README.md#run) 21 | and provide the mentioned config file. 22 | 23 | For the Azure example we use a local Azurite emulator which contains the cloud-backed `addresses.gpkg`: 24 | - Run `docker-compose -f docker-compose-features-azure.yaml up` 25 | - Open http://localhost:8080 to explore the landing page 26 | - Call http://localhost:8080/collections/dutch-addresses/items and notice in the Azurite log that features are streamed from blob storage 27 | 28 | ## OGC API 3D GeoVolumes example 29 | 30 | This example uses 3D tiles of New York. 31 | 32 | - Start GoKoala as specified in the root [README](../README.md#run) 33 | and provide `config_3d.yaml` as the config file. 34 | - Open http://localhost:8080 to explore the landing page 35 | - Call http://localhost:8080/collections/newyork/3dtiles/6/0/1.b3dm to download a specific 3D tile 36 | 37 | ## OGC API All/Complete example 38 | 39 | This example demonstrates multiple OGC APIs (tiles, styles, features, geovolumes) in a single API. 40 | 41 | - Start GoKoala as specified in the root [README](../README.md#run) 42 | and provide `config_all.yaml` as the config file. -------------------------------------------------------------------------------- /examples/config_3d.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | version: 1.0.0 3 | title: Example 3D 4 | # shortened title, used in breadcrumb path 5 | serviceIdentifier: 3D 6 | abstract: >- 7 | This is a description about the dataset in Markdown. 8 | # just a dummy picture, but you can put an actual thumbnail here 9 | thumbnail: 3d.png 10 | resources: 11 | directory: ./examples/resources 12 | license: 13 | name: CC0 1.0 14 | url: https://creativecommons.org/publicdomain/zero/1.0/deed.nl 15 | support: 16 | name: Example Support 17 | url: https://support.example.com 18 | lastUpdated: "2023-06-01T12:00:00Z" 19 | datasetCatalogUrl: https://www.pdok.nl/datasets 20 | baseUrl: http://localhost:8080 21 | availableLanguages: 22 | - nl 23 | - en 24 | ogcApi: 25 | 3dgeovolumes: 26 | tileServer: https://api.pdok.nl/kadaster/3d-basisvoorziening/ogc/v1/collections 27 | collections: 28 | - id: gebouwen 29 | uriTemplate3dTiles: "t/{level}/{x}/{y}.glb" 30 | -------------------------------------------------------------------------------- /examples/docker-compose-features-azure.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | services: 3 | azurite: 4 | image: mcr.microsoft.com/azure-storage/azurite:3.29.0 5 | container_name: "azurite" 6 | hostname: azurite 7 | command: "azurite-blob --blobHost 0.0.0.0 --blobPort 10000" 8 | ports: 9 | - "10000:10000" 10 | healthcheck: 11 | test: nc 127.0.0.1 10000 -z 12 | interval: 1s 13 | retries: 30 14 | 15 | azurite-seed: 16 | image: rclone/rclone:1.65 17 | depends_on: 18 | azurite: 19 | condition: service_healthy 20 | volumes: 21 | - ./:/examples 22 | environment: 23 | - RCLONE_CONFIG_BLOBS_TYPE=azureblob 24 | - RCLONE_CONFIG_BLOBS_ENDPOINT=http://azurite:10000/devstoreaccount1 25 | - RCLONE_CONFIG_BLOBS_USE_EMULATOR=true 26 | entrypoint: 27 | - sh 28 | - -c 29 | - | 30 | echo "create azure container" 31 | rclone mkdir blobs:example 32 | echo "upload cloud-backed sqlite/geopackage files (pre-created with blockcachevfsd CLI)" 33 | rclone copy /examples/resources/addresses-cloudbacked-gpkg/ blobs:example 34 | touch /tmp/finished 35 | echo "done" 36 | sleep 300 # because docker-compose --exit-code-from implies --abort-on-container-exit 37 | healthcheck: 38 | test: stat /tmp/finished 39 | interval: 1s 40 | retries: 30 41 | 42 | gokoala: 43 | build: 44 | context: ../ 45 | dockerfile: Dockerfile 46 | depends_on: 47 | azurite-seed: 48 | condition: service_healthy 49 | command: "--config-file ./examples/config_features_azure.yaml" 50 | volumes: 51 | - ./:/examples 52 | ports: 53 | - "8080:8080" 54 | healthcheck: 55 | test: /bin/curl --fail http://127.0.0.1:8080 || exit 1 56 | interval: 1s 57 | retries: 30 58 | 59 | smoketest: 60 | image: ghcr.io/osgeo/gdal:ubuntu-small-3.8.3 61 | depends_on: 62 | gokoala: 63 | condition: service_healthy 64 | entrypoint: 65 | - sh 66 | - -c 67 | - | 68 | set -e 69 | echo "test OGC API" 70 | ogrinfo -so OAPIF:http://gokoala:8080 dutch-addresses 71 | -------------------------------------------------------------------------------- /examples/resources/3d.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PDOK/gokoala/7e31a0320491af419fb89300d02e154ce5e597f8/examples/resources/3d.png -------------------------------------------------------------------------------- /examples/resources/addresses-cloudbacked-gpkg/.gitattributes: -------------------------------------------------------------------------------- 1 | *.bcv binary 2 | -------------------------------------------------------------------------------- /examples/resources/addresses-cloudbacked-gpkg/2D3C6C305DC57881A64984EB34ABD75A.bcv: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PDOK/gokoala/7e31a0320491af419fb89300d02e154ce5e597f8/examples/resources/addresses-cloudbacked-gpkg/2D3C6C305DC57881A64984EB34ABD75A.bcv -------------------------------------------------------------------------------- /examples/resources/addresses-cloudbacked-gpkg/40EE500D382D45156F565C7642343DC2.bcv: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PDOK/gokoala/7e31a0320491af419fb89300d02e154ce5e597f8/examples/resources/addresses-cloudbacked-gpkg/40EE500D382D45156F565C7642343DC2.bcv -------------------------------------------------------------------------------- /examples/resources/addresses-cloudbacked-gpkg/9CB36603436610FC3EA30DF696DE6E53.bcv: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PDOK/gokoala/7e31a0320491af419fb89300d02e154ce5e597f8/examples/resources/addresses-cloudbacked-gpkg/9CB36603436610FC3EA30DF696DE6E53.bcv -------------------------------------------------------------------------------- /examples/resources/addresses-cloudbacked-gpkg/DC8B8FCFB25550D9E39E7F863856CE42.bcv: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PDOK/gokoala/7e31a0320491af419fb89300d02e154ce5e597f8/examples/resources/addresses-cloudbacked-gpkg/DC8B8FCFB25550D9E39E7F863856CE42.bcv -------------------------------------------------------------------------------- /examples/resources/addresses-cloudbacked-gpkg/F6A73F9621F18A7E7E68E24E4F377E4E.bcv: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PDOK/gokoala/7e31a0320491af419fb89300d02e154ce5e597f8/examples/resources/addresses-cloudbacked-gpkg/F6A73F9621F18A7E7E68E24E4F377E4E.bcv -------------------------------------------------------------------------------- /examples/resources/addresses-cloudbacked-gpkg/bcv_kv.bcv: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PDOK/gokoala/7e31a0320491af419fb89300d02e154ce5e597f8/examples/resources/addresses-cloudbacked-gpkg/bcv_kv.bcv -------------------------------------------------------------------------------- /examples/resources/addresses-cloudbacked-gpkg/manifest.bcv: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PDOK/gokoala/7e31a0320491af419fb89300d02e154ce5e597f8/examples/resources/addresses-cloudbacked-gpkg/manifest.bcv -------------------------------------------------------------------------------- /examples/resources/addresses-crs84.gpkg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PDOK/gokoala/7e31a0320491af419fb89300d02e154ce5e597f8/examples/resources/addresses-crs84.gpkg -------------------------------------------------------------------------------- /examples/resources/addresses-etrs89.gpkg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PDOK/gokoala/7e31a0320491af419fb89300d02e154ce5e597f8/examples/resources/addresses-etrs89.gpkg -------------------------------------------------------------------------------- /examples/resources/addresses-rd.gpkg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PDOK/gokoala/7e31a0320491af419fb89300d02e154ce5e597f8/examples/resources/addresses-rd.gpkg -------------------------------------------------------------------------------- /examples/resources/bgt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PDOK/gokoala/7e31a0320491af419fb89300d02e154ce5e597f8/examples/resources/bgt.png -------------------------------------------------------------------------------- /examples/resources/dummy-style.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 8, 3 | "name": "Dummy Mapbox Style, just for example purposes", 4 | "id": "dummy_style", 5 | "pitch": 50, 6 | "center": [ 7 | 0, 8 | 0 9 | ], 10 | "layers": [ 11 | { 12 | "filter": [ 13 | "all", 14 | [ 15 | "==", 16 | "status", 17 | "Example" 18 | ] 19 | ], 20 | "id": "example", 21 | "type": "line", 22 | "paint": { 23 | "line-color": "rgb(170, 170, 170)", 24 | "line-width": 2 25 | }, 26 | "source": "example", 27 | "source-layer": "example" 28 | } 29 | ], 30 | "sources": { 31 | "bag": { 32 | "type": "vector", 33 | "tiles": [ 34 | "{{ .Config.BaseURL }}/tiles/{{ .Params.Projection }}/{z}/{y}/{x}?f=mvt" 35 | ], 36 | "minzoom": {{ .Params.ZoomLevelRange.Start }}, 37 | "maxzoom": {{ .Params.ZoomLevelRange.End }} 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /examples/resources/old.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PDOK/gokoala/7e31a0320491af419fb89300d02e154ce5e597f8/examples/resources/old.png -------------------------------------------------------------------------------- /hack/build-controller-gen.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -o errexit 4 | set -o nounset 5 | set -o pipefail 6 | 7 | # Note we build from PR https://github.com/kubernetes-sigs/controller-tools/pull/892 until its merged to master 8 | git clone https://github.com/kubernetes-sigs/controller-tools.git ct && \ 9 | cd ct && \ 10 | git fetch origin pull/892/head:pull892 && \ 11 | git checkout pull892 && \ 12 | cd cmd/controller-gen && \ 13 | go build -o /bin/controller-gen . && \ 14 | /bin/controller-gen --help -------------------------------------------------------------------------------- /hack/build-local-viewer.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -o errexit 4 | set -o nounset 5 | set -o pipefail 6 | 7 | GOKOALA_ROOT=$(dirname "${BASH_SOURCE[0]}")/.. 8 | cd "${GOKOALA_ROOT}" 9 | 10 | echo "npm install" 11 | npm install --force --prefix viewer 12 | 13 | echo "angular build" 14 | npm run build --prefix viewer 15 | 16 | echo "place viewer in assets" 17 | cp -r viewer/dist/view-component/browser assets/view-component 18 | 19 | echo "done, now start GoKoala (go run main.go) and checkout the vectortile viewer" 20 | -------------------------------------------------------------------------------- /hack/crd/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM docker.io/golang:1.24-bookworm AS build-env 2 | ADD hack/build-controller-gen.sh /build-controller-gen.sh 3 | RUN /build-controller-gen.sh 4 | 5 | FROM docker.io/golang:1.24-bookworm 6 | COPY --from=build-env /bin/controller-gen /bin/controller-gen 7 | ENTRYPOINT ["/bin/controller-gen"] -------------------------------------------------------------------------------- /hack/crd/README.md: -------------------------------------------------------------------------------- 1 | The `gokoala_types.go` file contains an example spec which we use to verify whether we can 2 | successfully generate a CRD (Custom Resource Definition) of GoKoala. The latter can be 3 | used in a Kubernetes controller/operator. -------------------------------------------------------------------------------- /hack/crd/go.mod: -------------------------------------------------------------------------------- 1 | module crd 2 | 3 | go 1.24.2 4 | 5 | require ( 6 | github.com/PDOK/gokoala v0.0.0 7 | k8s.io/apimachinery v0.32.3 8 | ) 9 | 10 | require ( 11 | dario.cat/mergo v1.0.1 // indirect 12 | github.com/bahlo/generic-list-go v0.2.0 // indirect 13 | github.com/buger/jsonparser v1.1.1 // indirect 14 | github.com/creasty/defaults v1.8.0 // indirect 15 | github.com/docker/go-units v0.5.0 // indirect 16 | github.com/elnormous/contenttype v1.0.4 // indirect 17 | github.com/fxamacker/cbor/v2 v2.8.0 // indirect 18 | github.com/gabriel-vasile/mimetype v1.4.9 // indirect 19 | github.com/go-logr/logr v1.4.2 // indirect 20 | github.com/go-playground/locales v0.14.1 // indirect 21 | github.com/go-playground/universal-translator v0.18.1 // indirect 22 | github.com/go-playground/validator/v10 v10.26.0 // indirect 23 | github.com/gogo/protobuf v1.3.2 // indirect 24 | github.com/google/gofuzz v1.2.0 // indirect 25 | github.com/json-iterator/go v1.1.12 // indirect 26 | github.com/leodido/go-urn v1.4.0 // indirect 27 | github.com/mailru/easyjson v0.9.0 // indirect 28 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 29 | github.com/modern-go/reflect2 v1.0.2 // indirect 30 | github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect 31 | github.com/x448/float16 v0.8.4 // indirect 32 | golang.org/x/crypto v0.37.0 // indirect 33 | golang.org/x/net v0.39.0 // indirect 34 | golang.org/x/sys v0.32.0 // indirect 35 | golang.org/x/text v0.24.0 // indirect 36 | gopkg.in/inf.v0 v0.9.1 // indirect 37 | gopkg.in/yaml.v3 v3.0.1 // indirect 38 | k8s.io/klog/v2 v2.130.1 // indirect 39 | k8s.io/utils v0.0.0-20250321185631-1f6e0b77f77e // indirect 40 | sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect 41 | sigs.k8s.io/structured-merge-diff/v4 v4.7.0 // indirect 42 | sigs.k8s.io/yaml v1.4.0 // indirect 43 | ) 44 | 45 | replace github.com/PDOK/gokoala v0.0.0 => ../../ 46 | -------------------------------------------------------------------------------- /hack/crd/gokoala_types.go: -------------------------------------------------------------------------------- 1 | // +groupName=pdok 2 | package crd 3 | 4 | import ( 5 | "github.com/PDOK/gokoala/config" 6 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 7 | ) 8 | 9 | type GoKoalaSpec struct { 10 | Service config.Config `json:"service"` 11 | } 12 | 13 | type GoKoala struct { 14 | metav1.TypeMeta `json:",inline"` 15 | metav1.ObjectMeta `json:"metadata,omitempty"` 16 | 17 | Spec GoKoalaSpec `json:"spec,omitempty"` 18 | } 19 | -------------------------------------------------------------------------------- /hack/generate-crd.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -o errexit 4 | set -o nounset 5 | set -o pipefail 6 | 7 | GOKOALA_ROOT=$(dirname "${BASH_SOURCE[0]}")/.. 8 | cd "${GOKOALA_ROOT}" 9 | 10 | if ! command -v controller-gen &> /dev/null 11 | then 12 | echo "controller-gen not found, using Docker container instead" 13 | # Build controller-tools 14 | docker build -f hack/crd/Dockerfile -t gokoala-controller-tools . 15 | 16 | # Run against GoKoala 17 | docker run -v `pwd`/:/gokoala gokoala-controller-tools crd paths="/gokoala/hack/crd/..." output:dir="/gokoala/hack/tmp" 18 | else 19 | echo "controller-gen found, using this local install instead of Docker container" 20 | 21 | # Run against GoKoala config 22 | controller-gen crd paths="$(pwd)/hack/crd/..." output:dir="$(pwd)/hack/tmp" 23 | fi 24 | 25 | # Assertions 26 | cat hack/tmp/pdok_gokoalas.yaml 27 | cat hack/tmp/pdok_gokoalas.yaml | grep "kind: GoKoala" 28 | 29 | -------------------------------------------------------------------------------- /hack/generate-deepcopy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -o errexit 4 | set -o nounset 5 | set -o pipefail 6 | 7 | GOKOALA_ROOT=$(dirname "${BASH_SOURCE[0]}")/.. 8 | cd "${GOKOALA_ROOT}" 9 | 10 | if ! command -v controller-gen &> /dev/null 11 | then 12 | echo "controller-gen not found, using Docker container instead" 13 | # Build controller-tools 14 | docker build -f hack/crd/Dockerfile -t gokoala-controller-tools . 15 | 16 | # Run against GoKoala config 17 | docker run -v `pwd`/:/gokoala gokoala-controller-tools object paths="/gokoala/config/..." output:dir="/gokoala/config/" 18 | else 19 | echo "controller-gen found, using this local install instead of Docker container" 20 | 21 | # Run against GoKoala config 22 | controller-gen object paths="$(pwd)/config/..." output:dir="$(pwd)/config/" 23 | fi 24 | -------------------------------------------------------------------------------- /hack/setup-jetbrains-gotemplates.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -o errexit 4 | set -o nounset 5 | set -o pipefail 6 | 7 | GOKOALA_ROOT=$(dirname "${BASH_SOURCE[0]}")/.. 8 | cd "${GOKOALA_ROOT}" 9 | 10 | traverse_templates() { 11 | # HTML templates 12 | find * -type f -iname "*.go.html" -print0 | while IFS= read -r -d '' file; do 13 | echo "Processing file: $file" 14 | echo "" >> ".idea/templateLanguages.xml" 15 | done 16 | # JSON templates 17 | find * -type f -iname "*.go.json" -print0 | while IFS= read -r -d '' file; do 18 | echo "Processing file: $file" 19 | echo "" >> ".idea/templateLanguages.xml" 20 | done 21 | # TileJSON templates 22 | find * -type f -iname "*.go.tilejson" -print0 | while IFS= read -r -d '' file; do 23 | echo "Processing file: $file" 24 | echo "" >> ".idea/templateLanguages.xml" 25 | done 26 | # XML templates 27 | find * -type f -iname "*.go.xml" -print0 | while IFS= read -r -d '' file; do 28 | echo "Processing file: $file" 29 | echo "" >> ".idea/templateLanguages.xml" 30 | done 31 | } 32 | 33 | mkdir -p ".idea/" 34 | 35 | cat << EOF > ".idea/templateLanguages.xml" 36 | 37 | 38 | 39 | EOF 40 | 41 | traverse_templates 42 | 43 | cat << EOF >> ".idea/templateLanguages.xml" 44 | 45 | 46 | EOF 47 | -------------------------------------------------------------------------------- /internal/README.md: -------------------------------------------------------------------------------- 1 | - The `ogc` [package](ogc/README.md) contains logic per specific OGC API 2 | building block. 3 | - The `engine` package should contain general logic. `ogc` may reference 4 | `engine`. **The other way around is not allowed!** -------------------------------------------------------------------------------- /internal/engine/health.go: -------------------------------------------------------------------------------- 1 | package engine 2 | 3 | import ( 4 | "log" 5 | "net/http" 6 | "net/url" 7 | "time" 8 | ) 9 | 10 | func newHealthEndpoint(e *Engine) { 11 | var target *url.URL 12 | if tilesConfig := e.Config.OgcAPI.Tiles; tilesConfig != nil { 13 | var err error 14 | switch { 15 | case tilesConfig.DatasetTiles != nil && *tilesConfig.DatasetTiles.HealthCheck.Enabled: 16 | target, err = url.Parse(tilesConfig.DatasetTiles.TileServer.String() + *tilesConfig.DatasetTiles.HealthCheck.TilePath) 17 | case len(tilesConfig.Collections) > 0 && tilesConfig.Collections[0].Tiles != nil && 18 | *tilesConfig.Collections[0].Tiles.GeoDataTiles.HealthCheck.Enabled: 19 | target, err = url.Parse(tilesConfig.Collections[0].Tiles.GeoDataTiles.TileServer.String() + *tilesConfig.Collections[0].Tiles.GeoDataTiles.HealthCheck.TilePath) 20 | default: 21 | log.Println("cannot determine health check tilepath or tiles health check is disabled, falling back to basic check") 22 | } 23 | if err != nil { 24 | log.Fatalf("invalid health check tilepath: %v", err) 25 | } 26 | } 27 | if target != nil { 28 | client := &http.Client{Timeout: time.Duration(500) * time.Millisecond} 29 | e.Router.Get("/health", func(w http.ResponseWriter, _ *http.Request) { 30 | resp, err := client.Head(target.String()) 31 | if err != nil { 32 | // the exact error is irrelevant for health monitoring, but log it for insight 33 | log.Printf("healthcheck failed: %v", err) 34 | w.WriteHeader(http.StatusNotFound) 35 | } else { 36 | w.WriteHeader(resp.StatusCode) 37 | resp.Body.Close() 38 | } 39 | }) 40 | } else { 41 | e.Router.Get("/health", func(w http.ResponseWriter, _ *http.Request) { 42 | SafeWrite(w.Write, []byte("OK")) 43 | }) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /internal/engine/i18n.go: -------------------------------------------------------------------------------- 1 | package engine 2 | 3 | import ( 4 | "github.com/PDOK/gokoala/config" 5 | "github.com/nicksnyder/go-i18n/v2/i18n" 6 | "golang.org/x/text/language" 7 | "gopkg.in/yaml.v3" 8 | ) 9 | 10 | func newLocalizers(availableLanguages []config.Language) map[language.Tag]i18n.Localizer { 11 | localizers := make(map[language.Tag]i18n.Localizer) 12 | // add localizer for each available language 13 | for _, lang := range availableLanguages { 14 | bundle := i18n.NewBundle(lang.Tag) 15 | bundle.RegisterUnmarshalFunc("yaml", yaml.Unmarshal) 16 | bundle.MustLoadMessageFile("assets/i18n/" + lang.String() + ".yaml") 17 | localizers[lang.Tag] = *i18n.NewLocalizer(bundle, lang.String()) 18 | } 19 | return localizers 20 | } 21 | -------------------------------------------------------------------------------- /internal/engine/resources.go: -------------------------------------------------------------------------------- 1 | package engine 2 | 3 | import ( 4 | "log" 5 | "net/http" 6 | "net/url" 7 | 8 | "github.com/go-chi/chi/v5" 9 | ) 10 | 11 | // Resources endpoint to serve static assets, either from local storage or through reverse proxy 12 | func newResourcesEndpoint(e *Engine) { 13 | res := e.Config.Resources 14 | if res == nil { 15 | return 16 | } 17 | var resourcesHandler http.Handler 18 | if res.Directory != nil && *res.Directory != "" { 19 | resourcesPath := *res.Directory 20 | resourcesHandler = http.StripPrefix("/resources", http.FileServer(http.Dir(resourcesPath))) 21 | } else if res.URL != nil && res.URL.String() != "" { 22 | resourcesHandler = proxy(e.ReverseProxy, res.URL.String()) 23 | } 24 | e.Router.Handle("/resources/*", resourcesHandler) 25 | } 26 | 27 | type revProxy func(w http.ResponseWriter, r *http.Request, target *url.URL, prefer204 bool, overwrite string) 28 | 29 | func proxy(reverseProxy revProxy, resourcesURL string) http.HandlerFunc { 30 | return func(w http.ResponseWriter, r *http.Request) { 31 | resourcePath, _ := url.JoinPath("/", chi.URLParam(r, "*")) 32 | target, err := url.ParseRequestURI(resourcesURL + resourcePath) 33 | if err != nil { 34 | log.Printf("invalid target url, can't proxy resources: %v", err) 35 | RenderProblem(ProblemServerError, w) 36 | return 37 | } 38 | reverseProxy(w, r, target, false, "") 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /internal/engine/sitemap.go: -------------------------------------------------------------------------------- 1 | package engine 2 | 3 | import "net/http" 4 | 5 | func newSitemap(e *Engine) { 6 | for path, template := range map[string]string{"/sitemap.xml": "sitemap.go.xml", "/robots.txt": "robots.go.txt"} { 7 | key := NewTemplateKey(templatesDir + template) 8 | e.renderTemplates(path, nil, nil, false, key) 9 | e.Router.Get(path, func(w http.ResponseWriter, r *http.Request) { 10 | e.Serve(w, r, ServeTemplate(key), ServeValidation(false, false)) 11 | }) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /internal/engine/templates/openapi/README.md: -------------------------------------------------------------------------------- 1 | # OGC OpenAPI specs 2 | 3 | We ship OpenAPI specs for the OGC endpoints that are supported out of the box by GoKoala. We strive to fully conform to 4 | the OGC specs but some endpoints or features aren't supported and therefore removed from the default OGC OpenAPI files. 5 | This is also the intent of the OGC: _"An implementation should only include the paths that are implemented and remove 6 | the references to the rest."_ source: `OGC API Tiles 1.0 spec`. 7 | 8 | The OpenAPI files/templates in this directory are merged into one spec by GoKoala. In addition, it's possible to provide 9 | GoKoala with a custom OpenAPI spec (using a CLI flag) and overwrite any defaults or specify additional endpoints. 10 | 11 | ## Sources 12 | 13 | While the OpenAPI specs/templates are modified to match the capabilities of GoKoala, it might be useful to know their origins: 14 | 15 | - OGC Common Core (Part 1): `common.go.json` is based on [common-1.0](https://developer.ogc.org/api/common/openapi.yaml) 16 | - OGC Common Core (Part 2): `common-collections.go.json` is based on [common-part-2-draft](https://developer.ogc.org/api/common/openapi2.yaml) 17 | - OGC Tiles: `tiles.go.json` is based on [ogcapi-tiles-1](https://schemas.opengis.net/ogcapi/tiles/part1/1.0/openapi/ogcapi-tiles-1.bundled.json) 18 | - 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) and [ogcapi-features-2](https://schemas.opengis.net/ogcapi/features/part2/1.0/openapi/ogcapi-features-2.yaml). Part 5 was added manually based on HTML spec. 19 | - OGC 3D GeoVolumes: `3dgeovolumes.go.json` is based on [ogcapi-3d-geovolumes-draft-0.0.2](https://raw.githubusercontent.com/opengeospatial/ogcapi-3d-geovolumes/main/standard/openapi/ogcapi-3d-geovolumes-draft-0.0.2.yaml) and [cologne_lod2](https://demo.ldproxy.net/cologne_lod2/api/?f=json) 20 | - OGC Styles: `styles.go.json` is based on [ogcapi-styles-1](https://developer.ogc.org/api/styles/openapi.yaml) 21 | 22 | Note: See the Git history of this file for more details. We stopped documenting every change to the specs 23 | since there were just too many and the benefit was lost. -------------------------------------------------------------------------------- /internal/engine/templates/openapi/preamble.go.json: -------------------------------------------------------------------------------- 1 | {{- /*gotype: github.com/PDOK/gokoala/internal/engine.TemplateData*/ -}} 2 | { 3 | "openapi": "3.0.0", 4 | "info": { 5 | "title": "{{ .Config.Title }}", 6 | {{/* Swagger supports markdown in the description, but only a limited set and it can 7 | messes up the JSON. So lets disable that for now*/}} 8 | "description": "{{ unmarkdown .Config.Abstract }}", 9 | "version": "{{ .Config.Version }}", 10 | {{- if .Config.Support -}} 11 | "contact": { 12 | "name": "{{ .Config.Support.Name }}", 13 | {{ if .Config.Support.Email }} 14 | "email": "{{ .Config.Support.Email }}", 15 | {{ end }} 16 | "url": "{{ .Config.Support.URL }}" 17 | }, 18 | {{- end -}} 19 | "termsOfService": "", 20 | "license": { 21 | "name": "{{ .Config.License.Name | default "onbekend" }}", 22 | "url": "{{ .Config.License.URL | default "onbekend" }}" 23 | } 24 | }, 25 | "servers": [ 26 | { 27 | "description": "API endpoint", 28 | "url": "{{ .Config.BaseURL }}" 29 | } 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /internal/engine/templates/openapi/problems.go.json: -------------------------------------------------------------------------------- 1 | {{ define "problems" }} 2 | "400": { 3 | "description": "Bad request: For example, invalid or unknown query parameters.", 4 | "content": { 5 | "application/problem+json": { 6 | "schema": { 7 | "$ref": "#/components/schemas/exception" 8 | } 9 | } 10 | } 11 | }, 12 | "404": { 13 | "description": "Not found: The requested resource does not exist on the server. For example, a path parameter had an incorrect value.", 14 | "content": { 15 | "application/problem+json": { 16 | "schema": { 17 | "$ref": "#/components/schemas/exception" 18 | } 19 | } 20 | } 21 | }, 22 | "406": { 23 | "description": "Not acceptable: The requested media type is not supported by this resource.", 24 | "content": { 25 | "application/problem+json": { 26 | "schema": { 27 | "$ref": "#/components/schemas/exception" 28 | } 29 | } 30 | } 31 | }, 32 | "500": { 33 | "description": "Internal server error: An unexpected server error occurred.", 34 | "content": { 35 | "application/problem+json": { 36 | "schema": { 37 | "$ref": "#/components/schemas/exception" 38 | } 39 | } 40 | } 41 | }, 42 | "502": { 43 | "description": "Bad Gateway: An unexpected error occurred while forwarding/proxying the request to another server.", 44 | "content": { 45 | "application/problem+json": { 46 | "schema": { 47 | "$ref": "#/components/schemas/exception" 48 | } 49 | } 50 | } 51 | } 52 | {{ end }} -------------------------------------------------------------------------------- /internal/engine/templates/robots.go.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Allow: / 3 | 4 | Sitemap: {{ .Config.BaseURL }}/sitemap.xml -------------------------------------------------------------------------------- /internal/engine/testdata/config_collections_order_alphabetic.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | version: 1.0.0 3 | title: FooBarBaz 4 | serviceIdentifier: Foo 5 | abstract: >- 6 | Contains collections in non-alphabetical order 7 | license: 8 | name: CC0 1.0 9 | url: https://creativecommons.org/publicdomain/zero/1.0/deed.nl 10 | baseUrl: http://localhost:8181 11 | ogcApi: 12 | 3dgeovolumes: 13 | tileServer: https://example.com 14 | collections: 15 | - id: b 16 | - id: z 17 | - id: c 18 | tiles: 19 | collections: 20 | - id: z 21 | tileServer: https://example.com 22 | - id: a 23 | tileServer: https://example.com 24 | -------------------------------------------------------------------------------- /internal/engine/testdata/config_collections_order_alphabetic_titles.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | version: 1.0.0 3 | title: FooBarBaz 4 | serviceIdentifier: Foo 5 | abstract: >- 6 | Contains collections in non-alphabetical order 7 | license: 8 | name: CC0 1.0 9 | url: https://creativecommons.org/publicdomain/zero/1.0/deed.nl 10 | baseUrl: http://localhost:8181 11 | ogcApi: 12 | 3dgeovolumes: 13 | tileServer: https://example.com 14 | collections: 15 | - id: b 16 | metadata: 17 | title: Bear 18 | - id: z 19 | metadata: 20 | title: Chicken 21 | - id: c 22 | metadata: 23 | title: Bird 24 | tiles: 25 | collections: 26 | - id: z 27 | tileServer: https://example.com 28 | metadata: 29 | title: Chicken 30 | - id: a 31 | tileServer: https://example.com 32 | metadata: 33 | title: Horse 34 | -------------------------------------------------------------------------------- /internal/engine/testdata/config_collections_order_literal.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | version: 1.0.0 3 | title: FooBarBaz 4 | serviceIdentifier: Foo 5 | abstract: >- 6 | Specifies collection in exact/literal order 7 | license: 8 | name: CC0 1.0 9 | url: https://creativecommons.org/publicdomain/zero/1.0/deed.nl 10 | baseUrl: http://localhost:8181 11 | collectionOrder: 12 | - z 13 | - c 14 | - a 15 | - b 16 | ogcApi: 17 | 3dgeovolumes: 18 | tileServer: https://example.com 19 | collections: 20 | - id: b 21 | - id: z 22 | - id: c 23 | tiles: 24 | collections: 25 | - id: z 26 | tileServer: https://example.com 27 | - id: a 28 | tileServer: https://example.com 29 | -------------------------------------------------------------------------------- /internal/engine/testdata/config_collections_unique.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | version: 1.0.0 3 | title: FooBarBaz 4 | serviceIdentifier: Foo 5 | abstract: >- 6 | This API offers a collection containing both features and 3D tiles. 7 | license: 8 | name: CC0 1.0 9 | url: https://creativecommons.org/publicdomain/zero/1.0/deed.nl 10 | baseUrl: http://localhost:8181 11 | ogcApi: 12 | 3dgeovolumes: 13 | tileServer: https://example.com 14 | collections: 15 | - id: foo_collection 16 | tiles: 17 | collections: 18 | - id: bar_collection 19 | tileServer: https://example.com 20 | - id: foo_collection 21 | tileServer: https://example.com 22 | -------------------------------------------------------------------------------- /internal/engine/testdata/config_invalid.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | version: 1.0.2.6.8 3 | title: Invalid config file 4 | abstract: Version is invalid according to semver validation. 5 | baseUrl: http://test.example 6 | serviceIdentifier: Min 7 | license: 8 | name: MIT 9 | url: https://www.tldrlegal.com/license/mit-license 10 | -------------------------------------------------------------------------------- /internal/engine/testdata/config_invalid_collection_ids.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | version: 1.0.0 3 | title: Invalid config file 4 | abstract: Invalid collection IDs 5 | baseUrl: http://test.example 6 | serviceIdentifier: Min 7 | license: 8 | name: MIT 9 | url: https://www.tldrlegal.com/license/mit-license 10 | ogcApi: 11 | features: 12 | collections: 13 | - id: InvalidWithCapital 14 | - id: Invalid With Spaces 15 | -------------------------------------------------------------------------------- /internal/engine/testdata/config_invalid_tiles_projection.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | version: 1.0.0 3 | title: Invalid config file 4 | abstract: Invalid collection IDs 5 | baseUrl: http://test.example 6 | serviceIdentifier: Min 7 | license: 8 | name: MIT 9 | url: https://www.tldrlegal.com/license/mit-license 10 | ogcApi: 11 | tiles: 12 | tileServer: 13 | http://localhost:9090 14 | types: 15 | - vector 16 | supportedSrs: 17 | - srs: EPSG:28992 18 | zoomLevelRange: 19 | start: 0 20 | end: 12 21 | - srs: EPSG:99999 22 | zoomLevelRange: 23 | start: 0 24 | end: 30 25 | -------------------------------------------------------------------------------- /internal/engine/testdata/config_minimal.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | version: 1.0.2 3 | title: Minimal OGC API 4 | abstract: This is a minimal OGC API, offering only OGC API Common 5 | baseUrl: http://localhost:8080 6 | serviceIdentifier: Min 7 | license: 8 | name: MIT 9 | url: https://www.tldrlegal.com/license/mit-license 10 | -------------------------------------------------------------------------------- /internal/engine/testdata/config_multiple_ogc_apis_single_collection.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | collectionMetadata: &collectionMetadata 3 | description: >- 4 | This is a description about the NewYork collection in Markdown. 5 | We offer both 3D Tiles and Features for this collection. 6 | keywords: 7 | - Keyword1 8 | - Keyword2 9 | thumbnail: 3d.png 10 | lastUpdated: "2023-05-10T12:00:00Z" 11 | temporalProperties: 12 | startDate: validfrom 13 | endDate: validto 14 | extent: 15 | srs: EPSG:3857 16 | bbox: ["-74.391538", "40.435655", "-73.430235", "41.030882"] 17 | interval: [ "\"1970-01-01T00:00:00Z\"", "null" ] 18 | 19 | version: 1.0.0 20 | title: New York 21 | # shortened title, used in breadcrumb path 22 | serviceIdentifier: New York 23 | abstract: >- 24 | This is a description about the dataset in Markdown. 25 | license: 26 | name: CC0 1.0 27 | url: https://creativecommons.org/publicdomain/zero/1.0/deed.nl 28 | baseUrl: http://localhost:8180 29 | resources: 30 | directory: examples/resources 31 | ogcApi: 32 | 3dgeovolumes: 33 | tileServer: https://maps.ecere.com/3DAPI/collections/ 34 | collections: 35 | - id: newyork 36 | # reference to common metadata 37 | metadata: *collectionMetadata 38 | tileServerPath: "NewYork/3DTiles" 39 | uriTemplate3dTiles: "3DTiles/{level}/{x}/{y}.b3m" 40 | features: 41 | datasources: 42 | defaultWGS84: 43 | geopackage: 44 | local: 45 | # Dutch addresses, but for example purposes let's pretend these are NYC addresses 46 | file: ./examples/resources/addresses-crs84.gpkg 47 | additional: 48 | - srs: EPSG:28992 49 | geopackage: 50 | local: 51 | file: ./examples/resources/addresses-rd.gpkg 52 | collections: 53 | - id: newyork 54 | tableName: addresses 55 | # reference to common metadata 56 | metadata: *collectionMetadata 57 | -------------------------------------------------------------------------------- /internal/engine/testdata/config_processes.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | version: 1.0.0 3 | title: PDOK Processor 4 | serviceIdentifier: PDOK Processor 5 | # yamllint disable rule:trailing-spaces 6 | abstract: | 7 | processor conform OGC API Processes 8 | license: 9 | name: CC0 1.0 10 | url: https://creativecommons.org/publicdomain/zero/1.0/deed.nl 11 | baseUrl: http://localhost:8181 12 | ogcApi: 13 | processes: 14 | processesServer: http://localhost:8184/ogcapi 15 | supportsCallback: false 16 | supportsDismiss: true 17 | -------------------------------------------------------------------------------- /internal/engine/testdata/config_resources_dir.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | version: 1.0.2 3 | title: Minimal OGC API 4 | abstract: This is a minimal OGC API, offering only OGC API Common 5 | baseUrl: http://localhost:8080 6 | serviceIdentifier: Min 7 | license: 8 | name: MIT 9 | url: https://www.tldrlegal.com/license/mit-license 10 | thumbnail: thumbnail.jpg 11 | resources: 12 | directory: internal/engine/testdata/test-resources-dir 13 | -------------------------------------------------------------------------------- /internal/engine/testdata/config_valid_collection_ids.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | version: 1.0.0 3 | title: Valid config file 4 | abstract: Valid collection IDs 5 | baseUrl: http://test.example 6 | serviceIdentifier: Min 7 | license: 8 | name: MIT 9 | url: https://www.tldrlegal.com/license/mit-license 10 | ogcApi: 11 | features: 12 | collections: 13 | - id: validlowercase 14 | - id: valid-lower-case-with-hyphens 15 | - id: valid_lower_case_with_underscores 16 | - id: "valid_lower-case-with_quotes" # most often used for style ID's 17 | -------------------------------------------------------------------------------- /internal/engine/testdata/expected_conformance.json: -------------------------------------------------------------------------------- 1 | { 2 | "links": [ 3 | { 4 | "rel": "self", 5 | "type": "application/json", 6 | "title": "Demo of all OGC specs in one API - Conformance", 7 | "href": "http://localhost:8080/conformance?f=json", 8 | "hreflang": "nl" 9 | }, 10 | { 11 | "rel": "alternate", 12 | "type": "text/html", 13 | "title": "Demo of all OGC specs in one API - Conformance", 14 | "href": "http://localhost:8080/conformance?f=html", 15 | "hreflang": "nl" 16 | } 17 | ], 18 | "conformsTo": [ 19 | "http://www.opengis.net/spec/ogcapi-common-1/1.0/conf/core", 20 | "http://www.opengis.net/spec/ogcapi-common-1/1.0/conf/json", 21 | "http://www.opengis.net/spec/ogcapi-common-1/1.0/conf/html", 22 | "http://www.opengis.net/spec/ogcapi-common-1/1.0/conf/oas30", 23 | "http://www.opengis.net/spec/ogcapi-common-2/1.0/conf/collections", 24 | "http://www.opengis.net/spec/ogcapi-features-1/1.0/conf/core", 25 | "http://www.opengis.net/spec/ogcapi-features-1/1.0/conf/html", 26 | "http://www.opengis.net/spec/ogcapi-features-1/1.0/conf/geojson", 27 | "http://www.opengis.net/spec/ogcapi-features-2/1.0/conf/crs", 28 | "http://www.opengis.net/spec/ogcapi-features-5/1.0/conf/schemas", 29 | "http://www.opengis.net/spec/ogcapi-features-5/1.0/conf/core-roles-features", 30 | "http://www.opengis.net/spec/ogcapi-features-5/1.0/conf/returnables-and-receivables", 31 | "http://www.opengis.net/spec/json-fg-1/0.2", 32 | "http://www.opengis.net/spec/ogcapi-styles-1/1.0/conf/core", 33 | "http://www.opengis.net/spec/ogcapi-styles-1/1.0/conf/mapbox-styles", 34 | "http://www.opengis.net/spec/ogcapi-geovolumes-1/1.0/conf/core", 35 | "http://www.opengis.net/spec/ogcapi-tiles-1/1.0/conf/core", 36 | "http://www.opengis.net/spec/ogcapi-tiles-1/1.0/conf/tileset", 37 | "http://www.opengis.net/spec/ogcapi-tiles-1/1.0/conf/tilesets-list", 38 | "http://www.opengis.net/spec/ogcapi-tiles-1/1.0/conf/dataset-tilesets", 39 | "http://www.opengis.net/spec/ogcapi-tiles-1/1.0/conf/geodata-tilesets", 40 | "http://www.opengis.net/spec/ogcapi-tiles-1/1.0/conf/mvt" 41 | ] 42 | } -------------------------------------------------------------------------------- /internal/engine/testdata/expected_dataset_collection.json: -------------------------------------------------------------------------------- 1 | { 2 | "@context": "https://schema.org/", 3 | "@type": "Dataset", 4 | "isPartOf": "http:\/\/localhost:8080?f=html", 5 | "name": "Demo of all OGC specs in one API - Addresses", 6 | "url": "http:\/\/localhost:8080/collections/addresses?f=html","license": "https:\/\/creativecommons.org\/publicdomain\/zero\/1.0\/deed.nl", 7 | "isAccessibleForFree": true 8 | } -------------------------------------------------------------------------------- /internal/engine/testdata/expected_dataset_landingpage.json: -------------------------------------------------------------------------------- 1 | { 2 | "@context": "https://schema.org/", 3 | "@type": "Dataset", 4 | "name": "Demo of all OGC specs in one API (OGC API)", 5 | "description": "This is example combines features, 3D and vector tiles in one API. Usually this would encompass one dataset but this demo uses data from various sources. So don\u0027t pay too much attention to the actual data. It\u0027s just an example\/demo of GoKoala\u0027s capabilities.", 6 | "url": "http:\/\/localhost:8080?f=html", 7 | "keywords": ["keyword1", "keyword2"], 8 | "license": "https:\/\/creativecommons.org\/publicdomain\/zero\/1.0\/deed.nl", 9 | "isAccessibleForFree": true, 10 | "hasPart": [ 11 | "http:\/\/localhost:8080/collections/addresses", 12 | "http:\/\/localhost:8080/collections/addresses2" 13 | ] 14 | } -------------------------------------------------------------------------------- /internal/engine/testdata/expected_multiple_ogc_apis_single_collection.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |

4 | 3D GeoVolumes 5 |

6 |
7 | 10 |
11 |
12 |
13 | 14 |
15 |
16 |

17 | Features 18 |

19 |
20 |

21 | Blader door de Features of ga direct naar de features in: 22 |

23 |

24 |

    25 |
  • 26 | 30 | als 31 | GeoJSON. 32 |
  • 33 |
  • 34 | 38 | als 39 | JSON-FG. 40 |
  • 41 |
42 |

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

This description of collection Barrr is quite long, and as such would distract the user from the rest of the content on overview pages. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec accumsan lectus id ipsum condimentum pretium. Aenean cursus et diam aliquam vestibulum. Cras at est risus. Suspendisse venenatis dignissim aliquet. Maecenas rhoncus mi vulputate mi ullamcorper…

4 | 5 | 6 |
7 | -------------------------------------------------------------------------------- /internal/ogc/features/testdata/expected_empty_feature_collection.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "FeatureCollection", 3 | "timeStamp": "2000-01-01T00:00:00Z", 4 | "links": [ 5 | { 6 | "rel": "self", 7 | "title": "This document as GeoJSON", 8 | "type": "application/geo+json", 9 | "href": "http://localhost:8080/collections/foo/items?f=json&straatnaam=doesnotexist" 10 | }, 11 | { 12 | "rel": "alternate", 13 | "title": "This document as JSON-FG", 14 | "type": "application/vnd.ogc.fg+json", 15 | "href": "http://localhost:8080/collections/foo/items?f=jsonfg&straatnaam=doesnotexist" 16 | }, 17 | { 18 | "rel": "alternate", 19 | "title": "This document as HTML", 20 | "type": "text/html", 21 | "href": "http://localhost:8080/collections/foo/items?f=html&straatnaam=doesnotexist" 22 | } 23 | ], 24 | "features": [], 25 | "numberReturned": 0 26 | } 27 | -------------------------------------------------------------------------------- /internal/ogc/features/testdata/expected_feature_4030.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "4030", 3 | "links": [ 4 | { 5 | "rel": "self", 6 | "title": "This document as GeoJSON", 7 | "type": "application/geo+json", 8 | "href": "http://localhost:8080/collections/foo/items/4030?f=json" 9 | }, 10 | { 11 | "rel": "alternate", 12 | "title": "This document as JSON-FG", 13 | "type": "application/vnd.ogc.fg+json", 14 | "href": "http://localhost:8080/collections/foo/items/4030?f=jsonfg" 15 | }, 16 | { 17 | "rel": "alternate", 18 | "title": "This document as HTML", 19 | "type": "text/html", 20 | "href": "http://localhost:8080/collections/foo/items/4030?f=html" 21 | }, 22 | { 23 | "rel": "collection", 24 | "title": "The collection to which this feature belongs", 25 | "type": "application/json", 26 | "href": "http://localhost:8080/collections/foo?f=json" 27 | } 28 | ], 29 | "type": "Feature", 30 | "geometry": { 31 | "type": "Point", 32 | "coordinates": [ 33 | 121100.455, 34 | 488900.976 35 | ] 36 | }, 37 | "properties": { 38 | "datum_doc": "1900-01-01", 39 | "datum_strt": "1900-01-01", 40 | "document": "GV00000402", 41 | "datum_eind": null, 42 | "huisnummer": 285, 43 | "huisletter": null, 44 | "toevoeging": null, 45 | "nummer_id": "0363200000428648", 46 | "postcode": "1013LH", 47 | "rdf_seealso": "http://bag.basisregistraties.overheid.nl/bag/id/nummeraanduiding/0363200000428648", 48 | "status": "Naamgeving uitgegeven", 49 | "straatnaam": "Bickersgracht", 50 | "type": "Ligplaats", 51 | "woonplaats": "Amsterdam" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /internal/ogc/features/testdata/expected_feature_4030_jsonfg.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "Feature", 3 | "time": null, 4 | "place": null, 5 | "geometry": { 6 | "type": "Point", 7 | "coordinates": [ 8 | 121100.455, 9 | 488900.976 10 | ] 11 | }, 12 | "properties": { 13 | "datum_doc": "1900-01-01", 14 | "datum_strt": "1900-01-01", 15 | "datum_eind": null, 16 | "document": "GV00000402", 17 | "huisnummer": 285, 18 | "huisletter": null, 19 | "toevoeging": null, 20 | "nummer_id": "0363200000428648", 21 | "postcode": "1013LH", 22 | "rdf_seealso": "http://bag.basisregistraties.overheid.nl/bag/id/nummeraanduiding/0363200000428648", 23 | "status": "Naamgeving uitgegeven", 24 | "straatnaam": "Bickersgracht", 25 | "type": "Ligplaats", 26 | "woonplaats": "Amsterdam" 27 | }, 28 | "coordRefSys": "http://www.opengis.net/def/crs/OGC/1.3/CRS84", 29 | "links": [ 30 | { 31 | "rel": "self", 32 | "title": "This document as JSON-FG", 33 | "type": "application/vnd.ogc.fg+json", 34 | "href": "http://localhost:8080/collections/foo/items/4030?f=jsonfg" 35 | }, 36 | { 37 | "rel": "alternate", 38 | "title": "This document as GeoJSON", 39 | "type": "application/geo+json", 40 | "href": "http://localhost:8080/collections/foo/items/4030?f=json" 41 | }, 42 | { 43 | "rel": "alternate", 44 | "title": "This document as HTML", 45 | "type": "text/html", 46 | "href": "http://localhost:8080/collections/foo/items/4030?f=html" 47 | }, 48 | { 49 | "rel": "collection", 50 | "title": "The collection to which this feature belongs", 51 | "type": "application/json", 52 | "href": "http://localhost:8080/collections/foo?f=json" 53 | } 54 | ], 55 | "conformsTo": [ 56 | "http://www.opengis.net/spec/json-fg-1/0.2/conf/core" 57 | ], 58 | "id": "4030" 59 | } 60 | -------------------------------------------------------------------------------- /internal/ogc/features/testdata/expected_feature_404.json: -------------------------------------------------------------------------------- 1 | { 2 | "detail": "the requested feature with id: 999999 does not exist in collection 'dutch-addresses'", 3 | "status": 404, 4 | "timeStamp": "2000-01-01T00:00:00Z", 5 | "title": "Not Found" 6 | } -------------------------------------------------------------------------------- /internal/ogc/features/testdata/expected_feature_geom_empty_point.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "Feature", 3 | "properties": { 4 | "datum_doc": "1900-01-01", 5 | "datum_eind": null, 6 | "datum_strt": "1900-01-01", 7 | "document": "GV00000402", 8 | "huisletter": null, 9 | "huisnummer": 14, 10 | "nummer_id": "0363200000454013", 11 | "postcode": "1013CR", 12 | "rdf_seealso": "http://bag.basisregistraties.overheid.nl/bag/id/nummeraanduiding/0363200000454013", 13 | "status": "Naamgeving uitgegeven", 14 | "straatnaam": "Van Diemenkade", 15 | "toevoeging": null, 16 | "type": "Ligplaats", 17 | "woonplaats": "Amsterdam" 18 | }, 19 | "geometry": null, 20 | "id": "3542", 21 | "links": [ 22 | { 23 | "rel": "self", 24 | "title": "This document as GeoJSON", 25 | "type": "application/geo+json", 26 | "href": "http://localhost:8080/collections/foo/items/3542?f=json" 27 | }, 28 | { 29 | "rel": "alternate", 30 | "title": "This document as JSON-FG", 31 | "type": "application/vnd.ogc.fg+json", 32 | "href": "http://localhost:8080/collections/foo/items/3542?f=jsonfg" 33 | }, 34 | { 35 | "rel": "alternate", 36 | "title": "This document as HTML", 37 | "type": "text/html", 38 | "href": "http://localhost:8080/collections/foo/items/3542?f=html" 39 | }, 40 | { 41 | "rel": "collection", 42 | "title": "The collection to which this feature belongs", 43 | "type": "application/json", 44 | "href": "http://localhost:8080/collections/foo?f=json" 45 | } 46 | ] 47 | } 48 | -------------------------------------------------------------------------------- /internal/ogc/features/testdata/expected_feature_geom_empty_point_jsonfg.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "3542", 3 | "type": "Feature", 4 | "time": null, 5 | "place": null, 6 | "geometry": null, 7 | "properties": { 8 | "datum_doc": "1900-01-01", 9 | "datum_eind": null, 10 | "datum_strt": "1900-01-01", 11 | "document": "GV00000402", 12 | "huisletter": null, 13 | "huisnummer": 14, 14 | "nummer_id": "0363200000454013", 15 | "postcode": "1013CR", 16 | "rdf_seealso": "http://bag.basisregistraties.overheid.nl/bag/id/nummeraanduiding/0363200000454013", 17 | "status": "Naamgeving uitgegeven", 18 | "straatnaam": "Van Diemenkade", 19 | "toevoeging": null, 20 | "type": "Ligplaats", 21 | "woonplaats": "Amsterdam" 22 | }, 23 | "coordRefSys": "http://www.opengis.net/def/crs/OGC/1.3/CRS84", 24 | "links": [ 25 | { 26 | "rel": "self", 27 | "title": "This document as JSON-FG", 28 | "type": "application/vnd.ogc.fg+json", 29 | "href": "http://localhost:8080/collections/foo/items/3542?f=jsonfg" 30 | }, 31 | { 32 | "rel": "alternate", 33 | "title": "This document as GeoJSON", 34 | "type": "application/geo+json", 35 | "href": "http://localhost:8080/collections/foo/items/3542?f=json" 36 | }, 37 | { 38 | "rel": "alternate", 39 | "title": "This document as HTML", 40 | "type": "text/html", 41 | "href": "http://localhost:8080/collections/foo/items/3542?f=html" 42 | }, 43 | { 44 | "rel": "collection", 45 | "title": "The collection to which this feature belongs", 46 | "type": "application/json", 47 | "href": "http://localhost:8080/collections/foo?f=json" 48 | } 49 | ], 50 | "conformsTo": [ 51 | "http://www.opengis.net/spec/json-fg-1/0.2/conf/core" 52 | ] 53 | } 54 | -------------------------------------------------------------------------------- /internal/ogc/features/testdata/expected_feature_geom_null.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "Feature", 3 | "properties": { 4 | "datum_doc": "2021-02-26", 5 | "datum_eind": null, 6 | "datum_strt": "2021-02-26", 7 | "document": "SE05427877", 8 | "geom": null, 9 | "huisletter": null, 10 | "huisnummer": 6, 11 | "nummer_id": "0363200012163629", 12 | "postcode": "1013NK", 13 | "rdf_seealso": "http://bag.basisregistraties.overheid.nl/bag/id/nummeraanduiding/0363200012163629", 14 | "status": "Naamgeving uitgegeven", 15 | "straatnaam": "Bokkinghangen", 16 | "toevoeging": null, 17 | "type": "Ligplaats", 18 | "woonplaats": "Amsterdam" 19 | }, 20 | "geometry": null, 21 | "id": "6436", 22 | "links": [ 23 | { 24 | "rel": "self", 25 | "title": "This document as GeoJSON", 26 | "type": "application/geo+json", 27 | "href": "http://localhost:8080/collections/foo/items/6436?f=json" 28 | }, 29 | { 30 | "rel": "alternate", 31 | "title": "This document as JSON-FG", 32 | "type": "application/vnd.ogc.fg+json", 33 | "href": "http://localhost:8080/collections/foo/items/6436?f=jsonfg" 34 | }, 35 | { 36 | "rel": "alternate", 37 | "title": "This document as HTML", 38 | "type": "text/html", 39 | "href": "http://localhost:8080/collections/foo/items/6436?f=html" 40 | }, 41 | { 42 | "rel": "collection", 43 | "title": "The collection to which this feature belongs", 44 | "type": "application/json", 45 | "href": "http://localhost:8080/collections/foo?f=json" 46 | } 47 | ] 48 | } 49 | -------------------------------------------------------------------------------- /internal/ogc/features/testdata/expected_feature_geom_null_jsonfg.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "6436", 3 | "type": "Feature", 4 | "time": null, 5 | "place": null, 6 | "geometry": null, 7 | "properties": { 8 | "datum_doc": "2021-02-26", 9 | "datum_eind": null, 10 | "datum_strt": "2021-02-26", 11 | "document": "SE05427877", 12 | "geom": null, 13 | "huisletter": null, 14 | "huisnummer": 6, 15 | "nummer_id": "0363200012163629", 16 | "postcode": "1013NK", 17 | "rdf_seealso": "http://bag.basisregistraties.overheid.nl/bag/id/nummeraanduiding/0363200012163629", 18 | "status": "Naamgeving uitgegeven", 19 | "straatnaam": "Bokkinghangen", 20 | "toevoeging": null, 21 | "type": "Ligplaats", 22 | "woonplaats": "Amsterdam" 23 | }, 24 | "coordRefSys": "http://www.opengis.net/def/crs/OGC/1.3/CRS84", 25 | "links": [ 26 | { 27 | "rel": "self", 28 | "title": "This document as JSON-FG", 29 | "type": "application/vnd.ogc.fg+json", 30 | "href": "http://localhost:8080/collections/foo/items/6436?f=jsonfg" 31 | }, 32 | { 33 | "rel": "alternate", 34 | "title": "This document as GeoJSON", 35 | "type": "application/geo+json", 36 | "href": "http://localhost:8080/collections/foo/items/6436?f=json" 37 | }, 38 | { 39 | "rel": "alternate", 40 | "title": "This document as HTML", 41 | "type": "text/html", 42 | "href": "http://localhost:8080/collections/foo/items/6436?f=html" 43 | }, 44 | { 45 | "rel": "collection", 46 | "title": "The collection to which this feature belongs", 47 | "type": "application/json", 48 | "href": "http://localhost:8080/collections/foo?f=json" 49 | } 50 | ], 51 | "conformsTo": [ 52 | "http://www.opengis.net/spec/json-fg-1/0.2/conf/core" 53 | ] 54 | } 55 | -------------------------------------------------------------------------------- /internal/ogc/features/testdata/expected_feature_webconfig_snippet.html: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /internal/ogc/features/testdata/expected_features_3d_geoms.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "FeatureCollection", 3 | "timeStamp": "2000-01-01T00:00:00Z", 4 | "links": [ 5 | { 6 | "rel": "self", 7 | "title": "This document as GeoJSON", 8 | "type": "application/geo+json", 9 | "href": "http://localhost:8080/collections/foo/items?f=json&limit=5" 10 | }, 11 | { 12 | "rel": "alternate", 13 | "title": "This document as JSON-FG", 14 | "type": "application/vnd.ogc.fg+json", 15 | "href": "http://localhost:8080/collections/foo/items?f=jsonfg&limit=5" 16 | }, 17 | { 18 | "rel": "alternate", 19 | "title": "This document as HTML", 20 | "type": "text/html", 21 | "href": "http://localhost:8080/collections/foo/items?f=html&limit=5" 22 | } 23 | ], 24 | "features": [ 25 | { 26 | "type": "Feature", 27 | "properties": {}, 28 | "geometry": { 29 | "type": "LineString", 30 | "coordinates": [ 31 | [ 32 | 0, 33 | 0, 34 | 0 35 | ], 36 | [ 37 | 1, 38 | 1, 39 | 1 40 | ] 41 | ] 42 | }, 43 | "id": "1" 44 | }, 45 | { 46 | "type": "Feature", 47 | "properties": {}, 48 | "geometry": { 49 | "type": "LineString", 50 | "coordinates": [ 51 | [ 52 | 2, 53 | 2, 54 | 2 55 | ], 56 | [ 57 | 3, 58 | 3, 59 | 3 60 | ] 61 | ] 62 | }, 63 | "id": "2" 64 | } 65 | ], 66 | "numberReturned": 2 67 | } 68 | -------------------------------------------------------------------------------- /internal/ogc/features/testdata/expected_features_3d_geoms_jsonfg.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "FeatureCollection", 3 | "timeStamp": "2000-01-01T00:00:00Z", 4 | "coordRefSys": "http://www.opengis.net/def/crs/OGC/1.3/CRS84", 5 | "links": [ 6 | { 7 | "rel": "self", 8 | "title": "This document as JSON-FG", 9 | "type": "application/vnd.ogc.fg+json", 10 | "href": "http://localhost:8080/collections/foo/items?f=jsonfg&limit=5" 11 | }, 12 | { 13 | "rel": "alternate", 14 | "title": "This document as GeoJSON", 15 | "type": "application/geo+json", 16 | "href": "http://localhost:8080/collections/foo/items?f=json&limit=5" 17 | }, 18 | { 19 | "rel": "alternate", 20 | "title": "This document as HTML", 21 | "type": "text/html", 22 | "href": "http://localhost:8080/collections/foo/items?f=html&limit=5" 23 | } 24 | ], 25 | "conformsTo": [ 26 | "http://www.opengis.net/spec/json-fg-1/0.2/conf/core" 27 | ], 28 | "features": [ 29 | { 30 | "id": "1", 31 | "type": "Feature", 32 | "time": null, 33 | "place": null, 34 | "geometry": { 35 | "type": "LineString", 36 | "coordinates": [ 37 | [ 38 | 0, 39 | 0, 40 | 0 41 | ], 42 | [ 43 | 1, 44 | 1, 45 | 1 46 | ] 47 | ] 48 | }, 49 | "properties": {} 50 | }, 51 | { 52 | "id": "2", 53 | "type": "Feature", 54 | "time": null, 55 | "place": null, 56 | "geometry": { 57 | "type": "LineString", 58 | "coordinates": [ 59 | [ 60 | 2, 61 | 2, 62 | 2 63 | ], 64 | [ 65 | 3, 66 | 3, 67 | 3 68 | ] 69 | ] 70 | }, 71 | "properties": {} 72 | } 73 | ], 74 | "numberReturned": 2 75 | } 76 | -------------------------------------------------------------------------------- /internal/ogc/features/testdata/expected_features_3d_geoms_multipoint.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "FeatureCollection", 3 | "timeStamp": "2000-01-01T00:00:00Z", 4 | "links": [ 5 | { 6 | "rel": "self", 7 | "title": "This document as GeoJSON", 8 | "type": "application/geo+json", 9 | "href": "http://localhost:8080/collections/bar/items?f=json&limit=5" 10 | }, 11 | { 12 | "rel": "alternate", 13 | "title": "This document as JSON-FG", 14 | "type": "application/vnd.ogc.fg+json", 15 | "href": "http://localhost:8080/collections/bar/items?f=jsonfg&limit=5" 16 | }, 17 | { 18 | "rel": "alternate", 19 | "title": "This document as HTML", 20 | "type": "text/html", 21 | "href": "http://localhost:8080/collections/bar/items?f=html&limit=5" 22 | } 23 | ], 24 | "features": [ 25 | { 26 | "type": "Feature", 27 | "properties": {}, 28 | "geometry": { 29 | "type": "MultiPoint", 30 | "coordinates": [ 31 | [ 32 | 239833.392999999, 33 | 451859.4530000016, 34 | 0 35 | ] 36 | ] 37 | }, 38 | "id": "1" 39 | }, 40 | { 41 | "type": "Feature", 42 | "properties": {}, 43 | "geometry": { 44 | "type": "MultiPoint", 45 | "coordinates": [ 46 | [ 47 | 226269.9899999984, 48 | 444831.585999999, 49 | 0 50 | ] 51 | ] 52 | }, 53 | "id": "2" 54 | }, 55 | { 56 | "type": "Feature", 57 | "properties": {}, 58 | "geometry": { 59 | "type": "MultiPoint", 60 | "coordinates": [ 61 | [ 62 | 227163.449000001, 63 | 444071.0119999982, 64 | 0 65 | ] 66 | ] 67 | }, 68 | "id": "3" 69 | } 70 | ], 71 | "numberReturned": 3 72 | } 73 | -------------------------------------------------------------------------------- /internal/ogc/features/testdata/expected_features_webconfig_snippet.html: -------------------------------------------------------------------------------- 1 | 9 | -------------------------------------------------------------------------------- /internal/ogc/features/testdata/expected_features_with_rel_as_link.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "FeatureCollection", 3 | "timeStamp": "2000-01-01T00:00:00Z", 4 | "links": [ 5 | { 6 | "rel": "self", 7 | "title": "This document as GeoJSON", 8 | "type": "application/geo+json", 9 | "href": "http://localhost:8080/collections/standplaatsen/items?f=json" 10 | }, 11 | { 12 | "rel": "alternate", 13 | "title": "This document as JSON-FG", 14 | "type": "application/vnd.ogc.fg+json", 15 | "href": "http://localhost:8080/collections/standplaatsen/items?f=jsonfg" 16 | }, 17 | { 18 | "rel": "alternate", 19 | "title": "This document as HTML", 20 | "type": "text/html", 21 | "href": "http://localhost:8080/collections/standplaatsen/items?f=html" 22 | } 23 | ], 24 | "features": [ 25 | { 26 | "type": "Feature", 27 | "geometry": { 28 | "type": "Point", 29 | "coordinates": [ 30 | 121301.554, 31 | 489089.022 32 | ] 33 | }, 34 | "properties": { 35 | "datum_doc": "2020-05-20", 36 | "datum_eind": null, 37 | "datum_strt": "2020-05-20", 38 | "document": "PC20200520_PC00PC", 39 | "huisletter": null, 40 | "huisnummer": 1, 41 | "ligplaatsen.href": "http://localhost:8080/collections/ligplaatsen/items/093ae17f-8e83-5560-bbd0-b199227af170", 42 | "nummer_id": "0363200012157127", 43 | "postcode": "1013AJ", 44 | "rdf_seealso": "http://bag.basisregistraties.overheid.nl/bag/id/nummeraanduiding/0363200012157127", 45 | "status": "Naamgeving uitgegeven", 46 | "straatnaam": "Stenen Hoofd", 47 | "toevoeging": null, 48 | "type": "Standplaats", 49 | "woonplaats": "Amsterdam" 50 | }, 51 | "id": "40279463-b028-58da-9341-e0e88797a18c" 52 | } 53 | ], 54 | "numberReturned": 1 55 | } 56 | -------------------------------------------------------------------------------- /internal/ogc/features/testdata/expected_features_with_rel_as_link_snippet.html: -------------------------------------------------------------------------------- 1 | 2 | ligplaatsen.href 3 | http://localhost:8080/collections/ligplaatsen/items/093ae17f-8e83-5560-bbd0-b199227af170 4 | -------------------------------------------------------------------------------- /internal/ogc/features/testdata/expected_mapsheets.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /internal/ogc/features/testdata/expected_query_timeout_feature.json: -------------------------------------------------------------------------------- 1 | { 2 | "detail": "failed to retrieve feature 4030 in collection dutch-addresses: querying the feature took too long (timeout encountered). Try again, or contact support", 3 | "status": 500, 4 | "timeStamp": "2000-01-01T00:00:00Z", 5 | "title": "Internal Server Error" 6 | } 7 | -------------------------------------------------------------------------------- /internal/ogc/features/testdata/expected_query_timeout_features.json: -------------------------------------------------------------------------------- 1 | { 2 | "detail": "failed to retrieve feature collection dutch-addresses: querying the features took too long (timeout encountered). Simplify your request and try again, or contact support", 3 | "status": 500, 4 | "timeStamp": "2000-01-01T00:00:00Z", 5 | "title": "Internal Server Error" 6 | } 7 | -------------------------------------------------------------------------------- /internal/ogc/features/testdata/expected_schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json-schema.org/draft/2020-12/schema", 3 | "$id": "http://localhost:8080/collections/foo/schema", 4 | "title": "Foooo", 5 | "description": "Foooo", 6 | "type": "object", 7 | "required": [ 8 | "id" 9 | ], 10 | "properties": { 11 | "id": { 12 | "readOnly": true, 13 | "x-ogc-role": "id", 14 | "description": "De unieke identificatie van dit feature.", 15 | "type": "integer", 16 | "minimum": 0 17 | }, 18 | "datum_doc": { 19 | "type": "string" 20 | }, 21 | "datum_eind": { 22 | "type": "string" 23 | }, 24 | "datum_strt": { 25 | "type": "string" 26 | }, 27 | "document": { 28 | "type": "string" 29 | }, 30 | "geometry": { 31 | "x-ogc-role": "primary-geometry", 32 | "format": "geometry-point" 33 | }, 34 | "huisletter": { 35 | "type": "string" 36 | }, 37 | "huisnummer": { 38 | "type": "integer" 39 | }, 40 | "nummer_id": { 41 | "type": "string" 42 | }, 43 | "postcode": { 44 | "type": "string" 45 | }, 46 | "rdf_seealso": { 47 | "type": "string" 48 | }, 49 | "status": { 50 | "type": "string" 51 | }, 52 | "straatnaam": { 53 | "type": "string" 54 | }, 55 | "toevoeging": { 56 | "type": "string" 57 | }, 58 | "type": { 59 | "type": "string" 60 | }, 61 | "woonplaats": { 62 | "type": "string" 63 | } 64 | }, 65 | "additionalProperties": true 66 | } -------------------------------------------------------------------------------- /internal/ogc/features/testdata/expected_schema_3d_geoms.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json-schema.org/draft/2020-12/schema", 3 | "$id": "http://localhost:8080/collections/foo/schema", 4 | "title": "Foo", 5 | "description": "Contains 3D linestrings", 6 | "type": "object", 7 | "required": [ 8 | "id" 9 | ], 10 | "properties": { 11 | "id": { 12 | "readOnly": true, 13 | "x-ogc-role": "id", 14 | "description": "De unieke identificatie van dit feature.", 15 | "type": "integer", 16 | "minimum": 0 17 | }, 18 | "geometry": { 19 | "x-ogc-role": "primary-geometry", 20 | "format": "geometry-linestring" 21 | } 22 | }, 23 | "additionalProperties": true 24 | } -------------------------------------------------------------------------------- /internal/ogc/features/testdata/expected_schema_external_fid.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json-schema.org/draft/2020-12/schema", 3 | "$id": "http://localhost:8080/collections/ligplaatsen/schema", 4 | "title": "Ligplaatsen", 5 | "description": "Ligplaatsen example data", 6 | "type": "object", 7 | "required": [ 8 | "id" 9 | ], 10 | "properties": { 11 | "id": { 12 | "readOnly": true, 13 | "x-ogc-role": "id", 14 | "description": "De unieke identificatie van dit feature. Dit ID zou stabiel moeten zijn door de tijd heen en altijd naar hetzelfde feature moeten verwijzen.", 15 | "type": "string", 16 | "format": "uuid" 17 | }, 18 | "geometry": { 19 | "x-ogc-role": "primary-geometry", 20 | "format": "geometry-point" 21 | }, 22 | "nummer_id": { 23 | "type": "string" 24 | }, 25 | "straatnaam": { 26 | "type": "string" 27 | }, 28 | "huisnummer": { 29 | "type": "integer" 30 | }, 31 | "huisletter": { 32 | "type": "string" 33 | }, 34 | "toevoeging": { 35 | "type": "string" 36 | }, 37 | "woonplaats": { 38 | "type": "string" 39 | }, 40 | "postcode": { 41 | "type": "string" 42 | }, 43 | "type": { 44 | "type": "string" 45 | }, 46 | "status": { 47 | "type": "string" 48 | }, 49 | "datum_strt": { 50 | "type": "string" 51 | }, 52 | "datum_eind": { 53 | "type": "string" 54 | }, 55 | "document": { 56 | "type": "string" 57 | }, 58 | "datum_doc": { 59 | "type": "string" 60 | }, 61 | "rdf_seealso": { 62 | "type": "string" 63 | } 64 | }, 65 | "additionalProperties": true 66 | } -------------------------------------------------------------------------------- /internal/ogc/features/testdata/expected_schema_external_fid_snippet.html: -------------------------------------------------------------------------------- 1 | 2 | id 3 | uuid 4 | ja 5 | De unieke identificatie van dit feature. Dit ID zou stabiel moeten zijn door de tijd heen en altijd naar hetzelfde feature moeten verwijzen. 6 | -------------------------------------------------------------------------------- /internal/ogc/features/testdata/expected_schema_temporal.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json-schema.org/draft/2020-12/schema", 3 | "$id": "http://localhost:8080/collections/standplaatsen/schema", 4 | "title": "standplaatsen", 5 | "description": "standplaatsen", 6 | "type": "object", 7 | "required": [ 8 | "id" 9 | ], 10 | "properties": { 11 | "id": { 12 | "readOnly": true, 13 | "x-ogc-role": "id", 14 | "description": "De unieke identificatie van dit feature.", 15 | "type": "integer", 16 | "minimum": 0 17 | }, 18 | "geometry": { 19 | "x-ogc-role": "primary-geometry", 20 | "format": "geometry-point" 21 | }, 22 | "nummer_id": { 23 | "type": "string" 24 | }, 25 | "straatnaam": { 26 | "type": "string" 27 | }, 28 | "huisnummer": { 29 | "type": "integer" 30 | }, 31 | "huisletter": { 32 | "type": "string" 33 | }, 34 | "toevoeging": { 35 | "type": "string" 36 | }, 37 | "woonplaats": { 38 | "type": "string" 39 | }, 40 | "postcode": { 41 | "type": "string" 42 | }, 43 | "type": { 44 | "type": "string" 45 | }, 46 | "status": { 47 | "type": "string" 48 | }, 49 | "datum_strt": { 50 | "description": "Betreft startdatum die gebruikt wordt wanneer er op tijd wordt gefilterd, bijv. ?datetime=2050-01-01T00:00:00Z. ", 51 | "x-ogc-role": "primary-interval-start", 52 | "type": "string" 53 | }, 54 | "datum_eind": { 55 | "description": "Betreft einddatum die gebruikt wordt wanneer er op tijd wordt gefilterd, bijv. ?datetime=2050-01-01T00:00:00Z. ", 56 | "x-ogc-role": "primary-interval-end", 57 | "type": "string" 58 | }, 59 | "document": { 60 | "type": "string" 61 | }, 62 | "datum_doc": { 63 | "type": "string" 64 | }, 65 | "rdf_seealso": { 66 | "type": "string" 67 | } 68 | }, 69 | "additionalProperties": true 70 | } -------------------------------------------------------------------------------- /internal/ogc/features/testdata/expected_schema_temporal_snippet.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | datum_strt 4 | string 5 | nee 6 | 7 | 8 | Betreft startdatum die gebruikt wordt wanneer er op tijd wordt gefilterd, bijv. ?datetime=2050-01-01T00:00:00Z.
9 | 10 | 11 | 12 | 13 | datum_eind 14 | string 15 | nee 16 | 17 | 18 | Betreft einddatum die gebruikt wordt wanneer er op tijd wordt gefilterd, bijv. ?datetime=2050-01-01T00:00:00Z.
19 | 20 | 21 | -------------------------------------------------------------------------------- /internal/ogc/features/testdata/expected_straatnaam_and_postcode.json: -------------------------------------------------------------------------------- 1 | { 2 | "links": [ 3 | { 4 | "rel": "self", 5 | "title": "This document as GeoJSON", 6 | "type": "application/geo+json", 7 | "href": "http://localhost:8080/collections/foo/items?f=json&postcode=1104MM&straatnaam=Zandhoek" 8 | }, 9 | { 10 | "rel": "alternate", 11 | "title": "This document as JSON-FG", 12 | "type": "application/vnd.ogc.fg+json", 13 | "href": "http://localhost:8080/collections/foo/items?f=jsonfg&postcode=1104MM&straatnaam=Zandhoek" 14 | }, 15 | { 16 | "rel": "alternate", 17 | "title": "This document as HTML", 18 | "type": "text/html", 19 | "href": "http://localhost:8080/collections/foo/items?f=html&postcode=1104MM&straatnaam=Zandhoek" 20 | } 21 | ], 22 | "numberReturned": 1, 23 | "timeStamp": "2000-01-01T00:00:00Z", 24 | "type": "FeatureCollection", 25 | "features": [ 26 | { 27 | "id": "6313", 28 | "type": "Feature", 29 | "geometry": { 30 | "type": "Point", 31 | "coordinates": [ 32 | 121191.236, 33 | 489036.72 34 | ] 35 | }, 36 | "properties": { 37 | "datum_doc": "2021-09-29", 38 | "datum_eind": null, 39 | "datum_strt": "2021-09-29", 40 | "document": "PC20210929_PC00PC", 41 | "huisletter": null, 42 | "huisnummer": 24, 43 | "nummer_id": "0363200012111724", 44 | "postcode": "1104MM", 45 | "rdf_seealso": "http://bag.basisregistraties.overheid.nl/bag/id/nummeraanduiding/0363200012111724", 46 | "status": "Naamgeving uitgegeven", 47 | "straatnaam": "Zandhoek", 48 | "toevoeging": null, 49 | "type": "Ligplaats", 50 | "woonplaats": "Amsterdam" 51 | } 52 | } 53 | ] 54 | } 55 | -------------------------------------------------------------------------------- /internal/ogc/features/testdata/expected_straatnaam_not_allowed_value.json: -------------------------------------------------------------------------------- 1 | { 2 | "detail": "request doesn't conform to OpenAPI spec: parameter \"straatnaam\" in query has an error: value is not one of the allowed values [\"Silodam\",\"Westerdok\"]", 3 | "status": 400, 4 | "timeStamp": "2000-01-01T00:00:00Z", 5 | "title": "Bad Request" 6 | } 7 | -------------------------------------------------------------------------------- /internal/ogc/features/testdata/expected_straatnaam_silodam.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | 5 | 7 | 8 |
9 |
10 |
11 | 12 |
13 | 14 | 16 | 17 |
18 |
-------------------------------------------------------------------------------- /internal/ogc/features/testdata/expected_temporal.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "FeatureCollection", 3 | "timeStamp": "2000-01-01T00:00:00Z", 4 | "links": [ 5 | { 6 | "rel": "self", 7 | "title": "This document as GeoJSON", 8 | "type": "application/geo+json", 9 | "href": "http://localhost:8080/collections/standplaatsen/items?datetime=2020-05-20T00%3A00%3A00Z&f=json&limit=10" 10 | }, 11 | { 12 | "rel": "alternate", 13 | "title": "This document as JSON-FG", 14 | "type": "application/vnd.ogc.fg+json", 15 | "href": "http://localhost:8080/collections/standplaatsen/items?datetime=2020-05-20T00%3A00%3A00Z&f=jsonfg&limit=10" 16 | }, 17 | { 18 | "rel": "alternate", 19 | "title": "This document as HTML", 20 | "type": "text/html", 21 | "href": "http://localhost:8080/collections/standplaatsen/items?datetime=2020-05-20T00%3A00%3A00Z&f=html&limit=10" 22 | } 23 | ], 24 | "features": [ 25 | { 26 | "type": "Feature", 27 | "geometry": { 28 | "type": "Point", 29 | "coordinates": [ 30 | 121301.554, 31 | 489089.022 32 | ] 33 | }, 34 | "properties": { 35 | "datum_doc": "2020-05-20", 36 | "datum_eind": null, 37 | "datum_strt": "2020-05-20", 38 | "document": "PC20200520_PC00PC", 39 | "huisletter": null, 40 | "huisnummer": 1, 41 | "nummer_id": "0363200012157127", 42 | "postcode": "1013AJ", 43 | "rdf_seealso": "http://bag.basisregistraties.overheid.nl/bag/id/nummeraanduiding/0363200012157127", 44 | "status": "Naamgeving uitgegeven", 45 | "straatnaam": "Stenen Hoofd", 46 | "toevoeging": null, 47 | "type": "Standplaats", 48 | "woonplaats": "Amsterdam" 49 | }, 50 | "id": "11173" 51 | } 52 | ], 53 | "numberReturned": 1 54 | } 55 | -------------------------------------------------------------------------------- /internal/ogc/features/testdata/expected_webconfig_hyperlink_snippet.html: -------------------------------------------------------------------------------- 1 |
http://bag.basisregistraties.overheid.nl/bag/id/nummeraanduiding/0363200000428648 2 | -------------------------------------------------------------------------------- /internal/ogc/geovolumes/testdata/config_dtm.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | version: 1.0.2 3 | title: With DTM 4 | abstract: This is a minimal OGC API, with botch 3d Tiles and a Quantized Mesh DTM 5 | baseUrl: http://localhost:8080 6 | serviceIdentifier: dtm 7 | license: 8 | name: MIT 9 | url: https://www.tldrlegal.com/license/mit-license 10 | ogcApi: 11 | 3dgeovolumes: 12 | tileServer: http://localhost:9091 13 | collections: 14 | - id: container_1 # DTM and 3D tiles in same collection 15 | uriTemplate3dTiles: "tiles/{level}/{x}/{y}.glb" 16 | uriTemplateDTM: "dtm/tiles/{level}/{x}/{y}.terrain" 17 | - id: container_2 18 | uriTemplate3dTiles: "tiles2/{level}/{x}/{y}.i3d" 19 | - id: container_3 20 | uriTemplateDTM: "dtm/tiles/{level}/{x}/{y}.terrain" 21 | -------------------------------------------------------------------------------- /internal/ogc/geovolumes/testdata/config_minimal_3d.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | version: 1.0.2 3 | title: Minimal OGC API 4 | abstract: This is a minimal OGC API, offering only OGC API 3DGeoVolumes 5 | baseUrl: http://localhost:8080 6 | serviceIdentifier: Min 7 | license: 8 | name: MIT 9 | url: https://www.tldrlegal.com/license/mit-license 10 | ogcApi: 11 | 3dgeovolumes: 12 | tileServer: http://localhost:9091 13 | collections: 14 | - id: container_1 15 | uriTemplate3dTiles: "tiles/{level}/{x}/{y}.glb" 16 | - id: container_2 17 | uriTemplate3dTiles: "tiles2/{level}/{x}/{y}.i3d" 18 | -------------------------------------------------------------------------------- /internal/ogc/processes/main.go: -------------------------------------------------------------------------------- 1 | package processes 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/PDOK/gokoala/config" 7 | "github.com/PDOK/gokoala/internal/engine" 8 | ) 9 | 10 | type Processes struct { 11 | engine *engine.Engine 12 | } 13 | 14 | func NewProcesses(e *engine.Engine) *Processes { 15 | processes := &Processes{engine: e} 16 | e.Router.Handle("/jobs*", processes.forwarder(e.Config.OgcAPI.Processes.ProcessesServer)) 17 | e.Router.Handle("/processes*", processes.forwarder(e.Config.OgcAPI.Processes.ProcessesServer)) 18 | e.Router.Handle("/api*", processes.forwarder(e.Config.OgcAPI.Processes.ProcessesServer)) 19 | return processes 20 | } 21 | 22 | func (p *Processes) forwarder(processServer config.URL) http.HandlerFunc { 23 | return func(w http.ResponseWriter, r *http.Request) { 24 | targetURL := *processServer.URL 25 | targetURL.Path = processServer.URL.Path + r.URL.Path 26 | targetURL.RawQuery = r.URL.RawQuery 27 | p.engine.ReverseProxy(w, r, &targetURL, false, "") 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /internal/ogc/setup.go: -------------------------------------------------------------------------------- 1 | package ogc 2 | 3 | import ( 4 | "github.com/PDOK/gokoala/internal/engine" 5 | "github.com/PDOK/gokoala/internal/ogc/common/core" 6 | "github.com/PDOK/gokoala/internal/ogc/common/geospatial" 7 | "github.com/PDOK/gokoala/internal/ogc/features" 8 | "github.com/PDOK/gokoala/internal/ogc/geovolumes" 9 | "github.com/PDOK/gokoala/internal/ogc/processes" 10 | "github.com/PDOK/gokoala/internal/ogc/styles" 11 | "github.com/PDOK/gokoala/internal/ogc/tiles" 12 | ) 13 | 14 | func SetupBuildingBlocks(engine *engine.Engine) { 15 | // OGC Common Part 1, will always be started 16 | core.NewCommonCore(engine) 17 | 18 | // OGC Common part 2 19 | if engine.Config.HasCollections() { 20 | geospatial.NewCollections(engine) 21 | } 22 | // OGC 3D GeoVolumes API 23 | if engine.Config.OgcAPI.GeoVolumes != nil { 24 | geovolumes.NewThreeDimensionalGeoVolumes(engine) 25 | } 26 | // OGC Tiles API 27 | if engine.Config.OgcAPI.Tiles != nil { 28 | tiles.NewTiles(engine) 29 | } 30 | // OGC Styles API 31 | if engine.Config.OgcAPI.Styles != nil { 32 | styles.NewStyles(engine) 33 | } 34 | // OGC Features API 35 | if engine.Config.OgcAPI.Features != nil { 36 | features.NewFeatures(engine) 37 | } 38 | // OGC Processes API 39 | if engine.Config.OgcAPI.Processes != nil { 40 | processes.NewProcesses(engine) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /internal/ogc/styles/templates/style.go.html: -------------------------------------------------------------------------------- 1 | {{- /*gotype: github.com/PDOK/gokoala/internal/engine.TemplateData*/ -}} 2 | {{ define "content" }} 3 | {{ if .Params }} 4 | {{ $baseUrl := .Config.BaseURL }} 5 |
6 |

{{ .Config.Title }} - {{ .Params.Title }}

7 |
8 |
9 |
10 | {{ markdown .Params.Description }} 11 |

12 | {{ i18n "MapboxStyleText" }} 13 |

14 |
15 |
16 | 17 | 18 | 19 | 21 | 22 | 23 |
24 |
25 | {{end}} 26 | {{end}} 27 | -------------------------------------------------------------------------------- /internal/ogc/styles/testdata/config_legend.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | version: 1.0.2 3 | title: Minimal OGC API 4 | abstract: This is a minimal OGC API 5 | baseUrl: http://localhost:8080 6 | serviceIdentifier: Min 7 | resources: 8 | directory: internal/ogc/styles/testdata/resources 9 | license: 10 | name: MIT 11 | url: https://www.tldrlegal.com/license/mit-license 12 | ogcApi: 13 | # which OGC apis to enable. Possible values: tiles, styles, features, maps 14 | tiles: 15 | # base URL to webserver or object storage (e.g. azure blob or S3) 16 | # which hosts the tiles. 17 | tileServer: 18 | http://localhost:9090 19 | types: 20 | - vector 21 | supportedSrs: 22 | - srs: EPSG:28992 23 | zoomLevelRange: 24 | start: 0 25 | end: 12 26 | - srs: EPSG:3857 27 | zoomLevelRange: 28 | start: 0 29 | end: 30 30 | styles: 31 | default: default 32 | stylesDir: internal/ogc/styles/testdata/resources 33 | supportedStyles: 34 | - id: "default" 35 | title: "Test style with legend" 36 | legend: legend.png 37 | formats: 38 | - format: "mapbox" 39 | - id: "alternative" 40 | title: "Alternative test style without legend" 41 | formats: 42 | - format: "mapbox" -------------------------------------------------------------------------------- /internal/ogc/styles/testdata/config_minimal_styles.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | version: 1.0.2 3 | title: Minimal OGC API 4 | abstract: This is a minimal OGC API 5 | baseUrl: http://localhost:8080 6 | serviceIdentifier: Min 7 | license: 8 | name: MIT 9 | url: https://www.tldrlegal.com/license/mit-license 10 | ogcApi: 11 | # which OGC apis to enable. Possible values: tiles, styles, features, maps 12 | tiles: 13 | # base URL to webserver or object storage (e.g. azure blob or S3) 14 | # which hosts the tiles. 15 | tileServer: 16 | http://localhost:9090 17 | types: 18 | - vector 19 | supportedSrs: 20 | - srs: EPSG:28992 21 | zoomLevelRange: 22 | start: 0 23 | end: 12 24 | - srs: EPSG:3857 25 | zoomLevelRange: 26 | start: 0 27 | end: 30 28 | styles: 29 | default: default 30 | stylesDir: ./internal/ogc/styles/testdata/resources 31 | supportedStyles: 32 | - id: "default" 33 | title: "Test style" 34 | formats: 35 | - format: "mapbox" 36 | -------------------------------------------------------------------------------- /internal/ogc/styles/testdata/resources/alternative.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 8, 3 | "name": "Dummy Mapbox Style, just for testing purposes", 4 | "id": "alternative", 5 | "pitch": 50, 6 | "center": [ 7 | 0, 8 | 0 9 | ], 10 | "layers": [ 11 | { 12 | "filter": [ 13 | "all", 14 | [ 15 | "==", 16 | "status", 17 | "Testing" 18 | ] 19 | ], 20 | "id": "testing", 21 | "type": "line", 22 | "paint": { 23 | "line-color": "rgb(170, 170, 170)", 24 | "line-width": 200 25 | }, 26 | "source": "testing", 27 | "source-layer": "testing" 28 | } 29 | ], 30 | "sources": { 31 | "bag": { 32 | "type": "vector", 33 | "tiles": [ 34 | "{{ .Config.BaseURL }}/tiles/{{ .Params.Projection }}/{z}/{y}/{x}?f=mvt" 35 | ], 36 | "minzoom": {{ .Params.ZoomLevelRange.Start }}, 37 | "maxzoom": {{ .Params.ZoomLevelRange.End }} 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /internal/ogc/styles/testdata/resources/default.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 8, 3 | "name": "Dummy Mapbox Style, just for testing purposes", 4 | "id": "default", 5 | "pitch": 50, 6 | "center": [ 7 | 0, 8 | 0 9 | ], 10 | "layers": [ 11 | { 12 | "filter": [ 13 | "all", 14 | [ 15 | "==", 16 | "status", 17 | "Testing" 18 | ] 19 | ], 20 | "id": "testing", 21 | "type": "line", 22 | "paint": { 23 | "line-color": "rgb(170, 170, 170)", 24 | "line-width": 2 25 | }, 26 | "source": "testing", 27 | "source-layer": "testing" 28 | } 29 | ], 30 | "sources": { 31 | "bag": { 32 | "type": "vector", 33 | "tiles": [ 34 | "{{ .Config.BaseURL }}/tiles/{{ .Params.Projection }}/{z}/{y}/{x}?f=mvt" 35 | ], 36 | "minzoom": {{ .Params.ZoomLevelRange.Start }}, 37 | "maxzoom": {{ .Params.ZoomLevelRange.End }} 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /internal/ogc/styles/testdata/resources/legend.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PDOK/gokoala/7e31a0320491af419fb89300d02e154ce5e597f8/internal/ogc/styles/testdata/resources/legend.png -------------------------------------------------------------------------------- /internal/ogc/tiles/templates/tileMatrixSets.go.html: -------------------------------------------------------------------------------- 1 | {{- /*gotype: github.com/PDOK/gokoala/internal/engine.TemplateData*/ -}} 2 | {{define "content"}} 3 |
4 |

{{ .Config.Title }} - {{ i18n "TileMatrixSets" }}

5 |
6 |
7 |
8 |

9 | {{ if .Config.OgcAPI.Tiles.DatasetTiles }} 10 | {{ i18n "TileMatrixSetsDatasetText" }} 11 | {{ else if .Config.OgcAPI.Tiles.Collections }} 12 | {{ i18n "TileMatrixSetsCollectionText" }} 13 | {{ end }} 14 |

15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | {{ if .Config.OgcAPI.Tiles.HasProjection "EPSG:28992" }} 24 | 25 | 26 | 27 | 28 | {{end}} 29 | {{ if .Config.OgcAPI.Tiles.HasProjection "EPSG:3035" }} 30 | 31 | 32 | 33 | 34 | {{end}} 35 | {{ if .Config.OgcAPI.Tiles.HasProjection "EPSG:3857" }} 36 | 37 | 38 | 39 | 40 | {{end}} 41 | 42 |
Tile Matrix Set{{ i18n "DescriptionLabel" }}
NetherlandsRDNewQuadAmersfoort / RD New scheme for the Netherlands
EuropeanETRS89_LAEAQuadLambert Azimuthal Equal Area ETRS89 for Europe
WebMercatorQuadGoogle Maps Compatible for the World
43 |
44 |
45 | {{end}} 46 | -------------------------------------------------------------------------------- /internal/ogc/tiles/testdata/config_tiles_collectionlevel.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | version: 1.0.2 3 | title: OGC API 4 | abstract: This is an OGC API Tiles with tiles defined only at the collection-level 5 | baseUrl: http://localhost:8080 6 | serviceIdentifier: Min 7 | license: 8 | name: MIT 9 | url: https://www.tldrlegal.com/license/mit-license 10 | ogcApi: 11 | # which OGC apis to enable. Possible values: tiles, styles, features 12 | tiles: 13 | collections: 14 | - id: example 15 | metadata: 16 | title: Example 17 | # base URL to webserver or object storage (e.g. azure blob or S3) 18 | # which hosts the tiles. 19 | tileServer: 20 | http://localhost:9090 21 | types: 22 | - vector 23 | supportedSrs: 24 | - srs: EPSG:28992 25 | zoomLevelRange: 26 | start: 0 27 | end: 12 28 | - srs: EPSG:3857 29 | zoomLevelRange: 30 | start: 0 31 | end: 30 32 | styles: 33 | default: "some-default" 34 | stylesDir: /tmp 35 | supportedStyles: 36 | - id: "some-default" 37 | title: Default style 38 | formats: 39 | - format: mapbox 40 | -------------------------------------------------------------------------------- /internal/ogc/tiles/testdata/config_tiles_toplevel.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | version: 1.0.2 3 | title: Minimal OGC API 4 | abstract: This is an OGC API Tiles with tiles defined only at the top-level 5 | baseUrl: http://localhost:8080 6 | serviceIdentifier: Min 7 | license: 8 | name: MIT 9 | url: https://www.tldrlegal.com/license/mit-license 10 | ogcApi: 11 | # which OGC apis to enable. Possible values: tiles, styles, features 12 | tiles: 13 | # base URL to webserver or object storage (e.g. azure blob or S3) 14 | # which hosts the tiles. 15 | tileServer: 16 | http://localhost:9090 17 | types: 18 | - vector 19 | supportedSrs: 20 | - srs: EPSG:28992 21 | zoomLevelRange: 22 | start: 0 23 | end: 12 24 | - srs: EPSG:3857 25 | zoomLevelRange: 26 | start: 0 27 | end: 30 28 | styles: 29 | default: "some-default" 30 | stylesDir: /tmp 31 | supportedStyles: 32 | - id: "some-default" 33 | title: Default style 34 | formats: 35 | - format: mapbox 36 | -------------------------------------------------------------------------------- /internal/ogc/tiles/testdata/config_tiles_toplevel_and_collectionlevel.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | version: 1.0.2 3 | title: OGC API 4 | abstract: This is an OGC API Tiles with tiles defined at both top- and collection-level, without styles 5 | baseUrl: http://localhost:8080 6 | serviceIdentifier: Min 7 | license: 8 | name: MIT 9 | url: https://www.tldrlegal.com/license/mit-license 10 | ogcApi: 11 | # which OGC apis to enable. Possible values: tiles, styles, features 12 | tiles: 13 | # top-level tiles 14 | tileServer: 15 | http://localhost:9090 16 | types: 17 | - vector 18 | supportedSrs: 19 | - srs: EPSG:28992 20 | zoomLevelRange: 21 | start: 0 22 | end: 12 23 | - srs: EPSG:3857 24 | zoomLevelRange: 25 | start: 0 26 | end: 30 27 | # collection-level tiles 28 | collections: 29 | - id: example 30 | metadata: 31 | title: First Example 32 | tileServer: 33 | http://localhost:9090 34 | types: 35 | - vector 36 | supportedSrs: 37 | - srs: EPSG:3857 38 | zoomLevelRange: 39 | start: 0 40 | end: 30 41 | - id: example2 42 | metadata: 43 | title: Second Example 44 | tileServer: 45 | http://localhost:9090 46 | types: 47 | - vector 48 | supportedSrs: 49 | - srs: EPSG:28992 50 | zoomLevelRange: 51 | start: 0 52 | end: 12 53 | -------------------------------------------------------------------------------- /internal/ogc/tiles/testdata/config_tiles_urltemplate.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | version: 2.0.2 3 | title: OGC API 4 | abstract: This is an OGC API with a non-standard uriTemplateTiles 5 | baseUrl: http://localhost:8080 6 | serviceIdentifier: Min 7 | license: 8 | name: MIT 9 | url: https://www.tldrlegal.com/license/mit-license 10 | ogcApi: 11 | # which OGC apis to enable. Possible values: tiles, styles, features 12 | tiles: 13 | title: test 14 | abstract: test different abstract 15 | # base URL to webserver or object storage (e.g. azure blob or S3) 16 | # which hosts the tiles. 17 | tileServer: 18 | http://localhost:9090 19 | uriTemplateTiles: 20 | /foo/{tms}/{z}/{y}/{x} 21 | types: 22 | - vector 23 | supportedSrs: 24 | - srs: EPSG:3035 25 | zoomLevelRange: 26 | start: 0 27 | end: 14 28 | styles: 29 | default: "some-default" 30 | stylesDir: /tmp 31 | supportedStyles: 32 | - id: "some-default" 33 | title: Default style 34 | formats: 35 | - format: mapbox 36 | - format: sld10 37 | -------------------------------------------------------------------------------- /internal/ogc/tiles/tileMatrixSetLimits/EuropeanETRS89_LAEAQuad.yaml: -------------------------------------------------------------------------------- 1 | 0: 2 | minCol: 0 3 | maxCol: 0 4 | minRow: 0 5 | maxRow: 0 6 | 1: 7 | minCol: 0 8 | maxCol: 1 9 | minRow: 0 10 | maxRow: 1 11 | 2: 12 | minCol: 0 13 | maxCol: 3 14 | minRow: 0 15 | maxRow: 3 16 | 3: 17 | minCol: 0 18 | maxCol: 7 19 | minRow: 0 20 | maxRow: 7 21 | 4: 22 | minCol: 0 23 | maxCol: 15 24 | minRow: 0 25 | maxRow: 15 26 | 5: 27 | minCol: 0 28 | maxCol: 31 29 | minRow: 0 30 | maxRow: 31 31 | 6: 32 | minCol: 0 33 | maxCol: 63 34 | minRow: 0 35 | maxRow: 63 36 | 7: 37 | minCol: 0 38 | maxCol: 127 39 | minRow: 0 40 | maxRow: 127 41 | 8: 42 | minCol: 0 43 | maxCol: 255 44 | minRow: 0 45 | maxRow: 255 46 | 9: 47 | minCol: 0 48 | maxCol: 511 49 | minRow: 0 50 | maxRow: 511 51 | 10: 52 | minCol: 0 53 | maxCol: 1023 54 | minRow: 0 55 | maxRow: 1023 56 | 11: 57 | minCol: 0 58 | maxCol: 2047 59 | minRow: 0 60 | maxRow: 2047 61 | 12: 62 | minCol: 0 63 | maxCol: 4095 64 | minRow: 0 65 | maxRow: 4095 66 | 13: 67 | minCol: 0 68 | maxCol: 8191 69 | minRow: 0 70 | maxRow: 8191 71 | 14: 72 | minCol: 0 73 | maxCol: 16383 74 | minRow: 0 75 | maxRow: 16383 76 | 15: 77 | minCol: 0 78 | maxCol: 32767 79 | minRow: 0 80 | maxRow: 32767 81 | -------------------------------------------------------------------------------- /internal/ogc/tiles/tileMatrixSetLimits/NetherlandsRDNewQuad.yaml: -------------------------------------------------------------------------------- 1 | 0: 2 | minCol: 0 3 | maxCol: 0 4 | minRow: 0 5 | maxRow: 0 6 | 1: 7 | minCol: 0 8 | maxCol: 1 9 | minRow: 0 10 | maxRow: 1 11 | 2: 12 | minCol: 0 13 | maxCol: 3 14 | minRow: 0 15 | maxRow: 3 16 | 3: 17 | minCol: 0 18 | maxCol: 7 19 | minRow: 0 20 | maxRow: 7 21 | 4: 22 | minCol: 0 23 | maxCol: 15 24 | minRow: 0 25 | maxRow: 15 26 | 5: 27 | minCol: 0 28 | maxCol: 31 29 | minRow: 0 30 | maxRow: 31 31 | 6: 32 | minCol: 0 33 | maxCol: 63 34 | minRow: 0 35 | maxRow: 63 36 | 7: 37 | minCol: 0 38 | maxCol: 127 39 | minRow: 0 40 | maxRow: 127 41 | 8: 42 | minCol: 0 43 | maxCol: 255 44 | minRow: 0 45 | maxRow: 255 46 | 9: 47 | minCol: 0 48 | maxCol: 511 49 | minRow: 0 50 | maxRow: 511 51 | 10: 52 | minCol: 0 53 | maxCol: 1023 54 | minRow: 0 55 | maxRow: 1023 56 | 11: 57 | minCol: 0 58 | maxCol: 2047 59 | minRow: 0 60 | maxRow: 2047 61 | 12: 62 | minCol: 0 63 | maxCol: 4095 64 | minRow: 0 65 | maxRow: 4095 66 | 13: 67 | minCol: 0 68 | maxCol: 8191 69 | minRow: 0 70 | maxRow: 8191 71 | 14: 72 | minCol: 0 73 | maxCol: 16383 74 | minRow: 0 75 | maxRow: 16383 76 | 15: 77 | minCol: 0 78 | maxCol: 32767 79 | minRow: 0 80 | maxRow: 32767 81 | 16: 82 | minCol: 0 83 | maxCol: 65535 84 | minRow: 0 85 | maxRow: 65535 86 | -------------------------------------------------------------------------------- /tests/.gitignore: -------------------------------------------------------------------------------- 1 | # Node 2 | /node_modules 3 | npm-debug.log 4 | yarn-error.log 5 | 6 | # IDEs and editors 7 | .idea/ 8 | .vscode/ 9 | 10 | # Cypress 11 | cypress/videos 12 | cypress/screenshots 13 | downloads/ -------------------------------------------------------------------------------- /tests/README.md: -------------------------------------------------------------------------------- 1 | # End-to-end tests 2 | 3 | Besides unit- and integration tests (which are stored near the production code) we also use a couple of end-to-end tests. 4 | These tests are also part of the [CI workflow](../.github/workflows/e2e-test.yml). 5 | 6 | ## Cypress end-to-end tests 7 | 8 | The [cypress](./cypress/) directory holds [end-to-end tests](https://docs.cypress.io/guides/core-concepts/testing-types#What-is-E2E-Testing) written 9 | in Cypress targeted at a running GoKoala instance. 10 | 11 | > NOTE: The [viewer](../viewer/cypress) also contains Cypress tests, these are only focussed on viewer/map components. 12 | 13 | Run `npm run cypress:headless` in CI, run `npm run cypress:open` to author (new) tests. 14 | 15 | ## Cloud-backed GeoPackage smoke test 16 | 17 | See [OGC API Features example](../examples) involving the `config_features_azure.yaml` config file. 18 | 19 | ## OGC Compliance Validation 20 | 21 | GoKoala currently passes all OGC API Features and OGC API Tiles compliance tests. 22 | 23 | These are [validated on each PR in CI](.github/workflows/e2e-test.yml) using the OGC [TEAM Engine](https://github.com/opengeospatial/teamengine). 24 | More specifically using a CLI friendly version of this tool: 25 | 26 | - https://github.com/PDOK/ets-ogcapi-features10-docker 27 | - https://github.com/PDOK/ets-ogcapi-tiles10-docker 28 | 29 | -------------------------------------------------------------------------------- /tests/cypress.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "cypress"; 2 | import htmlvalidate from "cypress-html-validate/plugin"; 3 | 4 | export default defineConfig({ 5 | e2e: { 6 | baseUrl: 'http://localhost:8080', 7 | setupNodeEvents(on, config) { 8 | htmlvalidate.install(on, { 9 | rules: { 10 | "require-sri": "off", 11 | "element-permitted-content": "off" // only because we use RDFa breadcrumbs 12 | }, 13 | }); 14 | }, 15 | }, 16 | }); 17 | -------------------------------------------------------------------------------- /tests/cypress/e2e/features.cy.ts: -------------------------------------------------------------------------------- 1 | describe('OGC API Features tests', () => { 2 | 3 | // Fix for https://github.com/cypress-io/cypress/issues/1502#issuecomment-832403402 4 | Cypress.on("window:before:load", () => { 5 | cy.state("jQuery", Cypress.$); 6 | }); 7 | 8 | it('features page should have no a11y violations', () => { 9 | cy.visit('/collections/addresses/items') 10 | cy.injectAxe() 11 | cy.checkA11y() 12 | }) 13 | 14 | it("features page should have valid HTML", () => { 15 | cy.visit("/collections/addresses/items"); 16 | cy.htmlvalidate({ 17 | exclude: ["#featuremap"], // exclude viewer 18 | }) 19 | }) 20 | 21 | it('collection page should have no broken links', () => { 22 | cy.visit('/collections/addresses/items') 23 | cy.checkForBrokenLinks() 24 | }) 25 | 26 | it('feature page should have no a11y violations', () => { 27 | cy.visit('/collections/addresses/items/4285720b-1a60-50ce-b5fd-fc1c381bda0b') 28 | cy.injectAxe() 29 | cy.checkA11y() 30 | }) 31 | 32 | it("feature page should have valid HTML", () => { 33 | cy.visit('/collections/addresses/items/4285720b-1a60-50ce-b5fd-fc1c381bda0b'); 34 | cy.htmlvalidate({ 35 | exclude: ["#featuremap"], // exclude viewer 36 | }) 37 | }) 38 | 39 | it('feature page should have no broken links', () => { 40 | cy.visit('/collections/addresses/items/4285720b-1a60-50ce-b5fd-fc1c381bda0b') 41 | cy.checkForBrokenLinks() 42 | }) 43 | }) -------------------------------------------------------------------------------- /tests/cypress/e2e/styles.cy.ts: -------------------------------------------------------------------------------- 1 | describe('OGC API Styles tests', () => { 2 | 3 | // Fix for https://github.com/cypress-io/cypress/issues/1502#issuecomment-832403402 4 | Cypress.on("window:before:load", () => { 5 | cy.state("jQuery", Cypress.$); 6 | }); 7 | 8 | it('styles page should have no a11y violations', () => { 9 | cy.visit('/styles') 10 | cy.injectAxe() 11 | cy.checkA11y() 12 | }) 13 | 14 | it("styles page should have valid HTML", () => { 15 | cy.visit("/styles"); 16 | cy.htmlvalidate(); 17 | }) 18 | 19 | it('styles page should have no broken links', () => { 20 | cy.visit('/styles') 21 | cy.checkForBrokenLinks() 22 | }) 23 | 24 | it('style page should have no a11y violations', () => { 25 | cy.visit('/styles/dummy-style') 26 | cy.injectAxe() 27 | cy.checkA11y() 28 | }) 29 | 30 | it("style page should have valid HTML", () => { 31 | cy.visit("/styles/dummy-style"); 32 | cy.htmlvalidate(); 33 | }) 34 | 35 | it('style page should have no broken links', () => { 36 | cy.visit('/styles/dummy-style') 37 | cy.checkForBrokenLinks() 38 | }) 39 | 40 | it('styles metadata page should have no a11y violations', () => { 41 | cy.visit('/styles/dummy-style/metadata') 42 | cy.injectAxe() 43 | cy.checkA11y() 44 | }) 45 | 46 | it("styles metadata page should have valid HTML", () => { 47 | cy.visit("/styles/dummy-style/metadata"); 48 | cy.htmlvalidate(); 49 | }) 50 | 51 | it('styles metadata page should have no broken links', () => { 52 | cy.visit('/styles/dummy-style/metadata') 53 | cy.checkForBrokenLinks() 54 | }) 55 | }) -------------------------------------------------------------------------------- /tests/cypress/fixtures/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PDOK/gokoala/7e31a0320491af419fb89300d02e154ce5e597f8/tests/cypress/fixtures/.keep -------------------------------------------------------------------------------- /tests/cypress/support/commands.ts: -------------------------------------------------------------------------------- 1 | /// 2 | // *********************************************** 3 | // This example commands.ts shows you how to 4 | // create various custom commands and overwrite 5 | // existing commands. 6 | // 7 | // For more comprehensive examples of custom 8 | // commands please read more here: 9 | // https://on.cypress.io/custom-commands 10 | // *********************************************** 11 | // 12 | // 13 | // -- This is a parent command -- 14 | // Cypress.Commands.add('login', (email, password) => { ... }) 15 | // 16 | // 17 | // -- This is a child command -- 18 | // Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... }) 19 | // 20 | // 21 | // -- This is a dual command -- 22 | // Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... }) 23 | // 24 | // 25 | // -- This will overwrite an existing command -- 26 | // Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... }) 27 | // 28 | // declare global { 29 | // namespace Cypress { 30 | // interface Chainable { 31 | // login(email: string, password: string): Chainable 32 | // drag(subject: string, options?: Partial): Chainable 33 | // dismiss(subject: string, options?: Partial): Chainable 34 | // visit(originalFn: CommandOriginalFn, url: string, options: Partial): Chainable 35 | // } 36 | // } 37 | // } 38 | 39 | import 'cypress-axe' 40 | 41 | declare global { 42 | namespace Cypress { 43 | interface Chainable { 44 | checkForBrokenLinks(): Chainable 45 | } 46 | } 47 | } 48 | 49 | Cypress.Commands.add('checkForBrokenLinks', () =>{ 50 | cy.get('a').each(link => { 51 | const href = link.prop('href') 52 | if (href && !href.includes('example.com') && !href.includes('europa.eu') && !href.includes('opengis.net/spec')) { 53 | cy.request(href) 54 | } 55 | }) 56 | }) 57 | -------------------------------------------------------------------------------- /tests/cypress/support/e2e.ts: -------------------------------------------------------------------------------- 1 | // *********************************************************** 2 | // This example support/e2e.ts is processed and 3 | // loaded automatically before your test files. 4 | // 5 | // This is a great place to put global configuration and 6 | // behavior that modifies Cypress. 7 | // 8 | // You can change the location of this file or turn off 9 | // automatically serving support files with the 10 | // 'supportFile' configuration option. 11 | // 12 | // You can read more here: 13 | // https://on.cypress.io/configuration 14 | // *********************************************************** 15 | 16 | // Import commands.js using ES2015 syntax: 17 | import './commands' 18 | 19 | // Alternatively you can use CommonJS syntax: 20 | // require('./commands') 21 | import "cypress-html-validate/commands"; -------------------------------------------------------------------------------- /tests/cypress/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["es5", "dom"], 5 | "types": ["cypress", "node", "cypress-axe"] 6 | }, 7 | "include": ["**/*.ts"] 8 | } 9 | -------------------------------------------------------------------------------- /tests/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cypress", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "cypress:open": "cypress open", 8 | "cypress:headless": "cypress run" 9 | }, 10 | "author": "", 11 | "license": "MIT", 12 | "devDependencies": { 13 | "cypress": "^14.3.3", 14 | "cypress-axe": "^1.6.0", 15 | "cypress-html-validate": "^7.1.0", 16 | "html-validate": "^8.24.0", 17 | "typescript": "^5.3.3" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /viewer/.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see https://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.ts] 12 | quote_type = single 13 | 14 | [*.md] 15 | max_line_length = off 16 | trim_trailing_whitespace = false 17 | -------------------------------------------------------------------------------- /viewer/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "overrides": [ 4 | { 5 | "files": ["*.ts"], 6 | "extends": [ 7 | "eslint:recommended", 8 | "plugin:@typescript-eslint/recommended", 9 | "plugin:@angular-eslint/recommended", 10 | "plugin:@angular-eslint/template/process-inline-templates", 11 | "plugin:prettier/recommended" 12 | ], 13 | "rules": { 14 | "no-console": ["error"], 15 | "prettier/prettier": "error", 16 | "@angular-eslint/directive-selector": [ 17 | "error", 18 | { 19 | "type": "attribute", 20 | "prefix": "app", 21 | "style": "camelCase" 22 | } 23 | ], 24 | "@angular-eslint/component-selector": [ 25 | "error", 26 | { 27 | "type": "element", 28 | "prefix": "app", 29 | "style": "kebab-case" 30 | } 31 | ] 32 | } 33 | }, 34 | { 35 | "files": ["*.html"], 36 | "plugins": ["prettier"], 37 | "extends": [ 38 | "prettier", 39 | "plugin:@angular-eslint/template/recommended", 40 | "plugin:@angular-eslint/template/accessibility", 41 | "plugin:prettier/recommended" 42 | ], 43 | 44 | "rules": { 45 | "prettier/prettier": [ 46 | "error", 47 | { 48 | "parser": "angular", 49 | "singleQuote": true, 50 | "semi": false 51 | } 52 | ] 53 | } 54 | } 55 | ] 56 | } 57 | -------------------------------------------------------------------------------- /viewer/.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # Compiled output 4 | /dist 5 | /tmp 6 | /out-tsc 7 | /bazel-out 8 | 9 | # Node 10 | /node_modules 11 | npm-debug.log 12 | yarn-error.log 13 | 14 | # IDEs and editors 15 | .idea/ 16 | .project 17 | .classpath 18 | .c9/ 19 | *.launch 20 | .settings/ 21 | *.sublime-workspace 22 | 23 | # Visual Studio Code 24 | .vscode/* 25 | !.vscode/settings.json 26 | !.vscode/tasks.json 27 | !.vscode/launch.json 28 | !.vscode/extensions.json 29 | .history/* 30 | 31 | # Miscellaneous 32 | /.angular/cache 33 | .sass-cache/ 34 | /connect.lock 35 | /coverage 36 | /libpeerconnection.log 37 | testem.log 38 | /typings 39 | 40 | # System files 41 | .DS_Store 42 | Thumbs.db 43 | -------------------------------------------------------------------------------- /viewer/.prettierignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # Compiled output 4 | /dist 5 | /tmp 6 | /out-tsc 7 | /bazel-out 8 | 9 | # Node 10 | /node_modules 11 | npm-debug.log 12 | yarn-error.log 13 | 14 | # IDEs and editors 15 | .idea/ 16 | .project 17 | .classpath 18 | .c9/ 19 | *.launch 20 | .settings/ 21 | *.sublime-workspace 22 | 23 | # Visual Studio Code 24 | .vscode/* 25 | !.vscode/settings.json 26 | !.vscode/tasks.json 27 | !.vscode/launch.json 28 | !.vscode/extensions.json 29 | .history/* 30 | 31 | # Miscellaneous 32 | /.angular/cache 33 | .sass-cache/ 34 | /connect.lock 35 | /coverage 36 | /libpeerconnection.log 37 | testem.log 38 | /typings 39 | 40 | # System files 41 | .DS_Store 42 | Thumbs.db 43 | -------------------------------------------------------------------------------- /viewer/.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 2, 3 | "useTabs": false, 4 | "singleQuote": true, 5 | "semi": false, 6 | "bracketSpacing": true, 7 | "arrowParens": "avoid", 8 | "trailingComma": "es5", 9 | "bracketSameLine": true, 10 | "printWidth": 140, 11 | "endOfLine": "auto", 12 | "overrides": [ 13 | { 14 | "files": "*.html", 15 | "options": { 16 | "parser": "angular" 17 | } 18 | } 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /viewer/cypress.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'cypress' 2 | 3 | export default defineConfig({ 4 | component: { 5 | devServer: { 6 | framework: 'angular', 7 | bundler: 'webpack', 8 | }, 9 | specPattern: '**/*.cy.ts', 10 | }, 11 | 12 | e2e: { 13 | setupNodeEvents(on, config) { 14 | // implement node event listeners here 15 | }, 16 | }, 17 | }) 18 | -------------------------------------------------------------------------------- /viewer/cypress/README.md: -------------------------------------------------------------------------------- 1 | # Cypress Component Tests 2 | 3 | This directory holds [component-level tests](https://docs.cypress.io/guides/core-concepts/testing-types#What-is-Component-Testing) written in 4 | Cypress to test the GoKoala Viewer. 5 | 6 | > NOTE: For end-to-end tests, see [GoKoala end-to-end tests](../../tests). 7 | -------------------------------------------------------------------------------- /viewer/cypress/fixtures/172300.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PDOK/gokoala/7e31a0320491af419fb89300d02e154ce5e597f8/viewer/cypress/fixtures/172300.png -------------------------------------------------------------------------------- /viewer/cypress/fixtures/amsterdam-epgs28992.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "FeatureCollection", 3 | "features": [ 4 | { 5 | "id": 1, 6 | "type": "Feature", 7 | "geometry": { 8 | "type": "Point", 9 | "coordinates": [121329.8420159161, 487357.1123116203] 10 | }, 11 | "properties": { 12 | "name": "Dam Amsterdam", 13 | "geohack": "https://geohack.toolforge.org/geohack.php?language=nl¶ms=52_22_23_N_4_53_34_E_type:landmark_scale:3000_region:NL&pagename=Dam_(Amsterdam)" 14 | } 15 | }, 16 | { 17 | "id": 2, 18 | "type": "Feature", 19 | "geometry": { 20 | "type": "Point", 21 | 22 | "coordinates": [121863.87201149028, 488002.525313453] 23 | }, 24 | "properties": { 25 | "name": "Amsterdam Central station", 26 | "geohack": "https://geohack.toolforge.org/geohack.php?language=nl¶ms=52_22_44_N_4_54_2_E_type:landmark_scale:2000_region:NL&pagename=Station_Amsterdam_Centraal" 27 | } 28 | } 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /viewer/cypress/fixtures/amsterdam-epgs3035.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "FeatureCollection", 3 | "features": [ 4 | { 5 | "id": 1, 6 | "type": "Feature", 7 | "geometry": { 8 | "type": "Point", 9 | "coordinates": [3973495.0106637897, 3263707.2368934955] 10 | }, 11 | "properties": { 12 | "name": "Dam Amsterdam", 13 | "geohack": "https://geohack.toolforge.org/geohack.php?language=nl¶ms=52_22_23_N_4_53_34_E_type:landmark_scale:3000_region:NL&pagename=Dam_(Amsterdam)" 14 | } 15 | }, 16 | { 17 | "id": 2, 18 | "type": "Feature", 19 | "geometry": { 20 | "type": "Point", 21 | 22 | "coordinates": [3974068.86967292, 3264317.8242058353] 23 | }, 24 | "properties": { 25 | "name": "Amsterdam Central station", 26 | "geohack": "https://geohack.toolforge.org/geohack.php?language=nl¶ms=52_22_44_N_4_54_2_E_type:landmark_scale:2000_region:NL&pagename=Station_Amsterdam_Centraal" 27 | } 28 | } 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /viewer/cypress/fixtures/amsterdam-epsg4258.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "FeatureCollection", 3 | "features": [ 4 | { 5 | "id": 1, 6 | "type": "Feature", 7 | "geometry": { 8 | "type": "Point", 9 | "coordinates": [4.892778, 52.373056] 10 | }, 11 | "properties": { 12 | "name": "Dam Amsterdam", 13 | "geohack": "https://geohack.toolforge.org/geohack.php?language=nl¶ms=52_22_23_N_4_53_34_E_type:landmark_scale:3000_region:NL&pagename=Dam_(Amsterdam)" 14 | } 15 | }, 16 | { 17 | "id": 2, 18 | "type": "Feature", 19 | "geometry": { 20 | "type": "Point", 21 | 22 | "coordinates": [4.900556, 52.378889] 23 | }, 24 | "properties": { 25 | "name": "Amsterdam Central station", 26 | "geohack": "https://geohack.toolforge.org/geohack.php?language=nl¶ms=52_22_44_N_4_54_2_E_type:landmark_scale:2000_region:NL&pagename=Station_Amsterdam_Centraal" 27 | } 28 | } 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /viewer/cypress/fixtures/amsterdam-wgs84.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "FeatureCollection", 3 | "features": [ 4 | { 5 | "id": 1, 6 | "type": "Feature", 7 | "geometry": { 8 | "type": "Point", 9 | "coordinates": [4.892778, 52.373056] 10 | }, 11 | "properties": { 12 | "name": "Dam Amsterdam", 13 | "geohack": "https://geohack.toolforge.org/geohack.php?language=nl¶ms=52_22_23_N_4_53_34_E_type:landmark_scale:3000_region:NL&pagename=Dam_(Amsterdam)" 14 | } 15 | }, 16 | { 17 | "id": 2, 18 | "type": "Feature", 19 | "geometry": { 20 | "type": "Point", 21 | 22 | "coordinates": [4.900556, 52.378889] 23 | }, 24 | "properties": { 25 | "name": "Amsterdam Central station", 26 | "geohack": "https://geohack.toolforge.org/geohack.php?language=nl¶ms=52_22_44_N_4_54_2_E_type:landmark_scale:2000_region:NL&pagename=Station_Amsterdam_Centraal" 27 | } 28 | } 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /viewer/cypress/fixtures/amsterdam.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "FeatureCollection", 3 | "features": [ 4 | { 5 | "id": 1, 6 | "type": "Feature", 7 | "geometry": { 8 | "type": "Point", 9 | "coordinates": [4.895168, 52.370216] 10 | }, 11 | "properties": { 12 | "name": "Amsterdam" 13 | } 14 | } 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /viewer/cypress/fixtures/backgroundstub.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PDOK/gokoala/7e31a0320491af419fb89300d02e154ce5e597f8/viewer/cypress/fixtures/backgroundstub.png -------------------------------------------------------------------------------- /viewer/cypress/fixtures/teststyle-filter.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 8, 3 | "name": "teststyle", 4 | "id": "test style color function", 5 | "metadata": { 6 | "ol:webfonts": "https://test/resources/fonts/{font-family}/{fontweight}{-fontstyle}.css", 7 | "gokoala:title-items": "color,function" 8 | }, 9 | "layers": [ 10 | { 11 | "id": "Area print border", 12 | "type": "fill", 13 | "paint": { 14 | "fill-color": "rgb(255, 0, 0)", 15 | "fill-opacity": 0.1 16 | }, 17 | "filter": ["all", ["==", "function", "A"], ["==", "color", "red"]], 18 | "source": "test", 19 | "source-layer": "testArea" 20 | }, 21 | { 22 | "id": "Area label", 23 | "filter": ["all", ["==", "function", "A"], ["==", "color", "red"]], 24 | "type": "symbol", 25 | "paint": { 26 | "text-opacity": 1, 27 | "text-halo-width": 2, 28 | "text-color": "rgb(255, 0, 0)" 29 | }, 30 | "layout": { 31 | "symbol-placement": "point", 32 | "text-field": "{name}", 33 | "text-size": 12 34 | }, 35 | "source": "test", 36 | "source-layer": "testArea" 37 | }, 38 | { 39 | "id": "line", 40 | "filter": ["all", ["==", "function", "A"], ["==", "color", "red"]], 41 | "type": "line", 42 | "paint": { 43 | "line-color": "rgb(255, 0, 0)", 44 | "line-width": 2 45 | }, 46 | "source": "test", 47 | "source-layer": "testline" 48 | }, 49 | { 50 | "id": "circle", 51 | "filter": ["all", ["==", "function", "B"], ["==", "color", "green"]], 52 | "type": "circle", 53 | "source": "test", 54 | "source-layer": "testpoint", 55 | "paint": { 56 | "circle-color": "#7FDF0A", 57 | "circle-radius": 4.3, 58 | "circle-stroke-color": "#000000" 59 | } 60 | } 61 | ], 62 | "sources": { 63 | "test": { 64 | "type": "vector", 65 | "tiles": ["https://test/{z}/{y}/{x}?f=mvt"] 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /viewer/cypress/fixtures/teststyle-fonts.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 8, 3 | "name": "", 4 | "id": "data.example.com/dataset visualisation NetherlandsRDNewQuad", 5 | "metadata": { 6 | "ol:webfonts": "https://data.example.com/dataset/ogc/v1-demo/resources/fonts/{font-family}/{fontweight}{-fontstyle}.css", 7 | "gokoala:title-items": "id" 8 | }, 9 | "center": [], 10 | "pitch": 0, 11 | "sources": { 12 | "example": { 13 | "type": "vector", 14 | "tiles": ["https://data.example.com/dataset/ogc/v1-demo/tiles/NetherlandsRDNewQuad/{z}/{y}/{x}?f=mvt"], 15 | "minzoom": 12, 16 | "maxzoom": 12 17 | } 18 | }, 19 | "glyphs": "https://data.example.com/dataset/ogc/v1-demo/resources/fonts/{fontstack}/{range}.pbf", 20 | "layers": [ 21 | 22 | { 23 | "id": "a label", 24 | "type": "symbol", 25 | "source": "example", 26 | "source-layer": "example", 27 | "layout": { 28 | "text-field": "{somenummer}", 29 | "text-allow-overlap": true, 30 | "icon-allow-overlap": true, 31 | "text-size": ["step", ["zoom"], 6, 12, 8, 13, 8, 14, 9, 15, 12, 16, 14], 32 | "text-font": [ 33 | "Liberation Sans Italic", 34 | "system-ui", 35 | "Roboto", 36 | "Arial", 37 | "Noto Sans", 38 | "Liberation Sans", 39 | "sans-serif", 40 | "Noto Color Emoji" 41 | ] 42 | }, 43 | "paint": { 44 | "text-color": "#000000", 45 | "text-halo-color": "#FFFFFF", 46 | "text-halo-width": ["step", ["zoom"], 4, 12, 6, 13, 6, 14, 12, 15, 14, 16, 16] 47 | } 48 | } 49 | ] 50 | } 51 | -------------------------------------------------------------------------------- /viewer/cypress/fixtures/teststyle-id.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 8, 3 | "name": "teststyle", 4 | "id": "test style id", 5 | "metadata": { 6 | "ol:webfonts": "https://test/resources/fonts/{font-family}/{fontweight}{-fontstyle}.css", 7 | "gokoala:title-items": "id" 8 | }, 9 | "layers": [ 10 | { 11 | "id": "Area print border", 12 | "type": "fill", 13 | "paint": { 14 | "fill-color": "rgb(100, 100,100)", 15 | "fill-opacity": 0.1 16 | }, 17 | "source": "test", 18 | "source-layer": "testArea" 19 | }, 20 | { 21 | "id": "Area label", 22 | "type": "symbol", 23 | "paint": { 24 | "text-opacity": 1, 25 | "text-halo-width": 2, 26 | "text-color": "rgb(100,0,255)" 27 | }, 28 | "layout": { 29 | "symbol-placement": "point", 30 | "text-field": "{name}", 31 | "text-size": 12 32 | }, 33 | "source": "test", 34 | "source-layer": "testArea" 35 | }, 36 | { 37 | "id": "line", 38 | "type": "line", 39 | "paint": { 40 | "line-color": "rgb(255, 0, 0)", 41 | "line-width": 2 42 | }, 43 | "source": "test", 44 | "source-layer": "testline" 45 | }, 46 | { 47 | "id": "circle", 48 | "type": "circle", 49 | "source": "test", 50 | "source-layer": "testpoint", 51 | "paint": { 52 | "circle-color": "#B5DF0A", 53 | "circle-radius": 4.3, 54 | "circle-stroke-color": "#000000" 55 | } 56 | } 57 | ], 58 | "sources": { 59 | "test": { 60 | "type": "vector", 61 | "tiles": ["https://test/{z}/{y}/{x}?f=mvt"] 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /viewer/cypress/fixtures/teststyle.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 8, 3 | "name": "teststyle", 4 | "id": "test style no metadata", 5 | 6 | "layers": [ 7 | { 8 | "id": "Area print border", 9 | "type": "fill", 10 | "paint": { 11 | "fill-color": "rgb(100, 100,100)", 12 | "fill-opacity": 0.1 13 | }, 14 | "source": "test", 15 | "source-layer": "testArea" 16 | }, 17 | { 18 | "id": "Area label", 19 | "type": "symbol", 20 | "paint": { 21 | "text-opacity": 1, 22 | "text-halo-width": 2, 23 | "text-color": "rgb(100,0,255)" 24 | }, 25 | "layout": { 26 | "symbol-placement": "point", 27 | 28 | "text-field": "{name}", 29 | "text-size": 12 30 | }, 31 | "source": "test", 32 | "source-layer": "testArea" 33 | }, 34 | 35 | { 36 | "id": "line", 37 | "type": "line", 38 | "paint": { 39 | "line-color": "rgb(255, 0, 0)", 40 | "line-width": 2 41 | }, 42 | "source": "test", 43 | "source-layer": "testline" 44 | } 45 | ], 46 | 47 | "sources": { 48 | "test": { 49 | "type": "vector", 50 | "tiles": ["https://test/{z}/{y}/{x}?f=mvt"] 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /viewer/cypress/support/commands.ts: -------------------------------------------------------------------------------- 1 | /// 2 | // *********************************************** 3 | // This example commands.ts shows you how to 4 | // create various custom commands and overwrite 5 | // existing commands. 6 | // 7 | // For more comprehensive examples of custom 8 | // commands please read more here: 9 | // https://on.cypress.io/custom-commands 10 | // *********************************************** 11 | // 12 | // 13 | // -- This is a parent command -- 14 | /* Cypress.Commands.add('simulateOpenLayersEvent', (map, type, x, y) => { 15 | const viewport = map.getViewport() 16 | const position = viewport.getBoundingClientRect() 17 | cy.log(`left: ${position.left}, top: ${position.top}, width: ${position.width}, height: ${position.height}`) 18 | cy.get('canvas').trigger(type, { 19 | clientX: position.left + x + position.width / 2, 20 | clientY: position.top + y + position.height / 2, 21 | }) 22 | }) 23 | */ 24 | // Cypress.Commands.add('login', (email, password) => { ... }) 25 | // 26 | // 27 | // -- This is a child command -- 28 | // Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... }) 29 | // 30 | // 31 | // -- This is a dual command -- 32 | // Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... }) 33 | // 34 | // 35 | // -- This will overwrite an existing command -- 36 | // Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... }) 37 | // 38 | // declare global { 39 | // namespace Cypress { 40 | // interface Chainable { 41 | // login(email: string, password: string): Chainable 42 | // drag(subject: string, options?: Partial): Chainable 43 | // dismiss(subject: string, options?: Partial): Chainable 44 | // visit(originalFn: CommandOriginalFn, url: string, options: Partial): Chainable 45 | // } 46 | // } 47 | // } 48 | 49 | import 'cypress-axe' 50 | -------------------------------------------------------------------------------- /viewer/cypress/support/component-index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Components App 8 | 9 | 10 |
11 | 12 | 13 | -------------------------------------------------------------------------------- /viewer/cypress/support/component.ts: -------------------------------------------------------------------------------- 1 | // *********************************************************** 2 | // This example support/component.ts is processed and 3 | // loaded automatically before your test files. 4 | // 5 | // This is a great place to put global configuration and 6 | // behavior that modifies Cypress. 7 | // 8 | // You can change the location of this file or turn off 9 | // automatically serving support files with the 10 | // 'supportFile' configuration option. 11 | // 12 | // You can read more here: 13 | // https://on.cypress.io/configuration 14 | // *********************************************************** 15 | 16 | // Import commands.js using ES2015 syntax: 17 | import './commands' 18 | 19 | // Alternatively you can use CommonJS syntax: 20 | // require('./commands') 21 | 22 | import { mount } from 'cypress/angular' 23 | import { Options as AxeOptions, configureAxe, injectAxe } from 'cypress-axe' 24 | import * as axe from 'axe-core' 25 | 26 | // Augment the Cypress namespace to include type definitions for 27 | // your custom command. 28 | // Alternatively, can be defined in cypress/support/component.d.ts 29 | // with a at the top of your spec. 30 | 31 | declare const checkA11y: ( 32 | context?: string | Node | axe.ContextObject | undefined, 33 | options?: AxeOptions | undefined, 34 | violationCallback?: ((violations: axe.Result[]) => void) | undefined, 35 | skipFailures?: boolean 36 | ) => void 37 | declare global { 38 | // eslint-disable-next-line @typescript-eslint/no-namespace 39 | namespace Cypress { 40 | interface Chainable { 41 | mount: typeof mount 42 | injectAxe: typeof injectAxe 43 | configureAxe: typeof configureAxe 44 | checkA11y: typeof checkA11y 45 | } 46 | } 47 | } 48 | 49 | Cypress.Commands.add('mount', mount) 50 | 51 | // Example use: 52 | // cy.mount(MyComponent) 53 | -------------------------------------------------------------------------------- /viewer/cypress/support/e2e.ts: -------------------------------------------------------------------------------- 1 | // *********************************************************** 2 | // This example support/e2e.ts is processed and 3 | // loaded automatically before your test files. 4 | // 5 | // This is a great place to put global configuration and 6 | // behavior that modifies Cypress. 7 | // 8 | // You can change the location of this file or turn off 9 | // automatically serving support files with the 10 | // 'supportFile' configuration option. 11 | // 12 | // You can read more here: 13 | // https://on.cypress.io/configuration 14 | // *********************************************************** 15 | 16 | // Import commands.js using ES2015 syntax: 17 | import './commands' 18 | 19 | // Alternatively you can use CommonJS syntax: 20 | // require('./commands') 21 | -------------------------------------------------------------------------------- /viewer/cypress/support/index.d.ts: -------------------------------------------------------------------------------- 1 | declare namespace Cypress { 2 | interface Chainable {} 3 | } 4 | -------------------------------------------------------------------------------- /viewer/cypress/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["es5", "dom"], 5 | "types": ["cypress", "node", "cypress-axe"] 6 | }, 7 | "include": ["**/*.ts"] 8 | } 9 | -------------------------------------------------------------------------------- /viewer/examples/legend.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Legend Sample 7 | 8 | 9 | 10 | 11 | 12 | 13 |

legend sample 1

14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /viewer/src/app/feature-view/feature-view.component.css: -------------------------------------------------------------------------------- 1 | .featuremaparea { 2 | height: 100vh; 3 | } 4 | 5 | .maptooltip { 6 | visibility: hidden; 7 | font-weight: bold; 8 | margin: 1px; 9 | text-decoration: none; 10 | text-align: center; 11 | border-radius: 2px; 12 | font-size: 16px; 13 | background-color: white; 14 | border: 1px solid rgb(26, 30, 79); 15 | padding: 5px 10px; 16 | } 17 | -------------------------------------------------------------------------------- /viewer/src/app/feature-view/feature-view.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 | -------------------------------------------------------------------------------- /viewer/src/app/feature-view/fullboxcontrol.ts: -------------------------------------------------------------------------------- 1 | import { Control } from 'ol/control.js' 2 | 3 | import { EventEmitter } from '@angular/core' 4 | import { emitBox } from './boxcontrol' 5 | import { Geometry } from 'ol/geom' 6 | import { fromExtent } from 'ol/geom/Polygon' 7 | 8 | export class FullBoxControl extends Control { 9 | constructor( 10 | public boxEmitter: EventEmitter, 11 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 12 | optionalOptions: any 13 | ) { 14 | const options = optionalOptions || {} 15 | 16 | const button = document.createElement('button') 17 | 18 | const element = document.createElement('div') 19 | element.className = 'fullboxcontrol ol-unselectable ol-control' 20 | button.title = 'Get Features in map' 21 | element.appendChild(button) 22 | 23 | super({ 24 | element: element, 25 | target: options.target, 26 | }) 27 | 28 | button.innerHTML = ` 29 | ` 30 | 31 | button.addEventListener('click', this.addFullBox.bind(this), false) 32 | } 33 | 34 | addFullBox() { 35 | const map = this.getMap()! 36 | const extent = map.getView().calculateExtent(map.getSize()) 37 | const polygon = fromExtent(extent) as Geometry 38 | emitBox(map, polygon, this.boxEmitter) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /viewer/src/app/global-error-handler.service.ts: -------------------------------------------------------------------------------- 1 | import { HttpErrorResponse } from '@angular/common/http' 2 | import { ErrorHandler, Injectable } from '@angular/core' 3 | import { NGXLogger } from 'ngx-logger' 4 | import { Subject } from 'rxjs' 5 | 6 | export type ErrorDetail = { 7 | title: string 8 | detail: string 9 | error: unknown 10 | type: 'httpError' | 'unknownError' 11 | } 12 | 13 | @Injectable({ 14 | providedIn: 'root', 15 | }) 16 | export class GlobalErrorHandlerService implements ErrorHandler { 17 | initialErrorDetail: ErrorDetail = { 18 | title: 'unknown error', 19 | detail: 'unknown error', 20 | error: undefined, 21 | type: 'unknownError', 22 | } 23 | 24 | private _errorDetailSource = new Subject() 25 | public errorDetailStream$ = this._errorDetailSource.asObservable() 26 | 27 | constructor(private logger: NGXLogger) { 28 | this._errorDetailSource.next(this.initialErrorDetail) 29 | } 30 | 31 | handleError(error: unknown): void { 32 | const errorDetail: ErrorDetail = JSON.parse(JSON.stringify(this.initialErrorDetail)) 33 | 34 | if (error instanceof HttpErrorResponse) { 35 | this.logger.log('http error detected') 36 | const errorResponse = error as HttpErrorResponse 37 | this.logger.log(errorResponse.error) 38 | 39 | errorDetail.detail = errorResponse.error?.detail ?? 'No detail available' 40 | errorDetail.title = errorResponse.error?.title ?? 'No title available' 41 | errorDetail.type = 'httpError' 42 | } else { 43 | this.logger.error('error detected') 44 | this.logger.error(error) 45 | errorDetail.type = 'unknownError' 46 | } 47 | 48 | this._errorDetailSource.next(errorDetail) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /viewer/src/app/legend-view/legend-item/legend-item.component.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PDOK/gokoala/7e31a0320491af419fb89300d02e154ce5e597f8/viewer/src/app/legend-view/legend-item/legend-item.component.css -------------------------------------------------------------------------------- /viewer/src/app/legend-view/legend-item/legend-item.component.html: -------------------------------------------------------------------------------- 1 |
2 | -------------------------------------------------------------------------------- /viewer/src/app/legend-view/legend-view.component.css: -------------------------------------------------------------------------------- 1 | .legendContainer { 2 | display: flex; 3 | flex: 0 1 auto; 4 | flex-wrap: wrap; 5 | flex-direction: column; 6 | justify-content: flex-start; 7 | align-items: flex-start; 8 | max-height: 100%; 9 | padding: 5px; 10 | margin: 5px; 11 | min-height: 0; 12 | } 13 | 14 | .legendItem { 15 | max-width: 23vw; 16 | display: flex; 17 | flex-wrap: nowrap; 18 | flex-direction: row; 19 | align-items: center; 20 | flex: 0 1 auto; 21 | } 22 | 23 | .legendText { 24 | font-size: calc((var(--h) / 3)); 25 | margin-left: 1em; 26 | } 27 | -------------------------------------------------------------------------------- /viewer/src/app/legend-view/legend-view.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 |
{{ i.title }}
5 |
6 |
7 | -------------------------------------------------------------------------------- /viewer/src/app/link.ts: -------------------------------------------------------------------------------- 1 | export type Link = { 2 | /** 3 | * Supplies the URI to a remote resource (or resource fragment). 4 | */ 5 | href: string 6 | /** 7 | * A hint indicating what the language of the result of dereferencing the link should be. 8 | */ 9 | hreflang?: string 10 | length?: number 11 | /** 12 | * The type or semantics of the relation. 13 | */ 14 | rel: string 15 | /** 16 | * Use `true` if the `href` property contains a URI template with variables that needs to be substituted by values to get a URI 17 | */ 18 | templated?: boolean 19 | /** 20 | * Used to label the destination of a link such that it can be used as a human-readable identifier. 21 | */ 22 | title?: string 23 | /** 24 | * A hint indicating what the media type of the result of dereferencing the link should be. 25 | */ 26 | type?: string 27 | /** 28 | * Without this parameter you should repeat a link for each media type the resource is offered. 29 | * Adding this parameter allows listing alternative media types that you can use for this resource. The value in the `type` parameter becomes the recommended media type. 30 | */ 31 | types?: Array 32 | /** 33 | * A base path to retrieve semantic information about the variables used in URL template. 34 | */ 35 | varBase?: string 36 | } 37 | -------------------------------------------------------------------------------- /viewer/src/app/matrix-set.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core' 2 | import { HttpClient } from '@angular/common/http' 3 | import { Observable } from 'rxjs' 4 | import { Link } from './link' 5 | 6 | export interface MatrixSet { 7 | links: Link[] 8 | id: string 9 | title: string 10 | crs: string 11 | wellKnownScaleSet: string 12 | tileMatrices: TileMatrix[] 13 | orderedAxes: string[] 14 | } 15 | 16 | export interface TileMatrix { 17 | id: number 18 | tileWidth: number 19 | tileHeight: number 20 | matrixWidth: number 21 | matrixHeight: number 22 | scaleDenominator: number 23 | cellSize: number 24 | pointOfOrigin: number[] 25 | } 26 | 27 | export interface Matrix { 28 | title: string 29 | links: Link[] 30 | crs: string 31 | dataType: string 32 | tileMatrixSetId: string 33 | tileMatrixSetLimits: TileMatrixSetLimit[] 34 | } 35 | 36 | export interface TileMatrixSetLimit { 37 | tileMatrix: string 38 | minTileRow: number 39 | maxTileRow: number 40 | minTileCol: number 41 | maxTileCol: number 42 | } 43 | 44 | @Injectable({ 45 | providedIn: 'root', 46 | }) 47 | export class MatrixSetService { 48 | constructor(private http: HttpClient) {} 49 | getMatrixSet(url: string): Observable { 50 | return this.http.get(url) 51 | } 52 | 53 | getMatrix(url: string): Observable { 54 | return this.http.get(url) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /viewer/src/app/object-info/object-info.component.css: -------------------------------------------------------------------------------- 1 | .objectinfo { 2 | margin-top: 0; 3 | margin-bottom: 0; 4 | margin-right: 0; 5 | background-color: white; 6 | overflow-y: auto; 7 | overflow-x: auto; 8 | max-height: 98%; 9 | } 10 | 11 | .featuretable { 12 | text-align: left; 13 | } 14 | 15 | .featuretablecaption { 16 | text-align: left; 17 | font-weight: bold; 18 | } 19 | -------------------------------------------------------------------------------- /viewer/src/app/object-info/object-info.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |
4 | Object Informatie: 5 |
VeldWaarde
{{ f.title }}{{ f.value }}
17 |
18 | -------------------------------------------------------------------------------- /viewer/src/app/object-info/object-info.component.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectionStrategy, Component, Input, ViewEncapsulation } from '@angular/core' 2 | import { CommonModule } from '@angular/common' 3 | import RenderFeature from 'ol/render/Feature' 4 | import { WKT } from 'ol/format' 5 | 6 | type propRow = { 7 | title: string 8 | value: string 9 | } 10 | 11 | @Component({ 12 | selector: 'app-object-info', 13 | encapsulation: ViewEncapsulation.ShadowDom, 14 | imports: [CommonModule], 15 | changeDetection: ChangeDetectionStrategy.OnPush, 16 | templateUrl: './object-info.component.html', 17 | styleUrls: ['./object-info.component.css'], 18 | }) 19 | export class ObjectInfoComponent { 20 | @Input() feature!: RenderFeature 21 | 22 | public getFeatureProperties(): propRow[] { 23 | const propTable: propRow[] = [] 24 | if (this.feature) { 25 | const prop = this.feature.getProperties() 26 | 27 | for (const val in prop) { 28 | if (val !== 'mapbox-layer') { 29 | if (val === 'geometry') { 30 | const wktFormat = new WKT() 31 | const wktString = wktFormat.writeGeometry(prop[val]) 32 | const geop: propRow = { title: val, value: wktString } 33 | propTable.push(geop) 34 | } else { 35 | const p: propRow = { title: val, value: prop[val] } 36 | propTable.push(p) 37 | } 38 | } 39 | } 40 | 41 | return propTable 42 | } else { 43 | return [] 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /viewer/src/app/vectortile-view/vectortile-view.component.css: -------------------------------------------------------------------------------- 1 | .maparea { 2 | z-index: 1; 3 | position: relative; 4 | display: flex; 5 | background: white; 6 | } 7 | 8 | .toprightpanel { 9 | position: absolute; 10 | flex: 1; 11 | top: 6%; 12 | right: 1%; 13 | z-index: 2; 14 | max-height: 90%; 15 | max-width: 40%; 16 | height: 100%; 17 | } 18 | -------------------------------------------------------------------------------- /viewer/src/app/vectortile-view/vectortile-view.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 |
5 |
6 | -------------------------------------------------------------------------------- /viewer/src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PDOK/gokoala/7e31a0320491af419fb89300d02e154ce5e597f8/viewer/src/assets/.gitkeep -------------------------------------------------------------------------------- /viewer/src/environments/environment.development.ts: -------------------------------------------------------------------------------- 1 | import { NgxLoggerLevel } from 'ngx-logger' 2 | 3 | export const environment = { 4 | bgtBackgroundUrl: 'https://service.pdok.nl/brt/achtergrondkaart/wmts/v2_0?', 5 | loglevel: NgxLoggerLevel.DEBUG, 6 | } 7 | -------------------------------------------------------------------------------- /viewer/src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | import { NgxLoggerLevel } from 'ngx-logger' 2 | export const environment = { 3 | bgtBackgroundUrl: 'https://service.pdok.nl/brt/achtergrondkaart/wmts/v2_0?', 4 | loglevel: NgxLoggerLevel.OFF, 5 | } 6 | -------------------------------------------------------------------------------- /viewer/src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PDOK/gokoala/7e31a0320491af419fb89300d02e154ce5e597f8/viewer/src/favicon.ico -------------------------------------------------------------------------------- /viewer/src/main.ts: -------------------------------------------------------------------------------- 1 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic' 2 | 3 | import { AppModule } from './app/app.module' 4 | 5 | platformBrowserDynamic() 6 | .bootstrapModule(AppModule) 7 | // eslint-disable-next-line no-console 8 | .catch((err: unknown) => console.error(err)) 9 | -------------------------------------------------------------------------------- /viewer/src/styles.css: -------------------------------------------------------------------------------- 1 | .boundingboxcontrol { 2 | top: 60px; 3 | left: 0.5em; 4 | 5 | .innersvg { 6 | padding: 3px; 7 | } 8 | } 9 | 10 | .fullboxcontrol { 11 | top: 84px; 12 | left: 0.5em; 13 | 14 | .innersvg { 15 | padding: 3px; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /viewer/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "./tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "./out-tsc/app", 6 | "types": [] 7 | }, 8 | "files": ["src/main.ts"], 9 | "include": ["src/**/*.d.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /viewer/tsconfig.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "compileOnSave": false, 4 | "compilerOptions": { 5 | "skipLibCheck": true, 6 | "baseUrl": "./", 7 | "outDir": "./dist/out-tsc", 8 | "esModuleInterop": true, 9 | "forceConsistentCasingInFileNames": true, 10 | "strict": true, 11 | "noImplicitOverride": true, 12 | "noPropertyAccessFromIndexSignature": true, 13 | "noImplicitReturns": true, 14 | "noFallthroughCasesInSwitch": true, 15 | "sourceMap": true, 16 | "declaration": false, 17 | "experimentalDecorators": true, 18 | "moduleResolution": "node", 19 | "importHelpers": true, 20 | "target": "ES2022", 21 | "module": "ES2022", 22 | "useDefineForClassFields": false, 23 | "lib": ["ES2022", "dom"], 24 | "types": ["cypress", "node"] 25 | }, 26 | "angularCompilerOptions": { 27 | "enableI18nLegacyMessageIdFormat": false, 28 | "strictInjectionParameters": true, 29 | "strictInputAccessModifiers": true, 30 | "strictTemplates": true 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /viewer/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "./tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "./out-tsc/spec", 6 | "types": ["jasmine"] 7 | }, 8 | "include": ["src/**/*.spec.ts", "src/**/*.d.ts"] 9 | } 10 | --------------------------------------------------------------------------------