gendesign/frontend/src/components/site-finder/GasOutletPointsLayer.tsx
bot-backend 15757685c9
All checks were successful
Deploy / changes (push) Successful in 7s
Deploy / build-backend (push) Successful in 2m17s
Deploy / build-worker (push) Successful in 3m19s
Deploy / build-frontend (push) Successful in 3m54s
Deploy / deploy (push) Successful in 1m34s
feat(site-finder): Celery + §3 точки выхода газосети (#2119 B2, PR-4/4 — финал формы-6) (#2251)
2026-07-03 04:26:41 +00:00

163 lines
6.1 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.

"use client";
/**
* GasOutletPointsLayer — точки выхода газосети (форма 6 АО «Екатеринбурггаз»,
* ФАС №960) в радиусе от участка для §3 «Сети» (#2119 B2 PR-4). Отдельный слой
* поверх карты: CircleMarker per gas_outlet_point, цвет по ЗНАКУ свободной
* мощности (профицит → --success, дефицит → --danger, needs_calc → --warn).
*
* Меньший радиус (7) чем у центров питания Россетей (9) — точки выхода это
* низовой слой газосети, визуально второстепенный относительно ЦП. Свободная
* мощность здесь в млн м³/МЕС (НЕ тыс. м³/ч как у ГРС) — единица зашита в
* formatMlnM3Month, чтобы не перепутать.
*
* Только ~19% строк источника геокодированы → список часто пуст (это норма,
* слой не рисуется). Цвета/подписи/приведение чисел — из общего модуля
* connection-capacity.ts. Inline token-hex в popup — документированное
* исключение raw-Leaflet попапов (ui-tokens.md).
*/
import { CircleMarker, LayerGroup, Popup } from "react-leaflet";
import type { GasOutletPoint } from "@/hooks/useConnectionCapacity";
import {
classifyGasOutlet,
formatMeters,
formatMlnM3Month,
gasOutletColor,
toFiniteNumber,
} from "@/components/site-finder/connection-capacity";
// ── Point coordinate guard ─────────────────────────────────────────────────────
// lat/lon могут прийти строкой (Decimal→str) — приводим через toFiniteNumber, а
// не typeof-гвардом (иначе валидная точка молча не рисуется).
function pointLatLon(p: GasOutletPoint): [number, number] | null {
const lat = toFiniteNumber(p.lat);
const lon = toFiniteNumber(p.lon);
if (lat === null || lon === null) return null;
return [lat, lon];
}
// ── Popup ──────────────────────────────────────────────────────────────────────
function GasOutletPopup({ point }: { point: GasOutletPoint }) {
const state = classifyGasOutlet(point);
const color = gasOutletColor(point);
const consumer = point.consumer_type?.trim() || null;
const free = formatMlnM3Month(point.free_capacity_mln_m3);
const isDeficit = state === "deficit";
const distance = formatMeters(point.distance_m);
return (
<Popup>
<div style={{ fontSize: 12, lineHeight: 1.55, minWidth: 200 }}>
<div
style={{
fontWeight: 700,
marginBottom: 4,
display: "flex",
alignItems: "center",
gap: 5,
}}
>
<span
aria-hidden
style={{
display: "inline-block",
width: 10,
height: 10,
borderRadius: "50%",
background: color,
flexShrink: 0,
}}
/>
Точка выхода газосети
</div>
<div style={{ marginBottom: 4 }}>
<strong>{point.outlet_name}</strong>
</div>
{consumer && (
<div style={{ color: "#374151", marginBottom: 2 }}>
Тип потребителя: {consumer}
</div>
)}
{state === "calc" ? (
<div style={{ color: "#9A6700", marginBottom: 4 }}>
Свободная мощность: требует гидравлического расчёта
</div>
) : (
free && (
<div
style={{
color: isDeficit ? "#b91c1c" : "#374151",
marginBottom: 4,
}}
>
{isDeficit ? "Дефицит" : "Свободная мощность"}:{" "}
<strong style={{ color: isDeficit ? "#b91c1c" : "#111827" }}>
{free}
</strong>
</div>
)
)}
{distance && (
<div style={{ marginTop: 4, color: "#374151" }}>
До участка: {distance}
</div>
)}
<div
style={{
marginTop: 4,
fontSize: 11,
color: "#9ca3af",
borderTop: "1px solid #f3f4f6",
paddingTop: 4,
}}
>
Форма 6 АО «Екатеринбурггаз» · млн м³/мес
</div>
</div>
</Popup>
);
}
// ── Layer ──────────────────────────────────────────────────────────────────────
interface Props {
points: GasOutletPoint[];
}
export function GasOutletPointsLayer({ points }: Props) {
return (
<LayerGroup>
{points.map((point, idx) => {
const latLon = pointLatLon(point);
if (!latLon) return null;
const color = gasOutletColor(point);
return (
<CircleMarker
key={`gas-outlet-${point.outlet_name}-${idx}`}
center={latLon}
radius={7}
pathOptions={{
color: "#ffffff",
weight: 2,
fillColor: color,
fillOpacity: 0.85,
}}
>
<GasOutletPopup point={point} />
</CircleMarker>
);
})}
</LayerGroup>
);
}
// Число рисуемых точек (с валидными координатами) — для легенды/чекбокса.
export function countMappableGasOutlets(
points: GasOutletPoint[] | undefined,
): number {
if (!points) return 0;
return points.filter((p) => pointLatLon(p) !== null).length;
}