diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/App.vue | 92 | ||||
| -rw-r--r-- | src/components/TimeLine.vue | 55 | ||||
| -rw-r--r-- | src/lib.ts | 18 |
3 files changed, 133 insertions, 32 deletions
diff --git a/src/App.vue b/src/App.vue index 334d75e..cc71cfe 100644 --- a/src/App.vue +++ b/src/App.vue @@ -99,11 +99,95 @@ function onTimelineRemove(idx: number){ timelines.value.splice(idx, 1); } -// Event data -const eventTree: ShallowRef<RBTree<HistEvent>> = shallowRef(new RBTree(cmpHistEvent)); // Used to look up events by date range +// For storing and looking up events +const eventTree: ShallowRef<RBTree<HistEvent>> = shallowRef(new RBTree(cmpHistEvent)); +// For tracking ranges for which the server has no more events +let exhaustedRanges = new RBTree(cmpDatePairs); +function cmpDatePairs(datePair1: [HistDate, HistDate], datePair2: [HistDate, HistDate]){ + return datePair1[0].cmp(datePair2[0]); +} +function isExhaustedRange(startDate: HistDate, endDate: HistDate): boolean { + // Check if input range is contained in a stored exhausted range + let itr = exhaustedRanges.lowerBound([startDate, new HistDate(1)]); + let datePair = itr.data(); + if (datePair == null){ + datePair = itr.prev(); + if (datePair == null){ + return false; + } else { + return !datePair[1].isEarlier(endDate); + } + } else { + if (startDate.isEarlier(datePair[0])){ + return false; + } else { + return !datePair[1].isEarlier(endDate); + } + } +} +function addExhaustedRange(startDate: HistDate, endDate: HistDate){ + let rangesToRemove: HistDate[] = []; // Holds starts of ranges to remove + // Find ranges to remove + let itr = exhaustedRanges.lowerBound([startDate, new HistDate(1)]); + let prevRange = itr.prev(); + if (prevRange != null){ // Check for start-overlapping range + if (prevRange[1].isEarlier(startDate)){ + prevRange = null; + } else { + rangesToRemove.push(prevRange[0]); + } + } + let datePair = itr.next(); + while (datePair != null && !endDate.isEarlier(datePair[1])){ // Check for included ranges + rangesToRemove.push(datePair[0]); + datePair = itr.next(); + } + let nextRange = itr.data(); + if (nextRange != null){ // Check for end-overlapping range + if (endDate.isEarlier(nextRange[0])){ + nextRange = null; + } else { + rangesToRemove.push(nextRange[0]) + } + } + // Remove included/overlapping ranges + for (let start of rangesToRemove){ + exhaustedRanges.remove([start, new HistDate(1)]); + } + // Add possibly-merged range + if (prevRange != null){ + startDate = prevRange[0]; + } + if (nextRange != null){ + endDate = nextRange[1]; + } + exhaustedRanges.insert([startDate, endDate]); +} +// For getting events from server +const EVENT_REQ_LIMIT = 10; async function onEventReq(startDate: HistDate, endDate: HistDate){ + // Exclude exhausted range + if (isExhaustedRange(startDate, endDate)){ + return; + } + // Get existing events in range + let existingEventIds: number[] = []; + let itr = eventTree.value.lowerBound(new HistEvent(0, '', startDate)); + while (itr.data() != null){ + let event = itr.data()!; + itr.next(); + if (endDate.isEarlier(event.start)){ + break; + } + existingEventIds.push(event.id); + } // Get events from server - let urlParams = new URLSearchParams({type: 'events', range: `${startDate}.${endDate}`, limit: '10'}); + let urlParams = new URLSearchParams({ + type: 'events', + range: `${startDate}.${endDate}`, + limit: String(EVENT_REQ_LIMIT), + excl: existingEventIds.join('.'), + }); let responseObj: HistEventJson[] = await queryServer(urlParams); if (responseObj == null){ return; @@ -118,6 +202,8 @@ async function onEventReq(startDate: HistDate, endDate: HistDate){ // Notify components if new events were added if (added){ eventTree.value = rbtree_shallow_copy(eventTree.value); // Note: triggerRef(eventTree) does not work here + } else { + addExhaustedRange(startDate, endDate); // Mark as exhausted range } } diff --git a/src/components/TimeLine.vue b/src/components/TimeLine.vue index 62780c8..6c6b6fc 100644 --- a/src/components/TimeLine.vue +++ b/src/components/TimeLine.vue @@ -28,10 +28,10 @@ </text> </svg> <!-- Events --> - <div v-for="id in idToEvent.keys()" :key="id" + <div v-for="id in idToPos.keys()" :key="id" class="absolute bg-black text-white border border-white rounded animate-fadein" :style="eventStyles(id)"> - {{idToEvent.get(id)!.event.title}} + {{idToEvent.get(id)!.title}} </div> <!-- Buttons --> <icon-button :size="30" class="absolute top-2 right-2" @@ -43,7 +43,7 @@ </template> <script setup lang="ts"> -import {ref, onMounted, computed, watch, PropType, Ref, nextTick} from 'vue'; +import {ref, onMounted, computed, watch, PropType, Ref} from 'vue'; // Components import IconButton from './IconButton.vue'; // Icons @@ -254,31 +254,41 @@ const ticks = computed((): Ticks => { }); // For displayed events -const idToEvent = computed(() => { // Maps visible event IDs to HistEvent, x-pos, y-pos, width, and height - let idToPos: Map<number, {event: HistEvent, x: number, y: number, w: number, h: number}> = new Map(); - let numUnits = getNumVisibleUnits(); - let minorAxisStep = 0; - // Find events to display, and do basic layouting - let iter = props.eventTree.lowerBound(new HistEvent(0, '', startDate.value)); - while (iter.data() != null){ - let event = iter.data()!; - iter.next(); +let pendingReq = false; +const idToEvent = computed(() => { // Maps visible event IDs to HistEvents + let map: Map<number, HistEvent> = new Map(); + // Find events to display + let itr = props.eventTree.lowerBound(new HistEvent(0, '', startDate.value)); + while (itr.data() != null){ + let event = itr.data()!; + itr.next(); if (endDate.value.isEarlier(event.start)){ break; } + map.set(event.id, event); + } + pendingReq = false; + return map; +}); +const idToPos = computed(() => { + let map: Map<number, [number, number, number, number]> = new Map(); // Maps visible event IDs to x/y/w/h + let numUnits = getNumVisibleUnits(); + let minorAxisStep = 0; + for (let event of idToEvent.value.values()){ let unitOffset = getUnitDiff(event.start, startDate.value, scale.value) + startOffset.value; let posFrac = unitOffset / numUnits; let posX = props.vert ? minorAxisStep : availLen.value * posFrac; let posY = props.vert ? availLen.value * posFrac : minorAxisStep; - idToPos.set(event.id, {event, x: posX, y: posY, w: 100, h: 100}); + map.set(event.id, [posX, posY, 100, 100]); minorAxisStep += 10; } // If more events could be displayed, notify parent - if (idToPos.size < 3){ - nextTick(() => emit('event-req', startDate.value, endDate.value)); + if (map.size < 3 && !pendingReq){ + emit('event-req', startDate.value, endDate.value); } + pendingReq = true; // - return idToPos; + return map; }); // For panning/zooming @@ -729,13 +739,14 @@ function tickLabelStyles(idx: number){ } } function eventStyles(eventId: number){ - const evt = idToEvent.value.get(eventId)!; + const event = idToEvent.value.get(eventId)!; + const [x, y, w, h] = idToPos.value.get(eventId)!; return { - left: evt.x + 'px', - top: evt.y + 'px', - width: evt.w + 'px', - height: evt.h + 'px', - backgroundImage: `url(${getImagePath(evt.event.imgId)})`, + left: x + 'px', + top: y + 'px', + width: w + 'px', + height: h + 'px', + backgroundImage: `url(${getImagePath(event.imgId)})`, backgroundSize: 'cover', transitionProperty: skipTransition.value ? 'none' : 'all', transitionDuration, @@ -89,6 +89,15 @@ export class HistDate { return Math.floor(this.year / scale) == Math.floor(other.year / scale); } } + cmp(other: HistDate, scale=DAY_SCALE){ + if (this.isEarlier(other)){ + return -1; + } else if (!this.equals(other)){ + return 1; + } else { + return 0; + } + } isEarlier(other: HistDate, scale=DAY_SCALE){ const yearlyScale = scale != DAY_SCALE && scale != MONTH_SCALE; const thisYear = yearlyScale ? Math.floor(this.year / scale) : this.year; @@ -324,13 +333,8 @@ export class HistEvent { } } export function cmpHistEvent(event: HistEvent, event2: HistEvent){ - if (event.start.isEarlier(event2.start)){ - return -1; - } else if (!event.start.equals(event2.start)){ - return 1; - } else { - return event.id - event2.id; - } + let cmp = event.start.cmp(event2.start); + return cmp != 0 ? cmp : event.id - event2.id;; } // For server requests |
