vis-chronicle 0.0.2 → 0.0.3
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/README.md +21 -12
- package/package.json +34 -30
- package/src/fetch.js +131 -272
- package/src/global-data.json +34 -4
- package/src/index.js +7 -0
- package/src/render.js +552 -0
- package/src/sparql-builder.js +25 -8
- package/src/wikidata.js +67 -13
- package/styles/style.css +22 -5
package/src/render.js
ADDED
|
@@ -0,0 +1,552 @@
|
|
|
1
|
+
|
|
2
|
+
const moment = require('moment')
|
|
3
|
+
const assert = require('node:assert/strict');
|
|
4
|
+
const globalData = require("./global-data.json")
|
|
5
|
+
|
|
6
|
+
function postprocessDuration(duration)
|
|
7
|
+
{
|
|
8
|
+
if (duration.min)
|
|
9
|
+
duration.min = moment.duration(duration.min)
|
|
10
|
+
if (duration.max)
|
|
11
|
+
duration.max = moment.duration(duration.max)
|
|
12
|
+
if (duration.avg)
|
|
13
|
+
duration.avg = moment.duration(duration.avg)
|
|
14
|
+
return duration
|
|
15
|
+
}
|
|
16
|
+
function postprocessGlobalData()
|
|
17
|
+
{
|
|
18
|
+
for (const expectation of globalData.expectations)
|
|
19
|
+
{
|
|
20
|
+
if (expectation.duration)
|
|
21
|
+
{
|
|
22
|
+
postprocessDuration(expectation.duration)
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
postprocessGlobalData()
|
|
27
|
+
|
|
28
|
+
function momentSafeMin(a, b)
|
|
29
|
+
{
|
|
30
|
+
return a ? (b ? moment.min(a, b) : a) : b
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function momentSafeMax(a, b)
|
|
34
|
+
{
|
|
35
|
+
return a ? (b ? moment.max(a, b) : a) : b
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const renderer = {}
|
|
39
|
+
|
|
40
|
+
renderer.getExpectation = function(item)
|
|
41
|
+
{
|
|
42
|
+
if (item.expectedDuration)
|
|
43
|
+
{
|
|
44
|
+
return { "duration": item.expectedDuration };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
for (const expectation of globalData.expectations)
|
|
48
|
+
{
|
|
49
|
+
if (expectation.startQuery && item.startQuery != expectation.startQuery)
|
|
50
|
+
{
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
if (expectation.endQuery && item.endQuery != expectation.endQuery)
|
|
54
|
+
{
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
if (expectation.startEndQuery && item.startEndQuery != expectation.startEndQuery)
|
|
58
|
+
{
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
return expectation;
|
|
62
|
+
}
|
|
63
|
+
assert(false) // expect at least a universal fallback expectation
|
|
64
|
+
return undefined
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// produces JSON output from the queried data
|
|
68
|
+
renderer.produceOutput = function(inputSpec, items)
|
|
69
|
+
{
|
|
70
|
+
console.log("Producing output...")
|
|
71
|
+
|
|
72
|
+
if (inputSpec.chronicle.shareSuccessiveUncertainty)
|
|
73
|
+
{
|
|
74
|
+
// fill out missing data
|
|
75
|
+
for (const item of items)
|
|
76
|
+
{
|
|
77
|
+
if (item.start_min && !item.start_max && item.end_max) item.start_max = item.end_max.clone()
|
|
78
|
+
if (item.end_max && !item.end_min && item.start_min) item.end_min = item.start_min.clone()
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// group items with prev/next data into prev/next chains
|
|
82
|
+
//TODO: also use 'series ordinal' property for hinting
|
|
83
|
+
const successionChains = []
|
|
84
|
+
for (const item of items)
|
|
85
|
+
{
|
|
86
|
+
// try to append to an existing chain
|
|
87
|
+
//TODO: does not support branching chains
|
|
88
|
+
var nextForChain = null
|
|
89
|
+
var prevForChain = null
|
|
90
|
+
for (const chain of successionChains)
|
|
91
|
+
{
|
|
92
|
+
if ((item.next && item.next == chain[0].entity)
|
|
93
|
+
&& (chain[0].previous && item.entity == chain[0].previous))
|
|
94
|
+
{
|
|
95
|
+
prevForChain = chain
|
|
96
|
+
}
|
|
97
|
+
if ((item.previous && item.previous == chain.at(-1).entity)
|
|
98
|
+
&& (chain.at(-1).next && item.entity == chain.at(-1).next))
|
|
99
|
+
{
|
|
100
|
+
nextForChain = chain
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (nextForChain && prevForChain)
|
|
105
|
+
{
|
|
106
|
+
// merge chains
|
|
107
|
+
nextForChain.push(item)
|
|
108
|
+
if (nextForChain != prevForChain) //wtf
|
|
109
|
+
{
|
|
110
|
+
for (const prevItem of prevForChain) nextForChain.push(prevItem)
|
|
111
|
+
const prevForChainIdx = successionChains.indexOf(prevForChain)
|
|
112
|
+
successionChains.splice(prevForChainIdx, 1)
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
else if (nextForChain)
|
|
116
|
+
{
|
|
117
|
+
nextForChain.push(item)
|
|
118
|
+
}
|
|
119
|
+
else if (prevForChain)
|
|
120
|
+
{
|
|
121
|
+
prevForChain.unshift(item)
|
|
122
|
+
}
|
|
123
|
+
else
|
|
124
|
+
{
|
|
125
|
+
successionChains.push([ item ])
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// DEBUG: validate
|
|
129
|
+
/*for (const chain of successionChains)
|
|
130
|
+
{
|
|
131
|
+
for (var i2 = 0; i2 < chain.length - 1; i2++)
|
|
132
|
+
{
|
|
133
|
+
assert(chain[i2].entity == chain[i2+1].previous)
|
|
134
|
+
}
|
|
135
|
+
for (var i2 = 1; i2 < chain.length; i2++)
|
|
136
|
+
{
|
|
137
|
+
assert(chain[i2].entity == chain[i2-1].next)
|
|
138
|
+
}
|
|
139
|
+
}*/
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// split overlapped uncertain regions between adjacent items
|
|
143
|
+
//TODO: create a shared area that visually makes it more clear that the line can slide around?
|
|
144
|
+
for (const chain of successionChains)
|
|
145
|
+
{
|
|
146
|
+
for (var chainIndex = 0; chainIndex < chain.length - 1; chainIndex++)
|
|
147
|
+
{
|
|
148
|
+
var nextIndex = chainIndex + 1
|
|
149
|
+
var curr = chain[chainIndex]
|
|
150
|
+
var next = chain[nextIndex]
|
|
151
|
+
if (!curr.end_min || !next.start_min) continue
|
|
152
|
+
|
|
153
|
+
var overlapStart = moment.max(curr.end_min, next.start_min)
|
|
154
|
+
var overlapEnd = moment.min(curr.end_max, next.start_max)
|
|
155
|
+
if (overlapStart < overlapEnd)
|
|
156
|
+
{
|
|
157
|
+
// include any other items that are uncertain in the entire overlapped region
|
|
158
|
+
while (nextIndex < chain.length - 1)
|
|
159
|
+
{
|
|
160
|
+
// cannot proceed past items with certain regions
|
|
161
|
+
if (chain[nextIndex].start_max < chain[nextIndex].end_min) break
|
|
162
|
+
|
|
163
|
+
if (chain[nextIndex + 1].start_min <= overlapStart && chain[nextIndex + 1].start_max >= overlapEnd)
|
|
164
|
+
{
|
|
165
|
+
nextIndex++
|
|
166
|
+
}
|
|
167
|
+
else break
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// divide the overlapped region between the involved items
|
|
171
|
+
const itemCount = nextIndex - chainIndex + 1
|
|
172
|
+
const msStart = overlapStart.valueOf()
|
|
173
|
+
const msShare = (overlapEnd.valueOf() - msStart) / itemCount
|
|
174
|
+
chain[chainIndex].end_max = moment(msStart + msShare)
|
|
175
|
+
for (var j = 1; j < nextIndex - chainIndex; j++)
|
|
176
|
+
{
|
|
177
|
+
chain[chainIndex + j].start_min = moment(msStart + msShare * j).add(1, 'second')
|
|
178
|
+
chain[chainIndex + j].end_max = moment(msStart + msShare * (j+1))
|
|
179
|
+
|
|
180
|
+
// region was previously checked to be fully-uncertain
|
|
181
|
+
chain[chainIndex + j].start_max = chain[chainIndex + j].end_max.clone()
|
|
182
|
+
chain[chainIndex + j].end_min = chain[chainIndex + j].start_min.clone()
|
|
183
|
+
}
|
|
184
|
+
chain[nextIndex].start_min = moment(overlapEnd.valueOf() - msShare).add(1, 'second')
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// create timeline items
|
|
191
|
+
// a single input item might be built of multiple timeline segments
|
|
192
|
+
var outputObject = { items: [], groups: inputSpec.groups, options: inputSpec.options }
|
|
193
|
+
for (const item of items)
|
|
194
|
+
{
|
|
195
|
+
var outputItem = {
|
|
196
|
+
id: item.id,
|
|
197
|
+
content: item.label,
|
|
198
|
+
className: item.className,
|
|
199
|
+
comment: item.comment,
|
|
200
|
+
type: item.type,
|
|
201
|
+
wikidata: item.entity
|
|
202
|
+
}
|
|
203
|
+
if (item.group)
|
|
204
|
+
{
|
|
205
|
+
outputItem.group = item.group
|
|
206
|
+
outputItem.subgroup = item.subgroup ? item.subgroup : item.entity
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const isRangeType = !outputItem.type || outputItem.type == "range" || outputItem.type == "background"
|
|
210
|
+
|
|
211
|
+
// for debugging
|
|
212
|
+
outputItem.className = [ outputItem.className, item.entity ].join(' ')
|
|
213
|
+
|
|
214
|
+
// look up duration expectations
|
|
215
|
+
const expectation = this.getExpectation(item)
|
|
216
|
+
expectation.duration.avg = expectation.duration.avg ?? expectation.duration.max
|
|
217
|
+
assert(expectation && expectation.duration) // expect at least a universal fallback expectation
|
|
218
|
+
|
|
219
|
+
if (!item.start_min && !item.start_max && !item.end_min && !item.end_max)
|
|
220
|
+
{
|
|
221
|
+
//console.warn(`Item ${item.id} has no date data at all.`)
|
|
222
|
+
continue
|
|
223
|
+
}
|
|
224
|
+
assert(Boolean(item.start_min) == Boolean(item.start_max))
|
|
225
|
+
assert(Boolean(item.end_min) == Boolean(item.end_max))
|
|
226
|
+
assert(!(item.start_min > item.start_max))
|
|
227
|
+
assert(!(item.end_min > item.end_max))
|
|
228
|
+
|
|
229
|
+
// restrict uncertainty based on expectations
|
|
230
|
+
//TODO:
|
|
231
|
+
|
|
232
|
+
// Can bg subcomponents be overlaid on the item?
|
|
233
|
+
// If false, they will actually be deducted from the main item.
|
|
234
|
+
var permitBgOverlay = item.type != "background"
|
|
235
|
+
|
|
236
|
+
// Were any bg overlays actually added to this item?
|
|
237
|
+
var usesBgOverlays = false
|
|
238
|
+
|
|
239
|
+
// exclude items that violate itemRange constraints
|
|
240
|
+
//OPT: do this at an earlier stage? (e.g. when running the first query)
|
|
241
|
+
if (item.itemRange)
|
|
242
|
+
{
|
|
243
|
+
if (item.itemRange.min && moment(item.itemRange.min).isAfter(item.end_max))
|
|
244
|
+
continue
|
|
245
|
+
if (item.itemRange.max && moment(item.itemRange.max).isBefore(item.start_min))
|
|
246
|
+
continue
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
if (item.start_max && item.start_max.clone().add(1, 'second') >= item.end_min)
|
|
250
|
+
{
|
|
251
|
+
// no certainty at all: create a single uncertain range
|
|
252
|
+
outputItem.className = [ outputItem.className, 'visc-uncertain' ].join(' ')
|
|
253
|
+
outputItem.start = item.start_min
|
|
254
|
+
outputItem.end = item.end_max
|
|
255
|
+
outputObject.items.push(outputItem)
|
|
256
|
+
continue
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
if (!isRangeType)
|
|
260
|
+
{
|
|
261
|
+
// point type
|
|
262
|
+
//TODO: support ranged boxes etc?
|
|
263
|
+
outputItem.start = moment((item.start_min.valueOf() + item.start_max.valueOf()) / 2)
|
|
264
|
+
if (item.end_min && item.end_max)
|
|
265
|
+
outputItem.end = moment((item.end_min.valueOf() + item.end_max.valueOf()) / 2)
|
|
266
|
+
outputObject.items.push(outputItem)
|
|
267
|
+
continue
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// handle end date
|
|
271
|
+
if (item.end_min && item.end_max)
|
|
272
|
+
{
|
|
273
|
+
if (item.end_min < item.end_max)
|
|
274
|
+
{
|
|
275
|
+
// uncertain end
|
|
276
|
+
|
|
277
|
+
// find lower bound of uncertain region
|
|
278
|
+
const uncertainMin = item.end_min ?? outputItem.start_max
|
|
279
|
+
assert(uncertainMin)
|
|
280
|
+
|
|
281
|
+
// add uncertain range
|
|
282
|
+
const uncertainElement = {
|
|
283
|
+
id: outputItem.id + "-unc-end",
|
|
284
|
+
className: [outputItem.className, "visc-uncertain", "visc-left-connection"].join(' '),
|
|
285
|
+
type: outputItem.type,
|
|
286
|
+
content: item.label ? " " : "",
|
|
287
|
+
start: uncertainMin,
|
|
288
|
+
end: item.end_max,
|
|
289
|
+
group: item.group,
|
|
290
|
+
subgroup: outputItem.subgroup,
|
|
291
|
+
wikidata: item.entity
|
|
292
|
+
}
|
|
293
|
+
outputObject.items.push(uncertainElement)
|
|
294
|
+
|
|
295
|
+
if (permitBgOverlay)
|
|
296
|
+
{
|
|
297
|
+
uncertainElement.className = [uncertainElement.className, "visc-range-overlay"].join(' ')
|
|
298
|
+
outputItem.end = item.end_max
|
|
299
|
+
usesBgOverlays = true
|
|
300
|
+
}
|
|
301
|
+
else
|
|
302
|
+
{
|
|
303
|
+
// adjust normal range to match
|
|
304
|
+
outputItem.end = uncertainMin
|
|
305
|
+
outputItem.className = [ outputItem.className, 'visc-right-connection' ].join(' ')
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
else
|
|
309
|
+
{
|
|
310
|
+
// certain end
|
|
311
|
+
outputItem.end = item.end_max;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
else if (item.end_min && item.start_max < item.end_min)
|
|
315
|
+
{
|
|
316
|
+
// open-ended end with some certainty
|
|
317
|
+
var tailEnd
|
|
318
|
+
const useMax = expectation.duration.max ? expectation.duration.max : moment(expectation.duration.avg.asMilliseconds() * 2)
|
|
319
|
+
if (item.start_max < moment().subtract(useMax))
|
|
320
|
+
{
|
|
321
|
+
// max "possible" is less than 'now'; it is likely this duration is not ongoing but has an unknown end
|
|
322
|
+
//TODO: wikidata special 'no value' should cause the next branch to be taken
|
|
323
|
+
outputItem.end = item.start_max.clone()
|
|
324
|
+
tailEnd = item.start_max.clone().add(expectation.duration.avg)
|
|
325
|
+
}
|
|
326
|
+
else
|
|
327
|
+
{
|
|
328
|
+
// 'now' is within 'max' and so it is a reasonable guess that this duration is ongoing
|
|
329
|
+
const avgDuration = moment.duration(expectation.duration.avg) //HACK: TODO: consistently postprocess expectations, or don't
|
|
330
|
+
const actualDuration = moment.duration(moment().diff(item.start_max)) //TODO: average start here?
|
|
331
|
+
var excessDuration = moment.duration(avgDuration.asMilliseconds()).subtract(actualDuration)
|
|
332
|
+
excessDuration = moment.duration(Math.max(excessDuration.asMilliseconds(), avgDuration.asMilliseconds() * 0.25)) //HACK: magic number
|
|
333
|
+
|
|
334
|
+
outputItem.end = moment()
|
|
335
|
+
tailEnd = outputItem.end.add(excessDuration)
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// add a "tail" item after the end
|
|
339
|
+
outputObject.items.push({
|
|
340
|
+
id: outputItem.id + "-tail",
|
|
341
|
+
className: [outputItem.className, "visc-right-tail"].join(' '),
|
|
342
|
+
type: outputItem.type,
|
|
343
|
+
content: item.label ? " " : "",
|
|
344
|
+
start: outputItem.end,
|
|
345
|
+
end: tailEnd,
|
|
346
|
+
group: item.group,
|
|
347
|
+
subgroup: outputItem.subgroup,
|
|
348
|
+
wikidata: item.entity
|
|
349
|
+
})
|
|
350
|
+
|
|
351
|
+
outputItem.className = [ outputItem.className, 'visc-right-connection' ].join(' ')
|
|
352
|
+
}
|
|
353
|
+
else
|
|
354
|
+
{
|
|
355
|
+
if (item.start_min && item.start_max && item.start_max > item.start_min)
|
|
356
|
+
{
|
|
357
|
+
// entire range is open-ended, but with an uncertain start region
|
|
358
|
+
outputItem.end = item.start_max.clone()
|
|
359
|
+
tailEnd = item.start_max.clone().add(expectation.duration.avg)
|
|
360
|
+
|
|
361
|
+
// add a "tail" item after the end
|
|
362
|
+
outputObject.items.push({
|
|
363
|
+
id: outputItem.id + "-tail",
|
|
364
|
+
className: [outputItem.className, "visc-right-tail"].join(' '),
|
|
365
|
+
type: outputItem.type,
|
|
366
|
+
content: item.label ? " " : "",
|
|
367
|
+
start: outputItem.end,
|
|
368
|
+
end: tailEnd,
|
|
369
|
+
group: item.group,
|
|
370
|
+
subgroup: outputItem.subgroup,
|
|
371
|
+
wikidata: item.entity
|
|
372
|
+
})
|
|
373
|
+
|
|
374
|
+
outputItem.className = [ outputItem.className, 'visc-right-connection' ].join(' ')
|
|
375
|
+
}
|
|
376
|
+
else
|
|
377
|
+
{
|
|
378
|
+
// entire range is open-ended
|
|
379
|
+
outputItem.start = item.start_min ?? item.start_max
|
|
380
|
+
outputItem.end = outputItem.start.clone().add(expectation.duration.avg)
|
|
381
|
+
outputItem.className = [ outputItem.className, 'visc-open-right' ].join(' ')
|
|
382
|
+
outputObject.items.push(outputItem)
|
|
383
|
+
continue
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// handle start date
|
|
388
|
+
if (item.start_min && item.start_max)
|
|
389
|
+
{
|
|
390
|
+
if (item.start_max > item.start_min)
|
|
391
|
+
{
|
|
392
|
+
// uncertain start
|
|
393
|
+
|
|
394
|
+
// find upper bound of uncertain region
|
|
395
|
+
var uncertainMax
|
|
396
|
+
if (item.start_max)
|
|
397
|
+
uncertainMax = item.start_max
|
|
398
|
+
else if (outputItem.start)
|
|
399
|
+
uncertainMax = outputItem.start
|
|
400
|
+
else
|
|
401
|
+
uncertainMax = outputItem.end
|
|
402
|
+
assert(uncertainMax)
|
|
403
|
+
|
|
404
|
+
// add uncertain range
|
|
405
|
+
const uncertainElement = {
|
|
406
|
+
id: outputItem.id + "-unc-start",
|
|
407
|
+
className: [outputItem.className, "visc-uncertain", "visc-right-connection"].join(' '),
|
|
408
|
+
type: outputItem.type,
|
|
409
|
+
content: item.label ? " " : "",
|
|
410
|
+
start: item.start_min,
|
|
411
|
+
end: uncertainMax,
|
|
412
|
+
group: item.group,
|
|
413
|
+
subgroup: outputItem.subgroup,
|
|
414
|
+
wikidata: item.entity
|
|
415
|
+
}
|
|
416
|
+
outputObject.items.push(uncertainElement)
|
|
417
|
+
|
|
418
|
+
if (permitBgOverlay)
|
|
419
|
+
{
|
|
420
|
+
uncertainElement.className = [uncertainElement.className, "visc-range-overlay"].join(' ')
|
|
421
|
+
outputItem.start = item.start_min
|
|
422
|
+
usesBgOverlays = true
|
|
423
|
+
}
|
|
424
|
+
else
|
|
425
|
+
{
|
|
426
|
+
// adjust normal range to match
|
|
427
|
+
outputItem.start = uncertainMax
|
|
428
|
+
outputItem.className = [ outputItem.className, 'visc-left-connection' ].join(' ')
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
else
|
|
432
|
+
{
|
|
433
|
+
// certain start
|
|
434
|
+
outputItem.start = item.start_min;
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
else if (!item.start_min)
|
|
438
|
+
{
|
|
439
|
+
// open-ended start
|
|
440
|
+
outputItem.start = outputItem.end.clone().subtract(expectation.duration.avg)
|
|
441
|
+
outputItem.className = [outputItem.className, "visc-open-left"].join(' ')
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
//TODO: missing death dates inside expected duration: solid to NOW, fade after NOW
|
|
445
|
+
//TODO: accept expected durations and place uncertainly before/after those
|
|
446
|
+
|
|
447
|
+
// if using bg overlays, the label needs to be on its own element so it can sort on top of them
|
|
448
|
+
if (usesBgOverlays && item.label)
|
|
449
|
+
{
|
|
450
|
+
const labelItem = {...outputItem}
|
|
451
|
+
labelItem.id += "-label"
|
|
452
|
+
labelItem.className = [labelItem.className, "visc-toplabel"].join(' '),
|
|
453
|
+
outputObject.items.push(labelItem)
|
|
454
|
+
outputItem.content = outputItem.content ? " " : ""
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
outputObject.items.push(outputItem)
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// sort the objects into subgroups
|
|
461
|
+
//NOTE: allows same subgroup to be separate across different groups, unlike vis natively
|
|
462
|
+
const stackGroups = {}
|
|
463
|
+
for (const outputItem of outputObject.items)
|
|
464
|
+
{
|
|
465
|
+
if (outputItem.type == "background") continue
|
|
466
|
+
|
|
467
|
+
var stackGroup = stackGroups[outputItem.group]
|
|
468
|
+
if (!stackGroup) stackGroup = stackGroups[outputItem.group] = {}
|
|
469
|
+
var stackSubgroup = stackGroup[outputItem.subgroup]
|
|
470
|
+
if (!stackSubgroup) stackSubgroup = stackGroup[outputItem.subgroup] = { objects: [] }
|
|
471
|
+
stackSubgroup.objects.push(outputItem)
|
|
472
|
+
assert(outputItem.start)
|
|
473
|
+
stackSubgroup.min = momentSafeMin(stackSubgroup.min, outputItem.start)
|
|
474
|
+
if (outputItem.end)
|
|
475
|
+
stackSubgroup.max = momentSafeMax(stackSubgroup.max, outputItem.end)
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
for (const stackGroupKey in stackGroups)
|
|
479
|
+
{
|
|
480
|
+
const stackGroup = stackGroups[stackGroupKey]
|
|
481
|
+
|
|
482
|
+
// sort the subgroups from this group by start time ascending
|
|
483
|
+
const stackSubgroupsArr = []
|
|
484
|
+
for (const stackSubgroup of Object.values(stackGroup)) stackSubgroupsArr.push(stackSubgroup)
|
|
485
|
+
stackSubgroupsArr.sort((a, b) => a.min.valueOf() - b.min.valueOf())
|
|
486
|
+
|
|
487
|
+
// drop subgroups into interlocking rows
|
|
488
|
+
// visjs can do this with stackSubgroups: false, but it's kind of twitchy, so we'll do it statically here instead
|
|
489
|
+
const sublines = []
|
|
490
|
+
for (const stackSubgroup of stackSubgroupsArr)
|
|
491
|
+
{
|
|
492
|
+
var placed = false
|
|
493
|
+
for (var i = 0; i < sublines.length; i++)
|
|
494
|
+
{
|
|
495
|
+
// if the new subgroup overlaps nothing in this line, it can be added
|
|
496
|
+
var overlap = false
|
|
497
|
+
for (const sublineItem of sublines[i])
|
|
498
|
+
{
|
|
499
|
+
if (stackSubgroup.min < sublineItem.max && stackSubgroup.max > sublineItem.min)
|
|
500
|
+
{
|
|
501
|
+
overlap = true
|
|
502
|
+
break
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
if (!overlap)
|
|
506
|
+
{
|
|
507
|
+
sublines[i].push(stackSubgroup)
|
|
508
|
+
placed = true
|
|
509
|
+
break
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
if (!placed)
|
|
513
|
+
{
|
|
514
|
+
sublines.push([stackSubgroup])
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
// reassign all items to new subgroups based on the line they are in
|
|
519
|
+
for (const sublineIndex in sublines)
|
|
520
|
+
{
|
|
521
|
+
const sublineSubgroup = `${stackGroupKey}#${sublineIndex}`
|
|
522
|
+
for (const stackSubgroup of sublines[sublineIndex])
|
|
523
|
+
{
|
|
524
|
+
for (const object of stackSubgroup.objects)
|
|
525
|
+
{
|
|
526
|
+
object.subgroup = sublineSubgroup
|
|
527
|
+
object.lineNum = parseInt(sublineIndex)
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
// explicitly order the lines
|
|
533
|
+
var groupData = outputObject.groups.find(g => g.id == stackGroupKey)
|
|
534
|
+
if (!groupData) groupData = outputObject.groups[stackGroupKey] = { id: stackGroupKey }
|
|
535
|
+
if (!groupData.subgroupOrder) groupData.subgroupOrder = "lineNum"
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
// finalize all item times from moment to string
|
|
539
|
+
for (const item of outputObject.items)
|
|
540
|
+
{
|
|
541
|
+
assert(item.start)
|
|
542
|
+
item.start = item.start.format("YYYYYY-MM-DDThh:mm:ss")
|
|
543
|
+
if (item.end)
|
|
544
|
+
item.end = item.end.format("YYYYYY-MM-DDThh:mm:ss")
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
delete outputObject.chronicle
|
|
548
|
+
|
|
549
|
+
return JSON.stringify(outputObject, undefined, "\t") //TODO: configure space
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
module.exports = renderer
|
package/src/sparql-builder.js
CHANGED
|
@@ -3,14 +3,17 @@ const assert = require('node:assert/strict')
|
|
|
3
3
|
|
|
4
4
|
module.exports = class SparqlBuilder
|
|
5
5
|
{
|
|
6
|
+
distinct = false
|
|
7
|
+
|
|
6
8
|
constructor()
|
|
7
9
|
{
|
|
8
10
|
this.outParams = []
|
|
11
|
+
this.groupParams = []
|
|
9
12
|
this.queryTerms = []
|
|
10
13
|
}
|
|
11
14
|
|
|
12
15
|
// Adds an output parameter to the query
|
|
13
|
-
addOutParam(paramName)
|
|
16
|
+
addOutParam(paramName, params)
|
|
14
17
|
{
|
|
15
18
|
//TODO: validate name more
|
|
16
19
|
assert(paramName)
|
|
@@ -18,6 +21,18 @@ module.exports = class SparqlBuilder
|
|
|
18
21
|
{
|
|
19
22
|
this.outParams.push(paramName)
|
|
20
23
|
}
|
|
24
|
+
if (params && params.groupBy)
|
|
25
|
+
{
|
|
26
|
+
this.addGroupParam(paramName)
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
addGroupParam(paramName)
|
|
31
|
+
{
|
|
32
|
+
if (this.groupParams.indexOf(paramName) < 0)
|
|
33
|
+
{
|
|
34
|
+
this.groupParams.push(paramName)
|
|
35
|
+
}
|
|
21
36
|
}
|
|
22
37
|
|
|
23
38
|
addQueryTerm(term)
|
|
@@ -38,21 +53,21 @@ module.exports = class SparqlBuilder
|
|
|
38
53
|
this.queryTerms.push(`OPTIONAL{${term}}`)
|
|
39
54
|
}
|
|
40
55
|
|
|
41
|
-
addTimeTerm(term, valueVar, timeVar, precisionVar)
|
|
56
|
+
addTimeTerm(term, valueVar, timeVar, precisionVar, params)
|
|
42
57
|
{
|
|
43
58
|
assert(term)
|
|
44
59
|
|
|
45
|
-
this.addOutParam(timeVar)
|
|
46
|
-
this.addOutParam(precisionVar)
|
|
60
|
+
this.addOutParam(timeVar, params)
|
|
61
|
+
this.addOutParam(precisionVar, params)
|
|
47
62
|
this.addQueryTerm(`${term} ${valueVar} wikibase:timeValue ${timeVar}. ${valueVar} wikibase:timePrecision ${precisionVar}.`)
|
|
48
63
|
}
|
|
49
64
|
|
|
50
|
-
addOptionalTimeTerm(term, valueVar, timeVar, precisionVar)
|
|
65
|
+
addOptionalTimeTerm(term, valueVar, timeVar, precisionVar, params)
|
|
51
66
|
{
|
|
52
67
|
assert(term)
|
|
53
68
|
|
|
54
|
-
this.addOutParam(timeVar)
|
|
55
|
-
this.addOutParam(precisionVar)
|
|
69
|
+
this.addOutParam(timeVar, params)
|
|
70
|
+
this.addOutParam(precisionVar, params)
|
|
56
71
|
this.addOptionalQueryTerm(`${term} ${valueVar} wikibase:timeValue ${timeVar}. ${valueVar} wikibase:timePrecision ${precisionVar}.`)
|
|
57
72
|
}
|
|
58
73
|
|
|
@@ -68,6 +83,8 @@ module.exports = class SparqlBuilder
|
|
|
68
83
|
assert(this.queryTerms.length > 0)
|
|
69
84
|
|
|
70
85
|
//TODO: prevent injection
|
|
71
|
-
|
|
86
|
+
const distinct = this.distinct ? "DISTINCT" : ""
|
|
87
|
+
const groupBy = this.groupParams.length > 0 ? `GROUP BY ${this.groupParams.join(' ')}` : ''
|
|
88
|
+
return `SELECT ${distinct} ${this.outParams.join(" ")} WHERE{${this.queryTerms.join(" ")}}${groupBy}`
|
|
72
89
|
}
|
|
73
90
|
}
|