163 lines
6.1 KiB
TypeScript
163 lines
6.1 KiB
TypeScript
"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;
|
||
}
|