contacts-pane 2.4.8 → 2.4.12-beta4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.eslintrc +3 -1
- package/.github/workflows/ci.yml +74 -0
- package/.nvmrc +1 -1
- package/LICENSE.md +0 -0
- package/Makefile +0 -0
- package/README.md +0 -0
- package/__tests__/unit/data-reformat-test.js +166 -0
- package/__tests__/unit/data-reformat-test.ts +185 -0
- package/__tests__/unit/setup.js +74 -0
- package/__tests__/unit/setup.ts +77 -0
- package/babel.config.js +13 -0
- package/card.ai +0 -0
- package/card.png +0 -0
- package/contactLogic.js +84 -7
- package/contactsPane.js +64 -20
- package/diff.txt +0 -0
- package/exampleOfOpenData/mit-wikidata-details.ttl +0 -0
- package/exampleOfOpenData/mit-wikidata-query.sparql +0 -0
- package/exampleOfOpenData/wikidata-get.json +0 -0
- package/groupMembershipControl.js +56 -13
- package/individual.js +3 -2
- package/individualForm.js +0 -0
- package/jest.config.js +11 -0
- package/jest.setup.ts +10 -0
- package/lib/autocompleteBar.js +99 -0
- package/lib/autocompleteBar.js.map +1 -0
- package/lib/autocompleteField.js +157 -0
- package/lib/autocompleteField.js.map +1 -0
- package/lib/autocompletePicker.js +240 -0
- package/lib/autocompletePicker.js.map +1 -0
- package/lib/forms.js +315 -0
- package/lib/instituteDetailsQuery.js +38 -0
- package/lib/publicData.js +387 -0
- package/lib/publicData.js.map +1 -0
- package/lib/vcard.js +916 -0
- package/mintNewAddressBook.js +5 -4
- package/mugshotGallery.js +16 -4
- package/organizationForm.js +0 -0
- package/organizationForm.ttl +0 -0
- package/package.json +26 -13
- package/shapes/contacts-shapes.ttl +0 -0
- package/src/autocompleteBar.ts +92 -0
- package/src/autocompleteField.ts +180 -0
- package/src/autocompletePicker.ts +253 -0
- package/src/forms.ttl +1 -1
- package/src/instituteDetailsQuery.sparql +0 -0
- package/src/publicData.ts +385 -0
- package/src/vcard.ttl +0 -0
- package/toolsPane.js +70 -19
- package/tsconfig.json +16 -0
- package/webidControl.js +49 -4
|
@@ -0,0 +1,385 @@
|
|
|
1
|
+
/* Logic to access public data stores
|
|
2
|
+
*
|
|
3
|
+
* including filtering resut by natural language etc
|
|
4
|
+
*/
|
|
5
|
+
import { Literal, NamedNode, parse } from 'rdflib'
|
|
6
|
+
import { store } from 'solid-logic'
|
|
7
|
+
import { ns } from 'solid-ui'
|
|
8
|
+
import * as instituteDetailsQuery from '../lib/instituteDetailsQuery.js'
|
|
9
|
+
|
|
10
|
+
export const AUTOCOMPLETE_LIMIT = 3000 // How many to get from server
|
|
11
|
+
|
|
12
|
+
const subjectRegexp = /\$\(subject\)/g
|
|
13
|
+
|
|
14
|
+
interface Term {
|
|
15
|
+
type: string;
|
|
16
|
+
value: string
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface Binding {
|
|
20
|
+
subject: Term;
|
|
21
|
+
name?: Term
|
|
22
|
+
location?: Term
|
|
23
|
+
coordinates?: Term
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
type Bindings = Binding[]
|
|
27
|
+
|
|
28
|
+
export type QueryParameters =
|
|
29
|
+
{ label: string;
|
|
30
|
+
logo: string;
|
|
31
|
+
searchByNameQuery?: string;
|
|
32
|
+
searchByNameURI?: string;
|
|
33
|
+
insitituteDetailsQuery?: string;
|
|
34
|
+
endpoint?: string;
|
|
35
|
+
class: object
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Schema.org seems to suggest NGOs are non-profit and Corporaions are for-profit
|
|
39
|
+
// but doesn't have explicit classes
|
|
40
|
+
export const wikidataClasses = {
|
|
41
|
+
Corporation: 'http://www.wikidata.org/entity/Q6881511', // Enterprise is for-profit
|
|
42
|
+
EducationalOrganization: 'http://www.wikidata.org/entity/Q178706', // insitution
|
|
43
|
+
GovernmentOrganization: 'http://www.wikidata.org/entity/Q327333', // government agency
|
|
44
|
+
MedicalOrganization: 'http://www.wikidata.org/entity/Q4287745',
|
|
45
|
+
MusicGroup: 'http://www.wikidata.org/entity/Q32178211', // music organization
|
|
46
|
+
NGO: 'http://www.wikidata.org/entity/Q163740', // nonprofit organization @@
|
|
47
|
+
Occupation: 'http://www.wikidata.org/entity/Q28640', // Profession
|
|
48
|
+
// Organization: 'http://www.wikidata.org/entity/Q43229',
|
|
49
|
+
Project: 'http://www.wikidata.org/entity/Q170584',
|
|
50
|
+
SportsOrganization: 'http://www.wikidata.org/entity/Q4438121',
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export async function getPreferredLanguages () {
|
|
54
|
+
return [ 'fr', 'en', 'de', 'it'] // @@ testing only -- code me later
|
|
55
|
+
}
|
|
56
|
+
export const escoParameters:QueryParameters = {
|
|
57
|
+
label: 'ESCO',
|
|
58
|
+
logo: 'https://ec.europa.eu/esco/portal/static_resource2/images/logo/logo_en.gif',
|
|
59
|
+
searchByNameQuery: null, // No sparql endpoint
|
|
60
|
+
searchByNameURI: 'https://ec.europa.eu/esco/api/search?language=$(language)&type=occupation&text=$(name)',
|
|
61
|
+
endpoint: null,
|
|
62
|
+
class: {}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export const dbpediaParameters:QueryParameters = {
|
|
66
|
+
label: 'DBPedia',
|
|
67
|
+
logo: 'https://upload.wikimedia.org/wikipedia/commons/thumb/7/73/DBpediaLogo.svg/263px-DBpediaLogo.svg.png',
|
|
68
|
+
searchByNameQuery: `select distinct ?subject, ?name where {
|
|
69
|
+
?subject a $(class); rdfs:label ?name
|
|
70
|
+
FILTER regex(?name, "$(name)", "i")
|
|
71
|
+
} LIMIT $(limit)`,
|
|
72
|
+
endpoint: 'https://dbpedia.org/sparql/',
|
|
73
|
+
class: { AcademicInsitution: 'http://umbel.org/umbel/rc/EducationalOrganization'}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export const wikidataParameters = {
|
|
77
|
+
label: 'WikiData',
|
|
78
|
+
logo: 'https://www.wikimedia.org/static/images/project-logos/wikidatawiki.png',
|
|
79
|
+
endpoint: 'https://query.wikidata.org/sparql',
|
|
80
|
+
class: { AcademicInsitution: 'http://www.wikidata.org/entity/Q4671277',
|
|
81
|
+
Enterprise: 'http://www.wikidata.org/entity/Q6881511',
|
|
82
|
+
Business: 'http://www.wikidata.org/entity/Q4830453',
|
|
83
|
+
NGO: 'http://www.wikidata.org/entity/Q79913',
|
|
84
|
+
CharitableOrganization: 'http://www.wikidata.org/entity/Q708676',
|
|
85
|
+
Insitute: 'http://www.wikidata.org/entity/Q1664720',
|
|
86
|
+
},
|
|
87
|
+
searchByNameQuery: `SELECT ?subject ?name
|
|
88
|
+
WHERE {
|
|
89
|
+
?klass wdt:P279* $(class) .
|
|
90
|
+
?subject wdt:P31 ?klass .
|
|
91
|
+
?subject rdfs:label ?name.
|
|
92
|
+
FILTER regex(?name, "$(name)", "i")
|
|
93
|
+
} LIMIT $(limit) `, // was SERVICE wikibase:label { bd:serviceParam wikibase:language "[AUTO_LANGUAGE],en" }
|
|
94
|
+
|
|
95
|
+
insitituteDetailsQuery: `CONSTRUCT
|
|
96
|
+
{ wd:Q49108 schema:name ?itemLabel;
|
|
97
|
+
schema:logo ?logo;
|
|
98
|
+
schema:logo ?sealImage;
|
|
99
|
+
schema:subOrganization ?subsidiary .
|
|
100
|
+
?subsidiary schema:name ?subsidiaryLabel .
|
|
101
|
+
}
|
|
102
|
+
WHERE
|
|
103
|
+
{
|
|
104
|
+
wd:Q49108 # rdfs:label ?itemLabel ;
|
|
105
|
+
wdt:P154 ?logo;
|
|
106
|
+
wdt:P158 ?sealImage ;
|
|
107
|
+
wdt:P355 ?subsidiary .
|
|
108
|
+
# ?subsidiary rdfs:label ?subsidiaryLabel .
|
|
109
|
+
|
|
110
|
+
SERVICE wikibase:label { bd:serviceParam wikibase:language "[AUTO_LANGUAGE], fr". }
|
|
111
|
+
}`
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/* From an array of bindings with a names for each row,
|
|
115
|
+
* remove dupliacte names for the same thing, leaving the user's
|
|
116
|
+
* preferred language version
|
|
117
|
+
*/
|
|
118
|
+
export function filterByLanguage (bindings, languagePrefs) {
|
|
119
|
+
let uris = {}
|
|
120
|
+
bindings.forEach(binding => { // Organize names by their subject
|
|
121
|
+
const uri = binding.subject.value
|
|
122
|
+
uris[uri] = uris[uri] || []
|
|
123
|
+
uris[uri].push(binding)
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
var languagePrefs2 = languagePrefs
|
|
127
|
+
languagePrefs2.reverse() // prefered last
|
|
128
|
+
|
|
129
|
+
var slimmed = []
|
|
130
|
+
for (const u in uris) { // needs hasOwnProperty ?
|
|
131
|
+
const bindings = uris[u]
|
|
132
|
+
const sortMe = bindings.map(binding => {
|
|
133
|
+
return [ languagePrefs2.indexOf(binding.name['xml:lang']), binding]
|
|
134
|
+
})
|
|
135
|
+
sortMe.sort() // best at th ebottom
|
|
136
|
+
sortMe.reverse() // best at the top
|
|
137
|
+
slimmed.push(sortMe[0][1])
|
|
138
|
+
} // map u
|
|
139
|
+
console.log(` Filter by language: ${bindings.length} -> ${slimmed.length}`)
|
|
140
|
+
return slimmed
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export var wikidataClassMap = {
|
|
144
|
+
'http://www.wikidata.org/entity/Q15936437': ns.schema('CollegeOrUniversity'), // research university
|
|
145
|
+
'http://www.wikidata.org/entity/Q1664720': ns.schema('EducationalOrganization'), // insitute @@
|
|
146
|
+
'http://www.wikidata.org/entity/Q43229': ns.schema('Organization'), // research university
|
|
147
|
+
'http://www.wikidata.org/entity/Q3918': ns.schema('CollegeOrUniversity'), // university
|
|
148
|
+
'http://www.wikidata.org/entity/Q170584': ns.schema('Project'), // university
|
|
149
|
+
'http://www.wikidata.org/entity/Q327333': ns.schema('GovernmentOrganization'), // gobvt agency
|
|
150
|
+
'http://www.wikidata.org/entity/Q2221906': ns.schema('Place'), // geographic location
|
|
151
|
+
|
|
152
|
+
}
|
|
153
|
+
export var predMap = { // allow other mappings top added in theory
|
|
154
|
+
class: ns.rdf('type'),
|
|
155
|
+
// logo: ns.schema('logo'),
|
|
156
|
+
sealImage: ns.schema('logo'),
|
|
157
|
+
//image: ns.schema('image'), defaults to shema
|
|
158
|
+
shortName:ns.foaf('nick'),
|
|
159
|
+
subsidiary: ns.schema('subOrganization')
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export function loadFromBindings (kb, solidSubject:NamedNode, bindings, doc) {
|
|
163
|
+
var results = {}
|
|
164
|
+
console.log(`loadFromBindings: subject: ${solidSubject}`)
|
|
165
|
+
console.log(` doc: ${doc}`)
|
|
166
|
+
bindings.forEach(binding => {
|
|
167
|
+
for (const key in binding) {
|
|
168
|
+
const result = binding[key]
|
|
169
|
+
const combined = JSON.stringify(result) // ( result.type, result.value )
|
|
170
|
+
results[key] = results[key] || new Set()
|
|
171
|
+
results[key].add(combined) // remove duplicates
|
|
172
|
+
}
|
|
173
|
+
})
|
|
174
|
+
for (const key in results) {
|
|
175
|
+
const values = results[key]
|
|
176
|
+
console.log(` results ${key} -> ${values}`)
|
|
177
|
+
values.forEach(combined => {
|
|
178
|
+
const result = JSON.parse(combined)
|
|
179
|
+
const { type, value } = result
|
|
180
|
+
var obj
|
|
181
|
+
if (type === 'uri') {
|
|
182
|
+
obj = kb.sym(value)
|
|
183
|
+
} else if (type === 'literal') {
|
|
184
|
+
obj = new Literal(value, result.language, result.datatype)
|
|
185
|
+
} else {
|
|
186
|
+
throw new Error(`loadFromBindings: unexpected type: ${type}`)
|
|
187
|
+
}
|
|
188
|
+
if (key == 'type') {
|
|
189
|
+
if (wikidataClassMap[value]) {
|
|
190
|
+
obj = wikidataClassMap[value]
|
|
191
|
+
} else {
|
|
192
|
+
console.warn('Unmapped Wikidata Class: ' + value)
|
|
193
|
+
}
|
|
194
|
+
} else if (key === 'coordinates') {
|
|
195
|
+
// const latlong = value // Like 'Point(-71.106111111 42.375)'
|
|
196
|
+
console.log(' @@@ hey a point: ' + value)
|
|
197
|
+
const regexp =/.*\(([-0-9\.-]*) ([-0-9\.-]*)\)/
|
|
198
|
+
const match = regexp.exec(value)
|
|
199
|
+
const float = ns.xsd('float')
|
|
200
|
+
const latitude = new Literal(match[1], null, float)
|
|
201
|
+
const longitude = new Literal(match[2], null, float)
|
|
202
|
+
kb.add(solidSubject, ns.schema('longitude'), longitude, doc)
|
|
203
|
+
kb.add(solidSubject, ns.schema('latitude'), latitude, doc)
|
|
204
|
+
} else if (predMap[key]) {
|
|
205
|
+
const pred = predMap[key] || ns.schema(key) // fallback to just using schema.org
|
|
206
|
+
kb.add(solidSubject, pred, obj, doc) // @@ deal with non-string and objects
|
|
207
|
+
console.log(` public data ${pred} ${obj}.`)
|
|
208
|
+
}
|
|
209
|
+
})
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/* ESCO sopecific
|
|
214
|
+
*/
|
|
215
|
+
|
|
216
|
+
/* Query all entities of given class and partially matching name
|
|
217
|
+
*/
|
|
218
|
+
export async function queryESCODataByName (filter: string, theClass:NamedNode, queryTarget: QueryParameters): Promise<Bindings> {
|
|
219
|
+
const queryURI = queryTarget.searchByNameURI
|
|
220
|
+
.replace('$(name)', filter)
|
|
221
|
+
.replace('$(limit)', '' + AUTOCOMPLETE_LIMIT)
|
|
222
|
+
.replace('$(class)', theClass)
|
|
223
|
+
console.log('Querying ESCO data - uri: ' + queryURI)
|
|
224
|
+
|
|
225
|
+
const options = { credentials: 'omit',
|
|
226
|
+
headers: { 'Accept': 'application/json'}
|
|
227
|
+
} // CORS
|
|
228
|
+
var response
|
|
229
|
+
response = await store.fetcher.webOperation('GET', queryURI, options)
|
|
230
|
+
//complain('Error querying db of organizations: ' + err)
|
|
231
|
+
const text = response.responseText
|
|
232
|
+
console.log(' Query result text' + text.slice(0,500) + '...')
|
|
233
|
+
if (text.length === 0) throw new Error('Wot no text back from ESCO query ' + queryURI)
|
|
234
|
+
const json = JSON.parse(text)
|
|
235
|
+
console.log(' Query result JSON' + JSON.stringify(json, null, 4).slice(0,500) + '...')
|
|
236
|
+
|
|
237
|
+
const results = json._embedded.results // Array
|
|
238
|
+
const bindings = results.map(result => {
|
|
239
|
+
const name = result.title
|
|
240
|
+
const uri = result.uri // like http://data.europa.eu/esco/occupation/57af9090-55b4-4911-b2d0-86db01c00b02
|
|
241
|
+
return { name: { value: name, type: 'literal'}, uri: {type: 'IRI', value: uri}} // simulate SPARQL bindings
|
|
242
|
+
})
|
|
243
|
+
return bindings
|
|
244
|
+
// return queryPublicDataSelect(sparql, queryTarget)
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
/* Query all entities of given class and partially matching name
|
|
249
|
+
*/
|
|
250
|
+
export async function queryPublicDataByName (filter: string, theClass:NamedNode, queryTarget: QueryParameters): Promise<Bindings> {
|
|
251
|
+
const sparql = queryTarget.searchByNameQuery
|
|
252
|
+
.replace('$(name)', filter)
|
|
253
|
+
.replace('$(limit)', '' + AUTOCOMPLETE_LIMIT)
|
|
254
|
+
.replace('$(class)', theClass)
|
|
255
|
+
console.log('Querying public data - sparql: ' + sparql)
|
|
256
|
+
return queryPublicDataSelect(sparql, queryTarget)
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
export async function queryPublicDataSelect (sparql: string, queryTarget: QueryParameters): Promise<Bindings> {
|
|
260
|
+
const myUrlWithParams = new URL(queryTarget.endpoint);
|
|
261
|
+
myUrlWithParams.searchParams.append("query", sparql);
|
|
262
|
+
const queryURI = myUrlWithParams.href
|
|
263
|
+
console.log(' queryPublicDataSelect uri: ' + queryURI);
|
|
264
|
+
|
|
265
|
+
const options = { credentials: 'omit',
|
|
266
|
+
headers: { 'Accept': 'application/json'}
|
|
267
|
+
} // CORS
|
|
268
|
+
var response
|
|
269
|
+
response = await store.fetcher.webOperation('GET', queryURI, options)
|
|
270
|
+
//complain('Error querying db of organizations: ' + err)
|
|
271
|
+
const text = response.responseText
|
|
272
|
+
// console.log(' Query result text' + text.slice(0,100) + '...')
|
|
273
|
+
if (text.length === 0) throw new Error('Wot no text back from query ' + queryURI)
|
|
274
|
+
const json = JSON.parse(text)
|
|
275
|
+
console.log(' Query result JSON' + JSON.stringify(json, null, 4).slice(0,100) + '...')
|
|
276
|
+
const bindings = json.results.bindings
|
|
277
|
+
return bindings
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
export async function queryPublicDataConstruct (sparql: string, pubicId: NamedNode, queryTarget: QueryParameters): Promise<Bindings> {
|
|
281
|
+
console.log('queryPublicDataConstruct: sparql:', sparql)
|
|
282
|
+
const myUrlWithParams = new URL(queryTarget.endpoint);
|
|
283
|
+
myUrlWithParams.searchParams.append("query", sparql);
|
|
284
|
+
const queryURI = myUrlWithParams.href
|
|
285
|
+
console.log(' queryPublicDataConstruct uri: ' + queryURI);
|
|
286
|
+
const options = { credentials: 'omit', // CORS
|
|
287
|
+
headers: { 'Accept': 'text/turtle'}
|
|
288
|
+
}
|
|
289
|
+
const response = await store.fetcher.webOperation('GET', queryURI, options)
|
|
290
|
+
const text = response.responseText
|
|
291
|
+
const report = text.lenth > 500 ? text.slice(0,200) + ' ... ' + text.slice(-200) : text
|
|
292
|
+
console.log(' queryPublicDataConstruct result text:' + report)
|
|
293
|
+
if (text.length === 0) throw new Error('queryPublicDataConstruct: No text back from construct query:' + queryURI)
|
|
294
|
+
parse(text, store, pubicId.uri, 'text/turtle')
|
|
295
|
+
return
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
export async function loadPublicDataThing (kb, subject: NamedNode, publicDataID: NamedNode) {
|
|
299
|
+
|
|
300
|
+
if (publicDataID.uri.startsWith('https://dbpedia.org/resource/')) {
|
|
301
|
+
return getDbpediaDetails(kb, subject, publicDataID)
|
|
302
|
+
} else if (publicDataID.uri.match(/^https?:\/\/www\.wikidata\.org\/entity\/.*/)) {
|
|
303
|
+
const QId = publicDataID.uri.split('/')[4]
|
|
304
|
+
const dataURI = `http://www.wikidata.org/wiki/Special:EntityData/${QId}.ttl`
|
|
305
|
+
// In fact loading the data URI gives much to much irrelevant data, from wikidata.
|
|
306
|
+
await getWikidataDetails(kb, subject, publicDataID)
|
|
307
|
+
// await getWikidataLocation(kb, subject, publicDataID) -- should get that in the details query now
|
|
308
|
+
} else {
|
|
309
|
+
const iDToFetch = publicDataID.uri.startsWith('http:') ? kb.sym('https:' + publicDataID.uri.slice(5))
|
|
310
|
+
: publicDataID
|
|
311
|
+
return kb.fetcher.load(iDToFetch, { credentials: 'omit',
|
|
312
|
+
headers: { 'Accept': 'text/turtle'}
|
|
313
|
+
})
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
export async function getWikidataDetails (kb, solidSubject:NamedNode, publicDataID:NamedNode) {
|
|
318
|
+
const subjRegexp = /wd:Q49108/g
|
|
319
|
+
const sparql = instituteDetailsQuery.replace(subjRegexp, publicDataID)
|
|
320
|
+
await queryPublicDataConstruct(sparql, publicDataID, wikidataParameters)
|
|
321
|
+
console.log('getWikidataDetails: loaded.', publicDataID)
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
export async function getWikidataDetailsOld (kb, solidSubject:NamedNode, publicDataID:NamedNode) {
|
|
325
|
+
const sparql = `select distinct * where {
|
|
326
|
+
optional { $(subject) wdt:P31 ?class } # instance of
|
|
327
|
+
optional { $(subject) wdt:P154 ?logo }
|
|
328
|
+
optional { $(subject) wdt:P158 ?sealImage }
|
|
329
|
+
# optional { $(subject) wdt:P159 ?headquartersLocation }
|
|
330
|
+
|
|
331
|
+
optional { $(subject) wdt:P17 ?country }
|
|
332
|
+
optional { $(subject) wdt:P18 ?image }
|
|
333
|
+
optional { $(subject) wdt:P1813 ?shortName }
|
|
334
|
+
|
|
335
|
+
optional { $(subject) wdt:P355 ?subsidiary }
|
|
336
|
+
# SERVICE wikibase:label { bd:serviceParam wikibase:language "fr,en,de,it" }
|
|
337
|
+
}`
|
|
338
|
+
.replace(subjectRegexp, publicDataID)
|
|
339
|
+
const bindings = await queryPublicDataSelect(sparql, wikidataParameters)
|
|
340
|
+
loadFromBindings (kb, publicDataID, bindings, publicDataID.doc()) //arg2 was solidSubject
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
export async function getWikidataLocation (kb, solidSubject:NamedNode, publicDataID:NamedNode) {
|
|
344
|
+
const sparql = `select distinct * where {
|
|
345
|
+
|
|
346
|
+
$(subject) wdt:P276 ?location .
|
|
347
|
+
|
|
348
|
+
optional { ?location wdt:P2044 ?elevation }
|
|
349
|
+
optional { ?location wdt:P131 ?region }
|
|
350
|
+
optional { ?location wdt:P625 ?coordinates }
|
|
351
|
+
optional { ?location wdt:P17 ?country }
|
|
352
|
+
|
|
353
|
+
# SERVICE wikibase:label { bd:serviceParam wikibase:language "fr,en,de,it" }
|
|
354
|
+
}`.replace(subjectRegexp, publicDataID)
|
|
355
|
+
console.log( ' location query sparql:' + sparql)
|
|
356
|
+
const bindings = await queryPublicDataSelect(sparql, wikidataParameters)
|
|
357
|
+
console.log(' location query bindings:', bindings)
|
|
358
|
+
loadFromBindings (kb, publicDataID, bindings, publicDataID.doc()) // was solidSubject
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
export async function getDbpediaDetails (kb, solidSubject:NamedNode, publicDataID:NamedNode) {
|
|
363
|
+
// Note below the string form of the named node with <> works in SPARQL
|
|
364
|
+
const sparql = `select distinct ?city, ?state, ?country, ?homepage, ?logo, ?lat, ?long, WHERE {
|
|
365
|
+
OPTIONAL { <${publicDataID}> <http://dbpedia.org/ontology/city> ?city }
|
|
366
|
+
OPTIONAL { ${publicDataID} <http://dbpedia.org/ontology/state> ?state }
|
|
367
|
+
OPTIONAL { ${publicDataID} <http://dbpedia.org/ontology/country> ?country }
|
|
368
|
+
OPTIONAL { ${publicDataID} foaf:homepage ?homepage }
|
|
369
|
+
OPTIONAL { ${publicDataID} foaf:lat ?lat; foaf:long ?long }
|
|
370
|
+
OPTIONAL { ${publicDataID} <http://dbpedia.org/ontology/country> ?country }
|
|
371
|
+
}`
|
|
372
|
+
const predMap = {
|
|
373
|
+
city: ns.vcard('locality'),
|
|
374
|
+
state: ns.vcard('region'),
|
|
375
|
+
country: ns.vcard('country-name'),
|
|
376
|
+
homepage: ns.foaf('homepage'),
|
|
377
|
+
lat: ns.geo('latitude'),
|
|
378
|
+
long: ns.geo('longitude'),
|
|
379
|
+
}
|
|
380
|
+
const bindings = await queryPublicDataSelect(sparql, dbpediaParameters)
|
|
381
|
+
bindings.forEach(binding => {
|
|
382
|
+
const uri = binding.subject.value // @@ To be written
|
|
383
|
+
const name = binding.name.value
|
|
384
|
+
})
|
|
385
|
+
}
|
package/src/vcard.ttl
CHANGED
|
File without changes
|
package/toolsPane.js
CHANGED
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
/* global confirm, $rdf */
|
|
4
4
|
|
|
5
5
|
import * as UI from 'solid-ui'
|
|
6
|
-
import {
|
|
6
|
+
import { store } from 'solid-logic'
|
|
7
|
+
import { saveNewGroup, addPersonToGroup, groupMembers } from './contactLogic'
|
|
7
8
|
export function toolsPane (
|
|
8
9
|
selectAllGroups,
|
|
9
10
|
selectedGroups,
|
|
@@ -13,7 +14,7 @@ export function toolsPane (
|
|
|
13
14
|
me
|
|
14
15
|
) {
|
|
15
16
|
const dom = dataBrowserContext.dom
|
|
16
|
-
const kb =
|
|
17
|
+
const kb = store
|
|
17
18
|
const ns = UI.ns
|
|
18
19
|
const VCARD = ns.vcard
|
|
19
20
|
|
|
@@ -68,7 +69,7 @@ export function toolsPane (
|
|
|
68
69
|
|
|
69
70
|
//
|
|
70
71
|
try {
|
|
71
|
-
await UI.
|
|
72
|
+
await UI.login.registrationControl(context, book, ns.vcard('AddressBook'))
|
|
72
73
|
} catch (e) {
|
|
73
74
|
UI.widgets.complain(context, 'registrationControl: ' + e)
|
|
74
75
|
}
|
|
@@ -84,7 +85,9 @@ export function toolsPane (
|
|
|
84
85
|
function stats () {
|
|
85
86
|
const totalCards = kb.each(undefined, VCARD('inAddressBook'), book).length
|
|
86
87
|
log('' + totalCards + ' cards loaded. ')
|
|
87
|
-
|
|
88
|
+
let groups = kb.each(book, VCARD('includesGroup'))
|
|
89
|
+
const strings = new Set(groups.map(group => group.uri)) // remove dups
|
|
90
|
+
groups = [...strings].map(uri => kb.sym(uri))
|
|
88
91
|
log('' + groups.length + ' total groups. ')
|
|
89
92
|
const gg = []
|
|
90
93
|
for (const g in selectedGroups) {
|
|
@@ -136,7 +139,7 @@ export function toolsPane (
|
|
|
136
139
|
|
|
137
140
|
for (let i = 0; i < gg.length; i++) {
|
|
138
141
|
const g = kb.sym(gg[i])
|
|
139
|
-
const a = kb
|
|
142
|
+
const a = groupMembers(kb, g)
|
|
140
143
|
log(UI.utils.label(g) + ': ' + a.length + ' members')
|
|
141
144
|
for (let j = 0; j < a.length; j++) {
|
|
142
145
|
const card = a[j]
|
|
@@ -160,7 +163,7 @@ export function toolsPane (
|
|
|
160
163
|
stats.nameEmailIndex = kb.any(book, ns.vcard('nameEmailIndex'))
|
|
161
164
|
log('Loading name index...')
|
|
162
165
|
|
|
163
|
-
|
|
166
|
+
store.fetcher.nowOrWhenFetched(
|
|
164
167
|
stats.nameEmailIndex,
|
|
165
168
|
undefined,
|
|
166
169
|
function (_ok, _message) {
|
|
@@ -356,6 +359,7 @@ export function toolsPane (
|
|
|
356
359
|
const other = stats.nameLessIndex[cardText]
|
|
357
360
|
if (other) {
|
|
358
361
|
log(' Matches with ' + other)
|
|
362
|
+
// alain not sure it works we may need to concat with 'sameAs' group.doc (.map(st => st.why))
|
|
359
363
|
const cardGroups = kb.each(null, ns.vcard('hasMember'), card)
|
|
360
364
|
const otherGroups = kb.each(null, ns.vcard('hasMember'), other)
|
|
361
365
|
for (let j = 0; j < cardGroups.length; j++) {
|
|
@@ -431,9 +435,9 @@ export function toolsPane (
|
|
|
431
435
|
for (let i = 0; i < stats.uniques.length; i++) {
|
|
432
436
|
stats.uniquesSet[stats.uniques[i].uri] = true
|
|
433
437
|
}
|
|
434
|
-
stats.groupMembers =
|
|
435
|
-
|
|
436
|
-
.map(
|
|
438
|
+
stats.groupMembers = []
|
|
439
|
+
kb.each(null, ns.vcard('hasMember'))
|
|
440
|
+
.map(group => { stats.groupMembers = stats.groupMembers.concat(groupMembers(kb, group)) })
|
|
437
441
|
log(' Naive group members ' + stats.groupMembers.length)
|
|
438
442
|
stats.groupMemberSet = []
|
|
439
443
|
for (let j = 0; j < stats.groupMembers.length; j++) {
|
|
@@ -574,7 +578,10 @@ export function toolsPane (
|
|
|
574
578
|
log(' Regenerating group of uniques...' + cleanGroup)
|
|
575
579
|
const data = sz.statementsToN3(sts)
|
|
576
580
|
|
|
577
|
-
return kb.fetcher.webOperation('PUT', cleanGroup, {
|
|
581
|
+
return kb.fetcher.webOperation('PUT', cleanGroup, {
|
|
582
|
+
data: data,
|
|
583
|
+
contentType: 'text/turtle'
|
|
584
|
+
})
|
|
578
585
|
})
|
|
579
586
|
.then(() => {
|
|
580
587
|
log(' Done uniques group ' + cleanGroup)
|
|
@@ -614,12 +621,14 @@ export function toolsPane (
|
|
|
614
621
|
.then(scanForDuplicates)
|
|
615
622
|
.then(checkGroupMembers)
|
|
616
623
|
.then(checkAllNameless)
|
|
617
|
-
.then((
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
624
|
+
.then(() => {
|
|
625
|
+
return new Promise(function (resolve, reject) {
|
|
626
|
+
if (confirm('Write new clean versions?')) {
|
|
627
|
+
resolve(true)
|
|
628
|
+
} else {
|
|
629
|
+
reject()
|
|
630
|
+
}
|
|
631
|
+
})
|
|
623
632
|
})
|
|
624
633
|
.then(saveCleanPeople)
|
|
625
634
|
.then(saveAllGroups)
|
|
@@ -659,13 +668,15 @@ export function toolsPane (
|
|
|
659
668
|
|
|
660
669
|
const reverseIndex = {}
|
|
661
670
|
const groupless = []
|
|
662
|
-
|
|
663
|
-
|
|
671
|
+
let groups = kb.each(book, VCARD('includesGroup'))
|
|
672
|
+
const strings = new Set(groups.map(group => group.uri)) // remove dups
|
|
673
|
+
groups = [...strings].map(uri => kb.sym(uri))
|
|
664
674
|
log('' + groups.length + ' total groups. ')
|
|
665
675
|
|
|
666
676
|
for (let i = 0; i < groups.length; i++) {
|
|
667
677
|
const g = groups[i]
|
|
668
|
-
const a = kb
|
|
678
|
+
const a = groupMembers(kb, g)
|
|
679
|
+
|
|
669
680
|
log(UI.utils.label(g) + ': ' + a.length + ' members')
|
|
670
681
|
for (let j = 0; j < a.length; j++) {
|
|
671
682
|
kb.allAliases(a[j]).forEach(function (y) {
|
|
@@ -713,6 +724,46 @@ export function toolsPane (
|
|
|
713
724
|
fixGrouplessButton.style.cssText = buttonStyle
|
|
714
725
|
fixGrouplessButton.textContent = 'Put all individuals with no group in a new group'
|
|
715
726
|
fixGrouplessButton.addEventListener('click', _event => fixGroupless(book))
|
|
727
|
+
|
|
728
|
+
async function fixToOldDataModel (book) {
|
|
729
|
+
async function updateToOldDataModel(groups) {
|
|
730
|
+
let ds = []
|
|
731
|
+
let ins = []
|
|
732
|
+
groups.forEach(group => {
|
|
733
|
+
let vcardOrWebids = kb.statementsMatching(null, ns.owl('sameAs'), null, group.doc()).map(st => st.subject)
|
|
734
|
+
const strings = new Set(vcardOrWebids.map(contact => contact.uri)) // remove dups
|
|
735
|
+
vcardOrWebids = [...strings].map(uri => kb.sym(uri))
|
|
736
|
+
vcardOrWebids.forEach(item => {
|
|
737
|
+
if (!kb.each(item, ns.vcard('fn'), null, group.doc()).length) {
|
|
738
|
+
// delete item this is a new data model, item is a webid not a card.
|
|
739
|
+
ds = ds.concat(kb
|
|
740
|
+
.statementsMatching(item, ns.owl('sameAs'), null, group.doc())
|
|
741
|
+
.concat(kb.statementsMatching(undefined, undefined, item, group.doc())))
|
|
742
|
+
// add webid card to group
|
|
743
|
+
const cards = kb.each(item, ns.owl('sameAs'), null, group.doc())
|
|
744
|
+
cards.forEach(card => {
|
|
745
|
+
ins = ins.concat($rdf.st(card, ns.owl('sameAs'), item, group.doc()))
|
|
746
|
+
.concat($rdf.st(group, ns.vcard('hasMember'), card, group.doc()))
|
|
747
|
+
})
|
|
748
|
+
}
|
|
749
|
+
})
|
|
750
|
+
})
|
|
751
|
+
if (ds.length && confirm('Groups can be updated to old data model ?')) {
|
|
752
|
+
await kb.updater.updateMany(ds, ins)
|
|
753
|
+
alert('Update done')
|
|
754
|
+
} else { if (!ds.length) alert('Nothing to update.\nAll Groups already use the old data model.')}
|
|
755
|
+
}
|
|
756
|
+
let groups = kb.each(book, VCARD('includesGroup'))
|
|
757
|
+
const strings = new Set(groups.map(group => group.uri)) // remove dups
|
|
758
|
+
groups = [...strings].map(uri => kb.sym(uri))
|
|
759
|
+
updateToOldDataModel(groups)
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
const fixToOldDataModelButton = pane.appendChild(dom.createElement('button'))
|
|
763
|
+
fixToOldDataModelButton.style.cssText = buttonStyle
|
|
764
|
+
fixToOldDataModelButton.textContent = 'Revert groups to old data model'
|
|
765
|
+
fixToOldDataModelButton.addEventListener('click', _event => fixToOldDataModel(book))
|
|
766
|
+
|
|
716
767
|
} // main
|
|
717
768
|
main()
|
|
718
769
|
return pane
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2015",
|
|
4
|
+
"module": "commonjs",
|
|
5
|
+
"strict": false,
|
|
6
|
+
"outDir": "lib",
|
|
7
|
+
"sourceMap": true,
|
|
8
|
+
"allowSyntheticDefaultImports" : true // https://github.com/inrupt/solid-client-authn-js/issues/3219
|
|
9
|
+
// "esModuleInterop": true // ,
|
|
10
|
+
// "skipLibCheck": true // otherwise it takes *.d.ts from node_modules see: https://stackoverflow.com/questions/51634361/how-to-force-tsc-to-ignore-node-modules-folder
|
|
11
|
+
},
|
|
12
|
+
"include": [
|
|
13
|
+
"./src/**/*"
|
|
14
|
+
],
|
|
15
|
+
"exclude": ["**/*.spec.ts", "**/*.test.ts"]
|
|
16
|
+
}
|