mirror of
https://gitlab.com/openstapps/openstapps.git
synced 2026-01-11 12:12:55 +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 {
|
ion-card {
|
||||||
|
width: inherit;
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
|
|
||||||
ion-grid {
|
|
||||||
padding: 0;
|
|
||||||
margin: 0;
|
|
||||||
|
|
||||||
ion-row {
|
|
||||||
ion-col {
|
|
||||||
height: 5px;
|
|
||||||
padding: 0;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ion-card-header {
|
ion-card-header {
|
||||||
height: available;
|
height: available;
|
||||||
width: 100%;
|
|
||||||
|
|
||||||
ion-card-title {
|
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 {ScheduleProvider} from '../../../calendar/schedule.provider';
|
||||||
import {SCISO8601Duration, SCUuid} from '@openstapps/core';
|
import {SCISO8601Duration, SCUuid} from '@openstapps/core';
|
||||||
import {materialFade} from '../../../../animation/material-motion';
|
import {materialFade} from '../../../../animation/material-motion';
|
||||||
|
import {groupRangeOverlaps} from './range-overlap';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'schedule-day',
|
selector: 'schedule-day',
|
||||||
@@ -36,14 +37,20 @@ export class ScheduleDayComponent {
|
|||||||
|
|
||||||
@Input() frequencies?: SCISO8601Duration[];
|
@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) {}
|
constructor(protected readonly scheduleProvider: ScheduleProvider) {}
|
||||||
|
|
||||||
// ngOnInit() {
|
|
||||||
// this.dateSeries = this.fetchDateSeries();
|
|
||||||
// }
|
|
||||||
|
|
||||||
// TODO: backend bug results in the wrong date series being returned
|
// TODO: backend bug results in the wrong date series being returned
|
||||||
/* async fetchDateSeries(): Promise<ScheduleEvent[]> {
|
/* async fetchDateSeries(): Promise<ScheduleEvent[]> {
|
||||||
const dateSeries = await this.scheduleProvider.getDateSeries(
|
const dateSeries = await this.scheduleProvider.getDateSeries(
|
||||||
|
|||||||
@@ -13,6 +13,18 @@
|
|||||||
~ this program. If not, see <https://www.gnu.org/licenses/>.
|
~ this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
-->
|
-->
|
||||||
<div>
|
<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>
|
<div class="vertical-line"></div>
|
||||||
<stapps-schedule-cursor
|
<stapps-schedule-cursor
|
||||||
class="cursor"
|
class="cursor"
|
||||||
@@ -21,15 +33,4 @@
|
|||||||
[scale]="scale"
|
[scale]="scale"
|
||||||
>
|
>
|
||||||
</stapps-schedule-cursor>
|
</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>
|
</div>
|
||||||
|
|||||||
@@ -14,12 +14,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
.schedule-card {
|
.schedule-card {
|
||||||
position: absolute;
|
overflow: hidden;
|
||||||
top: 13px;
|
|
||||||
left: 0;
|
|
||||||
z-index: 4;
|
|
||||||
|
|
||||||
width: 100%;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
div {
|
div {
|
||||||
@@ -27,6 +22,22 @@ div {
|
|||||||
width: 100%;
|
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 {
|
.vertical-line {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
|
|||||||
Reference in New Issue
Block a user