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 99x 99x 99x 99x 99x 99x 174x 174x 120x 54x 54x 99x 99x 99x 195x 11x 11x 11x 195x 195x 195x 195x 195x 22x 22x 22x 22x 195x 195x 195x 173x 195x 195x 195x 141x 195x 99x 99x 99x 65x 99x 99x 99x 99x         99x 99x 99x 1x 76x 76x 173x 195x 195x 195x 195x 173x 76x 76x 1x 195x 195x 195x 195x 195x 195x 195x 119x 119x 119x 119x 119x 119x 119x 11x 195x 195x 1x 292x 292x 292x  
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;
}