166 lines
5.3 KiB
TypeScript
166 lines
5.3 KiB
TypeScript
/**
|
|
* EventLayoutCoordinator - Coordinates event layout calculations
|
|
*
|
|
* Separates layout logic from rendering concerns.
|
|
* Calculates stack levels, groups events, and determines rendering strategy.
|
|
*/
|
|
|
|
import { CalendarEvent } from '../types/CalendarTypes';
|
|
import { EventStackManager, EventGroup, StackLink } from './EventStackManager';
|
|
import { PositionUtils } from '../utils/PositionUtils';
|
|
|
|
export interface GridGroupLayout {
|
|
events: CalendarEvent[];
|
|
stackLevel: number;
|
|
position: { top: number };
|
|
columns: CalendarEvent[][]; // Events grouped by column (events in same array share a column)
|
|
}
|
|
|
|
export interface StackedEventLayout {
|
|
event: CalendarEvent;
|
|
stackLink: StackLink;
|
|
position: { top: number; height: number };
|
|
}
|
|
|
|
export interface ColumnLayout {
|
|
gridGroups: GridGroupLayout[];
|
|
stackedEvents: StackedEventLayout[];
|
|
}
|
|
|
|
export class EventLayoutCoordinator {
|
|
private stackManager: EventStackManager;
|
|
|
|
constructor() {
|
|
this.stackManager = new EventStackManager();
|
|
}
|
|
|
|
/**
|
|
* Calculate complete layout for a column of events
|
|
*/
|
|
public calculateColumnLayout(columnEvents: CalendarEvent[]): ColumnLayout {
|
|
if (columnEvents.length === 0) {
|
|
return { gridGroups: [], stackedEvents: [] };
|
|
}
|
|
|
|
// Step 1: Calculate stack levels for ALL events first (to understand overlaps)
|
|
const allStackLinks = this.stackManager.createOptimizedStackLinks(columnEvents);
|
|
|
|
// Step 2: Find grid candidates (start together ±15 min)
|
|
const groups = this.stackManager.groupEventsByStartTime(columnEvents);
|
|
const gridGroups = groups.filter(group => {
|
|
if (group.events.length <= 1) return false;
|
|
group.containerType = this.stackManager.decideContainerType(group);
|
|
return group.containerType === 'GRID';
|
|
});
|
|
|
|
// Step 3: Build grid group layouts
|
|
const gridGroupLayouts: GridGroupLayout[] = [];
|
|
const renderedEventIds = new Set<string>();
|
|
|
|
gridGroups.forEach(group => {
|
|
const gridStackLevel = this.calculateGridGroupStackLevel(group, columnEvents, allStackLinks);
|
|
const earliestEvent = group.events[0];
|
|
const position = PositionUtils.calculateEventPosition(earliestEvent.start, earliestEvent.end);
|
|
const columns = this.allocateColumns(group.events);
|
|
|
|
gridGroupLayouts.push({
|
|
events: group.events,
|
|
stackLevel: gridStackLevel,
|
|
position: { top: position.top + 1 },
|
|
columns
|
|
});
|
|
|
|
group.events.forEach(e => renderedEventIds.add(e.id));
|
|
});
|
|
|
|
// Step 4: Build stacked event layouts for remaining events
|
|
const remainingEvents = columnEvents.filter(e => !renderedEventIds.has(e.id));
|
|
const stackedEventLayouts: StackedEventLayout[] = remainingEvents.map(event => {
|
|
const stackLink = allStackLinks.get(event.id)!;
|
|
const position = PositionUtils.calculateEventPosition(event.start, event.end);
|
|
|
|
return {
|
|
event,
|
|
stackLink,
|
|
position: { top: position.top + 1, height: position.height - 3 }
|
|
};
|
|
});
|
|
|
|
return {
|
|
gridGroups: gridGroupLayouts,
|
|
stackedEvents: stackedEventLayouts
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Calculate stack level for a grid group based on what it overlaps OUTSIDE the group
|
|
*/
|
|
private calculateGridGroupStackLevel(
|
|
group: EventGroup,
|
|
allEvents: CalendarEvent[],
|
|
stackLinks: Map<string, StackLink>
|
|
): number {
|
|
const groupEventIds = new Set(group.events.map(e => e.id));
|
|
|
|
// Find all events OUTSIDE this group
|
|
const outsideEvents = allEvents.filter(e => !groupEventIds.has(e.id));
|
|
|
|
// Find the highest stackLevel of any event that overlaps with ANY event in the grid group
|
|
let maxOverlappingLevel = -1;
|
|
|
|
for (const gridEvent of group.events) {
|
|
for (const outsideEvent of outsideEvents) {
|
|
if (this.stackManager.doEventsOverlap(gridEvent, outsideEvent)) {
|
|
const outsideLink = stackLinks.get(outsideEvent.id);
|
|
if (outsideLink) {
|
|
maxOverlappingLevel = Math.max(maxOverlappingLevel, outsideLink.stackLevel);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Grid group should be one level above the highest overlapping event
|
|
return maxOverlappingLevel + 1;
|
|
}
|
|
|
|
/**
|
|
* Allocate events to columns within a grid group
|
|
*
|
|
* Events that don't overlap can share the same column.
|
|
* Uses a greedy algorithm to minimize the number of columns.
|
|
*
|
|
* @param events - Events in the grid group (should already be sorted by start time)
|
|
* @returns Array of columns, where each column is an array of events
|
|
*/
|
|
private allocateColumns(events: CalendarEvent[]): CalendarEvent[][] {
|
|
if (events.length === 0) return [];
|
|
if (events.length === 1) return [[events[0]]];
|
|
|
|
const columns: CalendarEvent[][] = [];
|
|
|
|
// For each event, try to place it in an existing column where it doesn't overlap
|
|
for (const event of events) {
|
|
let placed = false;
|
|
|
|
// Try to find a column where this event doesn't overlap with any existing event
|
|
for (const column of columns) {
|
|
const hasOverlap = column.some(colEvent =>
|
|
this.stackManager.doEventsOverlap(event, colEvent)
|
|
);
|
|
|
|
if (!hasOverlap) {
|
|
column.push(event);
|
|
placed = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// If no suitable column found, create a new one
|
|
if (!placed) {
|
|
columns.push([event]);
|
|
}
|
|
}
|
|
|
|
return columns;
|
|
}
|
|
}
|