From 6438f8e880df5b04ff974179897ae507bb30285c Mon Sep 17 00:00:00 2001 From: Terry Truong Date: Tue, 3 Jan 2023 21:31:02 +1100 Subject: Query server when zoomed in enough even if within an already queried range For server query parameter range=date1.date2, exclude date2 Fix unnecessary calendar-to-year date conversion when building sql query For frontend DateRangeTree class, rename has() to contains() --- backend/histplorer.py | 11 +++++----- backend/tests/test_histplorer.py | 4 ++-- src/App.vue | 46 +++++++++++++++++++++++++++++++++++----- src/components/TimeLine.vue | 4 ++-- src/lib.ts | 2 +- tests/lib.test.ts | 10 ++++----- 6 files changed, 57 insertions(+), 20 deletions(-) diff --git a/backend/histplorer.py b/backend/histplorer.py index b8dc40e..830705b 100755 --- a/backend/histplorer.py +++ b/backend/histplorer.py @@ -9,7 +9,7 @@ Expected HTTP query parameters: - range: With type=events, specifies a historical-date range If absent, the default is 'all of time'. Examples: - range=1000.1910-10-09 means '1000 AD to 09/10/1910 (inclusive)' + range=1000.1910-10-09 means '1000 AD up to and excluding 09/10/1910' range=-13000. means '13000 BC onwards' - scale: With type=events, specifies a date scale - incl: With type=events, specifies an event to include, as an event ID @@ -230,7 +230,9 @@ def lookupEvents(start: HistDate | None, end: HistDate | None, scale: int, ctg: year = start.year if start.month == 1 and start.day == 1 else start.year + 1 params.extend([startJdn, year]) if end is not None: - constraint = '(start <= ? AND fmt > 0 OR start <= ? AND fmt = 0)' + constraint = '(start < ? AND fmt > 0 OR start < ? AND fmt = 0)' + if scale < 1 and (end.month > 1 or end.day > 1): + constraint = '(start < ? AND fmt > 0 OR start <= ? AND fmt = 0)' if end.gcal is None: endJdn = gregorianToJdn(end.year, 1, 1) if end.year >= MIN_CAL_YEAR else -1 constraints.append(constraint) @@ -238,8 +240,7 @@ def lookupEvents(start: HistDate | None, end: HistDate | None, scale: int, ctg: else: endJdn = gregorianToJdn(end.year, end.month, end.day) constraints.append(constraint) - year = end.year if end.month == 12 and end.day == 31 else end.year - 1 - params.extend([endJdn, year]) + params.extend([endJdn, end.year]) # Constrain by event category if ctg is not None: constraints.append('ctg = ?') @@ -289,7 +290,7 @@ def lookupUnitCounts( query += ' AND unit >= ?' params.append(dateToUnit(start, scale)) if end: - query += ' AND unit <= ?' + query += ' AND unit < ?' params.append(dateToUnit(end, scale)) query += ' ORDER BY unit ASC LIMIT ' + str(MAX_REQ_UNIT_COUNTS + 1) # Get results diff --git a/backend/tests/test_histplorer.py b/backend/tests/test_histplorer.py index 592d534..68aae8f 100644 --- a/backend/tests/test_histplorer.py +++ b/backend/tests/test_histplorer.py @@ -2,7 +2,7 @@ import unittest import tempfile, os from tests.common import createTestDbTable -from histplorer import handleReq, HistDate, Event, EventResponse, ImgInfo, EventInfo, SuggResponse +from histplorer import handleReq, HistDate, Event, ImgInfo, EventInfo, SuggResponse def initTestDb(dbFile: str) -> None: createTestDbTable( @@ -115,7 +115,7 @@ class TestHandleReq(unittest.TestCase): Event(3, 'event three', HistDate(True, 1990, 10, 10), HistDate(True, 2000, 10, 10), None, None, 'discovery', 30, 0), ]) - self.assertEqual(response.unitCounts, {1900: 2, 1990: 1, 2000: 1, 2001: 1, 2002: 1}) + self.assertEqual(response.unitCounts, {1900: 2, 1990: 1, 2000: 1, 2001: 1}) response = handleReq(self.dbFile, {'QUERY_STRING': 'type=events&range=.1999-11-27&scale=1&ctg=event'}) self.assertEqual(response.events, [ Event(4, 'event four', HistDate(False, -2000, 10, 10), None, HistDate(False, 1, 10, 10), None, diff --git a/src/App.vue b/src/App.vue index b97e55a..de127f6 100644 --- a/src/App.vue +++ b/src/App.vue @@ -102,7 +102,8 @@ function onTimelineRemove(idx: number){ // For storing and looking up events const eventTree: ShallowRef> = shallowRef(new RBTree(cmpHistEvent)); let idToEvent: Map = new Map(); -const unitCountMaps: Ref[]> = ref(SCALES.map(() => new Map())); // For each scale, maps units to event counts +const unitCountMaps: Ref[]> = ref(SCALES.map(() => new Map())); + // For each scale, maps units to event counts // For keeping event data under a memory limit const EXCESS_EVENTS_THRESHOLD = 10000; let displayedEvents: Map = new Map(); // Maps TimeLine IDs to IDs of displayed events @@ -143,6 +144,7 @@ function reduceEvents(){ } // For getting events from server const EVENT_REQ_LIMIT = 300; +const MAX_EVENTS_PER_UNIT = 4; // Should equal MAX_DISPLAYED_PER_UNIT in backend gen_disp_data.py let queriedRanges: DateRangeTree[] = SCALES.map(() => new DateRangeTree()); // For each scale, holds date ranges for which data has already been queried fromm the server let pendingReq = false; // Used to serialise event-req handling @@ -152,10 +154,44 @@ async function onEventDisplay( await timeout(100); } pendingReq = true; - // Skip if exhausted range - if (queriedRanges[scaleIdx].has([firstDate, lastDate])){ - pendingReq = false; - return; + // Skip if range has been queried, and enough of its events have been obtained + if (queriedRanges[scaleIdx].contains([firstDate, lastDate])){ + // Get number of events in range, server-side + let fullCount = 0; + let date = firstDate.clone(); + let eventCounts: Map = new Map(); // For calculating number of events, client-side + while (date.isEarlier(lastDate)){ + let unit = dateToUnit(date, SCALES[scaleIdx]); + if (unitCountMaps.value[scaleIdx].has(unit)){ + fullCount += Math.min(MAX_EVENTS_PER_UNIT, unitCountMaps.value[scaleIdx].get(unit)!); + } + eventCounts.set(unit, 0); + stepDate(date, SCALES[scaleIdx], {inplace: true}); + } + if (fullCount > 0){ + // Get number of events, client-side + let eventCount = 0; + let itr = eventTree.value.lowerBound(new HistEvent(0, '', firstDate)) + while (itr.data() != null){ + let event = itr.data()!; + itr.next(); + if (!event.start.isEarlier(lastDate)){ + break; + } + let unit = dateToUnit(event.start, SCALES[scaleIdx]); + if (eventCounts.has(unit)){ + eventCounts.set(unit, eventCounts.get(unit)! + 1); + } + } + for (let [, count] of eventCounts.entries()){ + eventCount += Math.min(MAX_EVENTS_PER_UNIT, count); + } + // If we have enough events + if (eventCount >= fullCount || eventCount >= EVENT_REQ_LIMIT){ + pendingReq = false; + return; + } + } } // Get events from server let urlParams = new URLSearchParams({ diff --git a/src/components/TimeLine.vue b/src/components/TimeLine.vue index 6595bed..76d78d0 100644 --- a/src/components/TimeLine.vue +++ b/src/components/TimeLine.vue @@ -407,7 +407,7 @@ const endIsLastVisible = computed(() => { function dateToOffset(date: HistDate){ // Assumes 'date' is >=firstDate and <=lastDate // Find containing major tick let tickIdx = firstIdx.value; - for (let i = tickIdx + 1; i <= lastIdx.value; i++){ + for (let i = tickIdx + 1; i < lastIdx.value; i++){ if (ticks.value[i].major){ if (!date.isEarlier(ticks.value[i].date)){ tickIdx = i; @@ -436,7 +436,7 @@ const idToEvent = computed(() => { // Maps visible event IDs to HistEvents while (itr.data() != null){ let event = itr.data()!; itr.next(); - if (lastDate.value.isEarlier(event.start)){ + if (!event.start.isEarlier(lastDate.value)){ break; } map.set(event.id, event); diff --git a/src/lib.ts b/src/lib.ts index da18d94..eef06cd 100644 --- a/src/lib.ts +++ b/src/lib.ts @@ -584,7 +584,7 @@ export class DateRangeTree { const endDate = nextRange != null ? nextRange[1] : range[1]; this.tree.insert([startDate, endDate]); } - has(range: DateRange): boolean { + contains(range: DateRange): boolean { const itr = this.tree.lowerBound([range[0], new YearDate()]); let r = itr.data(); if (r == null){ diff --git a/tests/lib.test.ts b/tests/lib.test.ts index 85e17bc..2ab5503 100644 --- a/tests/lib.test.ts +++ b/tests/lib.test.ts @@ -163,14 +163,14 @@ test('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) - expect(ranges.has([new CalDate(300, 1, 1), new CalDate(400, 1, 1)])).toBe(true) + expect(ranges.contains([new CalDate(300, 1, 1), new CalDate(400, 1, 1)])).toBe(true) ranges.add([new CalDate(-100, 1, 1), new CalDate(150, 1, 1)]) ranges.add([new CalDate(400, 1, 1), new CalDate(500, 1, 1)]) expect(ranges.tree.size).toBe(2) - expect(ranges.has([new CalDate(-100, 1, 1), new CalDate(200, 1, 1)])).toBe(true) - expect(ranges.has([new CalDate(300, 1, 1), new CalDate(500, 1, 1)])).toBe(true) + expect(ranges.contains([new CalDate(-100, 1, 1), new CalDate(200, 1, 1)])).toBe(true) + expect(ranges.contains([new CalDate(300, 1, 1), new CalDate(500, 1, 1)])).toBe(true) ranges.add([new CalDate(-1000, 1, 1), new CalDate(310, 10, 2)]) expect(ranges.tree.size).toBe(1) - expect(ranges.has([new CalDate(-1000, 1, 1), new CalDate(500, 1, 1)])).toBe(true) - expect(ranges.has([new CalDate(-1, 1, 1), new CalDate(1, 1, 1)])).toBe(true) + expect(ranges.contains([new CalDate(-1000, 1, 1), new CalDate(500, 1, 1)])).toBe(true) + expect(ranges.contains([new CalDate(-1, 1, 1), new CalDate(1, 1, 1)])).toBe(true) }) -- cgit v1.2.3