gendesign/frontend/src/components/site-finder/connection-capacity.ts
bot-backend 21608eced1
All checks were successful
Deploy / changes (push) Successful in 9s
Deploy / build-backend (push) Successful in 3m35s
Deploy / build-worker (push) Successful in 5m31s
Deploy / build-frontend (push) Successful in 7m23s
Deploy / deploy (push) Successful in 2m17s
fix(site-finder): три честности-фикса — B1 ГРС «выход №N» + околонуль + Росреестр-квартал (#2257)
2026-07-03 06:27:27 +00:00

259 lines
11 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 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<LoadIndex, string> = {
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<GasOutletState, string> = {
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} точек`;
}