- data/sql/98_*: rename from 89 to avoid collision with existing 89_drop_dead_brin - _get_red_lines: ST_Length(ST_Intersection(planar)::geography) instead of ST_Area(ST_Intersection(::geography, ::geography)) — fixes PostGIS 3.4 tolerance error and returns correct non-zero length for LINESTRING intersections - Rename intersection_area_sqm → intersection_length_m across schema, TS types, frontend component, and tests; add test_get_red_lines_db_exception_returns_empty Addresses review-bot feedback on PR #220.
136 lines
4.1 KiB
TypeScript
136 lines
4.1 KiB
TypeScript
"use client";
|
||
|
||
import type { RedLine } from "@/types/nspd";
|
||
|
||
interface Props {
|
||
redLines: RedLine[] | null | undefined;
|
||
}
|
||
|
||
function formatLength(m: number | null): string | null {
|
||
if (m === null) return null;
|
||
if (m >= 1000) return `${(m / 1000).toFixed(2)} км`;
|
||
return `${Math.round(m)} м`;
|
||
}
|
||
|
||
function formatDistance(m: number | null): string | null {
|
||
if (m === null) return null;
|
||
if (m < 1000) return `${Math.round(m)} м`;
|
||
return `${(m / 1000).toFixed(1)} км`;
|
||
}
|
||
|
||
export function NspdRedLinesBlock({ redLines }: Props) {
|
||
const lines = redLines ?? [];
|
||
|
||
if (lines.length === 0) {
|
||
return null;
|
||
}
|
||
|
||
const intersecting = lines.filter((l) => l.intersection_length_m !== null);
|
||
const nearbyOnly = lines.filter((l) => l.intersection_length_m === null);
|
||
const hasIntersect = intersecting.length > 0;
|
||
|
||
return (
|
||
<div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
|
||
{/* Alert banner when red lines intersect the parcel */}
|
||
{hasIntersect && (
|
||
<div
|
||
style={{
|
||
background: "#fef2f2",
|
||
border: "1px solid #fecaca",
|
||
borderRadius: 8,
|
||
padding: "10px 14px",
|
||
display: "flex",
|
||
alignItems: "flex-start",
|
||
gap: 10,
|
||
}}
|
||
>
|
||
<span
|
||
style={{
|
||
background: "#fee2e2",
|
||
color: "#991b1b",
|
||
borderRadius: 5,
|
||
padding: "2px 10px",
|
||
fontSize: 12,
|
||
fontWeight: 700,
|
||
whiteSpace: "nowrap",
|
||
flexShrink: 0,
|
||
}}
|
||
>
|
||
ПЕРЕСЕЧЕНИЕ
|
||
</span>
|
||
<div>
|
||
<div style={{ fontSize: 13, fontWeight: 600, color: "#991b1b" }}>
|
||
Красные линии пересекают участок — отступы могут быть критичны
|
||
</div>
|
||
<div style={{ fontSize: 12, color: "#7f1d1d", marginTop: 4 }}>
|
||
{intersecting.map((l, idx) => {
|
||
const lenStr = formatLength(l.intersection_length_m);
|
||
return (
|
||
<div key={idx}>
|
||
Линия {idx + 1}
|
||
{lenStr ? `: длина пересечения ${lenStr}` : ""}
|
||
</div>
|
||
);
|
||
})}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* Nearby-only red lines */}
|
||
{nearbyOnly.length > 0 && (
|
||
<div
|
||
style={{
|
||
background: "#fff7ed",
|
||
border: "1px solid #fed7aa",
|
||
borderRadius: 8,
|
||
padding: "10px 14px",
|
||
display: "flex",
|
||
alignItems: "flex-start",
|
||
gap: 10,
|
||
}}
|
||
>
|
||
<span
|
||
style={{
|
||
background: "#ffedd5",
|
||
color: "#9a3412",
|
||
borderRadius: 5,
|
||
padding: "2px 10px",
|
||
fontSize: 12,
|
||
fontWeight: 600,
|
||
whiteSpace: "nowrap",
|
||
flexShrink: 0,
|
||
}}
|
||
>
|
||
РЯДОМ
|
||
</span>
|
||
<div>
|
||
<div style={{ fontSize: 13, fontWeight: 500, color: "#9a3412" }}>
|
||
{nearbyOnly.length} кр. лини{nearbyOnly.length === 1 ? "я" : "и"}{" "}
|
||
вблизи участка
|
||
</div>
|
||
<div style={{ fontSize: 12, color: "#7c2d12", marginTop: 4 }}>
|
||
{nearbyOnly.map((l, idx) => {
|
||
const distStr = formatDistance(l.distance_m);
|
||
return (
|
||
<div key={idx}>
|
||
Линия {idx + 1}
|
||
{distStr ? `: ${distStr} от границы` : ""}
|
||
</div>
|
||
);
|
||
})}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* Summary line */}
|
||
<div style={{ fontSize: 12, color: "#6b7280" }}>
|
||
Итого: {lines.length} красн. лини{lines.length === 1 ? "я" : "и"}
|
||
{hasIntersect
|
||
? `, из них ${intersecting.length} пересека${intersecting.length === 1 ? "ет" : "ют"} участок`
|
||
: ""}
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|