/** * connection-capacity — общие хелперы для §3 «Сети»: ресурсные резервы (ЦП * Россетей + Водоканал). Используются и карточкой «Ресурсные резервы» * (Section2NetworksUtilities), и слоем центров питания на карте * (PowerConnectionPointsLayer) — чтобы цвета/подписи/приведение чисел были * идентичны в обоих местах. */ import type { GasOutletPoint, PowerConnectionPoint, WaterCapacityRow, } from "@/hooks/useConnectionCapacity"; // ── Coercion (Pydantic Decimal → строка) ────────────────────────────────────── // // NUMERIC-поля БД сериализуются Pydantic v2 как СТРОКИ («12.5»), хотя OpenAPI-тип // объявлен number | null. `typeof === "number"`-гварды молча роняют такое поле // (см. урок MarketLayers.toFiniteNumber). Приводим руками. Локальная копия по // образцу MarketLayers — намеренно, чтобы не тянуть map-модуль в неграфические // места. export function toFiniteNumber( v: number | string | null | undefined, ): number | null { if (v === null || v === undefined) return null; const n = typeof v === "string" ? Number(v) : v; return Number.isFinite(n) ? n : null; } // ── Индекс загрузки центра питания (load_index) ──────────────────────────────── export type LoadIndex = "open" | "limited" | "closed"; export function classifyLoadIndex( raw: string | null | undefined, ): LoadIndex | null { switch (raw) { case "open": case "limited": case "closed": return raw; default: return null; } } // Бейдж статуса ТП по индексу загрузки. Цвета — semantic-палитра карточек §3 // (совпадает с SEVERITY_COLOR в Pipeline24moBlock; для карты — документированное // token-hex исключение popup'ов). export const LOAD_INDEX_BADGE: Record< LoadIndex, { label: string; bg: string; fg: string; border: string } > = { open: { label: "открыт для ТП", bg: "#dcfce7", fg: "#15803d", border: "#86efac", }, limited: { label: "ограничен", bg: "#fef9c3", fg: "#a16207", border: "#fde68a", }, closed: { label: "закрыт", bg: "#fee2e2", fg: "#b91c1c", border: "#fca5a5", }, }; // Цвет маркера ЦП на карте по индексу загрузки (null → нейтральный серый). export const LOAD_INDEX_MARKER_COLOR: Record = { open: "#15803d", limited: "#a16207", closed: "#b91c1c", }; export const LOAD_INDEX_NULL_COLOR = "#6b7280"; export function loadIndexColor(raw: string | null | undefined): string { const idx = classifyLoadIndex(raw); return idx ? LOAD_INDEX_MARKER_COLOR[idx] : LOAD_INDEX_NULL_COLOR; } export function humanizeLoadIndex( raw: string | null | undefined, ): string | null { const idx = classifyLoadIndex(raw); return idx ? LOAD_INDEX_BADGE[idx].label : null; } // ── Форматирование чисел ─────────────────────────────────────────────────────── // МВА с одним знаком после запятой (резерв/мощность ЦП). Приводит строку→число. // unit — необязательный override единицы измерения (бэк отдаёт reserve_unit; // напр. «кВт» вместо «МВА» для отдельных ЦП). null/undefined → дефолт «МВА». export function formatMva( v: number | string | null | undefined, unit?: string | null, ): string | null { const n = toFiniteNumber(v); if (n === null) return null; const u = unit?.trim() || "МВА"; return `${n.toLocaleString("ru-RU", { minimumFractionDigits: 1, maximumFractionDigits: 1, })} ${u}`; } // тыс. м³/сут (резерв воды/канализации). Может быть ОТРИЦАТЕЛЬНЫМ (дефицит) — // знак сохраняем (Unicode-минус даёт toLocaleString). export function formatThousandM3Day( v: number | string | null | undefined, ): string | null { const n = toFiniteNumber(v); if (n === null) return null; return `${n.toLocaleString("ru-RU", { minimumFractionDigits: 1, maximumFractionDigits: 1, })} тыс. м³/сут`; } export function formatMeters( v: number | string | null | undefined, ): string | null { const n = toFiniteNumber(v); if (n === null) return null; return `${Math.round(n).toLocaleString("ru-RU")} м`; } // тыс. м³/ч (свободная/проектная мощность ГРС). Приводит строку→число (Decimal→str). // // Честность околонуля: при 1 знаке «0,044» округлился бы в «0,0» и ГРС с // НЕнулевой (но крохотной) свободной мощностью выглядела бы закрытой. Для // 0 < |v| < 0,05 показываем 2 знака («0,04» / «−0,04»); настоящий 0 — «0,0». export function formatThousandM3Hour( v: number | string | null | undefined, ): string | null { const n = toFiniteNumber(v); if (n === null) return null; const digits = n !== 0 && Math.abs(n) < 0.05 ? 2 : 1; return `${n.toLocaleString("ru-RU", { minimumFractionDigits: digits, maximumFractionDigits: digits, })} тыс. м³/ч`; } // млн. м³/мес (свободная мощность точки выхода газосети, форма 6 Екатеринбурггаза — // ВНИМАНИЕ: единица отличается от ГРС, где тыс. м³/ч). Может быть ОТРИЦАТЕЛЬНОЙ // (дефицит со знаком) — знак сохраняем. Три знака после запятой: источник даёт // «0.000». Приводит строку→число (Decimal→str). export function formatMlnM3Month( v: number | string | null | undefined, ): string | null { const n = toFiniteNumber(v); if (n === null) return null; return `${n.toLocaleString("ru-RU", { minimumFractionDigits: 3, maximumFractionDigits: 3, })} млн м³/мес`; } // Гкал/ч (резерв тепловой мощности системы теплоснабжения). Может быть // ОТРИЦАТЕЛЬНЫМ (система исчерпана) — знак сохраняем. Приводит строку→число. export function formatGcalHour( v: number | string | null | undefined, ): string | null { const n = toFiniteNumber(v); if (n === null) return null; return `${n.toLocaleString("ru-RU", { minimumFractionDigits: 1, maximumFractionDigits: 1, })} Гкал/ч`; } // ── Вода: выбор «городской» системы + группировка ────────────────────────────── // // Городская система ЦСВ/ЦСК выделяется в карточке отдельной строкой: сначала по // имени (содержит «ЗФС» — Западная фильтровальная станция ЕКБ), иначе — самая // крупная по |резерв| (крупнейшая система города). Остальные сворачиваются. export function pickCitySystem( rows: WaterCapacityRow[], ): WaterCapacityRow | null { if (rows.length === 0) return null; const byName = rows.find((r) => (r.system_name ?? "").includes("ЗФС")); if (byName) return byName; // Самая крупная по модулю резерва (city-scale система). let best: WaterCapacityRow | null = null; let bestAbs = -Infinity; for (const r of rows) { const n = toFiniteNumber(r.reserve_thousand_m3_day); const abs = n === null ? -Infinity : Math.abs(n); if (abs > bestAbs) { bestAbs = abs; best = r; } } return best ?? rows[0]; } // Множественное число «система» для «+N посёлковых систем». export function pluralizeSystems(n: number): string { const mod10 = n % 10; const mod100 = n % 100; if (mod10 === 1 && mod100 !== 11) return `${n} система`; if (mod10 >= 2 && mod10 <= 4 && (mod100 < 10 || mod100 >= 20)) return `${n} системы`; return `${n} систем`; } // Ближайший ЦП с резервом → строка-summary для карточки. Порядок нужен, чтобы // НЕ раскрывать null-поля муторным «нет данных». export function powerPointReserveIsNegative(p: PowerConnectionPoint): boolean { const n = toFiniteNumber(p.reserve_mva); return n !== null && n <= 0; } // ── Точки выхода газосети (форма 6 Екатеринбурггаза) ─────────────────────────── // // Свободная мощность точки выхода классифицируется по ЗНАКУ: // profit — free_capacity_mln_m3 > 0 (профицит), зелёный; // deficit — < 0 (дефицит со знаком), красный; // calc — needs_calc=true / null (нужен гидравлический расчёт), жёлтый. // Маркеры карты используют hex токенов --success/--danger/--warn (документированное // исключение raw-Leaflet попапов, ui-tokens.md), карточка — CSS-переменные напрямую. export type GasOutletState = "profit" | "deficit" | "calc"; // Hex-зеркала токенов (--success/--danger/--warn) для CircleMarker fillColor. export const GAS_OUTLET_MARKER_COLOR: Record = { profit: "#0A7A3A", // --success deficit: "#B3261E", // --danger calc: "#9A6700", // --warn }; export function classifyGasOutlet(p: GasOutletPoint): GasOutletState { if (p.needs_calc) return "calc"; const n = toFiniteNumber(p.free_capacity_mln_m3); if (n === null) return "calc"; return n < 0 ? "deficit" : "profit"; } export function gasOutletColor(p: GasOutletPoint): string { return GAS_OUTLET_MARKER_COLOR[classifyGasOutlet(p)]; } // Множественное число «точка» для «Точки выхода газа (N)» / счётчиков. export function pluralizeOutletPoints(n: number): string { const mod10 = n % 10; const mod100 = n % 100; if (mod10 === 1 && mod100 !== 11) return `${n} точка`; if (mod10 >= 2 && mod10 <= 4 && (mod100 < 10 || mod100 >= 20)) return `${n} точки`; return `${n} точек`; }