Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions design_decisions/data_sources/barcelona.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,7 @@ Barcelona data sets that can be used for testing.
- API Key request form: https://www.ambmobilitat.cat/Principales/OpenData.aspx
- Swagger documentation: https://api.ambmobilitat.cat/v1/swagger-ui.html
- **Format**: JSON

## Demo

Scripts that validate and profile the AMB GTFS feed and an EDC workflow that publishes it as a data product: [`barcelona_demo/`](barcelona_demo/README.md).
32 changes: 32 additions & 0 deletions design_decisions/data_sources/barcelona_demo/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Barcelona demo: AMB bus GTFS through the data space

An end-to-end demo of the AMB bus GTFS feed listed in [`../barcelona.md`](../barcelona.md):
validate and profile the feed, then publish, discover, negotiate and transfer it through EDC
connectors using the same request shapes as test
[2.2.3.1A](../../../tests/data_product_publication/publication/publication_on_emds_catalogue/test_2_2_3_1a/test.md).

| File | What it does | Run where |
| --- | --- | --- |
| `validate_gtfs.sh` | Checks required files, record counts, referential integrity and feed validity dates | Anywhere with `sh`, `awk`, `sort`, `comm` |
| `profile_gtfs.sh` | Operators, busiest lines, wheelchair-accessible stops, modes | Same |
| `bcn-amb-gtfs.workflow.yaml` | EDC management API calls: asset, policy, contract definition, catalogue, negotiation, transfer | Bruno or curl, against a provider and a consumer connector |

## Run the data scripts

```sh
curl -sL -o amb_gtfs.zip https://www.ambmobilitat.cat/OpenData/google_transit.zip
unzip -o -q amb_gtfs.zip -d gtfs
sh validate_gtfs.sh gtfs
sh profile_gtfs.sh gtfs
```

Run on 2026-10-21 against feed version 20261021 (8,295,351 bytes,
sha256 `bc87858b215c1bdf9069a593a0247edd848ad66fb910d5a651aac64823cddc3b`): validation PASS,
11 operators, 140 routes, 4,970 stops, 30,145 trips, 950,579 stop times.

## Connector steps

The workflow file reuses the request shapes from test 2.2.3.1A. It needs a running provider and
consumer connector and their management API key. These steps have not been executed against a
live connector yet; fill in the hosts, the key and the MobilityDCAT-AP theme URI from your own
environment, and never commit them.
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
# Barcelona demo: publish the AMB bus GTFS feed as an EDC data product and consume it.
# Same request shapes as the sanitized Bruno workflow in
# tests/data_product_publication/publication/publication_on_emds_catalogue/test_2_2_3_1a.
# Fill in the hosts and the management API key from your own environment; never commit them.

variables:
provider: "<provider-host>"
consumer: "<consumer-host>"
asset-id: "bcn-amb-gtfs-bus"
policy-id: "bcn-amb-open-use"
contract-id: "bcn-amb-gtfs-offer"

requests:
- name: 1 Create asset (provider)
method: POST
url: https://{{provider}}/api/management/v3/assets
expected_status: 200
body:
"@id": "{{asset-id}}"
"properties":
"dct:title": "AMB metropolitan bus timetables (GTFS)"
"dct:description": "Scheduled services of the Barcelona metropolitan bus network"
"dct:publisher":
"foaf:name": "AMB Mobilitat"
"dct:format": "GTFS"
"mobilitydcatap:mobilityTheme": "<public-transport mobility theme URI>"
"dqv:hasQualityAnnotation": "validate_gtfs.sh: PASS (0 orphan references)"
"dataAddress":
"edc:type": "HttpData"
"edc:baseUrl": "https://www.ambmobilitat.cat/OpenData/google_transit.zip"

- name: 2 Create policy (provider)
method: POST
url: https://{{provider}}/api/management/v3/policydefinitions
expected_status: 200
body:
"@id": "{{policy-id}}"
"policy":
"@type": "odrl:Set"
"permission":
- "odrl:action": "odrl:use"
"prohibition": []
"obligation": []

- name: 3 Create contract definition (provider)
method: POST
url: https://{{provider}}/api/management/v3/contractdefinitions
expected_status: 200
body:
"@id": "{{contract-id}}"
"accessPolicyId": "{{policy-id}}"
"contractPolicyId": "{{policy-id}}"
"assetsSelector":
"operandLeft": "https://w3id.org/edc/v0.0.1/ns/id"
"operator": "="
"operandRight": "{{asset-id}}"

- name: 4 Query catalogue (consumer)
method: POST
url: https://{{consumer}}/api/management/v3/catalog/request
expected_status: 200
body:
"counterPartyAddress": "https://{{provider}}/api/dsp"
"counterPartyId": "did:web:<provider-host>"
"protocol": "dataspace-protocol-http"
"querySpec":
"offset": 0
"limit": 100
# assert: a dcat:dataset with @id == {{asset-id}} is present; save its odrl:hasPolicy @id as offer-id

- name: 5 Start contract negotiation (consumer)
method: POST
url: https://{{consumer}}/api/management/v3/contractnegotiations
expected_status: 200
body:
"@type": "ContractRequest"
"counterPartyAddress": "https://{{provider}}/api/dsp"
"protocol": "dataspace-protocol-http"
"policy":
"@id": "{{offer-id}}"
"@type": "odrl:Offer"
"assigner": "did:web:<provider-host>"
"target": "{{asset-id}}"
# poll GET /v3/contractnegotiations/{{negotiation-id}} until state == FINALIZED; save contractAgreementId

- name: 6 Start transfer (consumer)
method: POST
url: https://{{consumer}}/api/management/v3/transferprocesses
expected_status: 200
body:
"@type": "TransferRequest"
"counterPartyAddress": "https://{{provider}}/api/dsp"
"protocol": "dataspace-protocol-http"
"contractId": "{{agreement-id}}"
"transferType": "HttpData-PULL"
# poll until state == STARTED, then GET /v3/edrs/{{transfer-id}}/dataaddress for endpoint + token,
# download through the provider data plane and compare its sha256 with the source feed
22 changes: 22 additions & 0 deletions design_decisions/data_sources/barcelona_demo/profile_gtfs.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
#!/usr/bin/env sh
# Profile a GTFS feed: the quick analysis a consumer runs after the transfer.
# Usage: ./profile_gtfs.sh <gtfs-dir>
set -eu
d="${1:-gtfs}"

echo "== Operators (routes per agency)"
awk -F, 'NR==FNR{if(FNR>1)name[$1]=$2;next} FNR>1{c[$2]++} END{for(a in c)print c[a]"\t"name[a]}' \
"$d/agency.txt" "$d/routes.txt" | sort -rn | awk -F'\t' '{printf " %-24s %3d routes\n",$2,$1}'

echo "== Busiest lines (scheduled trips in feed window)"
awk -F'"' 'NR>1{split($0,f,",");print f[1]"\t"$2"\t"$4}' "$d/routes.txt" > /tmp/routes.$$
awk -F, 'NR>1{c[$1]++} END{for(r in c)print r"\t"c[r]}' "$d/trips.txt" \
| sort -t"$(printf '\t')" -k2 -rn | head -8 \
| awk -F'\t' 'NR==FNR{n[$1]=$2" "$3;next}{printf " %-42s %5d trips\n",n[$1],$2}' /tmp/routes.$$ -
rm -f /tmp/routes.$$

echo "== Accessibility"
awk -F, 'NR>1{n++; if($(NF-1)==1)w++} END{printf " wheelchair-boarding stops: %d of %d (%.0f%%)\n",w,n,100*w/n}' "$d/stops.txt"

echo "== Modes"
awk -F, 'NR==1{for(i=1;i<=NF;i++)if($i=="route_type")t=i} NR>1{c[$t]++} END{for(k in c)printf " route_type %s: %d routes%s\n",k,c[k],(k==3?" (bus)":"")}' "$d/routes.txt"
36 changes: 36 additions & 0 deletions design_decisions/data_sources/barcelona_demo/validate_gtfs.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
#!/usr/bin/env sh
# Validate an unpacked GTFS feed before it is registered as an EDC asset.
# Usage: ./validate_gtfs.sh <gtfs-dir>
set -eu
d="${1:-gtfs}"
fail=0

echo "== Required files"
for f in agency stops routes trips stop_times; do
if [ -f "$d/$f.txt" ]; then echo " ok $f.txt"; else echo " MISS $f.txt"; fail=1; fi
done
if [ -f "$d/calendar.txt" ] || [ -f "$d/calendar_dates.txt" ]; then echo " ok calendar(_dates).txt"; else echo " MISS calendar"; fail=1; fi

echo "== Record counts"
for f in agency routes stops trips stop_times; do
printf ' %-11s %8d\n' "$f" "$(($(wc -l < "$d/$f.txt") - 1))"
done

echo "== Referential integrity"
tmp=$(mktemp -d)
awk -F, 'NR>1{print $1}' "$d/routes.txt" | sort -u > "$tmp/routes"
awk -F, 'NR>1{print $1}' "$d/stops.txt" | sort -u > "$tmp/stops"
awk -F, 'NR>1{print $3}' "$d/trips.txt" | sort -u > "$tmp/trips"
r=$(awk -F, 'NR>1{print $1}' "$d/trips.txt" | sort -u | comm -23 - "$tmp/routes" | wc -l)
s=$(awk -F, 'NR>1{print $4}' "$d/stop_times.txt" | sort -u | comm -23 - "$tmp/stops" | wc -l)
t=$(awk -F, 'NR>1{print $1}' "$d/stop_times.txt" | sort -u | comm -23 - "$tmp/trips" | wc -l)
rm -rf "$tmp"
echo " trips -> routes orphans: $r"
echo " stop_times -> stops orphans: $s"
echo " stop_times -> trips orphans: $t"
[ "$r$s$t" = "000" ] || fail=1

echo "== Feed validity"
awk -F, 'NR==2{printf " publisher %s, valid %s to %s, version %s\n",$1,$4,$5,$6}' "$d/feed_info.txt"

if [ "$fail" -eq 0 ]; then echo "RESULT: PASS"; else echo "RESULT: FAIL"; exit 1; fi
20 changes: 20 additions & 0 deletions design_decisions/data_sources/stockholm.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
Stockholm data sets that can be used for testing, from SLB-analys (City of Stockholm).

# SLB-analys

## Air quality time series

- **URL**: <https://open.slb.nu/api/timeseries/v1.1b/get-timeseries/>
- **Type**: Open Data (public access key `lvl1`)
- **Format**: JSON (`format=FME`), hourly values
- **Example**: station `Norrlandsgatan 21` (street canyon, urban traffic), parameters `NO2,PM10`

## Noise

- **Source**: EnviroSuite noise meters on Sveavägen and Norrlandsgatan
- **Type**: API accessible via API key (held by SLB-analys; do not commit keys)
- **Format**: JSON, daily Lday (dB)

## Demo

Scripts that fetch and check the air data against the WHO guidelines, and an EDC workflow in which SLB shares air and noise data with Trafikkontoret for low emission zone follow-up: [`stockholm_demo/`](stockholm_demo/README.md).
28 changes: 28 additions & 0 deletions design_decisions/data_sources/stockholm_demo/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Stockholm LEZ demo: SLB air and noise data for Trafikkontoret

SLB-analys (data provider) shares street-level NO2, PM10 and noise with Trafikkontoret (TK, data
consumer) to follow up the low emission zone. TK uses the data in the Hus för hus service.
Data sources are listed in [`../stockholm.md`](../stockholm.md); the connector requests reuse the shapes from test
[2.2.3.1A](../../../tests/data_product_publication/publication/publication_on_emds_catalogue/test_2_2_3_1a/test.md).

| File | What it does |
| --- | --- |
| `fetch_slb.sh` | Downloads hourly NO2 and PM10 for one station from the open SLB API (`open.slb.nu`) |
| `analyse_slb.sh` | Hours, mean, peak, latest, busiest hour and days above the WHO 2021 24 h guidelines (NO2 25, PM10 45 µg/m³) |
| `slb-tk-lez.workflow.yaml` | EDC calls: air and noise assets, a purpose-bound policy for TK, catalogue, negotiation, transfer |

## Run

```sh
sh fetch_slb.sh 14 "Norrlandsgatan 21"
sh analyse_slb.sh slb.json
```

Run on 2026-10-21 (07–21 Oct, 675 hourly records): NO2 mean 11.8 µg/m³, peak 44.3 on 19 Oct
11:00, above the WHO 24 h guideline on 1 of 13 full days; PM10 mean 9.1 µg/m³, 0 days above.

## Notes

- The connector steps have not been run against live connectors; hosts, DIDs and keys are placeholders.
- Noise comes from EnviroSuite and needs an API key. Keep it in the provider connector's vault
(`envirosuite-key`), never in a browser page or this repository.
38 changes: 38 additions & 0 deletions design_decisions/data_sources/stockholm_demo/analyse_slb.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
#!/usr/bin/env sh
# Summarise an SLB timeseries response against the WHO 2021 guidelines.
# Usage: ./analyse_slb.sh [slb.json]
set -eu
f="${1:-slb.json}"

# One record per line: parameter <tab> local datetime <tab> value
tr '{' '\n' < "$f" | awk '
match($0,/"parameter":"[^"]*"/) { p=substr($0,RSTART+13,RLENGTH-14) }
match($0,/"value":[-0-9.]+/) { v=substr($0,RSTART+8,RLENGTH-8) }
match($0,/"Datetime local":"[^"]*"/) { t=substr($0,RSTART+18,19) }
p!="" && v!="" && t!="" { print p "\t" t "\t" v; p=v=t="" }' > records.tsv

echo "== Station"
grep -o '"station":"[^"]*","kommun":"[^"]*","stationstyp":"[^"]*"' "$f" | head -1 | tr -d '"' | sed 's/,/ /g'

awk -F'\t' '
BEGIN { who["NO2"]=25; who["PM10"]=45 }
{ n[$1]++; s[$1]+=$3
if (!($1 in mx) || $3>mx[$1]) { mx[$1]=$3; mxt[$1]=$2 }
if ($2>lt[$1]) { lt[$1]=$2; lv[$1]=$3 }
if (!($1 in first) || $2<first[$1]) first[$1]=$2
d=substr($2,1,10); ds[$1 SUBSEP d]+=$3; dn[$1 SUBSEP d]++
h=substr($2,12,2); hs[$1 SUBSEP h]+=$3; hn[$1 SUBSEP h]++ }
END {
for (p in n) {
days=0; above=0
for (k in dn) { split(k,a,SUBSEP); if (a[1]==p && dn[k]>=18) { days++; if (ds[k]/dn[k]>who[p]) above++ } }
best=-1; for (k in hn) { split(k,a,SUBSEP); if (a[1]==p && hs[k]/hn[k]>best) { best=hs[k]/hn[k]; bh=a[2] } }
printf "== %s (ug/m3)\n", p
printf " hours %d (from %s)\n", n[p], substr(first[p],1,16)
printf " mean %.1f\n", s[p]/n[p]
printf " peak hour %.1f at %s\n", mx[p], substr(mxt[p],1,16)
printf " latest %.1f at %s\n", lv[p], substr(lt[p],1,16)
printf " busiest hour %s:00 local, mean %.1f\n", bh, best
printf " days above WHO 24h guideline (%d): %d of %d\n", who[p], above, days
}
}' records.tsv | sed 's/ug\/m3/µg\/m³/'
15 changes: 15 additions & 0 deletions design_decisions/data_sources/stockholm_demo/fetch_slb.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
#!/usr/bin/env sh
# Fetch hourly NO2 and PM10 for one SLB station from the open SLB API.
# Usage: ./fetch_slb.sh [days] [station] (defaults: 14, "Norrlandsgatan 21")
set -eu
days="${1:-14}"
station="${2:-Norrlandsgatan 21}"
from=$(date -u -d "$days days ago" '+%Y-%m-%d %H:%M')

curl -s --max-time 60 -G "https://open.slb.nu/api/timeseries/v1.1b/get-timeseries/" \
--data-urlencode "key=lvl1" \
--data-urlencode "stations=$station" \
--data-urlencode "parameters=NO2,PM10" \
--data-urlencode "format=FME" \
--data-urlencode "from_datetime=$from" \
-o slb.json -w "HTTP %{http_code} %{size_download} bytes from $from\n"
Loading
Loading