aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rwxr-xr-xbackend/histplorer.py4
-rw-r--r--src/App.vue40
-rw-r--r--src/components/TimeLine.vue57
-rw-r--r--src/lib.ts15
-rw-r--r--tests/lib.test.ts25
5 files changed, 118 insertions, 23 deletions
diff --git a/backend/histplorer.py b/backend/histplorer.py
index c20116e..988b69d 100755
--- a/backend/histplorer.py
+++ b/backend/histplorer.py
@@ -249,7 +249,9 @@ def lookupEvents(start: HistDate | None, end: HistDate | None, scale: int, ctg:
incl = None
# Get any additional inclusion
if incl is not None:
- row = dbCur.execute(query + ' WHERE events.id = ?', (incl,)).fetchone()
+ constraints.append('events.id = ?')
+ params.append(incl)
+ row = dbCur.execute(query + ' WHERE ' + ' AND '.join(constraints), params).fetchone()
if row is not None:
if len(results) == resultLimit:
results.pop()
diff --git a/src/App.vue b/src/App.vue
index 2f5051a..cbd6825 100644
--- a/src/App.vue
+++ b/src/App.vue
@@ -23,7 +23,7 @@
:style="{backgroundColor: store.color.bg}" ref="contentAreaRef">
<time-line v-for="(state, idx) in timelines" :key="state.id"
:vert="vert" :initialState="state" :closeable="timelines.length > 1"
- :eventTree="eventTree" :unitCountMaps="unitCountMaps"
+ :eventTree="eventTree" :unitCountMaps="unitCountMaps" :searchTarget="timelineTargets[idx]"
class="grow basis-full min-h-0 outline outline-1"
@close="onTimelineClose(idx)" @state-chg="onTimelineChg($event, idx)" @event-display="onEventDisplay"
@info-click="onInfoClick"/>
@@ -54,9 +54,9 @@ import SettingsIcon from './components/icon/SettingsIcon.vue';
import PlusIcon from './components/icon/PlusIcon.vue';
import SearchIcon from './components/icon/SearchIcon.vue';
// Other
-import {HistDate, HistEvent, queryServer, EventResponseJson, jsonToHistEvent,
- SCALES, stepDate, TimelineState, cmpHistEvent, dateToUnit, DateRangeTree,
- EventInfo, EventInfoJson, jsonToEventInfo} from './lib';
+import {HistDate, HistEvent, queryServer,
+ EventResponseJson, jsonToHistEvent, EventInfo, EventInfoJson, jsonToEventInfo,
+ SCALES, stepDate, TimelineState, cmpHistEvent, dateToUnit, DateRangeTree} from './lib';
import {useStore} from './store';
import {RBTree, rbtree_shallow_copy} from './rbtree';
@@ -75,7 +75,7 @@ function updateAreaDims(){
contentWidth.value = contentAreaEl.offsetWidth;
contentHeight.value = contentAreaEl.offsetHeight;
}
-onMounted(updateAreaDims)
+onMounted(updateAreaDims);
// Timeline data
const timelines: Ref<TimelineState[]> = ref([]);
@@ -90,9 +90,10 @@ function addTimeline(){
last.endDate, last.startOffset, last.endOffset, last.scaleIdx
));
}
+ timelineTargets.value.push([null, false]);
nextTimelineId++;
}
-addTimeline();
+onMounted(addTimeline);
function onTimelineChg(state: TimelineState, idx: number){
timelines.value[idx] = state;
}
@@ -113,6 +114,7 @@ function onTimelineClose(idx: number){
return;
}
timelines.value.splice(idx, 1);
+ timelineTargets.value.splice(idx, 1);
}
// For storing and looking up events
@@ -175,8 +177,11 @@ async function onEventDisplay(
timelineId: number, eventIds: number[], firstDate: HistDate, lastDate: HistDate, scaleIdx: number){
async function handleEvent(
timelineId: number, eventIds: number[], firstDate: HistDate, lastDate: HistDate, scaleIdx: number){
+ let timelineIdx = timelines.value.findIndex((s : TimelineState) => s.id == timelineId);
+ let targetEvent = timelineTargets.value[timelineIdx][0];
// Skip if range has been queried, and enough of its events have been obtained
- if (queriedRanges[scaleIdx].contains([firstDate, lastDate])){
+ if (queriedRanges[scaleIdx].contains([firstDate, lastDate])
+ && (targetEvent == null || idToEvent.has(targetEvent.id))){
// Get number of events in range, server-side
let fullCount = 0;
let date = firstDate.clone();
@@ -214,17 +219,21 @@ async function onEventDisplay(
}
}
// Get events from server
- if (lastQueriedRange != null && lastQueriedRange[0].equals(firstDate) && lastQueriedRange[1].equals(lastDate)){
+ if (lastQueriedRange != null && lastQueriedRange[0].equals(firstDate) && lastQueriedRange[1].equals(lastDate)
+ && (targetEvent == null || idToEvent.has(targetEvent.id))){
console.log(`INFO: Skipping redundant server request from ${firstDate} to ${lastDate}`);
return;
}
- lastQueriedRange = [firstDate, lastDate]
+ lastQueriedRange = [firstDate, lastDate];
let urlParams = new URLSearchParams({
type: 'events',
range: `${firstDate}.${lastDate}`,
scale: String(SCALES[scaleIdx]),
limit: String(EVENT_REQ_LIMIT),
});
+ if (targetEvent != null){
+ urlParams.append('incl', String(targetEvent.id));
+ }
let responseObj: EventResponseJson | null = await queryServer(urlParams);
if (responseObj == null){
console.log('WARNING: Server gave null response to event query');
@@ -242,6 +251,12 @@ async function onEventDisplay(
titleToEvent.set(event.title, event);
}
}
+ if (targetEvent != null){
+ if (!idToEvent.has(targetEvent.id)){
+ console.log(`WARNING: Server response did not include event matching 'incl=${targetEvent.id}'`);
+ }
+ timelineTargets.value[timelineIdx][0] = null;
+ }
// Collect unit counts
const unitCounts = responseObj.unitCounts;
if (unitCounts == null){
@@ -289,9 +304,14 @@ async function onInfoClick(eventTitle: string){
// For search modal
const searchOpen = ref(false);
+const timelineTargets = ref([] as [HistEvent | null, boolean][]); // For communicating search results to timelines
+ // A boolean flag is used to trigger jumping even when the same event occurs twice
function onSearch(event: HistEvent){
searchOpen.value = false;
- console.log(`Need to jump to event "${event.title}" at ${event.start}`);
+ // Trigger jump in endmost timeline
+ let timelineIdx = timelineTargets.value.length - 1;
+ let oldFlag = timelineTargets.value[timelineIdx];
+ timelineTargets.value.splice(timelineIdx, 1, [event, !oldFlag[1]]);
}
// For resize handling
diff --git a/src/components/TimeLine.vue b/src/components/TimeLine.vue
index e5fbf34..e41a729 100644
--- a/src/components/TimeLine.vue
+++ b/src/components/TimeLine.vue
@@ -76,7 +76,7 @@ import CloseIcon from './icon/CloseIcon.vue';
// Other
import {WRITING_MODE_HORZ, MIN_DATE, MAX_DATE, MONTH_SCALE, DAY_SCALE, SCALES, MONTH_NAMES, MIN_CAL_DATE,
getDaysInMonth, HistDate, stepDate, getScaleRatio, getNumSubUnits, getUnitDiff,
- getEventPrecision, dateToUnit,
+ getEventPrecision, dateToUnit, dateToScaleDate,
moduloPositive, TimelineState, HistEvent} from '../lib';
import {useStore} from '../store';
import {RBTree} from '../rbtree';
@@ -95,6 +95,7 @@ const props = defineProps({
initialState: {type: Object as PropType<TimelineState>, required: true},
eventTree: {type: Object as PropType<RBTree<HistEvent>>, required: true},
unitCountMaps: {type: Object as PropType<Map<number, number>[]>, required: true},
+ searchTarget: {type: Object as PropType<[null | HistEvent, boolean]>, required: true},
});
const emit = defineEmits(['close', 'state-chg', 'event-display', 'info-click']);
@@ -444,6 +445,11 @@ const idToEvent = computed(() => { // Maps visible event IDs to HistEvents
}
return map;
});
+watch(idToEvent, () => { // Remove highlighting of search results that have become out of range
+ if (searchEvent.value != null && !idToEvent.value.has(searchEvent.value.id)){
+ searchEvent.value = null;
+ }
+});
const idToPos = computed(() => {
if (!mounted.value){
return new Map();
@@ -489,6 +495,13 @@ const idToPos = computed(() => {
let MAX_ANGLE = 30 / 180 * Math.PI; // Max event-line angle difference (radians) from perpendicular-to-mainline
let orderedEvents = [...idToEvent.value.values()];
orderedEvents.sort((x, y) => y.pop - x.pop);
+ if (searchEvent.value != null && idToEvent.value.has(searchEvent.value.id)){
+ // Prioritise layout of a searched-for event
+ let targetIdx = orderedEvents.findIndex((evt: HistEvent) => evt.id == searchEvent.value!.id);
+ let temp = orderedEvents[0];
+ orderedEvents[0] = orderedEvents[targetIdx];
+ orderedEvents[targetIdx] = temp;
+ }
let numUnits = getNumDisplayUnits();
const minOffset = store.spacing;
const maxOffset = availLen.value - eventMajorSz.value - store.spacing;
@@ -1105,6 +1118,45 @@ function onStateChg(){
}
watch(firstDate, onStateChg);
+// For jumping to search result
+const searchEvent = ref(null as null | HistEvent); // Holds most recent search result
+watch(() => props.searchTarget, () => {
+ const event = props.searchTarget[0];
+ if (event == null){
+ return;
+ }
+ if (!idToPos.value.has(event.id)){ // If not already visible
+ // Determine new time range
+ let tempScale = scale.value;
+ let targetDate = event.start;
+ if (targetDate.isEarlier(MIN_CAL_DATE) && tempScale < 1){ // Account for jumping out of calendar limits
+ tempScale = getEventPrecision(event);
+ }
+ targetDate = dateToScaleDate(targetDate, tempScale);
+ const startEndDiff = getUnitDiff(startDate.value, endDate.value, scale.value);
+ let targetStart = stepDate(targetDate, tempScale, {forward: false, count: Math.floor(startEndDiff / 2)});
+ if (targetStart.isEarlier(MIN_DATE)){
+ targetStart = MIN_DATE;
+ }
+ let targetEnd = stepDate(targetStart, tempScale, {count: startEndDiff});
+ if (MAX_DATE.isEarlier(targetEnd)){
+ if (targetStart != MIN_DATE){
+ targetStart = stepDate(targetStart, tempScale,
+ {forward: false, count: getUnitDiff(targetEnd, MAX_DATE, tempScale)});
+ if (targetStart.isEarlier(MIN_DATE)){
+ targetStart = MIN_DATE;
+ }
+ }
+ targetEnd = MAX_DATE;
+ }
+ // Jump to range
+ startDate.value = targetStart;
+ endDate.value = targetEnd;
+ scaleIdx.value = SCALES.findIndex((s: number) => s == tempScale);
+ }
+ searchEvent.value = event;
+});
+
// For skipping transitions on startup (and on horz/vert swap)
const skipTransition = ref(true);
onMounted(() => setTimeout(() => {skipTransition.value = false}, 100));
@@ -1163,13 +1215,14 @@ function eventStyles(eventId: number){
}
function eventImgStyles(eventId: number){
const event = idToEvent.value.get(eventId)!;
+ let isSearchResult = searchEvent.value != null && searchEvent.value.id == eventId;
return {
width: store.eventImgSz + 'px',
height: store.eventImgSz + 'px',
//backgroundImage: `url(${getImagePath(event.imgId)})`,
backgroundColor: 'black',
backgroundSize: 'cover',
- borderColor: event.ctg == 'discovery' ? store.color.alt2 : store.color.altDark,
+ borderColor: isSearchResult ? 'red' : (event.ctg == 'discovery' ? store.color.alt2 : store.color.altDark),
borderWidth: '1px',
};
}
diff --git a/src/lib.ts b/src/lib.ts
index f6147dc..d26c8df 100644
--- a/src/lib.ts
+++ b/src/lib.ts
@@ -565,6 +565,21 @@ export function dateToUnit(date: HistDate, scale: number): number {
}
}
}
+export function dateToScaleDate(date: HistDate, scale: number): HistDate {
+ // Returns a date representing the unit on 'scale' that 'date' is within
+ if (scale == DAY_SCALE){
+ return new CalDate(date.year, date.month, date.day);
+ } else if (scale == MONTH_SCALE){
+ return new CalDate(date.year, date.month, 1);
+ } else {
+ const year = Math.floor(date.year / scale) * scale;
+ if (year < MIN_CAL_YEAR){
+ return new YearDate(year);
+ } else {
+ return new CalDate(year == 0 ? 1 : year, 1, 1);
+ }
+ }
+}
// For sending timeline-bound data to BaseLine
export class TimelineState {
diff --git a/tests/lib.test.ts b/tests/lib.test.ts
index 2ab5503..c87627c 100644
--- a/tests/lib.test.ts
+++ b/tests/lib.test.ts
@@ -1,8 +1,9 @@
import {moduloPositive,
gregorianToJdn, julianToJdn, jdnToGregorian, jdnToJulian, gregorianToJulian, julianToGregorian, getDaysInMonth,
- HistDate, YearDate, CalDate, HistEvent,
+ YearDate, CalDate, HistEvent,
queryServer, jsonToHistDate, jsonToHistEvent,
- DAY_SCALE, MONTH_SCALE, stepDate, inDateScale, getScaleRatio, getUnitDiff, getEventPrecision, dateToUnit,
+ DAY_SCALE, MONTH_SCALE, stepDate, inDateScale, getScaleRatio, getUnitDiff,
+ getEventPrecision, dateToUnit, dateToScaleDate,
DateRangeTree,
} from '/src/lib.ts'
@@ -47,10 +48,6 @@ test('getDaysInMonth', () => {
})
describe('YearDate', () => {
- test('constructor', () => {
- expect(() => new YearDate(2000)).toThrowError()
- expect(() => new YearDate(-5000)).not.toThrowError()
- })
test('cmp', () => {
expect((new YearDate(-5000)).equals(new YearDate(-5000))).toBe(true)
expect((new YearDate(-6000)).equals(new YearDate(-5999))).toBe(false)
@@ -79,9 +76,9 @@ describe('CalDate', () => {
})
test('queryServer', async () => {
- let oldFetch = fetch
+ const oldFetch = fetch
fetch = vi.fn(() => ({json: () => ({test: 'value'})}))
- let json = await queryServer('', 'http://example.com/')
+ const json = await queryServer('', 'http://example.com/')
expect(json).toEqual({test: 'value'})
fetch = oldFetch
})
@@ -90,7 +87,7 @@ test('jsonToHistDate', () => {
expect(jsonToHistDate({gcal: null, year: -5000, month: 1, day: 1})).toEqual(new YearDate(-5000))
})
test('jsonToHistEvent', () => {
- let jsonEvent = {
+ const jsonEvent = {
id: 3,
title: 'abc',
start: {gcal: true, year: 2000, month: 10, day: 5},
@@ -157,9 +154,17 @@ test('dateToUnit', () => {
expect(dateToUnit(new CalDate(1911, 12, 3), MONTH_SCALE)).toBe(gregorianToJdn(1911, 12, 1))
expect(dateToUnit(new CalDate(1911, 12, 3, false), DAY_SCALE)).toBe(julianToJdn(1911, 12, 3))
})
+test('dateToScaleDate', () => {
+ expect(dateToScaleDate(new CalDate(2013, 10, 3), DAY_SCALE)).toEqual(new CalDate(2013, 10, 3))
+ expect(dateToScaleDate(new CalDate(2013, 10, 3, false), MONTH_SCALE)).toEqual(new CalDate(2013, 10, 1))
+ expect(dateToScaleDate(new CalDate(2013, 10, 3), 1)).toEqual(new CalDate(2013, 1, 1))
+ expect(dateToScaleDate(new CalDate(2013, 10, 3), 1e3)).toEqual(new CalDate(2000, 1, 1))
+ expect(dateToScaleDate(new CalDate(2013, 10, 3), 1e4)).toEqual(new CalDate(1, 1, 1))
+ expect(dateToScaleDate(new YearDate(-1222333), 1e6)).toEqual(new YearDate(-2000000))
+})
test('DateRangeTree', () => {
- let ranges = new DateRangeTree()
+ const ranges = new DateRangeTree()
ranges.add([new CalDate(100, 1, 1), new CalDate(200, 1, 1)])
ranges.add([new CalDate(300, 1, 1), new CalDate(400, 1, 1)])
expect(ranges.tree.size).toBe(2)