aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/App.vue123
-rw-r--r--src/components/BaseLine.vue4
-rw-r--r--src/components/TimeLine.vue32
-rw-r--r--src/lib.ts321
-rw-r--r--src/rbtree.ts4
5 files changed, 247 insertions, 237 deletions
diff --git a/src/App.vue b/src/App.vue
index c3cf6bf..d3fbd69 100644
--- a/src/App.vue
+++ b/src/App.vue
@@ -39,8 +39,8 @@ import PlusIcon from './components/icon/PlusIcon.vue';
import SettingsIcon from './components/icon/SettingsIcon.vue';
import HelpIcon from './components/icon/HelpIcon.vue';
// Other
-import {HistDate, YearDate, TimelineState, HistEvent, queryServer, HistEventJson, jsonToHistEvent, cmpHistEvent,
- timeout} from './lib';
+import {timeout, HistDate, YearDate, HistEvent, queryServer, HistEventJson, jsonToHistEvent,
+ TimelineState, cmpHistEvent, DateRangeTree} from './lib';
import {useStore} from './store';
import {RBTree, rbtree_shallow_copy} from './rbtree';
@@ -102,92 +102,7 @@ function onTimelineRemove(idx: number){
// For storing and looking up events
const eventTree: ShallowRef<RBTree<HistEvent>> = shallowRef(new RBTree(cmpHistEvent));
let idToEvent: Map<number, HistEvent> = new Map();
-// 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 YearDate()]);
- 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 YearDate()]);
- 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 YearDate()]);
- }
- // Add possibly-merged range
- if (prevRange != null){
- startDate = prevRange[0];
- }
- if (nextRange != null){
- endDate = nextRange[1];
- }
- exhaustedRanges.insert([startDate, endDate]);
-}
-// For keeping event data under a memory limit
-const EXCESS_EVENTS_THRESHOLD = 10000;
-let displayedEvents: Map<number, number[]> = new Map(); // Maps TimeLine IDs to IDs of displayed events
-function onEventDisplay(eventIds: number[], timelineId: number){
- displayedEvents.set(timelineId, eventIds);
-}
-function reduceEvents(){
- // Get events to keep
- let eventsToKeep: Map<number, HistEvent> = new Map();
- for (let [, ids] of displayedEvents){
- for (let id of ids){
- eventsToKeep.set(id, idToEvent.get(id)!);
- }
- }
- // Create new event tree
- let newTree = new RBTree(cmpHistEvent);
- for (let [, event] of eventsToKeep){
- newTree.insert(event);
- }
- // Replace old data
- eventTree.value = newTree;
- idToEvent = eventsToKeep;
- exhaustedRanges.clear();
-}
+let exhaustedRanges = new DateRangeTree(); // Holds ranges for which the server has no more events
// For getting events from server
const EVENT_REQ_LIMIT = 30;
const REQ_EXCLS_LIMIT = 100;
@@ -197,8 +112,8 @@ async function onEventReq(startDate: HistDate, endDate: HistDate){
await timeout(100);
}
pendingReq = true;
- // Exclude exhausted range
- if (isExhaustedRange(startDate, endDate)){
+ // Skip if exhausted range
+ if (exhaustedRanges.has([startDate, endDate])){
pendingReq = false;
return;
}
@@ -244,7 +159,7 @@ async function onEventReq(startDate: HistDate, endDate: HistDate){
if (added){
eventTree.value = rbtree_shallow_copy(eventTree.value); // Note: triggerRef(eventTree) does not work here
} else {
- addExhaustedRange(startDate, endDate); // Mark as exhausted range
+ exhaustedRanges.add([startDate, endDate]); // Mark as exhausted range
}
// Check memory limit
if (eventTree.value.size > EXCESS_EVENTS_THRESHOLD){
@@ -252,6 +167,30 @@ async function onEventReq(startDate: HistDate, endDate: HistDate){
}
pendingReq = false;
}
+// For keeping event data under a memory limit
+const EXCESS_EVENTS_THRESHOLD = 10000;
+let displayedEvents: Map<number, number[]> = new Map(); // Maps TimeLine IDs to IDs of displayed events
+function onEventDisplay(eventIds: number[], timelineId: number){
+ displayedEvents.set(timelineId, eventIds);
+}
+function reduceEvents(){
+ // Get events to keep
+ let eventsToKeep: Map<number, HistEvent> = new Map();
+ for (let [, ids] of displayedEvents){
+ for (let id of ids){
+ eventsToKeep.set(id, idToEvent.get(id)!);
+ }
+ }
+ // Create new event tree
+ let newTree = new RBTree(cmpHistEvent);
+ for (let [, event] of eventsToKeep){
+ newTree.insert(event);
+ }
+ // Replace old data
+ eventTree.value = newTree;
+ idToEvent = eventsToKeep;
+ exhaustedRanges.clear();
+}
// For resize handling
let lastResizeHdlrTime = 0; // Used to throttle resize handling
@@ -269,7 +208,7 @@ async function onResize(){
}
// Setup a handler to execute after ending a run of resize events
clearTimeout(afterResizeHdlr);
- afterResizeHdlr = setTimeout(async () => {
+ afterResizeHdlr = window.setTimeout(async () => {
afterResizeHdlr = 0;
await handleResize();
lastResizeHdlrTime = new Date().getTime();
diff --git a/src/components/BaseLine.vue b/src/components/BaseLine.vue
index 33d8a88..0187b20 100644
--- a/src/components/BaseLine.vue
+++ b/src/components/BaseLine.vue
@@ -43,7 +43,7 @@ const periods: Ref<Period[]> = ref([
const skipTransition = ref(true);
onMounted(() => setTimeout(() => {skipTransition.value = false}, 100));
-// For size tracking (used to prevent time spans shrinking below 1 pixel)
+// For size and mount-status tracking
const width = ref(0);
const height = ref(0);
const mounted = ref(false);
@@ -87,7 +87,7 @@ function spanStyles(state: TimelineState){
let start = state.startDate.clone();
let end = state.endDate.clone();
let scale = SCALES[state.scaleIdx];
- if (scale != MONTH_SCALE && scale != DAY_SCALE){ // Possibly incorporate offsets
+ if (scale != MONTH_SCALE && scale != DAY_SCALE){ // Account for offsets
stepDate(start, 1, {forward: false, count: Math.floor(state.startOffset * scale), inplace: true});
stepDate(end, 1, {count: Math.floor(state.endOffset * scale), inplace: true});
}
diff --git a/src/components/TimeLine.vue b/src/components/TimeLine.vue
index 8a6ddc4..fd119be 100644
--- a/src/components/TimeLine.vue
+++ b/src/components/TimeLine.vue
@@ -11,6 +11,8 @@
<stop offset="95%" stop-color="gold"/>
</linearGradient>
</defs>
+ <!-- Main line (unit horizontal line that gets transformed, with extra length to avoid gaps when panning) -->
+ <line :stroke="store.color.alt" stroke-width="2px" x1="-1" y1="0" x2="2" y2="0" :style="mainlineStyles"/>
<!-- Tick markers -->
<template v-for="date, idx in ticks.dates" :key="date.toInt()">
<line v-if="date.equals(MIN_DATE, scale) || date.equals(MAX_DATE, scale)"
@@ -24,19 +26,17 @@
:stroke="store.color.alt" stroke-width="1px"
:style="tickStyles(idx)" class="animate-fadein"/>
</template>
- <!-- Event lines -->
- <line v-for="id in eventLines.keys()" :key="id"
- x1="0" y1="0" x2="1" y2="0.01" stroke="url('#eventLineGradient')" stroke-width="1px"
- :style="eventLineStyles(id)" class="animate-fadein"/>
- <!-- Note: With a fully vertical or horizontal line, nothing gets displayed -->
<!-- Tick labels -->
<text v-for="date, idx in ticks.dates" :key="date.toInt()"
x="0" y="0" :text-anchor="vert ? 'start' : 'middle'" dominant-baseline="middle"
:fill="store.color.textDark" :style="tickLabelStyles(idx)" class="text-sm animate-fadein">
{{date.toDisplayString()}}
</text>
- <!-- Main line (unit horizontal line that gets transformed, with extra length to avoid gaps when panning) -->
- <line :stroke="store.color.alt" stroke-width="2px" x1="-1" y1="0" x2="2" y2="0" :style="mainlineStyles"/>
+ <!-- Event lines -->
+ <line v-for="id in eventLines.keys()" :key="id"
+ x1="0" y1="0" x2="1" y2="0.01" stroke="url('#eventLineGradient')" stroke-width="1px"
+ :style="eventLineStyles(id)" class="animate-fadein"/>
+ <!-- Note: With a fully vertical or horizontal line, nothing gets displayed -->
</svg>
<!-- Events -->
<div v-for="id in idToPos.keys()" :key="id" class="absolute animate-fadein" :style="eventStyles(id)">
@@ -88,7 +88,7 @@ const width = ref(0);
const height = ref(0);
const availLen = computed(() => props.vert ? height.value : width.value);
const availBreadth = computed(() => props.vert ? width.value : height.value);
-const prevVert = ref(props.vert); // For skipping transitions on horz/vert swap
+const prevVert = ref(props.vert); // Previous 'vert' value, used for skipping transitions on horz/vert swap
const mounted = ref(false);
onMounted(() => {
let rootEl = rootRef.value!;
@@ -121,7 +121,7 @@ const eventMajorSz = computed(() => props.vert ? eventHeight.value : eventWidth.
const eventMinorSz = computed(() => props.vert ? eventWidth.value : eventHeight.value)
const sideMainline = computed( // True if unable to fit mainline in middle with events on both sides
() => availBreadth.value < store.mainlineBreadth + (eventMinorSz.value + store.spacing * 2) * 2);
-const mainlineOffset = computed(() => { // Distance mainline-area line to side of display area
+const mainlineOffset = computed(() => { // Distance from mainline-area line to left/top of display area
if (!sideMainline.value){
return availBreadth.value / 2 - store.mainlineBreadth /2 + store.largeTickLen / 2;
} else {
@@ -675,9 +675,9 @@ function zoomTimeline(zoomRatio: number){
}
newNumUnits += newStartOffset + newEndOffset;
}
- // Possibly change the scale
+ // Possibly zoom in/out
let tickDiff = availLen.value / newNumUnits;
- if (tickDiff < store.minTickSep){ // Possibly zoom out
+ if (tickDiff < store.minTickSep){ // If trying to zoom out
if (scaleIdx.value == 0){
console.log('Reached zoom out limit');
return;
@@ -688,8 +688,8 @@ function zoomTimeline(zoomRatio: number){
newStartOffset /= oldUnitsPerNew;
newEndOffset /= oldUnitsPerNew;
// Shift starting and ending points to align with new scale
- // Note: There is some distortion due to not accounting for no year 0 CE here
- // But the result seems tolerable, and resolving it adds a fair bit of code complexity
+ // Note: There is some distortion due to not fully accounting for no year 0 CE here,
+ // but the result seems tolerable, and resolving it adds a fair bit of code complexity
let newStartSubUnits =
(scale.value == DAY_SCALE) ? getDaysInMonth(newStart.year, newStart.month) :
(scale.value == MONTH_SCALE) ? 12 :
@@ -742,7 +742,7 @@ function zoomTimeline(zoomRatio: number){
//
scaleIdx.value -= 1;
}
- } else { // Possibly zoom in
+ } else { // If trying to zoom in
if (scaleIdx.value == SCALES.length - 1){
if (newNumUnits < store.minLastTicks){
console.log('Reached zoom in limit');
@@ -824,7 +824,7 @@ function onPointerDown(evt: PointerEvent){
dragVelocity = 0;
vUpdateTime = Date.now();
vPrevPointer = null;
- vUpdater = setInterval(() => {
+ vUpdater = window.setInterval(() => {
if (vPrevPointer != null){
let time = Date.now();
let ptrDiff = (props.vert ? pointerY! : pointerX!) - vPrevPointer;
@@ -843,7 +843,7 @@ function onPointerMove(evt: PointerEvent){
// Handle pointer dragging
dragDiff += props.vert ? evt.clientY - pointerY! : evt.clientX - pointerX!;
if (dragHandler == 0){
- dragHandler = setTimeout(() => {
+ dragHandler = window.setTimeout(() => {
if (Math.abs(dragDiff) > 2){
panTimeline(-dragDiff / availLen.value);
dragDiff = 0;
diff --git a/src/lib.ts b/src/lib.ts
index fa803bc..4726387 100644
--- a/src/lib.ts
+++ b/src/lib.ts
@@ -2,10 +2,14 @@
* Project-wide globals
*/
+import {RBTree} from './rbtree';
+
export const DEBUG = true;
-export const WRITING_MODE_HORZ =
- window.getComputedStyle(document.body)['writing-mode' as any].startsWith('horizontal');
+export let WRITING_MODE_HORZ = true;
// Used with ResizeObserver callbacks, to determine which resized dimensions are width and height
+if ('writing-mode' in window.getComputedStyle(document.body)){ // Can be null when testing
+ WRITING_MODE_HORZ = window.getComputedStyle(document.body)['writing-mode' as any].startsWith('horizontal');
+}
// Similar to %, but for negative LHS, return a positive offset from a lower RHS multiple
export function moduloPositive(x: number, y: number){
@@ -16,7 +20,7 @@ export async function timeout(ms: number){
return new Promise(resolve => setTimeout(resolve, ms))
}
-// For calendar conversion. Mostly copied from backend/hist_data/cal.py
+// For calendar conversion (mostly copied from backend/hist_data/cal.py)
export function gregorianToJdn(year: number, month: number, day: number): number {
if (year < 0){
year += 1;
@@ -75,7 +79,7 @@ export function getDaysInMonth(year: number, month: number){
}
// For date representation
-export const MIN_CAL_YEAR = -4713; // Year after which months/day scales are usable
+export const MIN_CAL_YEAR = -4713; // Earliest year where months/day scales are usable
export class HistDate {
gcal: boolean | null;
year: number;
@@ -84,8 +88,8 @@ export class HistDate {
constructor(gcal: boolean | null, year: number, month: number, day: number){
this.gcal = gcal;
this.year = year;
- this.month = month;
- this.day = day;
+ this.month = gcal == null ? 1 : month;
+ this.day = gcal == null ? 1 : day;
}
equals(other: HistDate, scale=DAY_SCALE){
if (scale == DAY_SCALE){
@@ -96,6 +100,9 @@ export class HistDate {
return Math.floor(this.year / scale) == Math.floor(other.year / scale);
}
}
+ clone(){
+ return new HistDate(this.gcal, this.year, this.month, this.day);
+ }
isEarlier(other: HistDate, scale=DAY_SCALE){
const yearlyScale = scale != DAY_SCALE && scale != MONTH_SCALE;
const thisYear = yearlyScale ? Math.floor(this.year / scale) : this.year;
@@ -119,8 +126,33 @@ export class HistDate {
return 0;
}
}
- toInt(){
- return this.day + this.month * 50 + this.year * 1000;
+ getDayDiff(other: HistDate){ // Assumes neither date has gcal=null
+ const jdn2 = gregorianToJdn(this.year, this.month, this.day);
+ const jdn1 = gregorianToJdn(other.year, other.month, other.day);
+ return Math.abs(jdn1 - jdn2);
+ }
+ getMonthDiff(other: HistDate){
+ // Determine earlier date
+ let earlier = this as HistDate;
+ let later = other;
+ if (other.year < this.year || other.year == this.year && other.month < this.month){
+ earlier = other;
+ later = this as HistDate;
+ }
+ //
+ const yearDiff = earlier.getYearDiff(later);
+ if (yearDiff == 0){
+ return later.month - earlier.month;
+ } else {
+ return (13 - earlier.month) + (yearDiff - 1) * 12 + later.month - 1;
+ }
+ }
+ getYearDiff(other: HistDate){
+ let yearDiff = Math.abs(this.year - other.year);
+ if (this.year * other.year < 0){ // Account for no 0 CE
+ yearDiff -= 1;
+ }
+ return yearDiff;
}
toString(){
if (this.gcal != null){
@@ -175,36 +207,8 @@ export class HistDate {
}
}
}
- getDayDiff(other: HistDate){ // Assumes neither date has gcal=null
- const jdn2 = gregorianToJdn(this.year, this.month, this.day);
- const jdn1 = gregorianToJdn(other.year, other.month, other.day);
- return Math.abs(jdn1 - jdn2);
- }
- getMonthDiff(other: HistDate){
- // Determine earlier date
- let earlier = this as HistDate;
- let later = other;
- if (other.year < this.year || other.year == this.year && other.month < this.month){
- earlier = other;
- later = this as HistDate;
- }
- //
- const yearDiff = earlier.getYearDiff(later);
- if (yearDiff == 0){
- return later.month - earlier.month;
- } else {
- return (13 - earlier.month) + (yearDiff - 1) * 12 + later.month - 1;
- }
- }
- getYearDiff(other: HistDate){
- let yearDiff = Math.abs(this.year - other.year);
- if (this.year * other.year < 0){ // Account for no 0 CE
- yearDiff -= 1;
- }
- return yearDiff;
- }
- clone(){
- return new HistDate(this.gcal, this.year, this.month, this.day);
+ toInt(){ // Used for v-for keys
+ return this.day + this.month * 50 + this.year * 1000;
}
}
export class YearDate extends HistDate {
@@ -232,7 +236,97 @@ export class CalDate extends HistDate {
}
}
-// Timeline parameters
+// For event representation
+export class HistEvent {
+ id: number;
+ title: string;
+ start: HistDate;
+ startUpper: HistDate | null;
+ end: HistDate | null;
+ endUpper: HistDate | null;
+ ctg: string;
+ imgId: number;
+ pop: number;
+ constructor(
+ id: number, title: string, start: HistDate, startUpper: HistDate | null = null,
+ end: HistDate | null = null, endUpper: HistDate | null = null, ctg='', imgId=0, pop=0){
+ this.id = id;
+ this.title = title;
+ this.start = start;
+ this.startUpper = startUpper;
+ this.end = end;
+ this.endUpper = endUpper;
+ this.ctg = ctg;
+ this.imgId = imgId;
+ this.pop = pop;
+ }
+}
+export function cmpHistEvent(event: HistEvent, event2: HistEvent){
+ const cmp = event.start.cmp(event2.start);
+ return cmp != 0 ? cmp : event.id - event2.id;
+}
+
+// For server requests
+const SERVER_DATA_URL = (new URL(window.location.href)).origin + '/data/'
+const SERVER_IMG_PATH = '/hist_data/img/'
+export async function queryServer(params: URLSearchParams, serverDataUrl=SERVER_DATA_URL){
+ // Construct URL
+ const url = new URL(serverDataUrl);
+ url.search = params.toString();
+ // Query server
+ let responseObj;
+ try {
+ const response = await fetch(url.toString());
+ responseObj = await response.json();
+ } catch (error){
+ console.log(`Error with querying ${url.toString()}: ${error}`);
+ return null;
+ }
+ return responseObj;
+}
+export function getImagePath(imgId: number): string {
+ return SERVER_IMG_PATH + String(imgId) + '.jpg';
+}
+// For server responses
+export type HistDateJson = {
+ gcal: boolean | null,
+ year: number,
+ month: number,
+ day: number,
+}
+export type HistEventJson = {
+ id: number,
+ title: string,
+ start: HistDateJson,
+ startUpper: HistDateJson | null,
+ end: HistDateJson | null,
+ endUpper: HistDateJson | null,
+ ctg: string,
+ imgId: number,
+ pop: number,
+}
+export function jsonToHistDate(json: HistDateJson){
+ if (json.gcal == null){
+ return new YearDate(json.year);
+ } else {
+ return new CalDate(json.year, json.month, json.day, json.gcal);
+ }
+}
+export function jsonToHistEvent(json: HistEventJson){
+ return {
+ id: json.id,
+ title: json.title,
+ start: jsonToHistDate(json.start),
+ startUpper: json.startUpper == null ? null : jsonToHistDate(json.startUpper),
+ end: json.end == null ? null : jsonToHistDate(json.end),
+ endUpper: json.endUpper == null ? null : jsonToHistDate(json.endUpper),
+ ctg: json.ctg,
+ imgId: json.imgId,
+ pop: json.pop,
+ };
+}
+
+// For dates in a timeline
const currentDate = new Date();
export const MIN_DATE = new YearDate(-13.8e9);
export const MAX_DATE = new CalDate(currentDate.getFullYear(), currentDate.getMonth() + 1, currentDate.getDate());
@@ -259,7 +353,8 @@ if (DEBUG){
}
}
}
-export function stepDate(date: HistDate, scale: number, {forward=true, count=1, inplace=false} = {}): HistDate {
+export function stepDate( // If stepping by month or years, leaves day value unchanged
+ date: HistDate, scale: number, {forward=true, count=1, inplace=false} = {}): HistDate {
const newDate = inplace ? date : date.clone();
if (count < 0){
count = -count;
@@ -342,7 +437,7 @@ export function inDateScale(date: HistDate, scale: number): boolean {
}
}
export function getScaleRatio(scale: number, scale2: number){
- // Returns upper number of units in 'scale' per unit in 'scale2'
+ // Returns number of units in 'scale' per unit in 'scale2' (provides an upper value for days-per-month/year)
if (scale == DAY_SCALE){
scale = 1 / 12 / 31;
} else if (scale == MONTH_SCALE){
@@ -384,91 +479,67 @@ export class TimelineState {
}
}
-export class HistEvent {
- id: number;
- title: string;
- start: HistDate;
- startUpper: HistDate | null;
- end: HistDate | null;
- endUpper: HistDate | null;
- ctg: string;
- imgId: number;
- pop: number;
- constructor(
- id: number, title: string, start: HistDate, startUpper: HistDate | null = null,
- end: HistDate | null = null, endUpper: HistDate | null = null, ctg='', imgId=0, pop=0){
- this.id = id;
- this.title = title;
- this.start = start;
- this.startUpper = startUpper;
- this.end = end;
- this.endUpper = endUpper;
- this.ctg = ctg;
- this.imgId = imgId;
- this.pop = pop;
+// For managing sets of non-overlapping date ranges
+export type DateRange = [HistDate, HistDate];
+export class DateRangeTree {
+ tree: RBTree<DateRange>;
+ constructor(){
+ this.tree = new RBTree((r1: DateRange, r2: DateRange) => r1[0].cmp(r2[0]));
}
-}
-export function cmpHistEvent(event: HistEvent, event2: HistEvent){
- const cmp = event.start.cmp(event2.start);
- return cmp != 0 ? cmp : event.id - event2.id;
-}
-
-// For server requests
-const SERVER_DATA_URL = (new URL(window.location.href)).origin + '/data/'
-const SERVER_IMG_PATH = '/hist_data/img/'
-export async function queryServer(params: URLSearchParams){
- // Construct URL
- const url = new URL(SERVER_DATA_URL);
- url.search = params.toString();
- // Query server
- let responseObj;
- try {
- const response = await fetch(url.toString());
- responseObj = await response.json();
- } catch (error){
- console.log(`Error with querying ${url.toString()}: ${error}`);
- return null;
+ add(range: DateRange){
+ let rangesToRemove: HistDate[] = []; // Holds starts of ranges to remove
+ let dummyDate = new YearDate();
+ // Find ranges to remove
+ let itr = this.tree.lowerBound([range[0], dummyDate]);
+ let prevRange = itr.prev();
+ if (prevRange != null){ // Check for start-overlapping range
+ if (prevRange[1].isEarlier(range[0])){
+ prevRange = null;
+ } else {
+ rangesToRemove.push(prevRange[0]);
+ }
+ }
+ let r = itr.next();
+ while (r != null && !range[1].isEarlier(r[1])){ // Check for included ranges
+ rangesToRemove.push(r[0]);
+ r = itr.next();
+ }
+ let nextRange = itr.data();
+ if (nextRange != null){ // Check for end-overlapping range
+ if (range[1].isEarlier(nextRange[0])){
+ nextRange = null;
+ } else {
+ rangesToRemove.push(nextRange[0])
+ }
+ }
+ // Remove included/overlapping ranges
+ for (let start of rangesToRemove){
+ this.tree.remove([start, dummyDate]);
+ }
+ // Add possibly-merged range
+ let startDate = prevRange != null ? prevRange[0] : range[0];
+ let endDate = nextRange != null ? nextRange[1] : range[1];
+ this.tree.insert([startDate, endDate]);
}
- return responseObj;
-}
-export function getImagePath(imgId: number): string {
- return SERVER_IMG_PATH + String(imgId) + '.jpg';
-}
-// For server responses
-export type HistDateJson = {
- gcal: boolean | null,
- year: number,
- month: number,
- day: number,
-}
-export type HistEventJson = {
- id: number,
- title: string,
- start: HistDateJson,
- startUpper: HistDateJson | null,
- end: HistDateJson | null,
- endUpper: HistDateJson | null,
- ctg: string,
- imgId: number,
- pop: number,
-}
-export function jsonToHistDate(json: HistDateJson){
- if (json.gcal == null){
- return new YearDate(json.year);
- } else {
- return new CalDate(json.year, json.month, json.day, json.gcal);
+ has(range: DateRange): boolean {
+ let itr = this.tree.lowerBound([range[0], new YearDate()]);
+ let r = itr.data();
+ if (r == null){
+ r = itr.prev();
+ if (r == null){
+ return false;
+ } else {
+ return !r[1].isEarlier(range[1]);
+ }
+ } else {
+ if (range[0].isEarlier(r[0])){
+ return false;
+ } else {
+ return !r[1].isEarlier(range[1]);
+ }
+ }
+ }
+ clear(){
+ this.tree.clear();
}
-}
-export function jsonToHistEvent(json: HistEventJson){
- return {
- id: json.id,
- title: json.title,
- start: jsonToHistDate(json.start),
- startUpper: json.startUpper == null ? null : jsonToHistDate(json.startUpper),
- end: json.end == null ? null : jsonToHistDate(json.end),
- endUpper: json.endUpper == null ? null : jsonToHistDate(json.endUpper),
- ctg: json.ctg,
- imgId: json.imgId,
- pop: json.pop,
- };
}
diff --git a/src/rbtree.ts b/src/rbtree.ts
index 8422dec..b4ae540 100644
--- a/src/rbtree.ts
+++ b/src/rbtree.ts
@@ -1,6 +1,6 @@
// Copied from node_modules/bintrees/lib/, and adapted to use ES6, classes, and typescript
-class Node<T> {
+export class Node<T> {
data: T;
left: Node<T> | null;
right: Node<T> | null;
@@ -24,7 +24,7 @@ class Node<T> {
}
}
-class Iterator<T> {
+export class Iterator<T> {
_tree: RBTree<T>;
_ancestors: Node<T>[];
_cursor: Node<T> | null;