forked from MapComplete/MapComplete
Velopark: first decent, working version
This commit is contained in:
parent
890816d2dd
commit
5b6cd1d2ae
18 changed files with 7054 additions and 21769 deletions
|
@ -81,7 +81,10 @@
|
||||||
"render": {
|
"render": {
|
||||||
"special": {
|
"special": {
|
||||||
"type": "linked_data_from_website",
|
"type": "linked_data_from_website",
|
||||||
"key": "ref:velopark"
|
"key": "ref:velopark",
|
||||||
|
"useProxy": "no",
|
||||||
|
"host": "https://data.velopark.be",
|
||||||
|
"mode": "readonly"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -328,7 +331,9 @@
|
||||||
"render": {
|
"render": {
|
||||||
"special": {
|
"special": {
|
||||||
"type": "linked_data_from_website",
|
"type": "linked_data_from_website",
|
||||||
"key": "ref:velopark"
|
"key": "ref:velopark",
|
||||||
|
"useProxy": "no",
|
||||||
|
"host": "https://data.velopark.be"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
27022
package-lock.json
generated
27022
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -41,7 +41,7 @@
|
||||||
],
|
],
|
||||||
"country_coder_host": "https://raw.githubusercontent.com/pietervdvn/MapComplete-data/main/latlon2country",
|
"country_coder_host": "https://raw.githubusercontent.com/pietervdvn/MapComplete-data/main/latlon2country",
|
||||||
"nominatimEndpoint": "https://geocoding.geofabrik.de/b75350b1cfc34962ac49824fe5b582dc/",
|
"nominatimEndpoint": "https://geocoding.geofabrik.de/b75350b1cfc34962ac49824fe5b582dc/",
|
||||||
"jsonld-proxy": "https://cache.mapcomplete.org/extractgraph?url={url}",
|
"jsonld-proxy": "http://127.0.0.1:2346/extractgraph?url={url}",
|
||||||
"protomaps": {
|
"protomaps": {
|
||||||
"api-key": "2af8b969a9e8b692",
|
"api-key": "2af8b969a9e8b692",
|
||||||
"endpoint": "https://api.protomaps.com/tiles/",
|
"endpoint": "https://api.protomaps.com/tiles/",
|
||||||
|
@ -125,7 +125,9 @@
|
||||||
"not op_mini all"
|
"not op_mini all"
|
||||||
],
|
],
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@comunica/query-sparql": "^2.10.2",
|
"@comunica/core": "^3.0.1",
|
||||||
|
"@comunica/query-sparql": "^3.0.1",
|
||||||
|
"@comunica/query-sparql-link-traversal": "^0.3.0",
|
||||||
"@rgossiaux/svelte-headlessui": "^1.0.2",
|
"@rgossiaux/svelte-headlessui": "^1.0.2",
|
||||||
"@rgossiaux/svelte-heroicons": "^0.1.2",
|
"@rgossiaux/svelte-heroicons": "^0.1.2",
|
||||||
"@rollup/plugin-typescript": "^11.0.0",
|
"@rollup/plugin-typescript": "^11.0.0",
|
||||||
|
|
|
@ -176,7 +176,7 @@ export default class ScriptUtils {
|
||||||
const requestPromise = new Promise((resolve, reject) => {
|
const requestPromise = new Promise((resolve, reject) => {
|
||||||
try {
|
try {
|
||||||
headers = headers ?? {}
|
headers = headers ?? {}
|
||||||
headers.accept = "application/json"
|
headers.accept ??= "application/json"
|
||||||
console.log(" > ScriptUtils.Download(", url, ")")
|
console.log(" > ScriptUtils.Download(", url, ")")
|
||||||
const urlObj = new URL(url)
|
const urlObj = new URL(url)
|
||||||
const request = https.get(
|
const request = https.get(
|
||||||
|
|
22
scripts/downloadLinkedDataList.ts
Normal file
22
scripts/downloadLinkedDataList.ts
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
import Script from "./Script"
|
||||||
|
import LinkedDataLoader from "../src/Logic/Web/LinkedDataLoader"
|
||||||
|
import { writeFileSync } from "fs"
|
||||||
|
|
||||||
|
export default class DownloadLinkedDataList extends Script {
|
||||||
|
constructor() {
|
||||||
|
super("Downloads the localBusinesses from the given location. Usage: url [--no-proxy]")
|
||||||
|
}
|
||||||
|
|
||||||
|
async main([url, noProxy]: string[]): Promise<void> {
|
||||||
|
const useProxy = noProxy !== "--no-proxy"
|
||||||
|
const data = await LinkedDataLoader.fetchJsonLd(url, {}, useProxy)
|
||||||
|
const path = "linked_data_"+url.replace(/[^a-zA-Z0-9_]/g, "_")+".jsonld"
|
||||||
|
writeFileSync(path,
|
||||||
|
JSON.stringify(data),
|
||||||
|
"utf8"
|
||||||
|
)
|
||||||
|
console.log("Written",path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
new DownloadLinkedDataList().run()
|
|
@ -15,7 +15,7 @@ class CompareWebsiteData extends Script {
|
||||||
if(fs.existsSync(filename)){
|
if(fs.existsSync(filename)){
|
||||||
return JSON.parse(fs.readFileSync(filename, "utf-8"))
|
return JSON.parse(fs.readFileSync(filename, "utf-8"))
|
||||||
}
|
}
|
||||||
const jsonLd = await LinkedDataLoader.fetchJsonLdWithProxy(url)
|
const jsonLd = await LinkedDataLoader.fetchJsonLd(url, undefined, true)
|
||||||
console.log("Got:", jsonLd)
|
console.log("Got:", jsonLd)
|
||||||
fs.writeFileSync(filename, JSON.stringify(jsonLd))
|
fs.writeFileSync(filename, JSON.stringify(jsonLd))
|
||||||
return jsonLd
|
return jsonLd
|
||||||
|
|
|
@ -8,7 +8,8 @@ export class Server {
|
||||||
},
|
},
|
||||||
handle: {
|
handle: {
|
||||||
mustMatch: string | RegExp
|
mustMatch: string | RegExp
|
||||||
mimetype: string
|
mimetype: string,
|
||||||
|
addHeaders?: Record<string, string>,
|
||||||
handle: (path: string, queryParams: URLSearchParams) => Promise<string>
|
handle: (path: string, queryParams: URLSearchParams) => Promise<string>
|
||||||
}[]
|
}[]
|
||||||
) {
|
) {
|
||||||
|
@ -30,18 +31,18 @@ export class Server {
|
||||||
})
|
})
|
||||||
http.createServer(async (req: http.IncomingMessage, res) => {
|
http.createServer(async (req: http.IncomingMessage, res) => {
|
||||||
try {
|
try {
|
||||||
console.log(
|
|
||||||
req.method + " " + req.url,
|
|
||||||
"from:",
|
|
||||||
req.headers.origin,
|
|
||||||
new Date().toISOString()
|
|
||||||
)
|
|
||||||
|
|
||||||
const url = new URL(`http://127.0.0.1/` + req.url)
|
const url = new URL(`http://127.0.0.1/` + req.url)
|
||||||
let path = url.pathname
|
let path = url.pathname
|
||||||
while (path.startsWith("/")) {
|
while (path.startsWith("/")) {
|
||||||
path = path.substring(1)
|
path = path.substring(1)
|
||||||
}
|
}
|
||||||
|
console.log(
|
||||||
|
req.method + " " + req.url,
|
||||||
|
"from:",
|
||||||
|
req.headers.origin,
|
||||||
|
new Date().toISOString(),
|
||||||
|
path
|
||||||
|
)
|
||||||
if (options?.ignorePathPrefix) {
|
if (options?.ignorePathPrefix) {
|
||||||
for (const toIgnore of options.ignorePathPrefix) {
|
for (const toIgnore of options.ignorePathPrefix) {
|
||||||
if (path.startsWith(toIgnore)) {
|
if (path.startsWith(toIgnore)) {
|
||||||
|
@ -90,7 +91,11 @@ export class Server {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await handler.handle(path, url.searchParams)
|
const result = await handler.handle(path, url.searchParams)
|
||||||
res.writeHead(200, { "Content-Type": handler.mimetype })
|
if(typeof result !== "string"){
|
||||||
|
console.error("Internal server error: handling", url,"resulted in a ",typeof result," instead of a string:", result)
|
||||||
|
}
|
||||||
|
const extraHeaders = handler.addHeaders ?? {}
|
||||||
|
res.writeHead(200, { "Content-Type": handler.mimetype , ...extraHeaders})
|
||||||
res.write(result)
|
res.write(result)
|
||||||
res.end()
|
res.end()
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
|
@ -15,8 +15,12 @@ class ServerLdScrape extends Script {
|
||||||
{
|
{
|
||||||
mustMatch: "extractgraph",
|
mustMatch: "extractgraph",
|
||||||
mimetype: "application/ld+json",
|
mimetype: "application/ld+json",
|
||||||
|
addHeaders: {
|
||||||
|
"Cache-control":"max-age=3600, public"
|
||||||
|
},
|
||||||
async handle(content, searchParams: URLSearchParams) {
|
async handle(content, searchParams: URLSearchParams) {
|
||||||
const url = searchParams.get("url")
|
const url = searchParams.get("url")
|
||||||
|
console.log("URL", url)
|
||||||
if (cache[url] !== undefined) {
|
if (cache[url] !== undefined) {
|
||||||
const { date, contents } = cache[url]
|
const { date, contents } = cache[url]
|
||||||
console.log(">>>", date, contents)
|
console.log(">>>", date, contents)
|
||||||
|
@ -37,6 +41,15 @@ class ServerLdScrape extends Script {
|
||||||
return "{\"#\":\"timout reached\"}"
|
return "{\"#\":\"timout reached\"}"
|
||||||
}
|
}
|
||||||
} while (dloaded["redirect"])
|
} while (dloaded["redirect"])
|
||||||
|
|
||||||
|
if(dloaded["content"].startsWith("{")){
|
||||||
|
// This is probably a json
|
||||||
|
const snippet = JSON.parse(dloaded["content"])
|
||||||
|
console.log("Snippet is", snippet)
|
||||||
|
cache[url] = { contents: snippet, date: new Date() }
|
||||||
|
return JSON.stringify(snippet)
|
||||||
|
}
|
||||||
|
|
||||||
const parsed = parse(dloaded["content"])
|
const parsed = parse(dloaded["content"])
|
||||||
const scripts = Array.from(parsed.getElementsByTagName("script"))
|
const scripts = Array.from(parsed.getElementsByTagName("script"))
|
||||||
for (const script of scripts) {
|
for (const script of scripts) {
|
||||||
|
|
|
@ -2,8 +2,8 @@ import Script from "../Script"
|
||||||
import fs from "fs"
|
import fs from "fs"
|
||||||
import { Feature, FeatureCollection } from "geojson"
|
import { Feature, FeatureCollection } from "geojson"
|
||||||
import { GeoOperations } from "../../src/Logic/GeoOperations"
|
import { GeoOperations } from "../../src/Logic/GeoOperations"
|
||||||
import * as os from "os"
|
|
||||||
// vite-node scripts/velopark/compare.ts -- scripts/velopark/velopark_all_2024-02-14T12\:18\:41.772Z.geojson ~/Projecten/OSM/Fietsberaad/2024-02-02\ Fietsenstallingen_OSM_met_velopark_ref.geojson
|
// vite-node scripts/velopark/compare.ts -- scripts/velopark/velopark_all.geojson osm_with_velopark_link_.geojson
|
||||||
class Compare extends Script {
|
class Compare extends Script {
|
||||||
compare(
|
compare(
|
||||||
veloId: string,
|
veloId: string,
|
||||||
|
@ -30,6 +30,9 @@ class Compare extends Script {
|
||||||
Object.keys(osmParking.properties).concat(Object.keys(veloParking.properties))
|
Object.keys(osmParking.properties).concat(Object.keys(veloParking.properties))
|
||||||
)
|
)
|
||||||
for (const key of allKeys) {
|
for (const key of allKeys) {
|
||||||
|
if(["name","numberOfLevels"].indexOf(key) >= 0){
|
||||||
|
continue // We don't care about these tags
|
||||||
|
}
|
||||||
if (osmParking.properties[key] === veloParking.properties[key]) {
|
if (osmParking.properties[key] === veloParking.properties[key]) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
@ -42,16 +45,22 @@ class Compare extends Script {
|
||||||
diffs.push({
|
diffs.push({
|
||||||
key,
|
key,
|
||||||
osm: osmParking.properties[key],
|
osm: osmParking.properties[key],
|
||||||
velopark: veloParking.properties[key],
|
velopark: veloParking.properties[key]
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
let osmid = osmParking.properties["@id"] ?? osmParking["id"] /*Not in the properties, that is how overpass returns it*/
|
||||||
|
if (!osmid.startsWith("http")) {
|
||||||
|
osmid = "https://openstreetmap.org/" + osmid
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
ref: veloId,
|
ref: veloId,
|
||||||
osmid: osmParking.properties["@id"],
|
osmid,
|
||||||
distance,
|
distance,
|
||||||
diffs,
|
diffs
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async main(args: string[]): Promise<void> {
|
async main(args: string[]): Promise<void> {
|
||||||
let [velopark, osm, key] = args
|
let [velopark, osm, key] = args
|
||||||
key ??= "ref:velopark"
|
key ??= "ref:velopark"
|
||||||
|
@ -60,7 +69,7 @@ class Compare extends Script {
|
||||||
|
|
||||||
const veloparkById: Record<string, Feature> = {}
|
const veloparkById: Record<string, Feature> = {}
|
||||||
for (const parking of veloparkData.features) {
|
for (const parking of veloparkData.features) {
|
||||||
veloparkById[parking.properties[key]] = parking
|
veloparkById[parking.properties[key] ?? parking.properties.url] = parking
|
||||||
}
|
}
|
||||||
|
|
||||||
const diffs = []
|
const diffs = []
|
||||||
|
@ -73,9 +82,12 @@ class Compare extends Script {
|
||||||
}
|
}
|
||||||
diffs.push(this.compare(veloId, parking, veloparking))
|
diffs.push(this.compare(veloId, parking, veloparking))
|
||||||
}
|
}
|
||||||
|
console.log("Found ", diffs.length, " items with differences between OSM and the provided data")
|
||||||
|
|
||||||
fs.writeFileSync("report_diff.json", JSON.stringify(diffs))
|
fs.writeFileSync("report_diff.json", JSON.stringify(diffs, null, " "))
|
||||||
|
console.log("Written report_diff.json")
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super(
|
super(
|
||||||
"Compares a velopark geojson with OSM geojson. Usage: `compare velopark.geojson osm.geojson [key-to-compare-on]`. If key-to-compare-on is not given, `ref:velopark` will be used"
|
"Compares a velopark geojson with OSM geojson. Usage: `compare velopark.geojson osm.geojson [key-to-compare-on]`. If key-to-compare-on is not given, `ref:velopark` will be used"
|
||||||
|
|
|
@ -1,75 +1,175 @@
|
||||||
import Script from "../Script"
|
import Script from "../Script"
|
||||||
import fs from "fs"
|
import fs from "fs"
|
||||||
|
import LinkedDataLoader from "../../src/Logic/Web/LinkedDataLoader"
|
||||||
|
import { Utils } from "../../src/Utils"
|
||||||
|
import { Feature } from "geojson"
|
||||||
|
import { BBox } from "../../src/Logic/BBox"
|
||||||
import { Overpass } from "../../src/Logic/Osm/Overpass"
|
import { Overpass } from "../../src/Logic/Osm/Overpass"
|
||||||
import { RegexTag } from "../../src/Logic/Tags/RegexTag"
|
import { RegexTag } from "../../src/Logic/Tags/RegexTag"
|
||||||
import Constants from "../../src/Models/Constants"
|
|
||||||
import { ImmutableStore } from "../../src/Logic/UIEventSource"
|
import { ImmutableStore } from "../../src/Logic/UIEventSource"
|
||||||
import { BBox } from "../../src/Logic/BBox"
|
import Constants from "../../src/Models/Constants"
|
||||||
import LinkedDataLoader from "../../src/Logic/Web/LinkedDataLoader"
|
|
||||||
|
|
||||||
class VeloParkToGeojson extends Script {
|
class VeloParkToGeojson extends Script {
|
||||||
constructor() {
|
constructor() {
|
||||||
super(
|
super(
|
||||||
"Downloads the latest Velopark data and converts it to a geojson, which will be saved at the current directory",
|
"Downloads the latest Velopark data and converts it to a geojson, which will be saved at the current directory"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
exportTo(filename: string, features) {
|
private static exportGeojsonTo(filename: string, features: Feature[], extension = ".geojson") {
|
||||||
features = features.slice(0,25) // TODO REMOVE
|
const file = filename + "_" + /*new Date().toISOString() + */extension
|
||||||
const file = filename + "_" + /*new Date().toISOString() + */".geojson"
|
|
||||||
fs.writeFileSync(file,
|
fs.writeFileSync(file,
|
||||||
JSON.stringify(
|
JSON.stringify(
|
||||||
{
|
extension === ".geojson" ? {
|
||||||
type: "FeatureCollection",
|
type: "FeatureCollection",
|
||||||
"#":"Only 25 features are shown!", // TODO REMOVE
|
features
|
||||||
features,
|
} : features,
|
||||||
},
|
|
||||||
null,
|
null,
|
||||||
" ",
|
" "
|
||||||
),
|
)
|
||||||
)
|
)
|
||||||
console.log("Written",file)
|
console.log("Written", file, "("+features.length," features)")
|
||||||
}
|
}
|
||||||
|
|
||||||
async main(args: string[]): Promise<void> {
|
public static sumProperties(data: object, addTo: Record<string, Set<string>>) {
|
||||||
|
delete data["@context"]
|
||||||
|
for (const k in data) {
|
||||||
|
if (k === "@graph") {
|
||||||
|
for (const obj of data["@graph"]) {
|
||||||
|
this.sumProperties(obj, addTo)
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if (addTo[k] === undefined) {
|
||||||
|
addTo[k] = new Set<string>()
|
||||||
|
}
|
||||||
|
addTo[k].add(data[k])
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async downloadData() {
|
||||||
console.log("Downloading velopark data")
|
console.log("Downloading velopark data")
|
||||||
// Download data for NIS-code 1000. 1000 means: all of belgium
|
// Download data for NIS-code 1000. 1000 means: all of belgium
|
||||||
const url = "https://www.velopark.be/api/parkings/1000"
|
const url = "https://www.velopark.be/api/parkings/1000"
|
||||||
const allVelopark = await LinkedDataLoader.fetchJsonLd(url, { country: "be" })
|
const allVeloparkRaw: { url: string }[] = await Utils.downloadJson(url)
|
||||||
this.exportTo("velopark_all", allVelopark)
|
|
||||||
|
|
||||||
|
let failed = 0
|
||||||
|
console.log("Got", allVeloparkRaw.length, "items")
|
||||||
|
const allVelopark: Feature[] = []
|
||||||
|
const allProperties = {}
|
||||||
|
for (let i = 0; i < allVeloparkRaw.length; i++) {
|
||||||
|
const f = allVeloparkRaw[i]
|
||||||
|
console.log("Handling", i + "/" + allVeloparkRaw.length)
|
||||||
|
try {
|
||||||
|
const cachePath = "/home/pietervdvn/data/velopark_cache/" + f.url.replace(/[/:.]/g, "_")
|
||||||
|
if (!fs.existsSync(cachePath)) {
|
||||||
|
const data = await Utils.downloadJson(f.url)
|
||||||
|
fs.writeFileSync(cachePath, JSON.stringify(data), "utf-8")
|
||||||
|
console.log("Saved a backup to", cachePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.sumProperties(JSON.parse(fs.readFileSync(cachePath, "utf-8")), allProperties)
|
||||||
|
|
||||||
|
const linkedData = await LinkedDataLoader.fetchVeloparkEntry(f.url)
|
||||||
|
for (const sectionId in linkedData) {
|
||||||
|
const sectionInfo = linkedData[sectionId]
|
||||||
|
if (Object.keys(sectionInfo).length === 0) {
|
||||||
|
console.warn("No result for", f.url)
|
||||||
|
}
|
||||||
|
sectionInfo["ref:velopark"] = [sectionId ?? f.url]
|
||||||
|
allVelopark.push(sectionInfo)
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Loading ", f.url, " failed due to", e)
|
||||||
|
failed++
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.log("Fetching data done, got ", allVelopark.length + "/" + allVeloparkRaw.length, "failed:", failed)
|
||||||
|
VeloParkToGeojson.exportGeojsonTo("velopark_all.geojson", allVelopark)
|
||||||
|
for (const k in allProperties) {
|
||||||
|
allProperties[k] = Array.from(allProperties[k])
|
||||||
|
}
|
||||||
|
|
||||||
|
fs.writeFileSync("all_properties_mashup.json", JSON.stringify(allProperties, null, " "))
|
||||||
|
|
||||||
|
return allVelopark
|
||||||
|
}
|
||||||
|
|
||||||
|
private static loadFromFile(): Feature[] {
|
||||||
|
return JSON.parse(fs.readFileSync("velopark_all.geojson", "utf-8")).features
|
||||||
|
}
|
||||||
|
|
||||||
|
private static exportExtraAmenities(allVelopark: Feature[]) {
|
||||||
|
const amenities: Record<string, Feature[]> = {}
|
||||||
|
|
||||||
|
for (const bikeparking of allVelopark) {
|
||||||
|
const props = bikeparking.properties
|
||||||
|
if (!props["fixme_nearby_amenity"]) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if (props["fixme_nearby_amenity"]?.endsWith("CameraSurveillance")) {
|
||||||
|
delete props["fixme_nearby_amenity"]
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
const amenity = props["fixme_nearby_amenity"].split("#")[1]
|
||||||
|
if (!amenities[amenity]) {
|
||||||
|
amenities[amenity] = []
|
||||||
|
}
|
||||||
|
amenities[amenity].push(bikeparking)
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const k in amenities) {
|
||||||
|
this.exportGeojsonTo("velopark_amenity_" + k + ".geojson", amenities[k])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async createDiff(allVelopark: Feature[]) {
|
||||||
const bboxBelgium = new BBox([
|
const bboxBelgium = new BBox([
|
||||||
[2.51357303225, 49.5294835476],
|
[2.51357303225, 49.5294835476],
|
||||||
[6.15665815596, 51.4750237087],
|
[6.15665815596, 51.4750237087]
|
||||||
])
|
])
|
||||||
|
|
||||||
const alreadyLinkedQuery = new Overpass(
|
const alreadyLinkedQuery = new Overpass(
|
||||||
new RegexTag("ref:velopark", /.+/),
|
new RegexTag("ref:velopark", /.+/),
|
||||||
[],
|
[],
|
||||||
Constants.defaultOverpassUrls[0],
|
Constants.defaultOverpassUrls[0],
|
||||||
new ImmutableStore(60 * 5),
|
new ImmutableStore(60 * 5),
|
||||||
false,
|
false
|
||||||
)
|
)
|
||||||
const alreadyLinkedFeatures = await alreadyLinkedQuery.queryGeoJson(bboxBelgium)
|
const alreadyLinkedFeatures = (await alreadyLinkedQuery.queryGeoJson(bboxBelgium))[0]
|
||||||
const seenIds = new Set<string>(
|
const seenIds = new Set<string>(
|
||||||
alreadyLinkedFeatures[0].features.map((f) => f.properties["ref:velopark"]),
|
alreadyLinkedFeatures.features.map((f) => f.properties?.["ref:velopark"])
|
||||||
)
|
)
|
||||||
|
this.exportGeojsonTo("osm_with_velopark_link", <Feature[]> alreadyLinkedFeatures.features)
|
||||||
console.log("OpenStreetMap contains", seenIds.size, "bicycle parkings with a velopark ref")
|
console.log("OpenStreetMap contains", seenIds.size, "bicycle parkings with a velopark ref")
|
||||||
|
|
||||||
const features = allVelopark.filter((f) => !seenIds.has(f.properties["ref:velopark"]))
|
const features: Feature[] = allVelopark.filter((f) => !seenIds.has(f.properties["ref:velopark"]))
|
||||||
|
VeloParkToGeojson.exportGeojsonTo("velopark_nonsynced", features)
|
||||||
|
|
||||||
const allProperties = new Set<string>()
|
const allProperties = new Set<string>()
|
||||||
for (const feature of features) {
|
for (const feature of features) {
|
||||||
Object.keys(feature.properties).forEach((k) => allProperties.add(k))
|
Object.keys(feature).forEach((k) => allProperties.add(k))
|
||||||
}
|
}
|
||||||
this.exportTo("velopark_noncynced", features)
|
|
||||||
allProperties.delete("ref:velopark")
|
allProperties.delete("ref:velopark")
|
||||||
for (const feature of features) {
|
for (const feature of features) {
|
||||||
allProperties.forEach((k) => {
|
allProperties.forEach((k) => {
|
||||||
delete feature.properties[k]
|
delete feature[k]
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
this.exportTo("velopark_nonsynced_id_only", features)
|
this.exportGeojsonTo("velopark_nonsynced_id_only", features)
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
async main(args: string[]): Promise<void> {
|
||||||
|
const allVelopark = VeloParkToGeojson.loadFromFile() // VeloParkToGeojson.downloadData()
|
||||||
|
console.log("Got", allVelopark.length, " items")
|
||||||
|
// VeloParkToGeojson.exportExtraAmenities(allVelopark)
|
||||||
|
await VeloParkToGeojson.createDiff(allVelopark)
|
||||||
|
console.log("Use vite-node script/velopark/compare to compare the results and generate a diff file")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import type { Geometry } from "geojson"
|
import type { Feature, GeoJSON, Geometry, Polygon } from "geojson"
|
||||||
import jsonld from "jsonld"
|
import jsonld from "jsonld"
|
||||||
import { OH, OpeningHour } from "../../UI/OpeningHours/OpeningHours"
|
import { OH, OpeningHour } from "../../UI/OpeningHours/OpeningHours"
|
||||||
import { Utils } from "../../Utils"
|
import { Utils } from "../../Utils"
|
||||||
|
@ -7,10 +7,14 @@ import EmailValidator from "../../UI/InputElement/Validators/EmailValidator"
|
||||||
import { Validator } from "../../UI/InputElement/Validator"
|
import { Validator } from "../../UI/InputElement/Validator"
|
||||||
import UrlValidator from "../../UI/InputElement/Validators/UrlValidator"
|
import UrlValidator from "../../UI/InputElement/Validators/UrlValidator"
|
||||||
import Constants from "../../Models/Constants"
|
import Constants from "../../Models/Constants"
|
||||||
|
import TypedSparql, { default as S, SparqlResult } from "./TypedSparql"
|
||||||
|
|
||||||
interface JsonLdLoaderOptions {
|
interface JsonLdLoaderOptions {
|
||||||
country?: string
|
country?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type PropertiesSpec<T extends string> = Partial<Record<T, string | string[] | Partial<Record<T, string>>>>
|
||||||
|
|
||||||
export default class LinkedDataLoader {
|
export default class LinkedDataLoader {
|
||||||
private static readonly COMPACTING_CONTEXT = {
|
private static readonly COMPACTING_CONTEXT = {
|
||||||
name: "http://schema.org/name",
|
name: "http://schema.org/name",
|
||||||
|
@ -20,18 +24,23 @@ export default class LinkedDataLoader {
|
||||||
image: { "@id": "http://schema.org/image", "@type": "@id" },
|
image: { "@id": "http://schema.org/image", "@type": "@id" },
|
||||||
opening_hours: { "@id": "http://schema.org/openingHoursSpecification" },
|
opening_hours: { "@id": "http://schema.org/openingHoursSpecification" },
|
||||||
openingHours: { "@id": "http://schema.org/openingHours", "@container": "@set" },
|
openingHours: { "@id": "http://schema.org/openingHours", "@container": "@set" },
|
||||||
|
geo: { "@id": "http://schema.org/geo" }
|
||||||
geo: { "@id": "http://schema.org/geo" },
|
|
||||||
}
|
}
|
||||||
private static COMPACTING_CONTEXT_OH = {
|
private static COMPACTING_CONTEXT_OH = {
|
||||||
dayOfWeek: { "@id": "http://schema.org/dayOfWeek", "@container": "@set" },
|
dayOfWeek: { "@id": "http://schema.org/dayOfWeek", "@container": "@set" },
|
||||||
closes: { "@id": "http://schema.org/closes" },
|
closes: {
|
||||||
opens: { "@id": "http://schema.org/opens" },
|
"@id": "http://schema.org/closes",
|
||||||
|
"@type": "http://www.w3.org/2001/XMLSchema#time"
|
||||||
|
},
|
||||||
|
opens: {
|
||||||
|
"@id": "http://schema.org/opens",
|
||||||
|
"@type": "http://www.w3.org/2001/XMLSchema#time"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
private static formatters: Record<string, Validator> = {
|
private static formatters: Record<string, Validator> = {
|
||||||
phone: new PhoneValidator(),
|
phone: new PhoneValidator(),
|
||||||
email: new EmailValidator(),
|
email: new EmailValidator(),
|
||||||
website: new UrlValidator(undefined, undefined, true),
|
website: new UrlValidator(undefined, undefined, true)
|
||||||
}
|
}
|
||||||
private static ignoreKeys = [
|
private static ignoreKeys = [
|
||||||
"http://schema.org/logo",
|
"http://schema.org/logo",
|
||||||
|
@ -44,29 +53,58 @@ export default class LinkedDataLoader {
|
||||||
"http://schema.org/description",
|
"http://schema.org/description",
|
||||||
"http://schema.org/hasMap",
|
"http://schema.org/hasMap",
|
||||||
"http://schema.org/priceRange",
|
"http://schema.org/priceRange",
|
||||||
"http://schema.org/contactPoint",
|
"http://schema.org/contactPoint"
|
||||||
]
|
]
|
||||||
|
|
||||||
private static ignoreTypes = [
|
private static shapeToPolygon(str: string): Polygon {
|
||||||
"Breadcrumblist",
|
const polygon = str.substring("POLYGON ((".length, str.length - 2)
|
||||||
"http://schema.org/SearchAction"
|
return <Polygon>{
|
||||||
]
|
type: "Polygon",
|
||||||
|
coordinates: [polygon.split(",").map(coors => coors.trim().split(" ").map(n => Number(n)))]
|
||||||
static async geoToGeometry(geo): Promise<Geometry> {
|
|
||||||
const context = {
|
|
||||||
lat: {
|
|
||||||
"@id": "http://schema.org/latitude",
|
|
||||||
},
|
|
||||||
lon: {
|
|
||||||
"@id": "http://schema.org/longitude", // TODO formatting to decimal should be possible from this type?
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
const flattened = await jsonld.compact(geo, context)
|
}
|
||||||
|
|
||||||
|
private static async geoToGeometry(geo): Promise<Geometry> {
|
||||||
|
if (Array.isArray(geo)) {
|
||||||
|
const features = await Promise.all(geo.map(g => LinkedDataLoader.geoToGeometry(g)))
|
||||||
|
const polygon = features.find(f => f.type === "Polygon")
|
||||||
|
if (polygon) {
|
||||||
|
return polygon
|
||||||
|
}
|
||||||
|
const ls = features.find(f => f.type === "LineString")
|
||||||
|
if (ls) {
|
||||||
|
return ls
|
||||||
|
}
|
||||||
|
return features[0]
|
||||||
|
|
||||||
return {
|
|
||||||
type: "Point",
|
|
||||||
coordinates: [Number(flattened.lon), Number(flattened.lat)],
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (geo["@type"] === "http://schema.org/GeoCoordinates") {
|
||||||
|
|
||||||
|
const context = {
|
||||||
|
lat: {
|
||||||
|
"@id": "http://schema.org/latitude",
|
||||||
|
"@type": "http://www.w3.org/2001/XMLSchema#double"
|
||||||
|
},
|
||||||
|
lon: {
|
||||||
|
"@id": "http://schema.org/longitude",
|
||||||
|
"@type": "http://www.w3.org/2001/XMLSchema#double"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const flattened = await jsonld.compact(geo, context)
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: "Point",
|
||||||
|
coordinates: [Number(flattened.lon), Number(flattened.lat)]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (geo["@type"] === "http://schema.org/GeoShape" && geo["http://schema.org/polygon"] !== undefined) {
|
||||||
|
const str = geo["http://schema.org/polygon"]["@value"]
|
||||||
|
LinkedDataLoader.shapeToPolygon(str)
|
||||||
|
}
|
||||||
|
|
||||||
|
throw "Unsupported geo type: " + geo["@type"]
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -74,6 +112,8 @@ export default class LinkedDataLoader {
|
||||||
*
|
*
|
||||||
* // Weird data format from C&A
|
* // Weird data format from C&A
|
||||||
* LinkedDataLoader.ohStringToOsmFormat("MO 09:30-18:00 TU 09:30-18:00 WE 09:30-18:00 TH 09:30-18:00 FR 09:30-18:00 SA 09:30-18:00") // => "Mo-Sa 09:30-18:00"
|
* LinkedDataLoader.ohStringToOsmFormat("MO 09:30-18:00 TU 09:30-18:00 WE 09:30-18:00 TH 09:30-18:00 FR 09:30-18:00 SA 09:30-18:00") // => "Mo-Sa 09:30-18:00"
|
||||||
|
* LinkedDataLoader.ohStringToOsmFormat("MO 09:30-18:00 TU 09:30-18:00 WE 09:30-18:00 TH 09:30-18:00 FR 09:30-18:00 SA 09:30-18:00 SU 09:30-18:00") // => "09:30-18:00"
|
||||||
|
*
|
||||||
*/
|
*/
|
||||||
static ohStringToOsmFormat(oh: string) {
|
static ohStringToOsmFormat(oh: string) {
|
||||||
oh = oh.toLowerCase()
|
oh = oh.toLowerCase()
|
||||||
|
@ -82,7 +122,7 @@ export default class LinkedDataLoader {
|
||||||
}
|
}
|
||||||
const regex = /([a-z]+ [0-9:]+-[0-9:]+) (.*)/
|
const regex = /([a-z]+ [0-9:]+-[0-9:]+) (.*)/
|
||||||
let match = oh.match(regex)
|
let match = oh.match(regex)
|
||||||
let parts: string[] = []
|
const parts: string[] = []
|
||||||
while (match) {
|
while (match) {
|
||||||
parts.push(match[1])
|
parts.push(match[1])
|
||||||
oh = match[2]
|
oh = match[2]
|
||||||
|
@ -94,15 +134,29 @@ export default class LinkedDataLoader {
|
||||||
return OH.simplify(parts.join(";"))
|
return OH.simplify(parts.join(";"))
|
||||||
}
|
}
|
||||||
|
|
||||||
static async ohToOsmFormat(openingHoursSpecification): Promise<string> {
|
static async ohToOsmFormat(openingHoursSpecification): Promise<string | undefined> {
|
||||||
const compacted = await jsonld.flatten(
|
if (typeof openingHoursSpecification === "string") {
|
||||||
|
return openingHoursSpecification
|
||||||
|
}
|
||||||
|
const compacted = await jsonld.compact(
|
||||||
openingHoursSpecification,
|
openingHoursSpecification,
|
||||||
<any>LinkedDataLoader.COMPACTING_CONTEXT_OH
|
<any>LinkedDataLoader.COMPACTING_CONTEXT_OH
|
||||||
)
|
)
|
||||||
const spec: any = compacted["@graph"]
|
const spec: object = compacted["@graph"]
|
||||||
let allRules: OpeningHour[] = []
|
if (!spec) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
const allRules: OpeningHour[] = []
|
||||||
for (const rule of spec) {
|
for (const rule of spec) {
|
||||||
const dow: string[] = rule.dayOfWeek.map((dow) => dow.toLowerCase().substring(0, 2))
|
const dow: string[] = rule.dayOfWeek.map((dow) => {
|
||||||
|
if (typeof dow !== "string") {
|
||||||
|
dow = dow["@id"]
|
||||||
|
}
|
||||||
|
if (dow.startsWith("http://schema.org/")) {
|
||||||
|
dow = dow.substring("http://schema.org/".length)
|
||||||
|
}
|
||||||
|
return dow.toLowerCase().substring(0, 2)
|
||||||
|
})
|
||||||
const opens: string = rule.opens
|
const opens: string = rule.opens
|
||||||
const closes: string = rule.closes === "23:59" ? "24:00" : rule.closes
|
const closes: string = rule.closes === "23:59" ? "24:00" : rule.closes
|
||||||
allRules.push(...OH.ParseRule(dow + " " + opens + "-" + closes))
|
allRules.push(...OH.ParseRule(dow + " " + opens + "-" + closes))
|
||||||
|
@ -111,30 +165,20 @@ export default class LinkedDataLoader {
|
||||||
return OH.ToString(OH.MergeTimes(allRules))
|
return OH.ToString(OH.MergeTimes(allRules))
|
||||||
}
|
}
|
||||||
|
|
||||||
static async fetchJsonLdWithProxy(url: string, options?: JsonLdLoaderOptions): Promise<any> {
|
static async compact(data: object, options?: JsonLdLoaderOptions): Promise<object> {
|
||||||
const urlWithProxy = Constants.linkedDataProxy.replace("{url}", encodeURIComponent(url))
|
|
||||||
return await this.fetchJsonLd(urlWithProxy, options)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
if (Array.isArray(data)) {
|
||||||
*
|
return await Promise.all(data.map(point => LinkedDataLoader.compact(point, options)))
|
||||||
*
|
|
||||||
* {
|
|
||||||
* "content": "{\"@context\":\"http://schema.org\",\"@type\":\"LocalBusiness\",\"@id\":\"http://stores.delhaize.be/nl/ad-delhaize-munsterbilzen\",\"name\":\"AD Delhaize Munsterbilzen\",\"url\":\"http://stores.delhaize.be/nl/ad-delhaize-munsterbilzen\",\"logo\":\"https://stores.delhaize.be/build/images/web/shop/delhaize-be/favicon.ico\",\"image\":\"http://stores.delhaize.be/image/mobilosoft-testing?apiPath=rehab/delhaize-be/images/location/ad%20delhaize%20image%20ge%CC%81ne%CC%81rale%20%281%29%201652787176865&imageSize=h_500\",\"email\":\"\",\"telephone\":\"+3289413520\",\"address\":{\"@type\":\"PostalAddress\",\"streetAddress\":\"Waterstraat, 18\",\"addressLocality\":\"Bilzen\",\"postalCode\":\"3740\",\"addressCountry\":\"BE\"},\"geo\":{\"@type\":\"GeoCoordinates\",\"latitude\":50.8906898,\"longitude\":5.5260586},\"openingHoursSpecification\":[{\"@type\":\"OpeningHoursSpecification\",\"dayOfWeek\":\"Tuesday\",\"opens\":\"08:00\",\"closes\":\"18:30\"},{\"@type\":\"OpeningHoursSpecification\",\"dayOfWeek\":\"Wednesday\",\"opens\":\"08:00\",\"closes\":\"18:30\"},{\"@type\":\"OpeningHoursSpecification\",\"dayOfWeek\":\"Thursday\",\"opens\":\"08:00\",\"closes\":\"18:30\"},{\"@type\":\"OpeningHoursSpecification\",\"dayOfWeek\":\"Friday\",\"opens\":\"08:00\",\"closes\":\"18:30\"},{\"@type\":\"OpeningHoursSpecification\",\"dayOfWeek\":\"Saturday\",\"opens\":\"08:00\",\"closes\":\"18:30\"},{\"@type\":\"OpeningHoursSpecification\",\"dayOfWeek\":\"Sunday\",\"opens\":\"08:00\",\"closes\":\"12:00\"},{\"@type\":\"OpeningHoursSpecification\",\"dayOfWeek\":\"Monday\",\"opens\":\"12:00\",\"closes\":\"18:30\"}],\"@base\":\"https://stores.delhaize.be/nl/ad-delhaize-munsterbilzen\"}"
|
|
||||||
* }
|
|
||||||
*/
|
|
||||||
private static async compact(data: any, options?: JsonLdLoaderOptions): Promise<any>{
|
|
||||||
console.log("Compacting",data)
|
|
||||||
if(Array.isArray(data)) {
|
|
||||||
return await Promise.all(data.map(d => LinkedDataLoader.compact(d)))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const country = options?.country
|
const country = options?.country
|
||||||
const compacted = await jsonld.compact(data, <any> LinkedDataLoader.COMPACTING_CONTEXT)
|
const compacted = await jsonld.compact(data, <any>LinkedDataLoader.COMPACTING_CONTEXT)
|
||||||
|
|
||||||
compacted["opening_hours"] = await LinkedDataLoader.ohToOsmFormat(
|
compacted["opening_hours"] = await LinkedDataLoader.ohToOsmFormat(
|
||||||
compacted["opening_hours"]
|
compacted["opening_hours"]
|
||||||
)
|
)
|
||||||
if (compacted["openingHours"]) {
|
if (compacted["openingHours"]) {
|
||||||
const ohspec: string[] = <any> compacted["openingHours"]
|
const ohspec: string[] = <any>compacted["openingHours"]
|
||||||
compacted["opening_hours"] = OH.simplify(
|
compacted["opening_hours"] = OH.simplify(
|
||||||
ohspec.map((r) => LinkedDataLoader.ohStringToOsmFormat(r)).join("; ")
|
ohspec.map((r) => LinkedDataLoader.ohStringToOsmFormat(r)).join("; ")
|
||||||
)
|
)
|
||||||
|
@ -143,6 +187,8 @@ export default class LinkedDataLoader {
|
||||||
if (compacted["geo"]) {
|
if (compacted["geo"]) {
|
||||||
compacted["geo"] = <any>await LinkedDataLoader.geoToGeometry(compacted["geo"])
|
compacted["geo"] = <any>await LinkedDataLoader.geoToGeometry(compacted["geo"])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
for (const k in compacted) {
|
for (const k in compacted) {
|
||||||
if (compacted[k] === "") {
|
if (compacted[k] === "") {
|
||||||
delete compacted[k]
|
delete compacted[k]
|
||||||
|
@ -161,10 +207,14 @@ export default class LinkedDataLoader {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return <any>compacted
|
return compacted
|
||||||
|
|
||||||
}
|
}
|
||||||
static async fetchJsonLd(url: string, options?: JsonLdLoaderOptions): Promise<any> {
|
|
||||||
|
static async fetchJsonLd(url: string, options?: JsonLdLoaderOptions, useProxy: boolean = false): Promise<object> {
|
||||||
|
if (useProxy) {
|
||||||
|
url = Constants.linkedDataProxy.replace("{url}", encodeURIComponent(url))
|
||||||
|
}
|
||||||
const data = await Utils.downloadJson(url)
|
const data = await Utils.downloadJson(url)
|
||||||
return await LinkedDataLoader.compact(data, options)
|
return await LinkedDataLoader.compact(data, options)
|
||||||
}
|
}
|
||||||
|
@ -174,7 +224,7 @@ export default class LinkedDataLoader {
|
||||||
* @param externalData
|
* @param externalData
|
||||||
* @param currentData
|
* @param currentData
|
||||||
*/
|
*/
|
||||||
static removeDuplicateData(externalData: Record<string, string>, currentData: Record<string, string>) : Record<string, string>{
|
static removeDuplicateData(externalData: Record<string, string>, currentData: Record<string, string>): Record<string, string> {
|
||||||
const d = { ...externalData }
|
const d = { ...externalData }
|
||||||
delete d["@context"]
|
delete d["@context"]
|
||||||
for (const k in d) {
|
for (const k in d) {
|
||||||
|
@ -197,4 +247,384 @@ export default class LinkedDataLoader {
|
||||||
}
|
}
|
||||||
return d
|
return d
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static asGeojson(linkedData: Record<string, string[]>): Feature {
|
||||||
|
delete linkedData["@context"]
|
||||||
|
const properties: Record<string, string> = {}
|
||||||
|
for (const k in linkedData) {
|
||||||
|
if (linkedData[k].length > 1) {
|
||||||
|
throw "Found multiple values in properties for " + k + ": " + linkedData[k].join("; ")
|
||||||
|
}
|
||||||
|
properties[k] = linkedData[k].join("; ")
|
||||||
|
}
|
||||||
|
let geometry: Geometry = undefined
|
||||||
|
|
||||||
|
if (properties["latitude"] && properties["longitude"]) {
|
||||||
|
geometry = {
|
||||||
|
type: "Point",
|
||||||
|
coordinates: [Number(properties["longitude"]), Number(properties["latitude"])]
|
||||||
|
}
|
||||||
|
delete properties["latitude"]
|
||||||
|
delete properties["longitude"]
|
||||||
|
}
|
||||||
|
if (properties["shape"]) {
|
||||||
|
geometry = LinkedDataLoader.shapeToPolygon(properties["shape"])
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const geo: GeoJSON = {
|
||||||
|
type: "Feature",
|
||||||
|
properties,
|
||||||
|
geometry
|
||||||
|
}
|
||||||
|
delete linkedData.geo
|
||||||
|
delete properties.shape
|
||||||
|
delete properties.type
|
||||||
|
delete properties.parking
|
||||||
|
delete properties.g
|
||||||
|
delete properties.section
|
||||||
|
|
||||||
|
return geo
|
||||||
|
}
|
||||||
|
|
||||||
|
private static patchVeloparkProperties(input: Record<string, Set<string>>): Record<string, string[]> {
|
||||||
|
const output: Record<string, string[]> = {}
|
||||||
|
for (const k in input) {
|
||||||
|
output[k] = Array.from(input[k])
|
||||||
|
}
|
||||||
|
|
||||||
|
function on(key: string, applyF: (s: string) => string) {
|
||||||
|
if (!output[key]) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
output[key] = output[key].map(v => applyF(v))
|
||||||
|
}
|
||||||
|
|
||||||
|
function asBoolean(key: string, invert: boolean = false) {
|
||||||
|
on(key, str => {
|
||||||
|
const isTrue = ("" + str) === "true" || str === "True" || str === "yes"
|
||||||
|
if (isTrue != invert) {
|
||||||
|
return "yes"
|
||||||
|
}
|
||||||
|
return "no"
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
on("maxstay", (maxstay => {
|
||||||
|
const match = maxstay.match(/P([0-9]+)D/)
|
||||||
|
if (match) {
|
||||||
|
const days = Number(match[1])
|
||||||
|
if (days === 1) {
|
||||||
|
return "1 day"
|
||||||
|
}
|
||||||
|
return days + " days"
|
||||||
|
}
|
||||||
|
return maxstay
|
||||||
|
}))
|
||||||
|
|
||||||
|
function rename(source: string, target: string) {
|
||||||
|
if (output[source] === undefined || output[source] === null) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
output[target] = output[source]
|
||||||
|
delete output[source]
|
||||||
|
}
|
||||||
|
|
||||||
|
on("phone", (p => new PhoneValidator().reformat(p, () => "be")))
|
||||||
|
on("charge", (p => {
|
||||||
|
if(Number(p) === 0){
|
||||||
|
output["fee"] = ["no"]
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
return "€" + Number(p)
|
||||||
|
}))
|
||||||
|
if (output["charge"] && output["timeUnit"]) {
|
||||||
|
const duration = Number(output["chargeEnd"] ?? "1") - Number(output["chargeStart"] ?? "0")
|
||||||
|
const unit = output["timeUnit"][0]
|
||||||
|
let durationStr = ""
|
||||||
|
if (duration !== 1) {
|
||||||
|
durationStr = duration + ""
|
||||||
|
}
|
||||||
|
output["charge"] = output["charge"].map(c => c + "/" + (durationStr + unit))
|
||||||
|
}
|
||||||
|
delete output["chargeEnd"]
|
||||||
|
delete output["chargeStart"]
|
||||||
|
delete output["timeUnit"]
|
||||||
|
|
||||||
|
|
||||||
|
asBoolean("covered")
|
||||||
|
asBoolean("fee", true)
|
||||||
|
asBoolean("publicAccess")
|
||||||
|
|
||||||
|
|
||||||
|
output["images"]?.forEach((p, i) => {
|
||||||
|
if (i === 0) {
|
||||||
|
output["image"] = [p]
|
||||||
|
} else {
|
||||||
|
output["image:" + i] = [p]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
delete output["images"]
|
||||||
|
|
||||||
|
on("access", audience => {
|
||||||
|
|
||||||
|
if (["brede publiek", "iedereen", "bezoekers", "iedereen - vooral bezoekers gemeentehuis of bibliotheek."].indexOf(audience.toLowerCase()) >= 0) {
|
||||||
|
return "public"
|
||||||
|
}
|
||||||
|
if(audience.toLowerCase().startsWith("bezoekers")){
|
||||||
|
return "public"
|
||||||
|
}
|
||||||
|
if (["abonnees"].indexOf(audience.toLowerCase()) >= 0) {
|
||||||
|
return "members"
|
||||||
|
}
|
||||||
|
if(audience.indexOf("Blue-locker app") >= 0){
|
||||||
|
return "members"
|
||||||
|
}
|
||||||
|
if (["buurtbewoners"].indexOf(audience.toLowerCase()) >= 0) {
|
||||||
|
return "permissive"
|
||||||
|
// return "members"
|
||||||
|
}
|
||||||
|
if(audience.toLowerCase().startsWith("klanten") ||
|
||||||
|
audience.toLowerCase().startsWith("werknemers") ||
|
||||||
|
audience.toLowerCase().startsWith("personeel")){
|
||||||
|
return "customers"
|
||||||
|
}
|
||||||
|
|
||||||
|
console.warn("Suspicious 'access'-tag:", audience, "for", input["ref:velopark"]," assuming public")
|
||||||
|
return "public"
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
if(output["publicAccess"]?.[0] == "no"){
|
||||||
|
output["access"] =[ "private"]
|
||||||
|
}
|
||||||
|
delete output["publicAccess"]
|
||||||
|
|
||||||
|
if (output["restrictions"]?.[0] === "Geen bromfietsen, noch andere gemotoriseerde voertuigen") {
|
||||||
|
output["motor_vehicle"] = ["no"]
|
||||||
|
delete output["restrictions"]
|
||||||
|
}
|
||||||
|
|
||||||
|
if (output["cargoBikeType"]) {
|
||||||
|
output["cargo_bike"] = ["yes"]
|
||||||
|
delete output["cargoBikeType"]
|
||||||
|
}
|
||||||
|
rename("capacityCargobike", "capacity:cargo_bike")
|
||||||
|
|
||||||
|
if (output["tandemBikeType"]) {
|
||||||
|
output["tandem"] = ["yes"]
|
||||||
|
delete output["tandemBikeType"]
|
||||||
|
}
|
||||||
|
rename("capacityTandem", "capacity:tandem")
|
||||||
|
|
||||||
|
|
||||||
|
if (output["electricBikeType"]) {
|
||||||
|
output["electric_bicycle"] = ["yes"]
|
||||||
|
delete output["electricBikeType"]
|
||||||
|
}
|
||||||
|
rename("capacityElectric", "capacity:electric_bicycle")
|
||||||
|
|
||||||
|
delete output["name"]
|
||||||
|
delete output["numberOfLevels"]
|
||||||
|
|
||||||
|
return output
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async fetchVeloparkProperty<T extends string, G extends T>(url: string, property: string, variable?: string): Promise<SparqlResult<T, G>> {
|
||||||
|
const results = await new TypedSparql().typedSparql<T, G>(
|
||||||
|
{
|
||||||
|
schema: "http://schema.org/",
|
||||||
|
mv: "http://schema.mobivoc.org/",
|
||||||
|
gr: "http://purl.org/goodrelations/v1#",
|
||||||
|
vp: "https://data.velopark.be/openvelopark/vocabulary#",
|
||||||
|
vpt: "https://data.velopark.be/openvelopark/terms#"
|
||||||
|
},
|
||||||
|
[url],
|
||||||
|
undefined,
|
||||||
|
" ?parking a <http://schema.mobivoc.org/BicycleParkingStation>",
|
||||||
|
"?parking " + property + " " + (variable ?? "")
|
||||||
|
)
|
||||||
|
return results
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async fetchVeloparkGraphProperty<T extends string>(url: string, property: string, subExpr?: string):
|
||||||
|
Promise<SparqlResult<T, "section">> {
|
||||||
|
const results = await new TypedSparql().typedSparql<T, "g">(
|
||||||
|
{
|
||||||
|
schema: "http://schema.org/",
|
||||||
|
mv: "http://schema.mobivoc.org/",
|
||||||
|
gr: "http://purl.org/goodrelations/v1#",
|
||||||
|
vp: "https://data.velopark.be/openvelopark/vocabulary#",
|
||||||
|
vpt: "https://data.velopark.be/openvelopark/terms#"
|
||||||
|
},
|
||||||
|
[url],
|
||||||
|
"g",
|
||||||
|
" ?parking a <http://schema.mobivoc.org/BicycleParkingStation>",
|
||||||
|
|
||||||
|
S.graph("g",
|
||||||
|
"?section " + property + " " + (subExpr ?? ""),
|
||||||
|
"?section a ?type"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return results
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Merges many subresults into one result
|
||||||
|
* THis is a workaround for 'optional' not working decently
|
||||||
|
* @param r0
|
||||||
|
*/
|
||||||
|
public static mergeResults(...r0: SparqlResult<string, string>[]): SparqlResult<string, string> {
|
||||||
|
const r: SparqlResult<string> = { "default": {} }
|
||||||
|
for (const subResult of r0) {
|
||||||
|
if (Object.keys(subResult).length === 0) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for (const sectionKey in subResult) {
|
||||||
|
if (!r[sectionKey]) {
|
||||||
|
r[sectionKey] = {}
|
||||||
|
}
|
||||||
|
const section = subResult[sectionKey]
|
||||||
|
for (const key in section) {
|
||||||
|
r[sectionKey][key] ??= section[key]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (r["default"] !== undefined && Object.keys(r).length > 1) {
|
||||||
|
for (const section in r) {
|
||||||
|
if (section === "default") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for (const k in r.default) {
|
||||||
|
r[section][k] ??= r.default[k]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
delete r.default
|
||||||
|
}
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async fetchEntry<T extends string>(directUrl: string,
|
||||||
|
propertiesWithoutGraph: PropertiesSpec<T>,
|
||||||
|
propertiesInGraph: PropertiesSpec<T>,
|
||||||
|
extra?: string[]): Promise<SparqlResult<T, string>> {
|
||||||
|
const allPartialResults: SparqlResult<T, string>[] = []
|
||||||
|
for (const propertyName in propertiesWithoutGraph) {
|
||||||
|
const e = propertiesWithoutGraph[propertyName]
|
||||||
|
if (typeof e === "string") {
|
||||||
|
const variableName = e
|
||||||
|
const result = await this.fetchVeloparkProperty(directUrl, propertyName, "?" + variableName)
|
||||||
|
allPartialResults.push(result)
|
||||||
|
} else {
|
||||||
|
for (const subProperty in e) {
|
||||||
|
const variableName = e[subProperty]
|
||||||
|
const result = await this.fetchVeloparkProperty(directUrl,
|
||||||
|
propertyName, `[${subProperty} ?${variableName}] `)
|
||||||
|
allPartialResults.push(result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const propertyName in propertiesInGraph ?? {}) {
|
||||||
|
const e = propertiesInGraph[propertyName]
|
||||||
|
if (Array.isArray(e)) {
|
||||||
|
for (const subquery of e) {
|
||||||
|
let variableName = subquery
|
||||||
|
if (variableName.match(/[a-zA-Z_]+/)) {
|
||||||
|
variableName = "?" + subquery
|
||||||
|
}
|
||||||
|
const result = await this.fetchVeloparkGraphProperty(directUrl, propertyName, variableName)
|
||||||
|
allPartialResults.push(result)
|
||||||
|
}
|
||||||
|
} else if (typeof e === "string") {
|
||||||
|
let variableName = e
|
||||||
|
if (variableName.match(/[a-zA-Z_]+/)) {
|
||||||
|
variableName = "?" + e
|
||||||
|
}
|
||||||
|
const result = await this.fetchVeloparkGraphProperty(directUrl, propertyName, variableName)
|
||||||
|
allPartialResults.push(result)
|
||||||
|
} else {
|
||||||
|
for (const subProperty in e) {
|
||||||
|
const variableName = e[subProperty]
|
||||||
|
const result = await this.fetchVeloparkGraphProperty(directUrl,
|
||||||
|
propertyName, `[${subProperty} ?${variableName}] `)
|
||||||
|
allPartialResults.push(result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const e of extra) {
|
||||||
|
const r = await this.fetchVeloparkGraphProperty(directUrl, e)
|
||||||
|
allPartialResults.push(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
const results = this.mergeResults(...allPartialResults)
|
||||||
|
|
||||||
|
return results
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches all data relevant to velopark.
|
||||||
|
* The id will be saved as `ref:velopark`
|
||||||
|
* @param url
|
||||||
|
*/
|
||||||
|
public static async fetchVeloparkEntry(url: string): Promise<Feature[]> {
|
||||||
|
const withProxyUrl = Constants.linkedDataProxy.replace("{url}", encodeURIComponent(url))
|
||||||
|
const optionalPaths: Record<string, string | Record<string, string>> = {
|
||||||
|
"schema:interactionService": {
|
||||||
|
"schema:url": "website"
|
||||||
|
},
|
||||||
|
"schema:name": "name",
|
||||||
|
"mv:operatedBy": {
|
||||||
|
"gr:legalName": "operator"
|
||||||
|
|
||||||
|
},
|
||||||
|
"schema:contactPoint": {
|
||||||
|
"schema:email": "email",
|
||||||
|
"schema:telephone": "phone"
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const graphOptionalPaths = {
|
||||||
|
"vp:covered": "covered",
|
||||||
|
"vp:maximumParkingDuration": "maxstay",
|
||||||
|
"mv:totalCapacity": "capacity",
|
||||||
|
"schema:publicAccess": "publicAccess",
|
||||||
|
"schema:photos": "images",
|
||||||
|
"mv:numberOfLevels": "numberOfLevels",
|
||||||
|
"vp:intendedAudience": "access",
|
||||||
|
"schema:geo": {
|
||||||
|
"schema:latitude": "latitude",
|
||||||
|
"schema:longitude": "longitude",
|
||||||
|
"schema:polygon": "shape"
|
||||||
|
},
|
||||||
|
"schema:priceSpecification": {
|
||||||
|
"mv:freeOfCharge": "fee",
|
||||||
|
"schema:price": "charge"
|
||||||
|
},
|
||||||
|
"schema:amenityFeature": {
|
||||||
|
"a": "fixme_nearby_amenity"
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
const extra = [
|
||||||
|
"schema:priceSpecification [ mv:dueForTime [ mv:timeStartValue ?chargeStart; mv:timeEndValue ?chargeEnd; mv:timeUnit ?timeUnit ] ]",
|
||||||
|
"vp:allows [vp:bicycleType <https://data.velopark.be/openvelopark/terms#CargoBicycle>; vp:bicyclesAmount ?capacityCargobike; vp:bicycleType ?cargoBikeType]",
|
||||||
|
"vp:allows [vp:bicycleType <https://data.velopark.be/openvelopark/terms#ElectricBicycle>; vp:bicyclesAmount ?capacityElectric; vp:bicycleType ?electricBikeType]",
|
||||||
|
"vp:allows [vp:bicycleType <https://data.velopark.be/openvelopark/terms#TandemBicycle>; vp:bicyclesAmount ?capacityTandem; vp:bicycleType ?tandemBikeType]"
|
||||||
|
]
|
||||||
|
|
||||||
|
const unpatched = await this.fetchEntry(withProxyUrl, optionalPaths, graphOptionalPaths, extra)
|
||||||
|
const patched: Feature[] = []
|
||||||
|
for (const section in unpatched) {
|
||||||
|
const p = LinkedDataLoader.patchVeloparkProperties(unpatched[section])
|
||||||
|
p["ref:velopark"] = [section]
|
||||||
|
patched.push(LinkedDataLoader.asGeojson(p))
|
||||||
|
}
|
||||||
|
return patched
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
94
src/Logic/Web/TypedSparql.ts
Normal file
94
src/Logic/Web/TypedSparql.ts
Normal file
|
@ -0,0 +1,94 @@
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
import { QueryEngine } from "@comunica/query-sparql"
|
||||||
|
|
||||||
|
export type SparqlVar<T extends string> = `?${T}`
|
||||||
|
export type SparqlExpr = string
|
||||||
|
export type SparqlStmt<T extends string> = `${SparqlVar<T> | SparqlExpr} ${SparqlVar<T> | SparqlExpr} ${SparqlVar<T> | SparqlExpr}`
|
||||||
|
|
||||||
|
export type TypedExpression<T extends string> = SparqlStmt<T> | string
|
||||||
|
|
||||||
|
export type SparqlResult<T extends string, G extends string = "default"> = Record<G, Record<T, Set<string>>>
|
||||||
|
|
||||||
|
export default class TypedSparql {
|
||||||
|
private readonly comunica: QueryEngine
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.comunica = new QueryEngine()
|
||||||
|
}
|
||||||
|
|
||||||
|
public static optional<Vars extends string>(...statements: (TypedExpression<Vars> | string)[]): TypedExpression<Vars> {
|
||||||
|
return ` OPTIONAL { ${statements.join(". \n\t")} }`
|
||||||
|
}
|
||||||
|
|
||||||
|
public static graph<Vars extends string>(varname: Vars, ...statements: (string | TypedExpression<Vars>)[]): TypedExpression<Vars> {
|
||||||
|
return `GRAPH ?${varname} { ${statements.join(".\n")} }`
|
||||||
|
}
|
||||||
|
|
||||||
|
public static about<Vars extends string>(varname: Vars, ...statements: `${SparqlVar<Vars> | SparqlExpr} ${SparqlVar<Vars> | SparqlExpr}`[]): TypedExpression<Vars> {
|
||||||
|
return `?${varname} ${statements.join(";")}`
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param sources The source-urls where reading should start
|
||||||
|
* @param select all the variables name, without leading '?', e.g. ['s','p','o']
|
||||||
|
* @param query The main contents of the WHERE-part of the query
|
||||||
|
* @param prefixes the prefixes used by this query, e.g. {schema: 'http://schema.org/', vp: 'https://data.velopark.be/openvelopark/vocabulary#'}
|
||||||
|
* @param graphVariable optional: specify which variable has the tag data. If specified, the results will be tagged with the graph IRI
|
||||||
|
*/
|
||||||
|
public async typedSparql<VARS extends string, G extends string = undefined>(
|
||||||
|
prefixes: Record<string, string>,
|
||||||
|
sources: readonly [string, ...string[]], // array with at least one element
|
||||||
|
graphVariable: G | undefined,
|
||||||
|
...query: (TypedExpression<VARS> | string)[]
|
||||||
|
): Promise<SparqlResult<VARS, G>> {
|
||||||
|
const q: string = this.buildQuery(query, prefixes)
|
||||||
|
try {
|
||||||
|
const bindingsStream = await this.comunica.queryBindings(
|
||||||
|
q, { sources: [...sources], lenient: true }
|
||||||
|
)
|
||||||
|
const bindings = await bindingsStream.toArray()
|
||||||
|
|
||||||
|
const resultAllGraphs: SparqlResult<VARS, G> = <SparqlResult<VARS, G>>{}
|
||||||
|
|
||||||
|
bindings.forEach(item => {
|
||||||
|
const result = <Record<VARS | G, Set<string>>>{}
|
||||||
|
item.forEach(
|
||||||
|
(value, key) => {
|
||||||
|
if (!result[key.value]) {
|
||||||
|
result[key.value] = new Set()
|
||||||
|
}
|
||||||
|
result[key.value].add(value.value)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
if (graphVariable && result[graphVariable]?.size > 0) {
|
||||||
|
const id = Array.from(result[graphVariable])?.[0] ?? "default"
|
||||||
|
resultAllGraphs[id] = result
|
||||||
|
} else {
|
||||||
|
resultAllGraphs["default"] = result
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return resultAllGraphs
|
||||||
|
} catch (e) {
|
||||||
|
console.log("Running query failed. The query is", q)
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private buildQuery(
|
||||||
|
query: readonly string[],
|
||||||
|
prefixes: Record<string, string>): string {
|
||||||
|
return `
|
||||||
|
${Object.keys(prefixes).map(prefix => `PREFIX ${prefix}: <${prefixes[prefix]}>`).join("\n")}
|
||||||
|
SELECT *
|
||||||
|
WHERE {
|
||||||
|
${query.join(". \n")} .
|
||||||
|
}
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
||||||
|
static values<VARS extends string>(varname: VARS, ...values: string[]): TypedExpression<VARS> {
|
||||||
|
return `VALUES ?${varname} { ${values.join(" ")} }`
|
||||||
|
}
|
||||||
|
}
|
|
@ -69,12 +69,17 @@
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="low-interaction p-1 border-interactive">
|
<div class="low-interaction p-1 border-interactive">
|
||||||
<Tr t={t.loadedFrom.Subs({url: sourceUrl, source: sourceUrl})} />
|
{#if !readonly}
|
||||||
<h3>
|
<Tr t={t.loadedFrom.Subs({url: sourceUrl, source: sourceUrl})} />
|
||||||
<Tr t={t.conflicting.title} />
|
<h3>
|
||||||
</h3>
|
<Tr t={t.conflicting.title} />
|
||||||
<div class="flex flex-col gap-y-8">
|
</h3>
|
||||||
<Tr t={t.conflicting.intro} />
|
{/if}
|
||||||
|
|
||||||
|
<div class="flex flex-col" class:gap-y-8={!readonly}>
|
||||||
|
{#if !readonly}
|
||||||
|
<Tr t={t.conflicting.intro} />
|
||||||
|
{/if}
|
||||||
{#if different.length > 0}
|
{#if different.length > 0}
|
||||||
{#each different as key}
|
{#each different as key}
|
||||||
<div class="mx-2 rounded-2xl">
|
<div class="mx-2 rounded-2xl">
|
||||||
|
@ -102,7 +107,7 @@
|
||||||
</button>
|
</button>
|
||||||
{/if}
|
{/if}
|
||||||
{:else if currentStep === "applying_all"}
|
{:else if currentStep === "applying_all"}
|
||||||
<Loading/>
|
<Loading />
|
||||||
{:else if currentStep === "all_applied"}
|
{:else if currentStep === "all_applied"}
|
||||||
<div class="thanks">
|
<div class="thanks">
|
||||||
<Tr t={t.allAreApplied} />
|
<Tr t={t.allAreApplied} />
|
||||||
|
|
|
@ -32,7 +32,7 @@ export class OH {
|
||||||
th: 3,
|
th: 3,
|
||||||
fr: 4,
|
fr: 4,
|
||||||
sa: 5,
|
sa: 5,
|
||||||
su: 6,
|
su: 6
|
||||||
}
|
}
|
||||||
|
|
||||||
public static hhmm(h: number, m: number): string {
|
public static hhmm(h: number, m: number): string {
|
||||||
|
@ -82,7 +82,7 @@ export class OH {
|
||||||
|
|
||||||
const stringPerWeekday = partsPerWeekday.map((parts) => parts.sort().join(", "))
|
const stringPerWeekday = partsPerWeekday.map((parts) => parts.sort().join(", "))
|
||||||
|
|
||||||
const rules = []
|
const rules: string[] = []
|
||||||
|
|
||||||
let rangeStart = 0
|
let rangeStart = 0
|
||||||
let rangeEnd = 0
|
let rangeEnd = 0
|
||||||
|
@ -107,11 +107,17 @@ export class OH {
|
||||||
}
|
}
|
||||||
pushRule()
|
pushRule()
|
||||||
|
|
||||||
const oh = rules.join("; ")
|
if (rules.length === 1) {
|
||||||
if (oh === "Mo-Su 00:00-00:00") {
|
const rule = rules[0]
|
||||||
return "24/7"
|
if (rule === "Mo-Su 00:00-00:00") {
|
||||||
|
return "24/7"
|
||||||
|
}
|
||||||
|
if (rule.startsWith("Mo-Su ")) {
|
||||||
|
return rule.substring("Mo-Su ".length)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return oh
|
|
||||||
|
return rules.join("; ")
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -137,7 +143,7 @@ export class OH {
|
||||||
const queue = ohs.map((oh) => {
|
const queue = ohs.map((oh) => {
|
||||||
if (oh.endHour === 0 && oh.endMinutes === 0) {
|
if (oh.endHour === 0 && oh.endMinutes === 0) {
|
||||||
const newOh = {
|
const newOh = {
|
||||||
...oh,
|
...oh
|
||||||
}
|
}
|
||||||
newOh.endHour = 24
|
newOh.endHour = 24
|
||||||
return newOh
|
return newOh
|
||||||
|
@ -146,7 +152,7 @@ export class OH {
|
||||||
})
|
})
|
||||||
const newList = []
|
const newList = []
|
||||||
while (queue.length > 0) {
|
while (queue.length > 0) {
|
||||||
let maybeAdd = queue.pop()
|
const maybeAdd = queue.pop()
|
||||||
|
|
||||||
let doAddEntry = true
|
let doAddEntry = true
|
||||||
if (maybeAdd.weekday == undefined) {
|
if (maybeAdd.weekday == undefined) {
|
||||||
|
@ -205,7 +211,7 @@ export class OH {
|
||||||
startMinutes: startMinutes,
|
startMinutes: startMinutes,
|
||||||
endHour: endHour,
|
endHour: endHour,
|
||||||
endMinutes: endMinutes,
|
endMinutes: endMinutes,
|
||||||
weekday: guard.weekday,
|
weekday: guard.weekday
|
||||||
})
|
})
|
||||||
|
|
||||||
doAddEntry = false
|
doAddEntry = false
|
||||||
|
@ -273,7 +279,7 @@ export class OH {
|
||||||
startHour: start.hours,
|
startHour: start.hours,
|
||||||
startMinutes: start.minutes,
|
startMinutes: start.minutes,
|
||||||
endHour: end.hours,
|
endHour: end.hours,
|
||||||
endMinutes: end.minutes,
|
endMinutes: end.minutes
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -331,8 +337,8 @@ export class OH {
|
||||||
startHour: 0,
|
startHour: 0,
|
||||||
startMinutes: 0,
|
startMinutes: 0,
|
||||||
endHour: 24,
|
endHour: 24,
|
||||||
endMinutes: 0,
|
endMinutes: 0
|
||||||
},
|
}
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -350,10 +356,10 @@ export class OH {
|
||||||
const timeranges = OH.ParseHhmmRanges(split[1])
|
const timeranges = OH.ParseHhmmRanges(split[1])
|
||||||
return OH.multiply(weekdays, timeranges)
|
return OH.multiply(weekdays, timeranges)
|
||||||
}
|
}
|
||||||
return null
|
return []
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log("Could not parse weekday rule ", rule)
|
console.log("Could not parse weekday rule ", rule)
|
||||||
return null
|
return []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -382,13 +388,13 @@ export class OH {
|
||||||
str = str.trim()
|
str = str.trim()
|
||||||
if (str.toLowerCase() === "ph off") {
|
if (str.toLowerCase() === "ph off") {
|
||||||
return {
|
return {
|
||||||
mode: "off",
|
mode: "off"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (str.toLowerCase() === "ph open") {
|
if (str.toLowerCase() === "ph open") {
|
||||||
return {
|
return {
|
||||||
mode: "open",
|
mode: "open"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -404,7 +410,7 @@ export class OH {
|
||||||
return {
|
return {
|
||||||
mode: " ",
|
mode: " ",
|
||||||
start: OH.hhmm(timerange.startHour, timerange.startMinutes),
|
start: OH.hhmm(timerange.startHour, timerange.startMinutes),
|
||||||
end: OH.hhmm(timerange.endHour, timerange.endMinutes),
|
end: OH.hhmm(timerange.endHour, timerange.endMinutes)
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return null
|
return null
|
||||||
|
@ -570,8 +576,8 @@ This list will be sorted
|
||||||
lon: tags._lon,
|
lon: tags._lon,
|
||||||
address: {
|
address: {
|
||||||
country_code: country.toLowerCase(),
|
country_code: country.toLowerCase(),
|
||||||
state: undefined,
|
state: undefined
|
||||||
},
|
}
|
||||||
},
|
},
|
||||||
<any>{ tag_key: "opening_hours" }
|
<any>{ tag_key: "opening_hours" }
|
||||||
)
|
)
|
||||||
|
@ -747,7 +753,7 @@ This list will be sorted
|
||||||
isOpen: iterator.getState(),
|
isOpen: iterator.getState(),
|
||||||
comment: iterator.getComment(),
|
comment: iterator.getComment(),
|
||||||
startDate: iterator.getDate() as Date,
|
startDate: iterator.getDate() as Date,
|
||||||
endDate: endDate, // Should be overwritten by the next iteration
|
endDate: endDate // Should be overwritten by the next iteration
|
||||||
}
|
}
|
||||||
prevValue = value
|
prevValue = value
|
||||||
|
|
||||||
|
@ -885,7 +891,7 @@ This list will be sorted
|
||||||
startHour: timerange.startHour,
|
startHour: timerange.startHour,
|
||||||
startMinutes: timerange.startMinutes,
|
startMinutes: timerange.startMinutes,
|
||||||
endHour: timerange.endHour,
|
endHour: timerange.endHour,
|
||||||
endMinutes: timerange.endMinutes,
|
endMinutes: timerange.endMinutes
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
ohs.push({
|
ohs.push({
|
||||||
|
@ -893,14 +899,14 @@ This list will be sorted
|
||||||
startHour: timerange.startHour,
|
startHour: timerange.startHour,
|
||||||
startMinutes: timerange.startMinutes,
|
startMinutes: timerange.startMinutes,
|
||||||
endHour: 0,
|
endHour: 0,
|
||||||
endMinutes: 0,
|
endMinutes: 0
|
||||||
})
|
})
|
||||||
ohs.push({
|
ohs.push({
|
||||||
weekday: (weekday + 1) % 7,
|
weekday: (weekday + 1) % 7,
|
||||||
startHour: 0,
|
startHour: 0,
|
||||||
startMinutes: 0,
|
startMinutes: 0,
|
||||||
endHour: timerange.endHour,
|
endHour: timerange.endHour,
|
||||||
endMinutes: timerange.endMinutes,
|
endMinutes: timerange.endMinutes
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -961,7 +967,7 @@ export class ToTextualDescription {
|
||||||
"thursday",
|
"thursday",
|
||||||
"friday",
|
"friday",
|
||||||
"saturday",
|
"saturday",
|
||||||
"sunday",
|
"sunday"
|
||||||
]
|
]
|
||||||
|
|
||||||
function addRange(start: number, end: number) {
|
function addRange(start: number, end: number) {
|
||||||
|
@ -1019,7 +1025,7 @@ export class ToTextualDescription {
|
||||||
private static createRangeFor(range: OpeningRange): Translation {
|
private static createRangeFor(range: OpeningRange): Translation {
|
||||||
return Translations.t.general.opening_hours.ranges.Subs({
|
return Translations.t.general.opening_hours.ranges.Subs({
|
||||||
starttime: ToTextualDescription.timeString(range.startDate),
|
starttime: ToTextualDescription.timeString(range.startDate),
|
||||||
endtime: ToTextualDescription.timeString(range.endDate),
|
endtime: ToTextualDescription.timeString(range.endDate)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1031,7 +1037,7 @@ export class ToTextualDescription {
|
||||||
for (let i = 1; i < ranges.length; i++) {
|
for (let i = 1; i < ranges.length; i++) {
|
||||||
tr = Translations.t.general.opening_hours.rangescombined.Subs({
|
tr = Translations.t.general.opening_hours.rangescombined.Subs({
|
||||||
range0: tr,
|
range0: tr,
|
||||||
range1: ToTextualDescription.createRangeFor(ranges[i]),
|
range1: ToTextualDescription.createRangeFor(ranges[i])
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
return tr
|
return tr
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -25,7 +25,7 @@
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if $wikipediaDetails.wikidata}
|
{#if $wikipediaDetails.wikidata}
|
||||||
<ToSvelte construct={WikidataPreviewBox.WikidataResponsePreview($wikipediaDetails.wikidata)} />
|
<ToSvelte construct={() => WikidataPreviewBox.WikidataResponsePreview($wikipediaDetails.wikidata)} />
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if $wikipediaDetails.articleUrl}
|
{#if $wikipediaDetails.articleUrl}
|
||||||
|
|
11
src/test.ts
11
src/test.ts
|
@ -1,4 +1,15 @@
|
||||||
import SvelteUIElement from "./UI/Base/SvelteUIElement"
|
import SvelteUIElement from "./UI/Base/SvelteUIElement"
|
||||||
import Test from "./UI/Test.svelte"
|
import Test from "./UI/Test.svelte"
|
||||||
|
import LinkedDataLoader from "./Logic/Web/LinkedDataLoader"
|
||||||
|
import { src_url_equal } from "svelte/internal"
|
||||||
|
|
||||||
new SvelteUIElement(Test).AttachTo("maindiv")
|
new SvelteUIElement(Test).AttachTo("maindiv")
|
||||||
|
|
||||||
|
|
||||||
|
const url_multiple_sections = "https://data.velopark.be/data/Stad-Deinze_14"
|
||||||
|
const url_single_section = "https://data.velopark.be/data/NMBS_764"
|
||||||
|
const url_with_shape = "https://data.velopark.be/data/Stad-Leuven_APCOA_018"
|
||||||
|
const url_with_yearly_charge = "https://data.velopark.be/data/Cyclopark_AL02"
|
||||||
|
const url = url_multiple_sections /*/ url_single_section //*/
|
||||||
|
const results = await LinkedDataLoader.fetchVeloparkEntry(url_with_yearly_charge)
|
||||||
|
console.log(results)
|
||||||
|
|
275
test/Logic/Web/LinkedDataLoader.spec.ts
Normal file
275
test/Logic/Web/LinkedDataLoader.spec.ts
Normal file
|
@ -0,0 +1,275 @@
|
||||||
|
import { describe, expect, it } from "vitest"
|
||||||
|
import LinkedDataLoader from "../../../src/Logic/Web/LinkedDataLoader"
|
||||||
|
|
||||||
|
|
||||||
|
describe("LinkedDataLoader", () => {
|
||||||
|
it("should compact a shop entry", async () => {
|
||||||
|
const graph = {
|
||||||
|
"@context": "http://schema.org",
|
||||||
|
"@type": "LocalBusiness",
|
||||||
|
"@id": "http://stores.delhaize.be/nl/ad-delhaize-munsterbilzen",
|
||||||
|
"name": "AD Delhaize Munsterbilzen",
|
||||||
|
"url": "http://stores.delhaize.be/nl/ad-delhaize-munsterbilzen",
|
||||||
|
"logo": "https://stores.delhaize.be/build/images/web/shop/delhaize-be/favicon.ico",
|
||||||
|
"image": "http://stores.delhaize.be/image/mobilosoft-testing?apiPath=rehab/delhaize-be/images/location/ad%20delhaize%20image%20ge%CC%81ne%CC%81rale%20%281%29%201652787176865&imageSize=h_500",
|
||||||
|
"email": "",
|
||||||
|
"telephone": "+3289413520",
|
||||||
|
"address": {
|
||||||
|
"@type": "PostalAddress",
|
||||||
|
"streetAddress": "Waterstraat, 18",
|
||||||
|
"addressLocality": "Bilzen",
|
||||||
|
"postalCode": "3740",
|
||||||
|
"addressCountry": "BE",
|
||||||
|
},
|
||||||
|
"geo": {
|
||||||
|
"@type": "GeoCoordinates",
|
||||||
|
"latitude": 50.8906898,
|
||||||
|
"longitude": 5.5260586,
|
||||||
|
},
|
||||||
|
"openingHoursSpecification": [
|
||||||
|
{
|
||||||
|
"@type": "OpeningHoursSpecification",
|
||||||
|
"dayOfWeek": "Tuesday",
|
||||||
|
"opens": "08:00",
|
||||||
|
"closes": "18:30",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"@type": "OpeningHoursSpecification",
|
||||||
|
"dayOfWeek": "Wednesday",
|
||||||
|
"opens": "08:00",
|
||||||
|
"closes": "18:30",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"@type": "OpeningHoursSpecification",
|
||||||
|
"dayOfWeek": "Thursday",
|
||||||
|
"opens": "08:00",
|
||||||
|
"closes": "18:30",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"@type": "OpeningHoursSpecification",
|
||||||
|
"dayOfWeek": "Friday",
|
||||||
|
"opens": "08:00",
|
||||||
|
"closes": "18:30",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"@type": "OpeningHoursSpecification",
|
||||||
|
"dayOfWeek": "Saturday",
|
||||||
|
"opens": "08:00",
|
||||||
|
"closes": "18:30",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"@type": "OpeningHoursSpecification",
|
||||||
|
"dayOfWeek": "Sunday",
|
||||||
|
"opens": "08:00",
|
||||||
|
"closes": "12:00",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"@type": "OpeningHoursSpecification",
|
||||||
|
"dayOfWeek": "Monday",
|
||||||
|
"opens": "12:00",
|
||||||
|
"closes": "18:30",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"@base": "https://stores.delhaize.be/nl/ad-delhaize-munsterbilzen",
|
||||||
|
}
|
||||||
|
const compacted = await LinkedDataLoader.compact(graph)
|
||||||
|
expect(compacted.phone).equal("+32 89 41 35 20")
|
||||||
|
})
|
||||||
|
it("should handle velopark data", async () => {
|
||||||
|
const veloparkEntry = {
|
||||||
|
"@context": {
|
||||||
|
"xsd": "http://www.w3.org/2001/XMLSchema#",
|
||||||
|
"schema": "http://schema.org/",
|
||||||
|
"mv": "http://schema.mobivoc.org/",
|
||||||
|
"dct": "http://purl.org/dc/terms#",
|
||||||
|
"dbo": "http://dbpedia.org/ontology/",
|
||||||
|
"gr": "http://purl.org/goodrelations/v1#",
|
||||||
|
"vp": "https://data.velopark.be/openvelopark/vocabulary#",
|
||||||
|
"vpt": "https://data.velopark.be/openvelopark/terms#",
|
||||||
|
"PostalAddress": "schema:PostalAddress",
|
||||||
|
"GeoCoordinates": "schema:GeoCoordinates",
|
||||||
|
"GeoShape": "schema:GeoShape",
|
||||||
|
"Map": "schema:Map",
|
||||||
|
"ContactPoint": "schema:ContactPoint",
|
||||||
|
"Language": "schema:Language",
|
||||||
|
"OpeningHoursSpecification": "schema:OpeningHoursSpecification",
|
||||||
|
"WebSite": "schema:WebSite",
|
||||||
|
"PriceSpecification": "schema:PriceSpecification",
|
||||||
|
"Photograph": "schema:Photograph",
|
||||||
|
"Place": "schema:Place",
|
||||||
|
"BicycleParkingStation": "mv:BicycleParkingStation",
|
||||||
|
"Entrance": "mv:ParkingFacilityEntrance",
|
||||||
|
"Exit": "mv:ParkingFacilityExit",
|
||||||
|
"TimeSpecification": "mv:TimeSpecification",
|
||||||
|
"Bicycle": "vp:Bicycle",
|
||||||
|
"AllowedBicycle": "vp:AllowedBicycle",
|
||||||
|
"BikeParkingFeature": "vp:BikeParkingFeature",
|
||||||
|
"SecurityFeature": "vp:SecurityFeature",
|
||||||
|
"PublicBicycleParking": "vpt:PublicBicycleParking",
|
||||||
|
"ResidentBicycleParking": "vpt:ResidentBicycleParking",
|
||||||
|
"BicycleLocker": "vpt:BicycleLocker",
|
||||||
|
"RegularBicycle": "vpt:RegularBicycle",
|
||||||
|
"ElectricBicycle": "vpt:ElectricBicycle",
|
||||||
|
"CargoBicycle": "vpt:CargoBicycle",
|
||||||
|
"TandemBicycle": "vpt:TandemBicycle",
|
||||||
|
"CameraSurveillance": "vpt:CameraSurveillance",
|
||||||
|
"PersonnelSupervision": "vpt:PersonnelSupervision",
|
||||||
|
"ElectronicAccess": "vpt:ElectronicAccess",
|
||||||
|
"BicyclePump": "vpt:BicyclePump",
|
||||||
|
"MaintenanceService": "vpt:MaintenanceService",
|
||||||
|
"ChargingPoint": "vpt:ChargingPoint",
|
||||||
|
"LockerService": "vpt:LockerService",
|
||||||
|
"ToiletService": "vpt:ToiletService",
|
||||||
|
"BikeRentalService": "vpt:BikeRentalService",
|
||||||
|
"BusinessEntity": "gr:BusinessEntity",
|
||||||
|
"address": "schema:address",
|
||||||
|
"geo": "schema:geo",
|
||||||
|
"hasMap": "schema:hasMap",
|
||||||
|
"url": "schema:url",
|
||||||
|
"image": "schema:image",
|
||||||
|
"contactPoint": "schema:contactPoint",
|
||||||
|
"interactionService": "schema:interactionService",
|
||||||
|
"dueForTime": "mv:dueForTime",
|
||||||
|
"ownedBy": "mv:ownedBy",
|
||||||
|
"operatedBy": "mv:operatedBy",
|
||||||
|
"rights": "dct:rights",
|
||||||
|
"about": { "@id": "schema:about", "@type": "@id" },
|
||||||
|
"description": { "@id": "schema:description", "@type": "xsd:string" },
|
||||||
|
"dateModified": { "@id": "schema:dateModified", "@type": "xsd:dateTime" },
|
||||||
|
"name": { "@id": "schema:name", "@container": "@set" },
|
||||||
|
"value": { "@id": "schema:value", "@type": "xsd:boolean" },
|
||||||
|
"postalCode": { "@id": "schema:postalCode", "@type": "xsd:string" },
|
||||||
|
"streetAddress": { "@id": "schema:streetAddress", "@type": "xsd:string" },
|
||||||
|
"country": { "@id": "schema:addressCountry", "@type": "xsd:string" },
|
||||||
|
"polygon": { "@id": "schema:polygon", "@type": "xsd:string" },
|
||||||
|
"latitude": { "@id": "schema:latitude", "@type": "xsd:double" },
|
||||||
|
"longitude": { "@id": "schema:longitude", "@type": "xsd:double" },
|
||||||
|
"openingHoursSpecification": { "@id": "schema:openingHoursSpecification", "@container": "@set" },
|
||||||
|
"contactType": { "@id": "schema:contactType", "@type": "xsd:string" },
|
||||||
|
"email": { "@id": "schema:email", "@type": "xsd:string" },
|
||||||
|
"telephone": { "@id": "schema:telephone", "@type": "xsd:string" },
|
||||||
|
"availableLanguage": { "@id": "schema:availableLanguage", "@container": "@set" },
|
||||||
|
"hoursAvailable": { "@id": "schema:hoursAvailable", "@container": "@set" },
|
||||||
|
"dayOfWeek": { "@id": "schema:dayOfWeek", "@type": "@id" },
|
||||||
|
"opens": { "@id": "schema:opens", "@type": "xsd:time" },
|
||||||
|
"closes": { "@id": "schema:closes", "@type": "xsd:time" },
|
||||||
|
"sectionName": { "@id": "schema:name", "@type": "xsd:string" },
|
||||||
|
"publicAccess": { "@id": "schema:publicAccess", "@type": "xsd:boolean" },
|
||||||
|
"priceSpecification": { "@id": "schema:priceSpecification", "@container": "@set" },
|
||||||
|
"price": { "@id": "schema:price", "@type": "xsd:double" },
|
||||||
|
"currency": { "@id": "schema:priceCurrency", "@type": "xsd:string" },
|
||||||
|
"amenityFeature": { "@id": "schema:amenityFeature", "@container": "@set" },
|
||||||
|
"photos": { "@id": "schema:photos", "@container": "@set" },
|
||||||
|
"entrance": { "@id": "mv:entrance", "@container": "@set" },
|
||||||
|
"exit": { "@id": "mv:exit", "@container": "@set" },
|
||||||
|
"numberOfLevels": { "@id": "mv:numberOfLevels", "@type": "xsd:integer" },
|
||||||
|
"totalCapacity": { "@id": "mv:totalCapacity", "@type": "xsd:integer" },
|
||||||
|
"liveCapacity": { "@id": "mv:capacity", "@type": "@id" },
|
||||||
|
"currentValue": { "@id": "mv:currentValue", "@type": "xsd:integer" },
|
||||||
|
"freeOfCharge": { "@id": "mv:freeOfCharge", "@type": "xsd:boolean" },
|
||||||
|
"timeStartValue": { "@id": "mv:timeStartValue", "@type": "xsd:double" },
|
||||||
|
"timeEndValue": { "@id": "mv:timeEndValue", "@type": "xsd:double" },
|
||||||
|
"timeUnit": { "@id": "mv:timeUnit", "@type": "xsd:string" },
|
||||||
|
"startDate": { "@id": "vp:startDate", "@type": "xsd:dateTime" },
|
||||||
|
"endDate": { "@id": "vp:endDate", "@type": "xsd:dateTime" },
|
||||||
|
"allows": { "@id": "vp:allows", "@container": "@set" },
|
||||||
|
"covered": { "@id": "vp:covered", "@type": "xsd:boolean" },
|
||||||
|
"maximumParkingDuration": { "@id": "vp:maximumParkingDuration", "@type": "xsd:duration" },
|
||||||
|
"openingHoursExtraInformation": { "@id": "vp:openingHoursExtraInformation", "@type": "xsd:string" },
|
||||||
|
"intendedAudience": { "@id": "vp:intendedAudience", "@type": "xsd:string" },
|
||||||
|
"restrictions": { "@id": "vp:restrictions", "@type": "xsd:string" },
|
||||||
|
"removalConditions": { "@id": "vp:removalConditions", "@type": "xsd:string" },
|
||||||
|
"postRemovalAction": { "@id": "vp:postRemovalAction", "@type": "xsd:string" },
|
||||||
|
"bicycleType": { "@id": "vp:bicycleType", "@type": "@id" },
|
||||||
|
"bicyclesAmount": { "@id": "vp:bicyclesAmount", "@type": "xsd:integer" },
|
||||||
|
"countingSystem": { "@id": "vp:countingSystem", "@type": "xsd:boolean" },
|
||||||
|
"companyName": { "@id": "gr:legalName", "@type": "xsd:string" },
|
||||||
|
"identifier": { "@id": "dct:identifier", "@type": "xsd:string" },
|
||||||
|
"date": { "@id": "dct:date", "@type": "xsd:dateTime" },
|
||||||
|
"closeTo": { "@id": "dbo:closeTo", "@container": "@set" },
|
||||||
|
"temporarilyClosed": { "@id": "vp:temporarilyClosed", "@type": "xsd:boolean" },
|
||||||
|
},
|
||||||
|
"@id": "https://data.velopark.be/data/De-Lijn_303749",
|
||||||
|
"@type": "BicycleParkingStation",
|
||||||
|
"dateModified": "2020-04-28T12:34:06.227Z",
|
||||||
|
"identifier": "303749",
|
||||||
|
"name": [{ "@value": " Meise Van Dievoetlaan", "@language": "nl" }],
|
||||||
|
"temporarilyClosed": false,
|
||||||
|
"ownedBy": { "@id": "https://www.delijn.be/", "@type": "BusinessEntity", "companyName": "De Lijn" },
|
||||||
|
"operatedBy": { "@id": "https://www.delijn.be/", "@type": "BusinessEntity", "companyName": "De Lijn" },
|
||||||
|
"address": {
|
||||||
|
"@type": "PostalAddress",
|
||||||
|
"postalCode": "1860",
|
||||||
|
"streetAddress": "Nieuwelaan",
|
||||||
|
"country": "Belgium",
|
||||||
|
},
|
||||||
|
"hasMap": { "@type": "Map", "url": "https://www.openstreetmap.org/#map=18/50.94047/4.324813" },
|
||||||
|
"interactionService": { "@type": "WebSite", "url": "https://www.delijn.be/en/contact/" },
|
||||||
|
"@graph": [{
|
||||||
|
"@type": "https://data.velopark.be/openvelopark/terms#BicycleStand",
|
||||||
|
"openingHoursSpecification": [{
|
||||||
|
"@type": "OpeningHoursSpecification",
|
||||||
|
"dayOfWeek": "http://schema.org/Monday",
|
||||||
|
"opens": "00:00",
|
||||||
|
"closes": "23:59",
|
||||||
|
}, {
|
||||||
|
"@type": "OpeningHoursSpecification",
|
||||||
|
"dayOfWeek": "http://schema.org/Tuesday",
|
||||||
|
"opens": "00:00",
|
||||||
|
"closes": "23:59",
|
||||||
|
}, {
|
||||||
|
"@type": "OpeningHoursSpecification",
|
||||||
|
"dayOfWeek": "http://schema.org/Wednesday",
|
||||||
|
"opens": "00:00",
|
||||||
|
"closes": "23:59",
|
||||||
|
}, {
|
||||||
|
"@type": "OpeningHoursSpecification",
|
||||||
|
"dayOfWeek": "http://schema.org/Thursday",
|
||||||
|
"opens": "00:00",
|
||||||
|
"closes": "23:59",
|
||||||
|
}, {
|
||||||
|
"@type": "OpeningHoursSpecification",
|
||||||
|
"dayOfWeek": "http://schema.org/Friday",
|
||||||
|
"opens": "00:00",
|
||||||
|
"closes": "23:59",
|
||||||
|
}, {
|
||||||
|
"@type": "OpeningHoursSpecification",
|
||||||
|
"dayOfWeek": "http://schema.org/Saturday",
|
||||||
|
"opens": "00:00",
|
||||||
|
"closes": "23:59",
|
||||||
|
}, {
|
||||||
|
"@type": "OpeningHoursSpecification",
|
||||||
|
"dayOfWeek": "http://schema.org/Sunday",
|
||||||
|
"opens": "00:00",
|
||||||
|
"closes": "23:59",
|
||||||
|
}],
|
||||||
|
"maximumParkingDuration": "P30D",
|
||||||
|
"publicAccess": true,
|
||||||
|
"numberOfLevels": 1,
|
||||||
|
"covered": false,
|
||||||
|
"totalCapacity": 5,
|
||||||
|
"allows": [{
|
||||||
|
"@type": "AllowedBicycle",
|
||||||
|
"bicycleType": "https://data.velopark.be/openvelopark/terms#RegularBicycle",
|
||||||
|
"bicyclesAmount": 5,
|
||||||
|
"countingSystem": false,
|
||||||
|
}],
|
||||||
|
"geo": [{ "@type": "GeoCoordinates", "latitude": 50.94047, "longitude": 4.324813 }],
|
||||||
|
"priceSpecification": [{ "@type": "PriceSpecification", "freeOfCharge": true }],
|
||||||
|
"@id": "https://data.velopark.be/data/De-Lijn_303749#section1",
|
||||||
|
}],
|
||||||
|
}
|
||||||
|
|
||||||
|
const compacted = await LinkedDataLoader.compact(veloparkEntry)
|
||||||
|
expect(compacted.fee).equal("no")
|
||||||
|
expect(compacted.operator).equal("De Lijn")
|
||||||
|
|
||||||
|
expect(compacted.building).equal("bicycle_shed")
|
||||||
|
expect(compacted.access).equal("yes")
|
||||||
|
expect(compacted.max_stay).equal("30 days")
|
||||||
|
expect(compacted.opening_hours).equal("24/7")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
})
|
||||||
|
})
|
Loading…
Reference in a new issue