aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/App.vue40
-rw-r--r--src/components/TimeLine.vue57
-rw-r--r--src/lib.ts15
3 files changed, 100 insertions, 12 deletions
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 {