contacts-pane
Version:
Contacts Pane: Contacts manager for Address Book, Groups, and Individuals.
386 lines (346 loc) • 15.8 kB
text/typescript
/* 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
})
}