Skip to content

Commit

Permalink
Merge pull request #201 from PDOK/PDOK/downloads-in-features
Browse files Browse the repository at this point in the history
Downloads in features
  • Loading branch information
rkettelerij authored Jun 7, 2024
2 parents b7e11ce + 722fd43 commit cdb13d9
Show file tree
Hide file tree
Showing 13 changed files with 351 additions and 126 deletions.
3 changes: 1 addition & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,7 @@ datasets? Spin up a separate instance/container.
- [OGC API Features](https://ogcapi.ogc.org/features/) supports part 1 and part 2 of the spec. Serves features as HTML, GeoJSON or JSON-FG
from GeoPackages in multiple projections. No on-the-fly re-projections are applied, separate GeoPackages should
be configured ahead-of-time in each projection. Features can be served from local and/or
[Cloud-Backed](https://sqlite.org/cloudsqlite/doc/trunk/www/index.wiki) GeoPackages. Support for
property and temporal filter(s) is available.
[Cloud-Backed](https://sqlite.org/cloudsqlite/doc/trunk/www/index.wiki) GeoPackages. Support for property and temporal filter(s) is available.
- [OGC API Tiles](https://ogcapi.ogc.org/tiles/) serves HTML, JSON and TileJSON metadata. Act as a proxy in front
of a vector tiles engine (like Trex, Tegola, Martin) of your choosing. Currently, 3
projections (RD, ETRS89 and WebMercator) are supported.
Expand Down
3 changes: 3 additions & 0 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -499,6 +499,9 @@ type MapSheetDownloadProperties struct {

// Property containing file media type
MediaType MediaType `yaml:"mediaType" json:"mediaType" validate:"required"`

// Property containing the map sheet identifier
MapSheetID string `yaml:"mapSheetId" json:"mapSheetId" validate:"required"`
}

// +kubebuilder:object:generate=true
Expand Down
9 changes: 9 additions & 0 deletions internal/engine/template.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"path/filepath"
"strings"
texttemplate "text/template"
"time"

"github.com/PDOK/gokoala/config"
"github.com/docker/go-units"
Expand Down Expand Up @@ -38,6 +39,7 @@ func init() {
"unmarkdown": unmarkdown,
"humansize": humanSize,
"bytessize": bytesSize,
"isdate": isDate,
}
sprigFuncs := sprig.FuncMap() // we also support https://github.com/go-task/slim-sprig functions
globalTemplateFuncs = combineFuncMaps(customFuncs, sprigFuncs)
Expand Down Expand Up @@ -321,3 +323,10 @@ func bytesSize(s string) int64 {
}
return i
}

func isDate(v any) bool {
if _, ok := v.(time.Time); ok {
return true
}
return false
}
2 changes: 1 addition & 1 deletion internal/engine/templates/layout.go.html
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@
{{ end }}
{{ range $formatKey, $formatName := .AvailableFormats }}
<li class="breadcrumb-item"><a href="{{ empty $.Breadcrumbs | ternary "" $lastcrumb.Path }}{{ $.QueryString $formatKey }}" target="_blank"
aria-label="{{ i18n "SwitchFormat" }} {{ $formatName }}">{{ $formatName }}</a></li>
aria-label="{{ i18n "SwitchFormat" }} {{ $formatName }}" id="format-{{ $formatKey }}">{{ $formatName }}</a></li>
{{ end }}
</ol>
</nav>
Expand Down
21 changes: 17 additions & 4 deletions internal/ogc/features/templates/feature.go.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
{{define "content"}}
{{ $cfg := .Config }}
{{ $baseUrl := $cfg.BaseURL }}
{{ $mapSheetProperties := .Params.MapSheetProperties }}
<script>
function crsChange(value) {
const url = new URL(window.location.href);
Expand Down Expand Up @@ -39,15 +40,18 @@ <h1 class="title h2" id="title">{{ .Config.Title }} - {{ if and .Params.Metadata
</tr>
</thead>
<tbody>
<!-- For map sheets collection, skip properties pertaining to downloads, then add download button -->
{{ $mapSheetProperties := .Params.MapSheetProperties }}
{{/* for map sheets collection, skip some properties pertaining to downloads, then add download button */}}
{{ if $mapSheetProperties }}
{{ $skipKeys := list $mapSheetProperties.AssetURL $mapSheetProperties.Size }}
{{ range $key, $value := .Params.Properties }}
{{ if not (has $key $skipKeys) }}
<tr>
<td class="w-25">{{ $key }}</td>
{{ if isdate $value }}
<td>{{ $value | date "2006/01/02 15:04:05" }}</td>
{{ else }}
<td>{{ $value }}</td>
{{ end }}
</tr>
{{ end }}
{{ end }}
Expand All @@ -56,12 +60,16 @@ <h1 class="title h2" id="title">{{ .Config.Title }} - {{ if and .Params.Metadata
Download
</a> ({{ i18n "Size" }}: {{ humansize (get .Params.Properties $mapSheetProperties.Size) }})</td>
</tr>
<!-- For other collections, show all properties -->
{{/* for other collections, show all properties */}}
{{ else }}
{{ range $key, $value := .Params.Properties }}
<tr>
<td class="w-25">{{ $key }}</td>
{{ if isdate $value }}
<td>{{ $value | date "2006/01/02 15:04:05" }}</td>
{{ else }}
<td>{{ $value }}</td>
{{ end }}
</tr>
{{ end }}
{{ end }}
Expand All @@ -74,7 +82,12 @@ <h1 class="title h2" id="title">{{ .Config.Title }} - {{ if and .Params.Metadata
<script type="text/javascript" src="view-component/main.js"></script>
<script type="text/javascript" src="view-component/polyfills.js"></script>
<script type="text/javascript" src="view-component/runtime.js"></script>
<app-feature-view id="viewer" background-map="{{ $cfg.OgcAPI.Features.Basemap }}" show-bounding-box-button="false"></app-feature-view>
{{/* different viewer settings depending on whether features are map sheets or not */}}
{{ if $mapSheetProperties }}
<app-feature-view id="viewer" background-map="{{ $cfg.OgcAPI.Features.Basemap }}" mode="auto" fill-color="rgba(0,0,255,0)"></app-feature-view>
{{ else }}
<app-feature-view id="viewer" background-map="{{ $cfg.OgcAPI.Features.Basemap }}" mode="default" show-bounding-box-button="false"></app-feature-view>
{{ end }}
<script type="module">
const url = new URL(window.location.href)
url.searchParams.set('f', 'json');
Expand Down
84 changes: 61 additions & 23 deletions internal/ogc/features/templates/features.go.html
Original file line number Diff line number Diff line change
Expand Up @@ -6,21 +6,26 @@

<script>
{{- /* generic function to update query string parameters */ -}}
function updateQueryString(name, value) {
const url = new URL(window.location.href);
url.searchParams.delete('cursor'); // when filters change, we can't continue pagination.
if (value) {
if (name === 'datetime') {
url.searchParams.set(name, new Date(value).toISOString()); // input is %Y-%m-%d, but parameter value should be RFC3339
} else {
url.searchParams.set(name, value);
}
function updateQueryString(name, value, skipReload) {
const url = new URL(window.location.href)
url.searchParams.delete('cursor') // when filters change, we can't continue pagination.
if (value) {
if (name === 'datetime') {
url.searchParams.set(name, new Date(value).toISOString()) // input is %Y-%m-%d, but parameter value should be RFC3339
} else {
url.searchParams.delete(name);
url.searchParams.set(name, value)
}
window.location.href = url.toString();
}
} else {
url.searchParams.delete(name)
}

if (skipReload) {
window.history.pushState({}, '', url.toString()) // only change the url but don't reload the page
} else {
window.location.href = url.toString()
}
return url.toString()
}
</script>

<hgroup>
Expand Down Expand Up @@ -122,22 +127,47 @@ <h2 class="card-header h5">
<script type="text/javascript" src="view-component/main.js"></script>
<script type="text/javascript" src="view-component/polyfills.js"></script>
<script type="text/javascript" src="view-component/runtime.js"></script>
<app-feature-view id="viewer" background-map="{{ $cfg.OgcAPI.Features.Basemap }}" class="card"></app-feature-view>
{{/* different viewer settings depending on whether features are map sheets or not */}}
{{ if $mapSheetProperties }}
<app-feature-view id="viewer" background-map="{{ $cfg.OgcAPI.Features.Basemap }}" mode="auto" fill-color="rgba(0,0,255,0)"
label-field="{{ $mapSheetProperties.MapSheetID }}" class="card"></app-feature-view>
{{ else }}
<app-feature-view id="viewer" background-map="{{ $cfg.OgcAPI.Features.Basemap }}" mode="default" class="card"></app-feature-view>
{{ end }}
<script type="module">
const url = new URL(window.location.href)
url.searchParams.set('f','json');
const viewer = document.getElementById('viewer');
viewer.setAttribute('items-url', url);
url.searchParams.set('f', 'json')
const viewer = document.getElementById('viewer')
viewer.setAttribute('items-url', url)

if (url.searchParams.get('crs') !== null) {
let crs = url.searchParams.get('crs');
document.getElementById('srs-select').value = crs;
let crs = url.searchParams.get('crs')
document.getElementById('srs-select').value = crs
viewer.setAttribute('projection', crs)
}

viewer.addEventListener('box', selectbox => {
updateQueryString("bbox", selectbox.detail)
{{ if $mapSheetProperties }}
viewer.addEventListener('box', selectBox => {
let newUrl = new URL(updateQueryString('bbox', selectBox.detail, true))
// when moving the map to load additional sheets we don't want to do a full page reload (like we
// do when one draws a bbox). Therefor we update the browser URL + link references (like GeoJSON/JSON-FG)
// on the page manually.
viewer.setAttribute('items-url', newUrl.toString())
{{ range $formatKey, $formatName := .AvailableFormats }}
if (document.getElementById("format-{{ $formatKey }}")) {
newUrl.searchParams.set('f', '{{ $formatKey }}')
document.getElementById("format-{{ $formatKey }}").setAttribute('href', newUrl.toString())
}
{{ end }}
})
viewer.addEventListener('activeFeature', activeFeature => {
updateQueryString("{{ $mapSheetProperties.MapSheetID }}", activeFeature.detail.get('{{ $mapSheetProperties.MapSheetID }}'))
})
{{ else }}
viewer.addEventListener('box', selectBox => {
updateQueryString('bbox', selectBox.detail)
})
{{ end }}
</script>
<noscript>Enable Javascript to view features on a map</noscript>
</div>
Expand Down Expand Up @@ -174,14 +204,18 @@ <h2 class="card-header h5">
</tr>
</thead>
<tbody>
<!-- For map sheets collection, skip properties pertaining to downloads, then add download button -->
{{/* for map sheets collection, skip some properties pertaining to downloads, then add download button */}}
{{ if $mapSheetProperties }}
{{ $skipKeys := list $mapSheetProperties.AssetURL $mapSheetProperties.Size }}
{{ $skipKeys := list $mapSheetProperties.Size }}
{{ range $key, $value := $feat.Properties }}
{{ if not (has $key $skipKeys) }}
<tr>
<td class="w-20">{{ $key }}</td>
{{ if isdate $value }}
<td>{{ $value | date "2006/01/02 15:04:05" }}</td>
{{ else }}
<td>{{ $value }}</td>
{{ end }}
</tr>
{{ end }}
{{ end }}
Expand All @@ -190,12 +224,16 @@ <h2 class="card-header h5">
Download
</a> ({{ i18n "Size" }}: {{ humansize (get $feat.Properties $mapSheetProperties.Size) }})</td>
</tr>
<!-- For other collections, show all properties -->
{{/* for other collections, show all properties */}}
{{ else }}
{{ range $key, $value := $feat.Properties }}
<tr>
<td class="w-20">{{ $key }}</td>
{{ if isdate $value }}
<td>{{ $value | date "2006/01/02 15:04:05" }}</td>
{{ else }}
<td>{{ $value }}</td>
{{ end }}
</tr>
{{ end }}
{{ end }}
Expand Down
55 changes: 32 additions & 23 deletions viewer/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,30 +8,41 @@ This viewer is available as a [WebComponent](https://developer.mozilla.org/en-US

See [demo with samples](https://pdok.github.io/gokoala/).

## Embedding a vectortile map
## Parameters for vectortile view

Build the project and copy the style and javascript files into the application embedding the download component
The `<app-vectortile-view>` component has the following parameters:

Embed the webcomponent 'app-vectortile-view' in your web application
- **tileUrl** : Url to OGC vector tile service
- **styleUrl** Url to vector Mapbox tile style.
- **id** id for map
- **zoom** initial zoom level

- load styles and javascript in your html
The following values are emitted:

```html
<link rel="stylesheet" type="text/css" href="view-component/styles.css" />
<script type="text/javascript" src="view-component/main.js"></script>
<script type="text/javascript" src="view-component/polyfills.js"></script>
<script type="text/javascript" src="view-component/runtime.js"></script>
- **currentZoomLevel**
- **activeFeature**
- **activeTileUrl**
- **centerX**
- **centerY**

<app-vectortile-view
style="width: 800px; height: 600px;"
tile-url="https://api.pdok.nl/lv/bag/ogc/v0_1/tiles/NetherlandsRDNewQuad"
zoom="12"
center-x="5.3896944"
center-y="52.1562499">
</app-vectortile-view>
```
## Feature view parameters

## Embedding a vectortile legend
The `<app-feature-view>` component has the following parameters

- **itemsUrl**: A OGC API url as dataset for the features to show
- **backgroundMap**: Openstreetmap is used as default backgroundmap. Use value "BRT"to use Dutch "brt achtergrondkaart" as background
- **fillColor**: fill color (hex or RBG) for the features If not specified is used 'rgba(0,0,255)' use e.g. "rgba(0,0,255,0)" for a transparent fill
- **strokeColor**: Stroke color of the feature default color is '#3399CC'
- **mode**: Operation mode is 'default' or 'auto'. If 'auto' is used the bounding box of the view is emitted as boundingbox, and no buttons are visible.
- **showBoundingBoxButton**: in default mode the boundingbox select button is showed, hide 'show-bounding-box-button' is needed
- **showFillExtentButton**: in default mode the button to fill the view with features is not showed. Activate 'show-fill-extent-button' is needed
- **projection**: projection in opengis style e.g. '<http://www.opengis.net/def/crs/EPSG/0/4258>'
- **labelField**: field is show as label and feature is clickable. if not specified a popup is shown when hovering over feature

The following values are emitted:

- **box**
- **activeFeature**

```html
<link rel="stylesheet" type="text/css" href="view-component/styles.css" />
Expand All @@ -45,17 +56,15 @@ Embed the webcomponent 'app-vectortile-view' in your web application
</app-legend-view>
```

# Legend Parameters
## Legend Parameters

The legend has the following parameters:
The `<app-legend-view>` component has the following parameters:

- **style-url**: This is the URL to the Mapbox style that serves as input for the legend.

- **title-items**: By default, the source layer names are used to name legend items. However, this parameter can be used to split legend items based on different attributes.

Default layers are used for legend items. Attributes can be specified to create distinct items. For example, for the Dutch BGT, `titleItems = "type,plus_type,functie,fysiek_voorkomen,openbareruimtetype"` can be used. When `titleItems = "id"` is used, the "id" for the layer (layer name) is used to name the legend items.

Legend uses is shown in the [example directory](../examples/)
Legend uses is shown in the [example directory](./examples)

## Development server

Expand Down
2 changes: 1 addition & 1 deletion viewer/cypress/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Cypress Component Tests

This directory holds [component-level tests](https://docs.cypress.io/guides/core-concepts/testing-types#What-is-Component-Testing) written in
This directory holds [component-level tests](https://docs.cypress.io/guides/core-concepts/testing-types#What-is-Component-Testing) written in
Cypress to test the GoKoala Viewer.

> NOTE: For end-to-end tests, see [GoKoala end-to-end tests](../../tests).
25 changes: 16 additions & 9 deletions viewer/src/app/feature-view/boxcontrol.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,25 @@
import { Control } from 'ol/control.js'
import { Map } from 'ol'
import { Draw } from 'ol/interaction'
import { createBox } from 'ol/interaction/Draw'
import VectorSource from 'ol/source/Vector'

import { EventEmitter } from '@angular/core'
import { Fill, Stroke, Style } from 'ol/style'
import VectorLayer from 'ol/layer/Vector'
import { Geometry } from 'ol/geom'

export function emitBox(map: Map, geometry: Geometry, boxEmitter: EventEmitter<string>) {
if (map.getView().getProjection().getCode() === 'EPSG:3857') {
const box84 = geometry.transform(map.getView().getProjection(), 'EPSG:4326').getExtent()
const extString = box84.join(',')
boxEmitter.emit(extString)
} else {
const box = geometry.getExtent()
const extString = box.join(',')
boxEmitter.emit(extString)
}
}

export class boxControl extends Control {
/**
Expand Down Expand Up @@ -49,16 +63,9 @@ export class boxControl extends Control {
const map = this.getMap()!
const bbox = e.feature //this is the feature fired the event
const bboxGeometry = bbox.getGeometry()

if (bboxGeometry) {
if (map.getView().getProjection().getCode() === 'EPSG:3857') {
const box84 = bboxGeometry.transform(map.getView().getProjection(), 'EPSG:4326').getExtent()
const extString = box84.join(',')
this.boxEmitter.emit(extString)
} else {
const box = bboxGeometry.getExtent()
const extString = box.join(',')
this.boxEmitter.emit(extString)
}
emitBox(map, bboxGeometry, this.boxEmitter)

const bboxStyle = new Style({
stroke: new Stroke({
Expand Down
Loading

0 comments on commit cdb13d9

Please sign in to comment.