Example LDES Implementation Guide
How to consume the mobility hindrance LDES?
In order to consume an LDES, there is an LDES client needed. We will explain how the hindrance objects can be consumed as JSON objects with Javascript.
We created a demo script that shows how to parse the Gipod ID, description, time schedule, time period, status, WKT geometry and consequences.
First, create a ‘test.js’ file with following content:
const fs = require('fs');
const RRule = require('rrule').RRule;
const RRuleset = require('rrule').RRuleSet;
const pxd = require('parse-xsd-duration');
const context = JSON.parse(fs.readFileSync('./context.jsonld').toString());
const newEngine = require('@treecg/actor-init-ldes-client').newEngine;
let eventstreamSync; // LDES client
const FOLDER_OF_STATE = `./.ldes`;
const LOCATION_OF_STATE = `./.ldes/state.json`;
// When the consequences of a hindrance zone matches one of below list, the hindrance is returned
// Otherwise the hindrance is filtered
let interestedConsequences = [
"api/v1/taxonomies/mobility-hindrance/consequencetypes/e4ea1344-aa27-40e8-b5af-94ec5f7956f8",
"api/v1/taxonomies/mobility-hindrance/consequencetypes/8eda1611-902b-4c9a-8b3c-4c23a49d7c5d",
"api/v1/taxonomies/mobility-hindrance/consequencetypes/3c9d3c6e-c5bf-477b-a102-d12654ce5ef0",
"api/v1/taxonomies/mobility-hindrance/consequencetypes/6a71c816-511f-490c-9248-c68ded67ecd9",
"api/v1/taxonomies/mobility-hindrance/consequencetypes/c53813ab-814f-4ff4-8a87-6934c72e175f",
"api/v1/taxonomies/mobility-hindrance/consequencetypes/4981fd46-9536-415b-bd30-e53c0cedd799",
"api/v1/taxonomies/mobility-hindrance/consequencetypes/6dd14722-79ad-4d25-aa0e-91e7fc53877b",
"api/v1/taxonomies/mobility-hindrance/consequencetypes/23c9463a-c199-4db3-a8ce-40a088066cb3",
"api/v1/taxonomies/mobility-hindrance/consequencetypes/cd1bbb8c-503f-4968-8483-01a4c2092d51",
"api/v1/taxonomies/mobility-hindrance/consequencetypes/5f8ff25a-87c7-47c8-9332-35d4fabf4b07",
"api/v1/taxonomies/mobility-hindrance/consequencetypes/7cbd0430-f6d4-4c74-8b8e-21b677e6b3d7",
"api/v1/taxonomies/mobility-hindrance/consequencetypes/10c5101d-31fb-4909-a022-d76f868f7f50",
"api/v1/taxonomies/mobility-hindrance/consequencetypes/ee31fd67-b75e-4499-9ad4-0a595717a9c7",
"api/v1/taxonomies/mobility-hindrance/consequencetypes/5e5a1a0b-eaab-4b98-a5c8-6a4664cdb909",
"api/v1/taxonomies/mobility-hindrance/consequencetypes/e52587c7-5566-4c1c-889c-7fcc947e4c4b",
"api/v1/taxonomies/mobility-hindrance/consequencetypes/b922f580-68d3-471d-8712-8175417ad769",
"api/v1/taxonomies/mobility-hindrance/consequencetypes/427bc6a6-619c-482d-934c-bda986df18d1"
];
// If run takes longer than 50 minutes, pause the LDES Client
const TIMEOUT = 3000000; // milliseconds
// After pausing, the state will be written to LOCATION_OF_STATE
// When restarting, the state will be read from LOCATION_OF_STATE
setTimeout(() => {
console.log("Timeout - Pausing the LDES client to save state.");
eventstreamSync.pause();
}, TIMEOUT);
// Pause when exiting with CTRL+C
// process.on('SIGTERM', function() {
// console.log("Caught interrupt signal. Pausing the LDES client to save state.");
// eventstreamSync.pause();
// });
start();
async function start() {
await harvest();
}
function harvest() {
return new Promise((resolve, reject) => {
try {
let url = "https://private-api.gipod.beta-vlaanderen.be/api/v1/ldes/mobility-hindrances";
let options = {
"representation": "Object", //Object or Quads
"emitMemberOnce": true,
"disableSynchronization": true,
"jsonLdContext": context,
"processedURIsCount": 15000
};
let LDESClient = new newEngine();
// Retrieve state
const state = loadState();
if (state === null) {
eventstreamSync = LDESClient.createReadStream(url, options);
} else {
eventstreamSync = LDESClient.createReadStream(url, options, state);
}
eventstreamSync.on('data', (member) => {
const object = member.object;
console.log("test: " + member.id)
if (hindranceHasInterestingConsequence(member.object)) {
if (object.gipodId && object.gipodId.value) {
const gipodId = object.gipodId.value;
console.log("Gipod ID: " + gipodId);
}
if (object.createdOn) {
// Use this timestamp to update your database with the latest version (version materialisation)
console.log("Created on: " + object.createdOn);
}
const description = object.description;
console.log("Description: " + description);
let hindranceStillActive = false;
if (object.timeSchedule) {
for (let schedule of object.timeSchedule) {
console.log("Schedule: " + JSON.stringify(schedule));
const periodsFromSchedule = processSchedule(schedule);
for (let p of periodsFromSchedule) {
const start = p.start;
const end = p.end;
console.log("Time period: " + start + " - " + end);
if (new Date().getTime() < new Date(end).getTime()) hindranceStillActive = true;
}
}
}
if (object.period) {
for (let p of object.period) {
const start = p.start;
const end = p.end;
console.log("Time period: " + start + " - " + end);
if (new Date().getTime() < new Date(end).getTime()) hindranceStillActive = true;
}
}
if (hindranceStillActive) {
console.log("Hindrance is still active now or in the future");
}
if (object.status && object.status.prefLabel) {
console.log("Status: " + object.status.prefLabel);
}
for (let z of object.zone) {
const geometry = z['geometry'].wkt;
console.log("WKT geometry: " + geometry);
if (z.consequence && Array.isArray(z.consequence)) {
for (let con of z.consequence) {
if (con.prefLabel) {
console.log("Consequence: " + con.prefLabel);
}
}
} else if (z.consequence) {
if (z.consequence.prefLabel) {
console.log("Consequence: " + z.consequence.prefLabel);
}
}
}
}
resolve();
});
eventstreamSync.on('pause', () => {
console.log("Paused!")
// Export current state, but only when paused!
let state = eventstreamSync.exportState();
saveState(state);
});
eventstreamSync.on('end', () => {
console.log("No more data!");
// Save state
let state = eventstreamSync.exportState();
saveState(state);
resolve();
});
} catch (e) {
console.error(e);
reject(e);
}
});
}
function hindranceHasInterestingConsequence(object) {
if (object.zone) {
for (let z of object.zone) {
if (z.consequence && Array.isArray(z.consequence)) {
for (let con of z.consequence) {
if (interestedConsequences.includes(con.id)) {
return true;
}
}
} else if (z.consequence) {
if (interestedConsequences.includes(z.consequence.id)) {
return true;
}
}
}
}
return false;
}
function saveState(clientState) {
if (!fs.existsSync(FOLDER_OF_STATE)) {
fs.mkdirSync(FOLDER_OF_STATE, { recursive: true });
}
fs.writeFileSync(`${LOCATION_OF_STATE}`, JSON.stringify(clientState));
}
function loadState() {
if (fs.existsSync(LOCATION_OF_STATE)) {
return JSON.parse(fs.readFileSync(LOCATION_OF_STATE).toString());
}
return null;
}
function processSchedule(schedule) {
const frequency = schedule.repeatFrequency ? schedule.repeatFrequency : "unknown";
const repeatCount = schedule.repeatCount ? schedule.repeatCount : "unknown"; // Number of times it occurs.
const startDate = schedule.startDate ? schedule.startDate : "unknown";
const endDate = schedule.endDate ? schedule.endDate : "unknown";
const exceptDateArray = schedule.exceptDate ? schedule.exceptDate : [];
const byDay = schedule.byDay ? schedule.byDay : []; // Days of the month it takes place.
const byMonthDay = schedule.byMonthDay ? schedule.byMonthDay : []; // Days of the month it takes place.
const duration = schedule.duration ? schedule.duration : "unknown"; // How long an event specified by this schedule will last.
const startTime = schedule.startTime ? schedule.startTime : "unknown";
let rule = {
"interval": 1
};
//// Add frequency
switch (frequency) {
case 'P1W':
rule["freq"] = RRule.WEEKLY;
break;
case 'P1D':
rule["freq"] = RRule.DAILY;
break;
case 'P1Y':
rule["freq"] = RRule.YEARLY;
break;
case 'P1M':
rule["freq"] = RRule.MONTHLY;
break;
}
//// Add count
if (repeatCount != "unknown") rule["count"] = repeatCount;
//// Add weekday
let byWeekday = [];
for (let day of byDay) {
switch (day) {
case 'http://schema.org/Monday':
byWeekday.push(RRule.MO);
break;
case 'http://schema.org/Tuesday':
byWeekday.push(RRule.TU);
break;
case 'http://schema.org/Wednesday':
byWeekday.push(RRule.WE);
break;
case 'http://schema.org/Thursday':
byWeekday.push(RRule.TH);
break;
case 'http://schema.org/Friday':
byWeekday.push(RRule.FR);
break;
case 'http://schema.org/Saturday':
byWeekday.push(RRule.SA);
break;
case 'http://schema.org/Sunday':
byWeekday.push(RRule.SU);
break;
}
}
rule["byweekday"] = byWeekday;
// Add monthday
rule["bymonthday"] = byMonthDay;
//// Add dtstart
if (startDate != "unknown") rule["dtstart"] = new Date(startDate);
//// Add until
if (endDate != "unknown") rule["until"] = new Date(endDate);
else rule["until"] = new Date(startDate.getTime() + 630720000*1000); // Use 20 years as end
// Create a ruleset:
let rruleSet = new RRuleset();
rruleSet.rrule(new RRule(rule));
// Add exception dates
for (let exdate of exceptDateArray) {
rruleSet.exdate(new Date(exdate));
}
// Get all occurrence dates (Date instances):
let occurences = rruleSet.all();
// Based on the duration and starting time we can calculate the period intervals
const durationInSeconds = pxd.default(duration);
let periods = [];
for (let occurence of occurences) {
const year = occurence.getFullYear();
let month = occurence.getMonth() + 1;
if (month < 10) month = '0' + month;
let day = occurence.getDate();
if (day < 10) day = '0' + day;
const startOfOccurence = new Date(`${year}-${month}-${day}T${startTime}`);
const endOfOccurence = new Date(startOfOccurence.getTime() + durationInSeconds*1000);
periods.push({
"start": startOfOccurence.toISOString(),
"end": endOfOccurence.toISOString()
})
}
return periods;
}
Second, create a “package.json” file:
{
"name": "test",
"version": "1.0.0",
"description": "",
"main": "test.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC",
"dependencies": {
"@treecg/actor-init-ldes-client": "latest",
"parse-xsd-duration": "^0.5.0",
"rdf-data-factory": "^1.1.0",
"rdf-dereference": "^2.0.0",
"rdf-store-stream": "^1.3.0",
"rrule": "^2.7.0"
}
}
Third, create a “context.jsonld” file:
{
"@context": [{
"@base": "https://private-api.gipod.beta-vlaanderen.be",
"id": "@id",
"value": "@value",
"type": "@type",
"@language": "nl-BE",
"xsd": "http://www.w3.org/2001/XMLSchema#",
"dct": "http://purl.org/dc/terms/",
"dc": "http://purl.org/dc/elements/1.1",
"PublicDomainOccupancy": "https://data.vlaanderen.be/ns/mobiliteit#Inname",
"Groundwork": "https://data.vlaanderen.be/ns/mobiliteit#Grondwerk",
"Work": "https://data.vlaanderen.be/ns/mobiliteit#Werk",
"Event": "https://data.vlaanderen.be/ns/mobiliteit#Evenement",
"MobilityHindrance": "https://data.vlaanderen.be/ns/mobiliteit#Mobiliteitshinder",
"SignalingPermit": "https://data.vlaanderen.be/ns/mobiliteit#Signalisatievergunning",
"TrenchSynergyRequest": "https://data.vlaanderen.be/ns/mobiliteit#Synergieaanvraag",
"TrenchSynergy": "https://data.vlaanderen.be/ns/mobiliteit#Synergie",
"Organisation": "http://www.w3.org/ns/org#Organization",
"Identifier": "http://www.w3.org/ns/adms#Identifier",
"ContactOrganisationInRole": "http://www.w3.org/ns/org#Organization",
"ContactInfoPublic": "https://data.vlaanderen.be/ns/mobiliteit#ContactinfoPubliek",
"AddressRepresentation": "http://www.w3.org/ns/locn#Address",
"Geometry": "http://www.w3.org/ns/locn#Geometry",
"Period": "http://data.europa.eu/m8g/PeriodOfTime",
"PeriodWithDuration": "https://data.vlaanderen.be/ns/mobiliteit#PeriodeMetDuur",
"TimeSchedule": "https://schema.org/Schedule",
"Zone": "https://data.vlaanderen.be/ns/mobiliteit#Zone",
"identifier": {
"@id": "http://www.w3.org/ns/adms#identifier",
"@type": "@id",
"@container": "@set"
},
"Identifier.identifier": {
"@id": "http://www.w3.org/2004/02/skos/core#notation"
},
"assignedByName": {
"@id": "http://www.w3.org/ns/adms#schemaAgency"
},
"address": {
"@id": "http://www.w3.org/ns/locn#address",
"@type": "@id"
},
"isPublic": {
"@id": "https://data.vlaanderen.be/ns/mobiliteit#Inname.openbaarDomein"
},
"owner": {
"@id": "https://data.vlaanderen.be/ns/mobiliteit#beheerder",
"@type": "@id"
},
"description": {
"@id": "http://purl.org/dc/terms/description",
"@type": "http://www.w3.org/1999/02/22-rdf-syntax-ns#langString"
},
"duration": {
"@id": "https://data.vlaanderen.be/ns/generiek#Tijdsschema.duur",
"@type": "xsd:duration"
},
"contactname": {
"@id": "http://xmlns.com/foaf/0.1/name"
},
"contactOrganisation": {
"@id": "https://data.vlaanderen.be/ns/mobiliteit#contactOrganisatie",
"@type": "@id",
"@container": "@set"
},
"telephone": {
"@id": "http://schema.org/telephone",
"@container": "@set"
},
"preferredName": {
"@id": "http://www.w3.org/2004/02/skos/core#prefLabel",
"@type": "http://www.w3.org/1999/02/22-rdf-syntax-ns#langString"
},
"category": {
"@id": "https://data.vlaanderen.be/ns/mobiliteit#Grondwerk.categorie",
"@type": "@id"
},
"specification": {
"@id": "https://data.vlaanderen.be/ns/mobiliteit#specificatie",
"@type": "@id"
},
"isRelocationGroundwork": {
"@id": "https://data.vlaanderen.be/ns/mobiliteit#Grondwerk.verplaatsingswerk",
"@type": "xsd:boolean"
},
"causingGroundwork": {
"@id": "https://data.vlaanderen.be/ns/mobiliteit#heeftOorzaakGrondwerk",
"@type": "@id",
"@container": "@set"
},
"period": {
"@id": "https://data.vlaanderen.be/ns/mobiliteit#periode",
"@type": "@id",
"@container": "@set"
},
"start": {
"@id": "http://data.europa.eu/m8g/startTime",
"@type": "xsd:dateTime"
},
"end": {
"@id": "http://data.europa.eu/m8g/endTime",
"@type": "xsd:dateTime"
},
"periodDuration": {
"@id": "http://schema.org/duration",
"@type": "xsd:duration"
},
"timeSchedule": {
"@id": "http://schema.org/eventSchedule",
"@type": "@id",
"@container": "@set"
},
"startDate": {
"@id": "http://schema.org/startDate",
"@type": "xsd:date"
},
"startTime": {
"@id": "http://schema.org/startTime",
"@type": "xsd:time"
},
"endDate": {
"@id": "http://schema.org/endDate",
"@type": "xsd:date"
},
"endTime": {
"@id": "http://schema.org/endTime",
"@type": "xsd:time"
},
"consequence": {
"@id": "https://data.vlaanderen.be/ns/mobiliteit#gevolg",
"@type": "@id"
},
"status": {
"@id": "https://data.vlaanderen.be/ns/mobiliteit#Inname.status",
"@type": "@id"
},
"publicDomainOccupancyType": {
"@id": "https://data.vlaanderen.be/ns/mobiliteit#Inname.type",
"@type": "@id",
"@container": "@set"
},
"hasConsequence": {
"@id": "https://data.vlaanderen.be/ns/mobiliteit#Inname.heeftGevolg",
"@type": "@id"
},
"estimatedDuration": {
"@id": "https://data.vlaanderen.be/ns/mobiliteit#geschatteDuur",
"@type": "xsd:duration"
},
"zoneType": {
"@id": "https://data.vlaanderen.be/ns/mobiliteit#Zone.type",
"@type": "@id",
"@container": "@set"
},
"byDay": {
"@id": "http://schema.org/byDay"
},
"byMonth": {
"@id": "http://schema.org/byMonth",
"@type": "xsd:integer"
},
"byMonthDay": {
"@id": "http://schema.org/byMonthDay",
"@type": "xsd:integer",
"@container": "@set"
},
"repeatCount": {
"@id": "http://schema.org/repeatCount",
"@type": "xsd:integer"
},
"repeatFrequency": {
"@id": "http://schema.org/repeatFrequency"
},
"exceptDate": {
"@id": "http://schema.org/exceptDate",
"@type": "xsd:date",
"@container": "@set"
},
"bySetPos": {
"@id": "https://data.vlaanderen.be/ns/generiek#perSetpositie",
"@type": "xsd:integer",
"@container": "@set"
},
"geometry": {
"@id": "http://www.w3.org/ns/locn#geometry",
"@type": "@id"
},
"gipodId": {
"@id": "https://gipod.vlaanderen.be/ns/gipod#gipodId"
},
"isConsequenceOf": {
"@reverse": "https://data.vlaanderen.be/ns/mobiliteit#Inname.heeftGevolg",
"@type": "@id"
},
"permittedBy": {
"@id": "https://gipod.vlaanderen.be/ns/gipod#isVergundDoor",
"@type": "@id"
},
"prefLabel": {
"@id": "http://www.w3.org/2004/02/skos/core#prefLabel"
},
"reference": {
"@id": "https://gipod.vlaanderen.be/ns/gipod#reference"
},
"remark": {
"@id": "https://gipod.vlaanderen.be/ns/gipod#remark",
"@type": "http://www.w3.org/1999/02/22-rdf-syntax-ns#langString"
},
"addressRepresentation": {
"@id": "https://www.w3.org/ns/locn#address",
"@type": "@id"
},
"wkt": {
"@id": "http://www.opengis.net/ont/geosparql#asWKT",
"@type": "http://www.opengis.net/ont/geosparql#wktLiteral"
},
"zone": {
"@id": "https://data.vlaanderen.be/ns/mobiliteit#zone",
"@type": "@id",
"@container": "@set"
},
"lastModifiedOn": {
"@id": "dct:modified",
"@type": "xsd:dateTime"
},
"lastModifiedBy": {
"@id": "http://purl.org/dc/elements/1.1/contributor",
"@type": "@id"
},
"createdOn": {
"@id": "dct:created",
"@type": "xsd:dateTime"
},
"createdBy": {
"@id": "http://purl.org/dc/elements/1.1/creator",
"@type": "@id"
}
}, {
"adms": "http://www.w3.org/ns/adms#",
"prov": "http://www.w3.org/ns/prov#",
"ldes": "https://w3id.org/ldes#",
"tree": "https://w3id.org/tree#",
"eventName": "adms:versionNotes",
"EventStream": "ldes:EventStream",
"Node": "tree:Node",
"view": "tree:view",
"viewOf": {
"@reverse": "view",
"@type": "@id"
},
"member": "tree:member",
"relation": "tree:relation",
"memberOf": {
"@reverse": "member",
"@type": "@id"
},
"timestampPath": {
"@id": "ldes:timestampPath",
"@type": "@id"
},
"versionOfPath": {
"@id": "ldes:versionOfPath",
"@type": "@id"
},
"shape": {
"@id": "tree:shape",
"@type": "@id"
},
"tree:node": {
"@type": "@id"
},
"tree:path": {
"@type": "@id"
},
"tree:value": {
"@type": "xsd:dateTime"
},
"generatedAtTime": {
"@id": "prov:generatedAtTime",
"@type": "xsd:dateTime"
},
"isVersionOf": {
"@id": "dct:isVersionOf",
"@type": "@id"
},
"items": "@included",
"collectionInfo": "@included"
}]
}
And install the packages with following command in the terminal:
Now we can run the script as follows:
The script currently uses a test LDES with one hindrance object.
The outputted JSON object, which you will process further, looks like this:
The script shows how to parse the WKT geometry, time period, schedule etc.
The function hindranceHasInterestingConsequence
demonstrates how objects can be filtered on consequence type.
The result of the script is:
Restarting
In the script test.js, we also pause the stream after 50 minutes. This can be configured with the TIMEOUT property:
const TIMEOUT = 3000000; // milliseconds
setTimeout(() => eventstreamSync.pause(), TIMEOUT);
When pausing, this triggers the function “saveState”, which will save the state to the .ldes/state.json file.
When resuming, this state will be picked up again.
If you want to reset the client, remove the state file.