From 93c37b26cca7764dea66fb12c0e53acc8fd78d2b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thea=20Sch=C3=B6bl?= Date: Fri, 25 Mar 2022 10:52:40 +0000 Subject: [PATCH] feat: support overlapping timetable dates --- .../schedule/page/grid/range-overlap.spec.ts | 153 ++++++++++++++++++ .../schedule/page/grid/range-overlap.ts | 86 ++++++++++ .../schedule/page/grid/schedule-card.scss | 17 +- .../page/grid/schedule-day.component.ts | 17 +- .../schedule/page/grid/schedule-day.html | 23 +-- .../schedule/page/grid/schedule-day.scss | 23 ++- 6 files changed, 282 insertions(+), 37 deletions(-) create mode 100644 src/app/modules/schedule/page/grid/range-overlap.spec.ts create mode 100644 src/app/modules/schedule/page/grid/range-overlap.ts diff --git a/src/app/modules/schedule/page/grid/range-overlap.spec.ts b/src/app/modules/schedule/page/grid/range-overlap.spec.ts new file mode 100644 index 00000000..4ca97633 --- /dev/null +++ b/src/app/modules/schedule/page/grid/range-overlap.spec.ts @@ -0,0 +1,153 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +/* + * Copyright (C) 2022 StApps + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public Licens for + * more details. + * + * You should have received a copy of the GNU General Public License along with + * this program. If not, see . + */ +import {groupRangeOverlaps} from './range-overlap'; +import {shuffle} from 'lodash-es'; + +interface SimpleRange { + starty: number; + endy: number; +} + +const from = (range: SimpleRange) => range.starty; +const till = (range: SimpleRange) => range.endy; + +describe('RangeOverlaps', () => { + it('should handle empty ranges', () => { + expect(groupRangeOverlaps([], from, till)).toEqual([]); + }); + + it('should handle single range', () => { + expect( + groupRangeOverlaps([{starty: 0, endy: 1}], from, till), + ).toEqual([{start: 0, end: 1, elements: [{starty: 0, endy: 1}]}]); + }); + + it('should handle two non-overlapping ranges', () => { + expect( + groupRangeOverlaps( + shuffle([ + {starty: 0, endy: 1}, + {starty: 2, endy: 3}, + ]), + from, + till, + ), + ).toEqual([ + {start: 0, end: 1, elements: [{starty: 0, endy: 1}]}, + {start: 2, end: 3, elements: [{starty: 2, endy: 3}]}, + ]); + }); + + it('should not overlap two directly adjacent ranges', () => { + expect( + groupRangeOverlaps( + shuffle([ + {starty: 0, endy: 1}, + {starty: 1, endy: 2}, + ]), + from, + till, + ), + ).toEqual([ + {start: 0, end: 1, elements: [{starty: 0, endy: 1}]}, + {start: 1, end: 2, elements: [{starty: 1, endy: 2}]}, + ]); + }); + + it('should handle two overlapping ranges', () => { + expect( + groupRangeOverlaps( + shuffle([ + {starty: 0, endy: 2}, + {starty: 1, endy: 3}, + ]), + from, + till, + ), + ).toEqual([ + { + start: 0, + end: 3, + elements: [ + {starty: 0, endy: 2}, + {starty: 1, endy: 3}, + ], + }, + ]); + }); + + it('should handle multiple overlapping ranges', () => { + expect( + groupRangeOverlaps( + shuffle([ + {starty: 0, endy: 2}, + {starty: 1, endy: 3}, + {starty: 2, endy: 4}, + {starty: 3, endy: 5}, + ]), + from, + till, + ), + ).toEqual([ + { + start: 0, + end: 5, + elements: [ + {starty: 0, endy: 2}, + {starty: 1, endy: 3}, + {starty: 2, endy: 4}, + {starty: 3, endy: 5}, + ], + }, + ]); + }); + + it('should handle two groups of three overlapping ranges each', () => { + expect( + groupRangeOverlaps( + shuffle([ + {starty: 0, endy: 2}, + {starty: 1, endy: 3}, + {starty: 2, endy: 4}, + {starty: 5, endy: 7}, + {starty: 6, endy: 8}, + {starty: 7, endy: 9}, + ]), + from, + till, + ), + ).toEqual([ + { + start: 0, + end: 4, + elements: [ + {starty: 0, endy: 2}, + {starty: 1, endy: 3}, + {starty: 2, endy: 4}, + ], + }, + { + start: 5, + end: 9, + elements: [ + {starty: 5, endy: 7}, + {starty: 6, endy: 8}, + {starty: 7, endy: 9}, + ], + }, + ]); + }); +}); diff --git a/src/app/modules/schedule/page/grid/range-overlap.ts b/src/app/modules/schedule/page/grid/range-overlap.ts new file mode 100644 index 00000000..464d76a7 --- /dev/null +++ b/src/app/modules/schedule/page/grid/range-overlap.ts @@ -0,0 +1,86 @@ +/* + * Copyright (C) 2022 StApps + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public Licens for + * more details. + * + * You should have received a copy of the GNU General Public License along with + * this program. If not, see . + */ +import {flatMap, max, min, partition} from 'lodash-es'; + +export interface RangeInfo { + elements: T[]; + start: number; + end: number; +} + +/** + * Takes a list of ranges and groups by overlaps. + */ +export function groupRangeOverlaps( + ranges: T[], + start: (range: T) => number, + end: (range: T) => number, +): RangeInfo[] { + return internalGroupRangeOverlaps( + ranges + .sort((a, b) => start(a) - start(b)) + .map(range => ({ + elements: [range], + start: start(range), + end: end(range), + })), + ) + .map(range => ({ + ...range, + elements: range.elements.sort((a, b) => start(a) - start(b)), + })) + .sort((a, b) => a.start - b.start); +} + +/** + * + */ +function within(a: number, b: number, c: number): boolean { + return a > b && a < c; +} + +/** + * + */ +function hasOverlap(a1: number, b1: number, a2: number, b2: number): boolean { + return within(a1, a2, b2) || within(b1, a2, b2); +} + +/** + * Takes a list of ranges and groups by overlaps. + */ +function internalGroupRangeOverlaps(input: RangeInfo[]): RangeInfo[] { + const result: RangeInfo[] = []; + let ranges = [...input]; + let cumulativeReorders = 0; + while (ranges.length > 0) { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const range = ranges.pop()!; + const [overlaps, rest] = partition(ranges, r => + hasOverlap(range.start, range.end, r.start, r.end), + ); + cumulativeReorders += overlaps.length; + ranges = rest; + const elements = [range, ...overlaps]; + result.push({ + elements: flatMap(elements, 'elements'), + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + start: min(elements.map(it => it.start))!, + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + end: max(elements.map(it => it.end))!, + }); + } + return cumulativeReorders === 0 ? result : internalGroupRangeOverlaps(result); +} diff --git a/src/app/modules/schedule/page/grid/schedule-card.scss b/src/app/modules/schedule/page/grid/schedule-card.scss index 149599fe..3eb7c5a9 100644 --- a/src/app/modules/schedule/page/grid/schedule-card.scss +++ b/src/app/modules/schedule/page/grid/schedule-card.scss @@ -1,25 +1,12 @@ ion-card { + width: inherit; z-index: 2; - ion-grid { - padding: 0; - margin: 0; - - ion-row { - ion-col { - height: 5px; - padding: 0; - margin: 0; - } - } - } - ion-card-header { height: available; - width: 100%; ion-card-title { - width: 100%; + overflow-wrap: break-word; } } diff --git a/src/app/modules/schedule/page/grid/schedule-day.component.ts b/src/app/modules/schedule/page/grid/schedule-day.component.ts index 77b8e736..f4c0d190 100644 --- a/src/app/modules/schedule/page/grid/schedule-day.component.ts +++ b/src/app/modules/schedule/page/grid/schedule-day.component.ts @@ -18,6 +18,7 @@ import {Range, ScheduleEvent} from '../schema/schema'; import {ScheduleProvider} from '../../../calendar/schedule.provider'; import {SCISO8601Duration, SCUuid} from '@openstapps/core'; import {materialFade} from '../../../../animation/material-motion'; +import {groupRangeOverlaps} from './range-overlap'; @Component({ selector: 'schedule-day', @@ -36,14 +37,20 @@ export class ScheduleDayComponent { @Input() frequencies?: SCISO8601Duration[]; - @Input() dateSeries?: Record; + dateSeriesGroups?: ScheduleEvent[][]; + + @Input() set dateSeries(value: Record) { + if (!value) return; + + this.dateSeriesGroups = groupRangeOverlaps( + Object.values(value), + it => it.time.start, + it => it.time.start + moment.duration(it.time.duration).asHours(), + ).map(it => it.elements); + } constructor(protected readonly scheduleProvider: ScheduleProvider) {} - // ngOnInit() { - // this.dateSeries = this.fetchDateSeries(); - // } - // TODO: backend bug results in the wrong date series being returned /* async fetchDateSeries(): Promise { const dateSeries = await this.scheduleProvider.getDateSeries( diff --git a/src/app/modules/schedule/page/grid/schedule-day.html b/src/app/modules/schedule/page/grid/schedule-day.html index 7d78a1f2..cd97ad40 100644 --- a/src/app/modules/schedule/page/grid/schedule-day.html +++ b/src/app/modules/schedule/page/grid/schedule-day.html @@ -13,6 +13,18 @@ ~ this program. If not, see . -->
+
+
+ + +
+
-
- - - -
diff --git a/src/app/modules/schedule/page/grid/schedule-day.scss b/src/app/modules/schedule/page/grid/schedule-day.scss index 7a7cd89a..40640931 100644 --- a/src/app/modules/schedule/page/grid/schedule-day.scss +++ b/src/app/modules/schedule/page/grid/schedule-day.scss @@ -14,12 +14,7 @@ */ .schedule-card { - position: absolute; - top: 13px; - left: 0; - z-index: 4; - - width: 100%; + overflow: hidden; } div { @@ -27,6 +22,22 @@ div { width: 100%; } +.horizontal-group { + position: absolute; + top: 13px; + left: 0; + grid-column: 1; + grid-row: 1; + width: 100%; + + box-sizing: border-box; + max-width: inherit; + + display: flex; + flex-direction: row; + align-items: flex-start; +} + .vertical-line { position: absolute; top: 0;