diff options
| author | Terry Truong <terry06890@gmail.com> | 2023-01-06 02:32:59 +1100 |
|---|---|---|
| committer | Terry Truong <terry06890@gmail.com> | 2023-01-06 02:45:26 +1100 |
| commit | 559902e0211a06b349c4c2f50b0882a8d314f8b7 (patch) | |
| tree | ecdfcb0983454044ecb28ca8f8f4d781b1124047 | |
| parent | 442c0bbffc5c372c7ec3510914968f75ab6e4a4f (diff) | |
Jump to and highlight search results
Use a 'searchTarget' prop of Timeline to trigger jumping to a search result.
Make TimeLine prioritise search result in layout.
When querying for events in App, check for a search target, and use incl= to retrieve it.
On server, for the incl= query param, don't include the event if outside specified range.
| -rwxr-xr-x | backend/histplorer.py | 4 | ||||
| -rw-r--r-- | src/App.vue | 40 | ||||
| -rw-r--r-- | src/components/TimeLine.vue | 57 | ||||
| -rw-r--r-- | src/lib.ts | 15 | ||||
| -rw-r--r-- | tests/lib.test.ts | 25 |
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', }; } @@ -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) |
