mirror of
https://gitlab.com/openstapps/openstapps.git
synced 2026-01-09 19:22:51 +00:00
feat: support overlapping timetable dates
This commit is contained in:
153
src/app/modules/schedule/page/grid/range-overlap.spec.ts
Normal file
153
src/app/modules/schedule/page/grid/range-overlap.spec.ts
Normal 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},
|
||||
],
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
86
src/app/modules/schedule/page/grid/range-overlap.ts
Normal file
86
src/app/modules/schedule/page/grid/range-overlap.ts
Normal 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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user