2024-04-05 17:49:31 +02:00
import type { Feature , GeoJSON , Geometry , Polygon } from "geojson"
2024-02-22 18:58:34 +01:00
import jsonld from "jsonld"
import { OH , OpeningHour } from "../../UI/OpeningHours/OpeningHours"
import { Utils } from "../../Utils"
import PhoneValidator from "../../UI/InputElement/Validators/PhoneValidator"
import EmailValidator from "../../UI/InputElement/Validators/EmailValidator"
import { Validator } from "../../UI/InputElement/Validator"
import UrlValidator from "../../UI/InputElement/Validators/UrlValidator"
2024-02-26 02:24:46 +01:00
import Constants from "../../Models/Constants"
2024-04-05 17:49:31 +02:00
import TypedSparql , { default as S , SparqlResult } from "./TypedSparql"
2024-02-22 18:58:34 +01:00
2024-02-26 02:24:46 +01:00
interface JsonLdLoaderOptions {
country? : string
}
2024-04-05 17:49:31 +02:00
2024-04-13 02:40:21 +02:00
type PropertiesSpec < T extends string > = Partial <
Record < T , string | string [ ] | Partial < Record < T , string > > >
>
2024-04-05 17:49:31 +02:00
2024-02-22 18:58:34 +01:00
export default class LinkedDataLoader {
private static readonly COMPACTING_CONTEXT = {
name : "http://schema.org/name" ,
website : { "@id" : "http://schema.org/url" , "@type" : "@id" } ,
phone : { "@id" : "http://schema.org/telephone" } ,
email : { "@id" : "http://schema.org/email" } ,
image : { "@id" : "http://schema.org/image" , "@type" : "@id" } ,
opening_hours : { "@id" : "http://schema.org/openingHoursSpecification" } ,
openingHours : { "@id" : "http://schema.org/openingHours" , "@container" : "@set" } ,
2024-04-09 15:12:18 +02:00
geo : { "@id" : "http://schema.org/geo" } ,
2024-06-20 04:21:29 +02:00
alt_name : { "@id" : "http://schema.org/alternateName" } ,
2024-02-22 18:58:34 +01:00
}
private static COMPACTING_CONTEXT_OH = {
dayOfWeek : { "@id" : "http://schema.org/dayOfWeek" , "@container" : "@set" } ,
2024-04-05 17:49:31 +02:00
closes : {
"@id" : "http://schema.org/closes" ,
2024-06-20 04:21:29 +02:00
"@type" : "http://www.w3.org/2001/XMLSchema#time" ,
2024-04-05 17:49:31 +02:00
} ,
opens : {
"@id" : "http://schema.org/opens" ,
2024-06-20 04:21:29 +02:00
"@type" : "http://www.w3.org/2001/XMLSchema#time" ,
} ,
2024-02-22 18:58:34 +01:00
}
2024-04-08 17:05:49 +02:00
private static formatters : Record < "phone" | "email" | "website" , Validator > = {
2024-02-22 18:58:34 +01:00
phone : new PhoneValidator ( ) ,
email : new EmailValidator ( ) ,
2024-06-20 04:21:29 +02:00
website : new UrlValidator ( undefined , undefined , true ) ,
2024-02-22 18:58:34 +01:00
}
private static ignoreKeys = [
"http://schema.org/logo" ,
"http://schema.org/address" ,
"@type" ,
"@id" ,
"@base" ,
"http://schema.org/contentUrl" ,
"http://schema.org/datePublished" ,
"http://schema.org/description" ,
"http://schema.org/hasMap" ,
"http://schema.org/priceRange" ,
2024-06-20 04:21:29 +02:00
"http://schema.org/contactPoint" ,
2024-02-22 18:58:34 +01:00
]
2024-04-05 17:49:31 +02:00
private static shapeToPolygon ( str : string ) : Polygon {
const polygon = str . substring ( "POLYGON ((" . length , str . length - 2 )
return < Polygon > {
type : "Polygon" ,
2024-04-13 02:40:21 +02:00
coordinates : [
polygon . split ( "," ) . map ( ( coors ) = >
coors
. trim ( )
. split ( " " )
. map ( ( n ) = > Number ( n ) )
2024-06-20 04:21:29 +02:00
) ,
] ,
2024-04-05 17:49:31 +02:00
}
}
2024-02-26 02:24:46 +01:00
2024-04-05 17:49:31 +02:00
private static async geoToGeometry ( geo ) : Promise < Geometry > {
if ( Array . isArray ( geo ) ) {
2024-04-13 02:40:21 +02:00
const features = await Promise . all ( geo . map ( ( g ) = > LinkedDataLoader . geoToGeometry ( g ) ) )
const polygon = features . find ( ( f ) = > f . type === "Polygon" )
2024-04-05 17:49:31 +02:00
if ( polygon ) {
return polygon
}
2024-04-13 02:40:21 +02:00
const ls = features . find ( ( f ) = > f . type === "LineString" )
2024-04-05 17:49:31 +02:00
if ( ls ) {
return ls
}
return features [ 0 ]
}
if ( geo [ "@type" ] === "http://schema.org/GeoCoordinates" ) {
const context = {
lat : {
"@id" : "http://schema.org/latitude" ,
2024-06-20 04:21:29 +02:00
"@type" : "http://www.w3.org/2001/XMLSchema#double" ,
2024-04-05 17:49:31 +02:00
} ,
lon : {
"@id" : "http://schema.org/longitude" ,
2024-06-20 04:21:29 +02:00
"@type" : "http://www.w3.org/2001/XMLSchema#double" ,
} ,
2024-04-05 17:49:31 +02:00
}
const flattened = await jsonld . compact ( geo , context )
return {
type : "Point" ,
2024-06-20 04:21:29 +02:00
coordinates : [ Number ( flattened . lon ) , Number ( flattened . lat ) ] ,
2024-04-05 17:49:31 +02:00
}
2024-02-22 18:58:34 +01:00
}
2024-04-13 02:40:21 +02:00
if (
geo [ "@type" ] === "http://schema.org/GeoShape" &&
geo [ "http://schema.org/polygon" ] !== undefined
) {
2024-04-05 17:49:31 +02:00
const str = geo [ "http://schema.org/polygon" ] [ "@value" ]
LinkedDataLoader . shapeToPolygon ( str )
2024-02-22 18:58:34 +01:00
}
2024-04-05 17:49:31 +02:00
throw "Unsupported geo type: " + geo [ "@type" ]
2024-02-22 18:58:34 +01:00
}
/ * *
* Parses http : //schema.org/openingHours
*
* // 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"
2024-04-05 17:49:31 +02: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"
2024-06-20 01:11:22 +02: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 00:00-00:00" ) // => "Mo-Sa 09:30-18:00"
2024-02-22 18:58:34 +01:00
* /
static ohStringToOsmFormat ( oh : string ) {
oh = oh . toLowerCase ( )
if ( oh === "mo-su" ) {
return "24/7"
}
const regex = /([a-z]+ [0-9:]+-[0-9:]+) (.*)/
let match = oh . match ( regex )
2024-06-19 02:50:08 +02:00
let parts : string [ ] = [ ]
2024-02-22 18:58:34 +01:00
while ( match ) {
parts . push ( match [ 1 ] )
oh = match [ 2 ]
match = oh ? . match ( regex )
}
parts . push ( oh )
2024-06-19 02:50:08 +02:00
/ * o m i t e x p r e s s i o n s a s " s u 0 0 : 0 0 - 0 0 : 0 0 " . T h i s _ c a n _ b e i n t e r p r e t e d a s ' a l l d a y l o n g ' , b u t w i l l , i n p r a c t i c e , i n d i c a t e t h a t i t i s c l o s e d
Looking at you , C & A !
view - source :https : //www.c-and-a.com/stores/be-en/oost-vlaanderen/sint-niklaas/stationsstraat-100.html
* * /
2024-06-20 04:21:29 +02:00
parts = parts . filter ( ( p ) = > ! p . match ( /.. 00:00-00:00/ ) )
2024-02-22 18:58:34 +01:00
// actually the same as OSM-oh
return OH . simplify ( parts . join ( ";" ) )
}
2024-04-05 17:49:31 +02:00
static async ohToOsmFormat ( openingHoursSpecification ) : Promise < string | undefined > {
if ( typeof openingHoursSpecification === "string" ) {
return openingHoursSpecification
}
const compacted = await jsonld . compact (
2024-02-22 18:58:34 +01:00
openingHoursSpecification ,
< any > LinkedDataLoader . COMPACTING_CONTEXT_OH
)
2024-04-05 17:49:31 +02:00
const spec : object = compacted [ "@graph" ]
if ( ! spec ) {
return undefined
}
const allRules : OpeningHour [ ] = [ ]
2024-02-22 18:58:34 +01:00
for ( const rule of spec ) {
2024-04-05 17:49:31 +02:00
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 )
} )
2024-02-22 18:58:34 +01:00
const opens : string = rule . opens
const closes : string = rule . closes === "23:59" ? "24:00" : rule . closes
allRules . push ( . . . OH . ParseRule ( dow + " " + opens + "-" + closes ) )
}
return OH . ToString ( OH . MergeTimes ( allRules ) )
}
2024-04-05 17:49:31 +02:00
static async compact ( data : object , options? : JsonLdLoaderOptions ) : Promise < object > {
if ( Array . isArray ( data ) ) {
2024-04-13 02:40:21 +02:00
return await Promise . all ( data . map ( ( point ) = > LinkedDataLoader . compact ( point , options ) ) )
2024-02-26 02:24:46 +01:00
}
2024-04-05 17:49:31 +02:00
2024-02-26 02:24:46 +01:00
const country = options ? . country
2024-04-05 17:49:31 +02:00
const compacted = await jsonld . compact ( data , < any > LinkedDataLoader . COMPACTING_CONTEXT )
2024-02-22 18:58:34 +01:00
compacted [ "opening_hours" ] = await LinkedDataLoader . ohToOsmFormat (
compacted [ "opening_hours" ]
)
if ( compacted [ "openingHours" ] ) {
2024-04-05 17:49:31 +02:00
const ohspec : string [ ] = < any > compacted [ "openingHours" ]
2024-02-22 18:58:34 +01:00
compacted [ "opening_hours" ] = OH . simplify (
ohspec . map ( ( r ) = > LinkedDataLoader . ohStringToOsmFormat ( r ) ) . join ( "; " )
)
delete compacted [ "openingHours" ]
}
2024-05-08 14:20:59 +02:00
if ( compacted [ "opening_hours" ] === undefined ) {
2024-04-22 18:16:19 +02:00
delete compacted [ "opening_hours" ]
}
2024-02-22 18:58:34 +01:00
if ( compacted [ "geo" ] ) {
compacted [ "geo" ] = < any > await LinkedDataLoader . geoToGeometry ( compacted [ "geo" ] )
}
2024-04-05 17:49:31 +02:00
2024-04-10 13:23:54 +02:00
if ( compacted [ "alt_name" ] ) {
if ( compacted [ "alt_name" ] === compacted [ "name" ] ) {
delete compacted [ "alt_name" ]
2024-04-09 15:12:18 +02:00
}
}
2024-04-05 17:49:31 +02:00
2024-02-22 18:58:34 +01:00
for ( const k in compacted ) {
if ( compacted [ k ] === "" ) {
delete compacted [ k ]
continue
}
if ( this . ignoreKeys . indexOf ( k ) >= 0 ) {
delete compacted [ k ]
continue
}
const formatter = LinkedDataLoader . formatters [ k ]
if ( formatter ) {
if ( country ) {
compacted [ k ] = formatter . reformat ( < string > compacted [ k ] , ( ) = > country )
} else {
compacted [ k ] = formatter . reformat ( < string > compacted [ k ] )
}
}
}
2024-04-05 17:49:31 +02:00
return compacted
2024-02-26 02:24:46 +01:00
}
2024-04-05 17:49:31 +02:00
2024-04-13 02:40:21 +02:00
static async fetchJsonLd (
url : string ,
options? : JsonLdLoaderOptions ,
2024-06-20 03:30:14 +02:00
mode ? : "fetch-lod" | "fetch-raw" | "proxy"
2024-04-13 02:40:21 +02:00
) : Promise < object > {
2024-06-20 03:30:14 +02:00
mode ? ? = "fetch-lod"
2024-06-19 03:22:57 +02:00
if ( mode === "proxy" ) {
2024-04-05 17:49:31 +02:00
url = Constants . linkedDataProxy . replace ( "{url}" , encodeURIComponent ( url ) )
}
2024-06-19 03:22:57 +02:00
if ( mode !== "fetch-raw" ) {
const data = await Utils . downloadJson ( url )
return await LinkedDataLoader . compact ( data , options )
}
let htmlContent = await Utils . download ( url )
const div = document . createElement ( "div" )
div . innerHTML = htmlContent
2024-06-20 04:21:29 +02:00
const script = Array . from ( div . getElementsByTagName ( "script" ) ) . find (
( script ) = > script . type === "application/ld+json"
)
2024-06-19 03:22:57 +02:00
const snippet = JSON . parse ( script . textContent )
snippet [ "@base" ] = url
return await LinkedDataLoader . compact ( snippet , options )
2024-02-26 02:24:46 +01:00
}
/ * *
* Only returns different items
* @param externalData
* @param currentData
* /
2024-04-13 02:40:21 +02:00
static removeDuplicateData (
externalData : Record < string , string > ,
currentData : Record < string , string >
) : Record < string , string > {
2024-02-26 02:24:46 +01:00
const d = { . . . externalData }
delete d [ "@context" ]
for ( const k in d ) {
const v = currentData [ k ]
if ( ! v ) {
continue
}
if ( k === "opening_hours" ) {
2024-04-13 02:40:21 +02:00
const oh = [ ] . concat ( . . . v . split ( ";" ) . map ( ( r ) = > OH . ParseRule ( r ) ? ? [ ] ) )
2024-02-26 02:24:46 +01:00
const merged = OH . ToString ( OH . MergeTimes ( oh ? ? [ ] ) )
if ( merged === d [ k ] ) {
delete d [ k ]
continue
}
}
if ( v === d [ k ] ) {
delete d [ k ]
}
delete d . geo
}
return d
2024-02-22 18:58:34 +01:00
}
2024-04-05 17:49:31 +02:00
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 ) {
2024-04-13 02:40:21 +02:00
throw (
"Found multiple values in properties for " + k + ": " + linkedData [ k ] . join ( "; " )
)
2024-04-05 17:49:31 +02:00
}
properties [ k ] = linkedData [ k ] . join ( "; " )
}
let geometry : Geometry = undefined
if ( properties [ "latitude" ] && properties [ "longitude" ] ) {
geometry = {
type : "Point" ,
2024-06-20 04:21:29 +02:00
coordinates : [ Number ( properties [ "longitude" ] ) , Number ( properties [ "latitude" ] ) ] ,
2024-04-05 17:49:31 +02:00
}
delete properties [ "latitude" ]
delete properties [ "longitude" ]
}
if ( properties [ "shape" ] ) {
geometry = LinkedDataLoader . shapeToPolygon ( properties [ "shape" ] )
}
const geo : GeoJSON = {
type : "Feature" ,
properties ,
2024-06-20 04:21:29 +02:00
geometry ,
2024-04-05 17:49:31 +02:00
}
delete linkedData . geo
delete properties . shape
delete properties . type
delete properties . parking
delete properties . g
delete properties . section
return geo
}
2024-04-13 02:40:21 +02:00
private static patchVeloparkProperties (
input : Record < string , Set < string > >
) : Record < string , string [ ] > {
2024-04-05 17:49:31 +02:00
const output : Record < string , string [ ] > = { }
for ( const k in input ) {
output [ k ] = Array . from ( input [ k ] )
}
2024-06-14 01:01:41 +02:00
if ( output [ "type" ] ? . [ 0 ] === "https://data.velopark.be/openvelopark/terms#BicycleLocker" ) {
2024-04-10 13:23:54 +02:00
output [ "bicycle_parking" ] = [ "lockers" ]
}
2024-06-16 16:06:26 +02:00
if ( output [ "type" ] === undefined ) {
2024-06-14 01:01:41 +02:00
console . error ( "No type given for" , output )
}
2024-04-10 13:23:54 +02:00
delete output [ "type" ]
2024-04-05 17:49:31 +02:00
function on ( key : string , applyF : ( s : string ) = > string ) {
if ( ! output [ key ] ) {
return
}
2024-04-13 02:40:21 +02:00
output [ key ] = output [ key ] . map ( ( v ) = > applyF ( v ) )
2024-06-16 16:06:26 +02:00
if ( ! output [ key ] . some ( ( v ) = > v !== undefined ) ) {
2024-04-30 17:55:21 +02:00
delete output [ key ]
}
2024-04-05 17:49:31 +02:00
}
function asBoolean ( key : string , invert : boolean = false ) {
2024-04-13 02:40:21 +02:00
on ( key , ( str ) = > {
const isTrue = "" + str === "true" || str === "True" || str === "yes"
2024-04-05 17:49:31 +02:00
if ( isTrue != invert ) {
return "yes"
}
return "no"
} )
}
2024-04-13 02:40:21 +02:00
on ( "maxstay" , ( maxstay ) = > {
2024-04-05 17:49:31 +02:00
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
2024-04-13 02:40:21 +02:00
} )
2024-04-05 17:49:31 +02:00
function rename ( source : string , target : string ) {
if ( output [ source ] === undefined || output [ source ] === null ) {
return
}
output [ target ] = output [ source ]
delete output [ source ]
}
2024-04-13 02:40:21 +02:00
on ( "phone" , ( p ) = > this . formatters [ "phone" ] . reformat ( p , ( ) = > "be" ) )
2024-04-08 17:05:49 +02:00
for ( const attribute in LinkedDataLoader . formatters ) {
2024-04-13 02:40:21 +02:00
on ( attribute , ( p ) = > LinkedDataLoader . formatters [ attribute ] . reformat ( p ) )
2024-04-08 17:05:49 +02:00
}
2024-04-10 13:23:54 +02:00
rename ( "phone" , "operator:phone" )
rename ( "email" , "operator:email" )
rename ( "website" , "operator:website" )
2024-04-13 02:40:21 +02:00
on ( "charge" , ( p ) = > {
2024-04-10 13:23:54 +02:00
if ( Number ( p ) === 0 ) {
2024-04-05 17:49:31 +02:00
output [ "fee" ] = [ "no" ]
return undefined
}
return "€" + Number ( p )
2024-04-13 02:40:21 +02:00
} )
2024-04-30 17:55:21 +02:00
2024-04-05 17:49:31 +02:00
if ( output [ "charge" ] && output [ "timeUnit" ] ) {
2024-04-13 02:40:21 +02:00
const duration =
Number ( output [ "chargeEnd" ] ? ? "1" ) - Number ( output [ "chargeStart" ] ? ? "0" )
2024-04-05 17:49:31 +02:00
const unit = output [ "timeUnit" ] [ 0 ]
let durationStr = ""
if ( duration !== 1 ) {
durationStr = duration + ""
}
2024-04-13 02:40:21 +02:00
output [ "charge" ] = output [ "charge" ] . map ( ( c ) = > c + "/" + ( durationStr + unit ) )
2024-04-05 17:49:31 +02:00
}
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" ]
2024-04-13 02:40:21 +02:00
on ( "access" , ( audience ) = > {
if (
[
"brede publiek" ,
"iedereen" ,
"bezoekers" ,
2024-06-20 04:21:29 +02:00
"iedereen - vooral bezoekers gemeentehuis of bibliotheek." ,
2024-04-13 02:40:21 +02:00
] . indexOf ( audience . toLowerCase ( ) ) >= 0
) {
2024-04-10 13:23:54 +02:00
return "yes"
2024-04-05 17:49:31 +02:00
}
2024-04-10 13:23:54 +02:00
if ( audience . toLowerCase ( ) . startsWith ( "bezoekers" ) ) {
return "yes"
2024-04-05 17:49:31 +02:00
}
if ( [ "abonnees" ] . indexOf ( audience . toLowerCase ( ) ) >= 0 ) {
return "members"
}
2024-04-10 13:23:54 +02:00
if ( audience . indexOf ( "Blue-locker app" ) >= 0 ) {
2024-04-05 17:49:31 +02:00
return "members"
}
if ( [ "buurtbewoners" ] . indexOf ( audience . toLowerCase ( ) ) >= 0 ) {
return "permissive"
// return "members"
}
2024-04-13 02:40:21 +02:00
if (
audience . toLowerCase ( ) . startsWith ( "klanten" ) ||
2024-04-05 17:49:31 +02:00
audience . toLowerCase ( ) . startsWith ( "werknemers" ) ||
2024-04-13 02:40:21 +02:00
audience . toLowerCase ( ) . startsWith ( "personeel" )
) {
2024-04-05 17:49:31 +02:00
return "customers"
}
2024-04-13 02:40:21 +02:00
console . warn (
"Suspicious 'access'-tag:" ,
audience ,
"for" ,
input [ "ref:velopark" ] ,
" assuming yes"
)
2024-04-10 13:23:54 +02:00
return "yes"
2024-04-05 17:49:31 +02:00
} )
2024-04-10 13:23:54 +02:00
if ( output [ "publicAccess" ] ? . [ 0 ] == "no" ) {
output [ "access" ] = [ "private" ]
2024-04-05 17:49:31 +02:00
}
delete output [ "publicAccess" ]
2024-04-13 02:40:21 +02:00
if (
output [ "restrictions" ] ? . [ 0 ] ===
"Geen bromfietsen, noch andere gemotoriseerde voertuigen"
) {
2024-04-05 17:49:31 +02:00
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 [ "numberOfLevels" ]
return output
}
2024-04-13 02:40:21 +02:00
private static async fetchVeloparkProperty < T extends string , G extends T > (
url : string ,
property : string ,
variable? : string
) : Promise < SparqlResult < T , G > > {
2024-04-05 17:49:31 +02:00
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#" ,
2024-06-20 04:21:29 +02:00
vpt : "https://data.velopark.be/openvelopark/terms#" ,
2024-04-05 17:49:31 +02:00
} ,
[ url ] ,
undefined ,
" ?parking a <http://schema.mobivoc.org/BicycleParkingStation>" ,
"?parking " + property + " " + ( variable ? ? "" )
)
return results
}
2024-04-13 02:40:21 +02:00
private static async fetchVeloparkGraphProperty < T extends string > (
url : string ,
property : string ,
subExpr? : string
) : Promise < SparqlResult < T , "g" > > {
2024-04-10 13:23:54 +02:00
return await new TypedSparql ( ) . typedSparql < T , "g" > (
2024-04-05 17:49:31 +02:00
{
schema : "http://schema.org/" ,
mv : "http://schema.mobivoc.org/" ,
gr : "http://purl.org/goodrelations/v1#" ,
vp : "https://data.velopark.be/openvelopark/vocabulary#" ,
2024-06-20 04:21:29 +02:00
vpt : "https://data.velopark.be/openvelopark/terms#" ,
2024-04-05 17:49:31 +02:00
} ,
[ url ] ,
"g" ,
" ?parking a <http://schema.mobivoc.org/BicycleParkingStation>" ,
2024-04-13 02:40:21 +02:00
S . graph ( "g" , "?section " + property + " " + ( subExpr ? ? "" ) , "?section a ?type" )
2024-04-05 17:49:31 +02:00
)
}
/ * *
* Merges many subresults into one result
* THis is a workaround for 'optional' not working decently
* @param r0
* /
2024-04-13 02:40:21 +02:00
public static mergeResults (
. . . r0 : SparqlResult < string , string > [ ]
) : SparqlResult < string , string > {
const r : SparqlResult < string > = { default : { } }
2024-04-05 17:49:31 +02:00
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
}
2024-04-13 02:40:21 +02:00
public static async fetchEntry < T extends string > (
directUrl : string ,
propertiesWithoutGraph : PropertiesSpec < T > ,
propertiesInGraph : PropertiesSpec < T > ,
extra? : string [ ]
) : Promise < SparqlResult < T , string > > {
2024-04-05 17:49:31 +02:00
const allPartialResults : SparqlResult < T , string > [ ] = [ ]
for ( const propertyName in propertiesWithoutGraph ) {
const e = propertiesWithoutGraph [ propertyName ]
if ( typeof e === "string" ) {
const variableName = e
2024-04-13 02:40:21 +02:00
const result = await this . fetchVeloparkProperty (
directUrl ,
propertyName ,
"?" + variableName
)
2024-04-05 17:49:31 +02:00
allPartialResults . push ( result )
} else {
for ( const subProperty in e ) {
const variableName = e [ subProperty ]
2024-04-13 02:40:21 +02:00
const result = await this . fetchVeloparkProperty (
directUrl ,
propertyName ,
` [ ${ subProperty } ? ${ variableName } ] `
)
2024-04-05 17:49:31 +02:00
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
}
2024-04-13 02:40:21 +02:00
const result = await this . fetchVeloparkGraphProperty (
directUrl ,
propertyName ,
variableName
)
2024-04-05 17:49:31 +02:00
allPartialResults . push ( result )
}
} else if ( typeof e === "string" ) {
let variableName = e
if ( variableName . match ( /[a-zA-Z_]+/ ) ) {
variableName = "?" + e
}
2024-04-13 02:40:21 +02:00
const result = await this . fetchVeloparkGraphProperty (
directUrl ,
propertyName ,
variableName
)
2024-04-05 17:49:31 +02:00
allPartialResults . push ( result )
} else {
for ( const subProperty in e ) {
const variableName = e [ subProperty ]
2024-04-13 02:40:21 +02:00
const result = await this . fetchVeloparkGraphProperty (
directUrl ,
propertyName ,
` [ ${ subProperty } ? ${ variableName } ] `
)
2024-04-05 17:49:31 +02:00
allPartialResults . push ( result )
}
}
}
for ( const e of extra ) {
const r = await this . fetchVeloparkGraphProperty ( directUrl , e )
allPartialResults . push ( r )
}
2024-05-07 15:32:44 +02:00
return this . mergeResults ( . . . allPartialResults )
2024-04-05 17:49:31 +02:00
}
2024-05-08 14:20:59 +02:00
private static veloparkCache : Record < string , Feature [ ] > = { }
2024-04-16 14:45:04 +02:00
2024-04-05 17:49:31 +02:00
/ * *
* Fetches all data relevant to velopark .
* The id will be saved as ` ref:velopark `
* @param url
* /
2024-06-16 16:06:26 +02:00
public static async fetchVeloparkEntry (
url : string ,
includeExtras : boolean = false
) : Promise < Feature [ ] > {
2024-05-08 14:20:59 +02:00
const cacheKey = includeExtras + url
if ( this . veloparkCache [ cacheKey ] ) {
2024-05-07 15:32:44 +02:00
return this . veloparkCache [ cacheKey ]
2024-04-16 14:45:04 +02:00
}
2024-04-05 17:49:31 +02:00
const withProxyUrl = Constants . linkedDataProxy . replace ( "{url}" , encodeURIComponent ( url ) )
const optionalPaths : Record < string , string | Record < string , string > > = {
"schema:interactionService" : {
2024-06-20 04:21:29 +02:00
"schema:url" : "website" ,
2024-04-05 17:49:31 +02:00
} ,
"mv:operatedBy" : {
2024-06-20 04:21:29 +02:00
"gr:legalName" : "operator" ,
2024-04-05 17:49:31 +02:00
} ,
"schema:contactPoint" : {
"schema:email" : "email" ,
2024-06-20 04:21:29 +02:00
"schema:telephone" : "phone" ,
2024-04-10 13:23:54 +02:00
} ,
2024-06-20 04:21:29 +02:00
"schema:dateModified" : "_last_edit_timestamp" ,
2024-04-05 17:49:31 +02:00
}
2024-05-08 14:20:59 +02:00
if ( includeExtras ) {
2024-05-07 15:32:44 +02:00
optionalPaths [ "schema:address" ] = {
2024-06-20 04:21:29 +02:00
"schema:streetAddress" : "addr" ,
2024-05-07 15:32:44 +02:00
}
2024-05-08 14:20:59 +02:00
optionalPaths [ "schema:name" ] = "name"
2024-05-07 15:32:44 +02:00
optionalPaths [ "schema:description" ] = "description"
}
2024-04-05 17:49:31 +02:00
const graphOptionalPaths = {
2024-04-13 02:40:21 +02:00
a : "type" ,
2024-04-05 17:49:31 +02:00
"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" ,
2024-06-20 04:21:29 +02:00
"schema:polygon" : "shape" ,
2024-04-05 17:49:31 +02:00
} ,
"schema:priceSpecification" : {
"mv:freeOfCharge" : "fee" ,
2024-06-20 04:21:29 +02:00
"schema:price" : "charge" ,
} ,
2024-04-05 17:49:31 +02:00
}
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]" ,
2024-06-20 04:21:29 +02:00
"vp:allows [vp:bicycleType <https://data.velopark.be/openvelopark/terms#TandemBicycle>; vp:bicyclesAmount ?capacityTandem; vp:bicycleType ?tandemBikeType]" ,
2024-04-05 17:49:31 +02:00
]
2024-04-13 02:40:21 +02:00
const unpatched = await this . fetchEntry (
withProxyUrl ,
optionalPaths ,
graphOptionalPaths ,
extra
)
2024-04-05 17:49:31 +02:00
const patched : Feature [ ] = [ ]
for ( const section in unpatched ) {
const p = LinkedDataLoader . patchVeloparkProperties ( unpatched [ section ] )
p [ "ref:velopark" ] = [ section ]
patched . push ( LinkedDataLoader . asGeojson ( p ) )
}
2024-05-07 15:32:44 +02:00
this . veloparkCache [ cacheKey ] = patched
2024-04-05 17:49:31 +02:00
return patched
}
2024-02-22 18:58:34 +01:00
}