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 105x 6x 6x 6x 105x 105x 105x 105x 105x 12x 12x 12x 12x 105x 105x 105x 93x 105x 105x 105x 74x 105x 55x 55x 55x 31x 55x 55x 55x 55x         55x 55x 55x 1x 37x 37x 93x 105x 105x 105x 105x 93x 37x 37x 1x 105x 105x 105x 105x 105x 105x 105x 68x 68x 68x 68x 68x 68x 68x 6x 105x 105x 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;
}