All files / lib/calendar/calendar-content-days layout-algorithm.ts

96.03% Statements 97/101
100% Branches 24/24
80% Functions 4/5
96.03% Lines 97/101

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 1021x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 55x 55x 55x 55x 55x 55x 98x 98x 67x 31x 31x 55x 55x 55x 111x 6x 6x 6x 111x 111x 111x 111x 111x 12x 12x 12x 12x 111x 111x 111x 99x 111x 111x 111x 80x 111x 55x 55x 55x 37x 55x 55x 55x 55x         55x 55x 55x 1x 43x 43x 99x 111x 111x 111x 111x 99x 43x 43x 1x 111x 111x 111x 111x 111x 111x 111x 68x 68x 68x 68x 68x 68x 68x 6x 111x 111x 1x 167x 167x 167x  
interface Dimension {
  readonly min: number;
  readonly max: number;
}
 
export interface LaneLayoutEntry<T> {
  readonly data: T;
  readonly dimension: Dimension;
  readonly lane: { startLane: number; endLane: number; maxLane: number };
}
 
export interface LaneLayout<T> {
  get maxLanes(): number;
  readonly entries: readonly T[];
}
 
export function computeLaneLayout<T extends LaneLayoutEntry<unknown>>(
  layoutEntries: T[],
): LaneLayout<T> {
  let lanes: LaneLayoutEntry<unknown>[][] = [];
  let lastEnding: number | null = null;
 
  const rv: T[] = [...layoutEntries].sort((e1, e2) => {
    const rv = e1.dimension.min - e2.dimension.min;
    if (rv !== 0) {
      return rv;
    }
    return e1.dimension.max - e2.dimension.max;
  });
 
  rv.forEach(e => {
    if (lastEnding !== null && e.dimension.min >= lastEnding) {
      computeLanes(lanes);
      lanes = [];
      lastEnding = null;
    }
 
    let placed = false;
    for (const col of lanes) {
      if (!intersects(col[col.length - 1].dimension, e.dimension)) {
        col.push(e);
        placed = true;
        break;
      }
    }
 
    if (!placed) {
      lanes.push([e]);
    }
 
    if (lastEnding === null || e.dimension.max > lastEnding) {
      lastEnding = e.dimension.max;
    }
  });
 
  if (lanes.length > 0) {
    computeLanes(lanes);
  }
 
  return {
    get maxLanes() {
      return rv
        .map(e => e.lane.maxLane)
        .reduce((a, b) => Math.max(a, b), -Infinity);
    },
    entries: rv,
  };
}
 
function computeLanes(lanes: LaneLayoutEntry<unknown>[][]) {
  lanes.forEach((lane, index) => {
    lane.forEach(layoutEntry => {
      const colSpan = computeLaneSpan(layoutEntry, index, lanes);
      layoutEntry.lane.startLane = index;
      layoutEntry.lane.endLane = index + colSpan;
      layoutEntry.lane.maxLane = lanes.length;
    });
  });
}
 
function computeLaneSpan(
  layoutEntry: LaneLayoutEntry<unknown>,
  laneIndex: number,
  columns: LaneLayoutEntry<unknown>[][],
) {
  let colSpan = 1;
  for (let i = laneIndex + 1; i < columns.length; i++) {
    const col = columns[i];
    for (const e of col) {
      if (intersects(layoutEntry.dimension, e.dimension)) {
        return colSpan;
      }
    }
    colSpan++;
  }
  return colSpan;
}
 
function intersects(a: Dimension, b: Dimension) {
  return a.max > b.min && a.min < b.max;
}