b092e50dd8
Замена авто-чипа «полоса ×N» на ручной жест. Полоса переопределена:
один КА + один крен (ε) + подряд по времени; виток в границу не входит.
- slotStrips.ts: groupStrips → { idByKey, membersOf }; группировка по КА →
кластеры по крену (ROLL_EPSILON=0.5) → деление по каденции (CADENCE_GAP_FACTOR=1.5)
- slotSelection.ts: selectStripRange вместо toggleStripSelection
- TguSlotsTab.tsx: клик по строке = якорь + выбор; Shift+клик тянет полосу
между якорем и строкой; выход за полосу запрещён + подсказка; рельс полосы якоря
- tgu-planning.css: убраны стили чипа/hover; user-select:none на строках
- тесты обновлены (125 зелёных), tsc/build чисто
130 lines
5.7 KiB
TypeScript
130 lines
5.7 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import { ROLL_EPSILON, groupStrips, type StripGrouping } from "./slotStrips";
|
|
|
|
/** Минимальный слот для группировки. tn задаём как локальную ISO-строку (как отдаёт бэкенд). */
|
|
type S = {
|
|
satelliteId: number;
|
|
slotNumber: number;
|
|
cycle: number;
|
|
tn: string;
|
|
tk: string;
|
|
roll: number;
|
|
};
|
|
|
|
const keyOf = (s: S) => `${s.satelliteId}:${s.slotNumber}:${s.cycle}`;
|
|
|
|
/** Слот на минуту mm (от базовой даты) с длительностью 30 c. cycle ≈ «виток» (в границу не входит). */
|
|
function slot(satelliteId: number, slotNumber: number, roll: number, mm: number, cycle = 1): S {
|
|
const tn = `2026-06-17T10:${String(mm).padStart(2, "0")}:00`;
|
|
const tk = `2026-06-17T10:${String(mm).padStart(2, "0")}:30`;
|
|
return { satelliteId, slotNumber, cycle, tn, tk, roll };
|
|
}
|
|
|
|
/** Разбиение на полосы как множество множеств ключей — для сравнения, не зависящего от имён PolosaId. */
|
|
function partition(g: StripGrouping): string[][] {
|
|
const byStrip = new Map<string, string[]>();
|
|
for (const [key, strip] of g.idByKey) {
|
|
const arr = byStrip.get(strip) ?? [];
|
|
arr.push(key);
|
|
byStrip.set(strip, arr);
|
|
}
|
|
return [...byStrip.values()].map((keys) => keys.slice().sort()).sort((a, b) => (a[0]! < b[0]! ? -1 : 1));
|
|
}
|
|
|
|
describe("groupStrips", () => {
|
|
it("пустой вход → пустая разметка", () => {
|
|
const g = groupStrips([], keyOf);
|
|
expect(g.idByKey.size).toBe(0);
|
|
expect(g.membersOf.size).toBe(0);
|
|
});
|
|
|
|
it("один слот → своя полоса", () => {
|
|
const g = groupStrips([slot(1, 1, 5, 0)], keyOf);
|
|
expect(g.idByKey.get("1:1:1")).toBe("polosa:1:1:1");
|
|
expect(g.membersOf.get("polosa:1:1:1")).toEqual(["1:1:1"]);
|
|
});
|
|
|
|
it("веер кренов в один момент → разные полосы; равный крен вдоль времени → одна полоса", () => {
|
|
// КА 1: два нацеливания (крен 36.5 и 39.5, шаг 3° > ε), каждое снято в моменты mm=0 и mm=1.
|
|
const slots = [
|
|
slot(1, 1, 36.5, 0),
|
|
slot(1, 2, 39.5, 0),
|
|
slot(1, 3, 36.5, 1),
|
|
slot(1, 4, 39.5, 1),
|
|
];
|
|
expect(partition(groupStrips(slots, keyOf))).toEqual([
|
|
["1:1:1", "1:3:1"], // крен 36.5 вдоль времени
|
|
["1:2:1", "1:4:1"], // крен 39.5 вдоль времени
|
|
]);
|
|
});
|
|
|
|
it("смена витка (cycle) внутри одного крена и каденции НЕ рвёт полосу", () => {
|
|
const slots = [
|
|
slot(1, 1, 5, 0, 1),
|
|
slot(1, 2, 5, 1, 1),
|
|
slot(1, 3, 5, 2, 2), // виток сменился у экватора — но это та же полоса
|
|
slot(1, 4, 5, 3, 2),
|
|
];
|
|
expect(partition(groupStrips(slots, keyOf))).toEqual([
|
|
["1:1:1", "1:2:1", "1:3:2", "1:4:2"],
|
|
]);
|
|
});
|
|
|
|
it("большой разрыв по времени рвёт полосу на две (другой проход)", () => {
|
|
const slots = [
|
|
slot(1, 1, 5, 0),
|
|
slot(1, 2, 5, 1),
|
|
slot(1, 3, 5, 2),
|
|
slot(1, 4, 5, 3),
|
|
slot(1, 5, 5, 30), // разрыв ≫ каденции (1 мин) → новый проход
|
|
slot(1, 6, 5, 31),
|
|
slot(1, 7, 5, 32),
|
|
];
|
|
expect(partition(groupStrips(slots, keyOf))).toEqual([
|
|
["1:1:1", "1:2:1", "1:3:1", "1:4:1"],
|
|
["1:5:1", "1:6:1", "1:7:1"],
|
|
]);
|
|
});
|
|
|
|
it("разные КА — разные полосы (даже при одном крене и времени)", () => {
|
|
const slots = [slot(1, 1, 5, 0), slot(2, 1, 5, 0), slot(1, 2, 5, 1), slot(2, 2, 5, 1)];
|
|
expect(partition(groupStrips(slots, keyOf))).toEqual([
|
|
["1:1:1", "1:2:1"],
|
|
["2:1:1", "2:2:1"],
|
|
]);
|
|
});
|
|
|
|
it("граница ε по крену: разрыв == 0.5 склеивает, 0.6 рвёт", () => {
|
|
const same = groupStrips([slot(1, 1, 5.0, 0), slot(1, 2, 5 + ROLL_EPSILON, 1)], keyOf);
|
|
expect(partition(same)).toEqual([["1:1:1", "1:2:1"]]);
|
|
|
|
const broken = groupStrips([slot(1, 1, 5.0, 0), slot(1, 2, 5 + ROLL_EPSILON + 0.1, 1)], keyOf);
|
|
expect(partition(broken)).toEqual([["1:1:1"], ["1:2:1"]]);
|
|
});
|
|
|
|
it("устойчивость к порядку входа: разбиение одинаково при перемешивании", () => {
|
|
const slots = [slot(1, 1, 5, 0), slot(1, 2, 5, 1), slot(1, 3, 20, 0), slot(2, 1, 5, 0)];
|
|
const a = partition(groupStrips(slots, keyOf));
|
|
const b = partition(groupStrips([...slots].reverse(), keyOf));
|
|
const c = partition(groupStrips([slots[2]!, slots[0]!, slots[3]!, slots[1]!], keyOf));
|
|
expect(b).toEqual(a);
|
|
expect(c).toEqual(a);
|
|
});
|
|
|
|
it("PolosaId = polosa:${ключ первого по времени члена}; membersOf — в порядке времени", () => {
|
|
const g = groupStrips([slot(1, 2, 5, 1), slot(1, 1, 5, 0)], keyOf);
|
|
expect(g.idByKey.get("1:1:1")).toBe("polosa:1:1:1");
|
|
expect(g.idByKey.get("1:2:1")).toBe("polosa:1:1:1");
|
|
expect(g.membersOf.get("polosa:1:1:1")).toEqual(["1:1:1", "1:2:1"]);
|
|
});
|
|
|
|
it("NaN tn → синглтон без падения, не клеит и не рвёт валидных соседей", () => {
|
|
const broken: S = { satelliteId: 1, slotNumber: 9, cycle: 1, tn: "битый", tk: "битый", roll: 5 };
|
|
const slots = [slot(1, 1, 5, 0), broken, slot(1, 2, 5, 1)];
|
|
const g = groupStrips(slots, keyOf);
|
|
expect(g.idByKey.get("1:9:1")).toBe("polosa:1:9:1");
|
|
expect(g.idByKey.get("1:1:1")).toBe("polosa:1:1:1");
|
|
expect(g.idByKey.get("1:2:1")).toBe("polosa:1:1:1");
|
|
});
|
|
});
|