feat: support overlapping timetable dates

This commit is contained in:
Thea Schöbl
2022-03-25 10:52:40 +00:00
parent 2e2a5897b8
commit 93c37b26cc
6 changed files with 282 additions and 37 deletions

View File

@@ -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 <https://www.gnu.org/licenses/>.
*/
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<SimpleRange>([], from, till)).toEqual([]);
});
it('should handle single range', () => {
expect(
groupRangeOverlaps<SimpleRange>([{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<SimpleRange>(
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<SimpleRange>(
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<SimpleRange>(
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<SimpleRange>(
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<SimpleRange>(
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},
],
},
]);
});
});

View File

@@ -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 <https://www.gnu.org/licenses/>.
*/
import {flatMap, max, min, partition} from 'lodash-es';
export interface RangeInfo<T> {
elements: T[];
start: number;
end: number;
}
/**
* Takes a list of ranges and groups by overlaps.
*/
export function groupRangeOverlaps<T>(
ranges: T[],
start: (range: T) => number,
end: (range: T) => number,
): RangeInfo<T>[] {
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<T>(input: RangeInfo<T>[]): RangeInfo<T>[] {
const result: RangeInfo<T>[] = [];
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);
}

View File

@@ -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;
}
}

View File

@@ -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<string, ScheduleEvent>;
dateSeriesGroups?: ScheduleEvent[][];
@Input() set dateSeries(value: Record<string, ScheduleEvent>) {
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<ScheduleEvent[]> {
const dateSeries = await this.scheduleProvider.getDateSeries(

View File

@@ -13,6 +13,18 @@
~ this program. If not, see <https://www.gnu.org/licenses/>.
-->
<div>
<div *ngIf="dateSeriesGroups as groups">
<div *ngFor="let group of groups" class="horizontal-group">
<stapps-schedule-card
class="schedule-card"
*ngFor="let scheduleEvent of group"
[scheduleEvent]="scheduleEvent"
[fromHour]="hoursRange.from"
[scale]="scale"
>
</stapps-schedule-card>
</div>
</div>
<div class="vertical-line"></div>
<stapps-schedule-cursor
class="cursor"
@@ -21,15 +33,4 @@
[scale]="scale"
>
</stapps-schedule-cursor>
<div *ngIf="dateSeries as dateSeries">
<!-- TODO: entry/exit animation -->
<stapps-schedule-card
class="schedule-card"
*ngFor="let entry of dateSeries | entries"
[scheduleEvent]="entry"
[fromHour]="hoursRange.from"
[scale]="scale"
>
</stapps-schedule-card>
</div>
</div>

View File

@@ -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;