UNPKG

contacts-pane

Version:

Contacts Pane: Contacts manager for Address Book, Groups, and Individuals.

386 lines (346 loc) 15.8 kB
/* Logic to access public data stores * * including filtering resut by natural language etc */ import { Literal, NamedNode, parse } from 'rdflib' import { store } from 'solid-logic' import { ns } from 'solid-ui' import * as instituteDetailsQuery from '../lib/instituteDetailsQuery.js' export const AUTOCOMPLETE_LIMIT = 3000 // How many to get from server const subjectRegexp = /\$\(subject\)/g interface Term { type: string; value: string } interface Binding { subject: Term; name?: Term location?: Term coordinates?: Term } type Bindings = Binding[] export type QueryParameters = { label: string; logo: string; searchByNameQuery?: string; searchByNameURI?: string; insitituteDetailsQuery?: string; endpoint?: string; class: object } // Schema.org seems to suggest NGOs are non-profit and Corporaions are for-profit // but doesn't have explicit classes export const wikidataClasses = { Corporation: 'http://www.wikidata.org/entity/Q6881511', // Enterprise is for-profit EducationalOrganization: 'http://www.wikidata.org/entity/Q178706', // insitution GovernmentOrganization: 'http://www.wikidata.org/entity/Q327333', // government agency MedicalOrganization: 'http://www.wikidata.org/entity/Q4287745', MusicGroup: 'http://www.wikidata.org/entity/Q32178211', // music organization NGO: 'http://www.wikidata.org/entity/Q163740', // nonprofit organization @@ Occupation: 'http://www.wikidata.org/entity/Q28640', // Profession // Organization: 'http://www.wikidata.org/entity/Q43229', Project: 'http://www.wikidata.org/entity/Q170584', SportsOrganization: 'http://www.wikidata.org/entity/Q4438121', } export async function getPreferredLanguages () { return [ 'fr', 'en', 'de', 'it'] // @@ testing only -- code me later } export const escoParameters:QueryParameters = { label: 'ESCO', logo: 'https://ec.europa.eu/esco/portal/static_resource2/images/logo/logo_en.gif', searchByNameQuery: null, // No sparql endpoint searchByNameURI: 'https://ec.europa.eu/esco/api/search?language=$(language)&type=occupation&text=$(name)', endpoint: null, class: {} } export const dbpediaParameters:QueryParameters = { label: 'DBPedia', logo: 'https://upload.wikimedia.org/wikipedia/commons/thumb/7/73/DBpediaLogo.svg/263px-DBpediaLogo.svg.png', searchByNameQuery: `select distinct ?subject, ?name where { ?subject a $(class); rdfs:label ?name FILTER regex(?name, "$(name)", "i") } LIMIT $(limit)`, endpoint: 'https://dbpedia.org/sparql/', class: { AcademicInsitution: 'http://umbel.org/umbel/rc/EducationalOrganization'} } export const wikidataParameters = { label: 'WikiData', logo: 'https://www.wikimedia.org/static/images/project-logos/wikidatawiki.png', endpoint: 'https://query.wikidata.org/sparql', class: { AcademicInsitution: 'http://www.wikidata.org/entity/Q4671277', Enterprise: 'http://www.wikidata.org/entity/Q6881511', Business: 'http://www.wikidata.org/entity/Q4830453', NGO: 'http://www.wikidata.org/entity/Q79913', CharitableOrganization: 'http://www.wikidata.org/entity/Q708676', Insitute: 'http://www.wikidata.org/entity/Q1664720', }, searchByNameQuery: `SELECT ?subject ?name WHERE { ?klass wdt:P279* $(class) . ?subject wdt:P31 ?klass . ?subject rdfs:label ?name. FILTER regex(?name, "$(name)", "i") } LIMIT $(limit) `, // was SERVICE wikibase:label { bd:serviceParam wikibase:language "[AUTO_LANGUAGE],en" } insitituteDetailsQuery: `CONSTRUCT { wd:Q49108 schema:name ?itemLabel; schema:logo ?logo; schema:logo ?sealImage; schema:subOrganization ?subsidiary . ?subsidiary schema:name ?subsidiaryLabel . } WHERE { wd:Q49108 # rdfs:label ?itemLabel ; wdt:P154 ?logo; wdt:P158 ?sealImage ; wdt:P355 ?subsidiary . # ?subsidiary rdfs:label ?subsidiaryLabel . SERVICE wikibase:label { bd:serviceParam wikibase:language "[AUTO_LANGUAGE], fr". } }` } /* From an array of bindings with a names for each row, * remove dupliacte names for the same thing, leaving the user's * preferred language version */ export function filterByLanguage (bindings, languagePrefs) { let uris = {} bindings.forEach(binding => { // Organize names by their subject const uri = binding.subject.value uris[uri] = uris[uri] || [] uris[uri].push(binding) }) var languagePrefs2 = languagePrefs languagePrefs2.reverse() // prefered last var slimmed = [] for (const u in uris) { // needs hasOwnProperty ? const bindings = uris[u] const sortMe = bindings.map(binding => { return [ languagePrefs2.indexOf(binding.name['xml:lang']), binding] }) sortMe.sort() // best at th ebottom sortMe.reverse() // best at the top slimmed.push(sortMe[0][1]) } // map u console.log(` Filter by language: ${bindings.length} -> ${slimmed.length}`) return slimmed } export var wikidataClassMap = { 'http://www.wikidata.org/entity/Q15936437': ns.schema('CollegeOrUniversity'), // research university 'http://www.wikidata.org/entity/Q1664720': ns.schema('EducationalOrganization'), // insitute @@ 'http://www.wikidata.org/entity/Q43229': ns.schema('Organization'), // research university 'http://www.wikidata.org/entity/Q3918': ns.schema('CollegeOrUniversity'), // university 'http://www.wikidata.org/entity/Q170584': ns.schema('Project'), // university 'http://www.wikidata.org/entity/Q327333': ns.schema('GovernmentOrganization'), // gobvt agency 'http://www.wikidata.org/entity/Q2221906': ns.schema('Place'), // geographic location } export var predMap = { // allow other mappings top added in theory class: ns.rdf('type'), // logo: ns.schema('logo'), sealImage: ns.schema('logo'), //image: ns.schema('image'), defaults to shema shortName:ns.foaf('nick'), subsidiary: ns.schema('subOrganization') } export function loadFromBindings (kb, solidSubject:NamedNode, bindings, doc) { var results = {} console.log(`loadFromBindings: subject: ${solidSubject}`) console.log(` doc: ${doc}`) bindings.forEach(binding => { for (const key in binding) { const result = binding[key] const combined = JSON.stringify(result) // ( result.type, result.value ) results[key] = results[key] || new Set() results[key].add(combined) // remove duplicates } }) for (const key in results) { const values = results[key] console.log(` results ${key} -> ${values}`) values.forEach(combined => { const result = JSON.parse(combined) const { type, value } = result var obj if (type === 'uri') { obj = kb.sym(value) } else if (type === 'literal') { obj = new Literal(value, result.language, result.datatype) } else { throw new Error(`loadFromBindings: unexpected type: ${type}`) } if (key == 'type') { if (wikidataClassMap[value]) { obj = wikidataClassMap[value] } else { console.warn('Unmapped Wikidata Class: ' + value) } } else if (key === 'coordinates') { // const latlong = value // Like 'Point(-71.106111111 42.375)' console.log(' @@@ hey a point: ' + value) const regexp =/.*\(([-0-9\.-]*) ([-0-9\.-]*)\)/ const match = regexp.exec(value) const float = ns.xsd('float') const latitude = new Literal(match[1], null, float) const longitude = new Literal(match[2], null, float) kb.add(solidSubject, ns.schema('longitude'), longitude, doc) kb.add(solidSubject, ns.schema('latitude'), latitude, doc) } else if (predMap[key]) { const pred = predMap[key] || ns.schema(key) // fallback to just using schema.org kb.add(solidSubject, pred, obj, doc) // @@ deal with non-string and objects console.log(` public data ${pred} ${obj}.`) } }) } } /* ESCO sopecific */ /* Query all entities of given class and partially matching name */ export async function queryESCODataByName (filter: string, theClass:NamedNode, queryTarget: QueryParameters): Promise<Bindings> { const queryURI = queryTarget.searchByNameURI .replace('$(name)', filter) .replace('$(limit)', '' + AUTOCOMPLETE_LIMIT) .replace('$(class)', theClass) console.log('Querying ESCO data - uri: ' + queryURI) const options = { credentials: 'omit', headers: { 'Accept': 'application/json'} } // CORS var response response = await store.fetcher.webOperation('GET', queryURI, options) //complain('Error querying db of organizations: ' + err) const text = response.responseText console.log(' Query result text' + text.slice(0,500) + '...') if (text.length === 0) throw new Error('Wot no text back from ESCO query ' + queryURI) const json = JSON.parse(text) console.log(' Query result JSON' + JSON.stringify(json, null, 4).slice(0,500) + '...') const results = json._embedded.results // Array const bindings = results.map(result => { const name = result.title const uri = result.uri // like http://data.europa.eu/esco/occupation/57af9090-55b4-4911-b2d0-86db01c00b02 return { name: { value: name, type: 'literal'}, uri: {type: 'IRI', value: uri}} // simulate SPARQL bindings }) return bindings // return queryPublicDataSelect(sparql, queryTarget) } /* Query all entities of given class and partially matching name */ export async function queryPublicDataByName (filter: string, theClass:NamedNode, queryTarget: QueryParameters): Promise<Bindings> { const sparql = queryTarget.searchByNameQuery .replace('$(name)', filter) .replace('$(limit)', '' + AUTOCOMPLETE_LIMIT) .replace('$(class)', theClass) console.log('Querying public data - sparql: ' + sparql) return queryPublicDataSelect(sparql, queryTarget) } export async function queryPublicDataSelect (sparql: string, queryTarget: QueryParameters): Promise<Bindings> { const myUrlWithParams = new URL(queryTarget.endpoint); myUrlWithParams.searchParams.append("query", sparql); const queryURI = myUrlWithParams.href console.log(' queryPublicDataSelect uri: ' + queryURI); const options = { credentials: 'omit', headers: { 'Accept': 'application/json'} } // CORS var response response = await store.fetcher.webOperation('GET', queryURI, options) //complain('Error querying db of organizations: ' + err) const text = response.responseText // console.log(' Query result text' + text.slice(0,100) + '...') if (text.length === 0) throw new Error('Wot no text back from query ' + queryURI) const json = JSON.parse(text) console.log(' Query result JSON' + JSON.stringify(json, null, 4).slice(0,100) + '...') const bindings = json.results.bindings return bindings } export async function queryPublicDataConstruct (sparql: string, pubicId: NamedNode, queryTarget: QueryParameters): Promise<Bindings> { console.log('queryPublicDataConstruct: sparql:', sparql) const myUrlWithParams = new URL(queryTarget.endpoint); myUrlWithParams.searchParams.append("query", sparql); const queryURI = myUrlWithParams.href console.log(' queryPublicDataConstruct uri: ' + queryURI); const options = { credentials: 'omit', // CORS headers: { 'Accept': 'text/turtle'} } const response = await store.fetcher.webOperation('GET', queryURI, options) const text = response.responseText const report = text.lenth > 500 ? text.slice(0,200) + ' ... ' + text.slice(-200) : text console.log(' queryPublicDataConstruct result text:' + report) if (text.length === 0) throw new Error('queryPublicDataConstruct: No text back from construct query:' + queryURI) parse(text, store, pubicId.uri, 'text/turtle') return } export async function loadPublicDataThing (kb, subject: NamedNode, publicDataID: NamedNode) { if (publicDataID.uri.startsWith('https://dbpedia.org/resource/')) { return getDbpediaDetails(kb, subject, publicDataID) } else if (publicDataID.uri.match(/^https?:\/\/www\.wikidata\.org\/entity\/.*/)) { const QId = publicDataID.uri.split('/')[4] const dataURI = `http://www.wikidata.org/wiki/Special:EntityData/${QId}.ttl` // In fact loading the data URI gives much to much irrelevant data, from wikidata. await getWikidataDetails(kb, subject, publicDataID) // await getWikidataLocation(kb, subject, publicDataID) -- should get that in the details query now } else { const iDToFetch = publicDataID.uri.startsWith('http:') ? kb.sym('https:' + publicDataID.uri.slice(5)) : publicDataID return kb.fetcher.load(iDToFetch, { credentials: 'omit', headers: { 'Accept': 'text/turtle'} }) } } export async function getWikidataDetails (kb, solidSubject:NamedNode, publicDataID:NamedNode) { const subjRegexp = /wd:Q49108/g const sparql = instituteDetailsQuery.replace(subjRegexp, publicDataID) await queryPublicDataConstruct(sparql, publicDataID, wikidataParameters) console.log('getWikidataDetails: loaded.', publicDataID) } export async function getWikidataDetailsOld (kb, solidSubject:NamedNode, publicDataID:NamedNode) { const sparql = `select distinct * where { optional { $(subject) wdt:P31 ?class } # instance of optional { $(subject) wdt:P154 ?logo } optional { $(subject) wdt:P158 ?sealImage } # optional { $(subject) wdt:P159 ?headquartersLocation } optional { $(subject) wdt:P17 ?country } optional { $(subject) wdt:P18 ?image } optional { $(subject) wdt:P1813 ?shortName } optional { $(subject) wdt:P355 ?subsidiary } # SERVICE wikibase:label { bd:serviceParam wikibase:language "fr,en,de,it" } }` .replace(subjectRegexp, publicDataID) const bindings = await queryPublicDataSelect(sparql, wikidataParameters) loadFromBindings (kb, publicDataID, bindings, publicDataID.doc()) //arg2 was solidSubject } export async function getWikidataLocation (kb, solidSubject:NamedNode, publicDataID:NamedNode) { const sparql = `select distinct * where { $(subject) wdt:P276 ?location . optional { ?location wdt:P2044 ?elevation } optional { ?location wdt:P131 ?region } optional { ?location wdt:P625 ?coordinates } optional { ?location wdt:P17 ?country } # SERVICE wikibase:label { bd:serviceParam wikibase:language "fr,en,de,it" } }`.replace(subjectRegexp, publicDataID) console.log( ' location query sparql:' + sparql) const bindings = await queryPublicDataSelect(sparql, wikidataParameters) console.log(' location query bindings:', bindings) loadFromBindings (kb, publicDataID, bindings, publicDataID.doc()) // was solidSubject } export async function getDbpediaDetails (kb, solidSubject:NamedNode, publicDataID:NamedNode) { // Note below the string form of the named node with <> works in SPARQL const sparql = `select distinct ?city, ?state, ?country, ?homepage, ?logo, ?lat, ?long, WHERE { OPTIONAL { <${publicDataID}> <http://dbpedia.org/ontology/city> ?city } OPTIONAL { ${publicDataID} <http://dbpedia.org/ontology/state> ?state } OPTIONAL { ${publicDataID} <http://dbpedia.org/ontology/country> ?country } OPTIONAL { ${publicDataID} foaf:homepage ?homepage } OPTIONAL { ${publicDataID} foaf:lat ?lat; foaf:long ?long } OPTIONAL { ${publicDataID} <http://dbpedia.org/ontology/country> ?country } }` const predMap = { city: ns.vcard('locality'), state: ns.vcard('region'), country: ns.vcard('country-name'), homepage: ns.foaf('homepage'), lat: ns.geo('latitude'), long: ns.geo('longitude'), } const bindings = await queryPublicDataSelect(sparql, dbpediaParameters) bindings.forEach(binding => { const uri = binding.subject.value // @@ To be written const name = binding.name.value }) }