feat(ptica): wire real ПЗЗ regulation (КСИТ/height/zone/density) into cockpit
Read nspd_zoning.{max_far,max_height_m,max_floors,max_building_pct,
regulation_zone_index,regulation_source} (backend PR #1847) into the urban
scan-card, urban/buildability/potential drawers and the 3D massing КСИТ-target.
Defensive: every read is optional-chained + != null guarded → identical "—"
placeholders when fields absent, so the cockpit is unchanged until #1847 deploys,
then auto-lights-up. КСИТ density = area × max_far; пятно = area × max_building_pct
(percent 0..100, verified vs backend parser); sellable labeled estimate. 3D maxFar
= nspd_zoning.max_far ?? 3.5 (finite/>0 guarded). NspdZoning type +7 optional fields.
TS strict, no any.
This commit is contained in:
parent
06fa5b4910
commit
167fcbace2
4 changed files with 267 additions and 43 deletions
|
|
@ -51,7 +51,7 @@ import {
|
||||||
DrawerActions,
|
DrawerActions,
|
||||||
} from "@/components/site-finder/ptica/drawers/DrawerPrimitives";
|
} from "@/components/site-finder/ptica/drawers/DrawerPrimitives";
|
||||||
|
|
||||||
/** КСИТ-цель (FAR) — regulation default, see caption in the 3D sandbox. */
|
/** КСИТ-цель (FAR) — regulation default when НСПД max_far is absent. */
|
||||||
const MASSING_MAX_FAR = 3.5;
|
const MASSING_MAX_FAR = 3.5;
|
||||||
/** Fallback parcel area (m²) when geometry_suitability.area_ha is absent. */
|
/** Fallback parcel area (m²) when geometry_suitability.area_ha is absent. */
|
||||||
const MASSING_FALLBACK_AREA = 6800;
|
const MASSING_FALLBACK_AREA = 6800;
|
||||||
|
|
@ -65,6 +65,21 @@ function massingAreaM2(analysis: ParcelAnalysis): number {
|
||||||
return MASSING_FALLBACK_AREA;
|
return MASSING_FALLBACK_AREA;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Real КСИТ from НСПД-зонирование (PR #1847), else the regulation default. */
|
||||||
|
function massingMaxFar(analysis: ParcelAnalysis): number {
|
||||||
|
const maxFar = analysis.nspd_zoning?.max_far;
|
||||||
|
if (typeof maxFar === "number" && Number.isFinite(maxFar) && maxFar > 0) {
|
||||||
|
return maxFar;
|
||||||
|
}
|
||||||
|
return MASSING_MAX_FAR;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** True when the КСИТ used by the massing sandbox is the real НСПД value. */
|
||||||
|
function massingFarIsReal(analysis: ParcelAnalysis): boolean {
|
||||||
|
const maxFar = analysis.nspd_zoning?.max_far;
|
||||||
|
return typeof maxFar === "number" && Number.isFinite(maxFar) && maxFar > 0;
|
||||||
|
}
|
||||||
|
|
||||||
const SRC_UPDATED = "источник: /analyze";
|
const SRC_UPDATED = "источник: /analyze";
|
||||||
|
|
||||||
// ── shared helpers ────────────────────────────────────────────────────────────
|
// ── shared helpers ────────────────────────────────────────────────────────────
|
||||||
|
|
@ -807,22 +822,31 @@ export function OksContent() {
|
||||||
// ══════════════════════════════════════════════════════════════════════════════
|
// ══════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
export function Future3dContent({ analysis }: { analysis: ParcelAnalysis }) {
|
export function Future3dContent({ analysis }: { analysis: ParcelAnalysis }) {
|
||||||
|
const maxFar = massingMaxFar(analysis);
|
||||||
|
const farReal = massingFarIsReal(analysis);
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<DrawerSection>
|
<DrawerSection>
|
||||||
<SummaryBox>
|
<SummaryBox>
|
||||||
Генеративная масса по регламенту: КСИТ-цель {MASSING_MAX_FAR} · пятно
|
Генеративная масса по регламенту: КСИТ-цель {maxFar.toFixed(1)} ·
|
||||||
и высота подбираются под целевую ёмкость, GFA и КСИТ-факт считаются от
|
пятно и высота подбираются под целевую ёмкость, GFA и КСИТ-факт
|
||||||
вводных. Орбита, зум и солнце — интерактивные.
|
считаются от вводных. Орбита, зум и солнце — интерактивные.
|
||||||
</SummaryBox>
|
</SummaryBox>
|
||||||
</DrawerSection>
|
</DrawerSection>
|
||||||
<DrawerSection>
|
<DrawerSection>
|
||||||
<MassingViewer
|
<MassingViewer
|
||||||
areaM2={massingAreaM2(analysis)}
|
areaM2={massingAreaM2(analysis)}
|
||||||
maxFar={MASSING_MAX_FAR}
|
maxFar={maxFar}
|
||||||
|
farIsReal={farReal}
|
||||||
/>
|
/>
|
||||||
</DrawerSection>
|
</DrawerSection>
|
||||||
<SourceLine source="Песочница массинга · КСИТ-цель регламент-дефолт (НСПД)" />
|
<SourceLine
|
||||||
|
source={
|
||||||
|
farReal
|
||||||
|
? "Песочница массинга · КСИТ-цель регламент НСПД (max_far)"
|
||||||
|
: "Песочница массинга · КСИТ-цель регламент-дефолт (НСПД)"
|
||||||
|
}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -840,7 +864,8 @@ export function InsolationContent({ analysis }: { analysis: ParcelAnalysis }) {
|
||||||
<DrawerSection>
|
<DrawerSection>
|
||||||
<MassingViewer
|
<MassingViewer
|
||||||
areaM2={massingAreaM2(analysis)}
|
areaM2={massingAreaM2(analysis)}
|
||||||
maxFar={MASSING_MAX_FAR}
|
maxFar={massingMaxFar(analysis)}
|
||||||
|
farIsReal={massingFarIsReal(analysis)}
|
||||||
/>
|
/>
|
||||||
</DrawerSection>
|
</DrawerSection>
|
||||||
<DrawerSection heading="ЧТО ДОБАВИТСЯ В МОДУЛЕ">
|
<DrawerSection heading="ЧТО ДОБАВИТСЯ В МОДУЛЕ">
|
||||||
|
|
|
||||||
|
|
@ -26,8 +26,10 @@ import styles from "@/app/site-finder/analysis/[cad]/ptica/ptica.module.css";
|
||||||
export interface MassingSceneProps {
|
export interface MassingSceneProps {
|
||||||
/** Real parcel area in m² when available (fallback ≈ 6800). */
|
/** Real parcel area in m² when available (fallback ≈ 6800). */
|
||||||
areaM2?: number;
|
areaM2?: number;
|
||||||
/** КСИТ-цель (FAR target) — regulation default, not parcel-specific. */
|
/** КСИТ-цель (FAR target) — real НСПД-регламент when present, else default. */
|
||||||
maxFar?: number;
|
maxFar?: number;
|
||||||
|
/** True when `maxFar` is the real НСПД ПЗЗ value (drives the caption). */
|
||||||
|
farIsReal?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── constants (scene units = metres) ──────────────────────────────────────────
|
// ── constants (scene units = metres) ──────────────────────────────────────────
|
||||||
|
|
@ -298,6 +300,7 @@ function modelMetrics(model: MassingModel): Metrics {
|
||||||
export default function MassingScene({
|
export default function MassingScene({
|
||||||
areaM2,
|
areaM2,
|
||||||
maxFar,
|
maxFar,
|
||||||
|
farIsReal = false,
|
||||||
}: MassingSceneProps): React.JSX.Element {
|
}: MassingSceneProps): React.JSX.Element {
|
||||||
const parcelArea =
|
const parcelArea =
|
||||||
areaM2 && Number.isFinite(areaM2) && areaM2 > 0 ? areaM2 : DEFAULT_AREA;
|
areaM2 && Number.isFinite(areaM2) && areaM2 > 0 ? areaM2 : DEFAULT_AREA;
|
||||||
|
|
@ -703,7 +706,8 @@ export default function MassingScene({
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={styles.massingCaption}>
|
<div className={styles.massingCaption}>
|
||||||
КСИТ-цель {farTarget.toFixed(1)} · регламент-дефолт, источник НСПД ·
|
КСИТ-цель {farTarget.toFixed(1)} ·{" "}
|
||||||
|
{farIsReal ? "регламент НСПД" : "регламент-дефолт, источник НСПД"} ·
|
||||||
масса генеративная (этажи/секции/время — исследовательские вводные)
|
масса генеративная (этажи/секции/время — исследовательские вводные)
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ import type {
|
||||||
ParcelAnalysis,
|
ParcelAnalysis,
|
||||||
ParcelAnalysisCompetitor,
|
ParcelAnalysisCompetitor,
|
||||||
} from "@/types/site-finder";
|
} from "@/types/site-finder";
|
||||||
|
import type { NspdZoning } from "@/types/nspd";
|
||||||
import type { ForecastReport, ProductMixEntry } from "@/types/forecast";
|
import type { ForecastReport, ProductMixEntry } from "@/types/forecast";
|
||||||
|
|
||||||
// ── View-model ──────────────────────────────────────────────────────────────
|
// ── View-model ──────────────────────────────────────────────────────────────
|
||||||
|
|
@ -90,6 +91,29 @@ function formatRubPerM2(n: number): string {
|
||||||
return `${formatInt(n)} ₽/м²`;
|
return `${formatInt(n)} ₽/м²`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── НСПД-регламент (ПЗЗ) helpers ──────────────────────────────────────────────
|
||||||
|
|
||||||
|
/** КСИТ (max_far) as a plain RU number — e.g. "3,5". */
|
||||||
|
function formatFar(far: number): string {
|
||||||
|
return fmtNum(far, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Real parcel area in m² when geometry is known (area_ha × 10000); null
|
||||||
|
* otherwise. The single point at which we turn area_ha into m² for ёмкость.
|
||||||
|
*/
|
||||||
|
function parcelAreaM2(a: ParcelAnalysis): number | null {
|
||||||
|
const areaHa = a.geometry_suitability?.area_ha;
|
||||||
|
return areaHa != null ? areaHa * 10000 : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Human regulation zone — prefer the readable index ("Ж-2") over reg-number. */
|
||||||
|
function regulationZone(z: NspdZoning | null | undefined): string | null {
|
||||||
|
return z?.regulation_zone_index ?? z?.zone_code ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SRC_NSPD_ZONING = "источник НСПД-зонирование";
|
||||||
|
|
||||||
// ── Passport ──────────────────────────────────────────────────────────────────
|
// ── Passport ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
export interface PticaPassport {
|
export interface PticaPassport {
|
||||||
|
|
@ -296,25 +320,49 @@ function utilityLabel(subtype: string): string {
|
||||||
return UTILITY_LABELS[subtype] ?? subtype;
|
return UTILITY_LABELS[subtype] ?? subtype;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Градостроительство — mostly placeholder (НСПД-зонирование / источники). */
|
/**
|
||||||
|
* Градостроительство — REAL ПЗЗ-регламент when nspd_zoning carries it
|
||||||
|
* (PR #1847), honest placeholder otherwise. Зона prefers the human index
|
||||||
|
* ("Ж-2"); КСИТ / высота / плотность light up once the backend field flows.
|
||||||
|
*/
|
||||||
export function adaptUrbanCard(a: ParcelAnalysis): PticaScanCard {
|
export function adaptUrbanCard(a: ParcelAnalysis): PticaScanCard {
|
||||||
const zoneCode = a.nspd_zoning?.zone_code;
|
const z = a.nspd_zoning;
|
||||||
|
const zone = regulationZone(z);
|
||||||
|
const maxFar = z?.max_far;
|
||||||
|
const maxHeight = z?.max_height_m;
|
||||||
|
const areaM2 = parcelAreaM2(a);
|
||||||
|
|
||||||
|
// Плотность (КСИТ) ёмкость = площадь × КСИТ (м²), real only with both inputs.
|
||||||
|
const densityField =
|
||||||
|
maxFar != null
|
||||||
|
? areaM2 != null
|
||||||
|
? {
|
||||||
|
value: `${formatInt(areaM2 * maxFar)} м²`,
|
||||||
|
isReal: true,
|
||||||
|
caption: `КСИТ ${formatFar(maxFar)} · НСПД`,
|
||||||
|
}
|
||||||
|
: { value: formatFar(maxFar), isReal: true, caption: "КСИТ · НСПД" }
|
||||||
|
: placeholder(SRC_NSPD_ZONING);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
title: "Градостроительство",
|
title: "Градостроительство",
|
||||||
rows: [
|
rows: [
|
||||||
{
|
{
|
||||||
key: "Зона",
|
key: "Зона",
|
||||||
field: zoneCode
|
field: zone
|
||||||
? { value: zoneCode, isReal: true, caption: "НСПД-зонирование" }
|
? { value: zone, isReal: true, caption: "НСПД-зонирование" }
|
||||||
: placeholder("источник НСПД-зонирование"),
|
: placeholder(SRC_NSPD_ZONING),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "Пред. высота",
|
key: "Пред. высота",
|
||||||
field: placeholder("источник НСПД-зонирование"),
|
field:
|
||||||
|
maxHeight != null
|
||||||
|
? real(`${formatInt(maxHeight)} м`)
|
||||||
|
: placeholder(SRC_NSPD_ZONING),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "Плотность (КСИТ)",
|
key: "Плотность (КСИТ)",
|
||||||
field: placeholder("источник НСПД-зонирование"),
|
field: densityField,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "Пятно застройки",
|
key: "Пятно застройки",
|
||||||
|
|
@ -592,6 +640,40 @@ export function adaptBuildabilityDrawer(
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// ── Пятно / продаваемая площадь — REAL when ПЗЗ max_far flows (PR #1847) ──
|
||||||
|
// КСИТ ёмкость (надземная GFA) = площадь × max_far. Footprint (пятно) needs
|
||||||
|
// the building-coverage % (max_building_pct); sellable still depends on
|
||||||
|
// МОП/parking, so it stays an estimate placeholder.
|
||||||
|
const z = a.nspd_zoning;
|
||||||
|
const areaM2 = parcelAreaM2(a);
|
||||||
|
const maxFar = z?.max_far;
|
||||||
|
const farReal = maxFar != null;
|
||||||
|
const buildingPct = z?.max_building_pct;
|
||||||
|
|
||||||
|
const spotRow: DrawerKvRow =
|
||||||
|
areaM2 != null && buildingPct != null
|
||||||
|
? {
|
||||||
|
k: "Пятно застройки",
|
||||||
|
field: {
|
||||||
|
value: `${formatInt(areaM2 * (buildingPct / 100))} м²`,
|
||||||
|
isReal: true,
|
||||||
|
caption: `${formatInt(buildingPct)} % площади · НСПД`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: { k: "Пятно застройки", field: notReal(SRC_NSPD_ZONING) };
|
||||||
|
|
||||||
|
const sellableRow: DrawerKvRow =
|
||||||
|
farReal && areaM2 != null
|
||||||
|
? {
|
||||||
|
k: "Продаваемая площадь",
|
||||||
|
field: {
|
||||||
|
value: `≈ ${formatInt(areaM2 * maxFar)} м²`,
|
||||||
|
isReal: true,
|
||||||
|
caption: "оценка по КСИТ-ёмкости · без вычета МОП/паркинга",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: { k: "Продаваемая площадь", field: notReal("нужен max_far (НСПД)") };
|
||||||
|
|
||||||
const geometry: DrawerKvRow[] = [
|
const geometry: DrawerKvRow[] = [
|
||||||
{
|
{
|
||||||
k: "Площадь",
|
k: "Площадь",
|
||||||
|
|
@ -618,8 +700,8 @@ export function adaptBuildabilityDrawer(
|
||||||
? real(`${fmtNum(geom.suitability_score * 10, 1)} / 10`)
|
? real(`${fmtNum(geom.suitability_score * 10, 1)} / 10`)
|
||||||
: notReal("нет геометрии"),
|
: notReal("нет геометрии"),
|
||||||
},
|
},
|
||||||
{ k: "Пятно застройки", field: notReal("нужен max_far (НСПД)") },
|
spotRow,
|
||||||
{ k: "Продаваемая площадь", field: notReal("нужен max_far (НСПД)") },
|
sellableRow,
|
||||||
];
|
];
|
||||||
|
|
||||||
const penalties = (geom?.penalties ?? []).map((p) => ({
|
const penalties = (geom?.penalties ?? []).map((p) => ({
|
||||||
|
|
@ -627,10 +709,13 @@ export function adaptBuildabilityDrawer(
|
||||||
impact: "штраф",
|
impact: "штраф",
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const summary =
|
const summary = farReal
|
||||||
"Индекс застраиваемости — предварительная оценка по гейт-вердикту, геометрии " +
|
? "Индекс застраиваемости — предварительная оценка по гейт-вердикту, геометрии " +
|
||||||
"участка и близости инженерии. Это эвристика, а не итоговый индекс: точное " +
|
"участка и близости инженерии. Это эвристика, а не итоговый индекс: точное " +
|
||||||
"значение появится после расчёта потенциала (max_far НСПД) и финмодели.";
|
"значение появится после финмодели. КСИТ-ёмкость рассчитана по регламенту НСПД."
|
||||||
|
: "Индекс застраиваемости — предварительная оценка по гейт-вердикту, геометрии " +
|
||||||
|
"участка и близости инженерии. Это эвристика, а не итоговый индекс: точное " +
|
||||||
|
"значение появится после расчёта потенциала (max_far НСПД) и финмодели.";
|
||||||
|
|
||||||
return {
|
return {
|
||||||
gauge,
|
gauge,
|
||||||
|
|
@ -653,13 +738,34 @@ export interface PticaUrbanDrawer {
|
||||||
|
|
||||||
export function adaptUrbanDrawer(a: ParcelAnalysis): PticaUrbanDrawer {
|
export function adaptUrbanDrawer(a: ParcelAnalysis): PticaUrbanDrawer {
|
||||||
const zoning = a.nspd_zoning;
|
const zoning = a.nspd_zoning;
|
||||||
const zoneCode = zoning?.zone_code
|
// Территориальная зона: prefer human index ("Ж-2"), else reg-number code.
|
||||||
? real(zoning.zone_code)
|
const zone = regulationZone(zoning);
|
||||||
: notReal("источник НСПД-зонирование");
|
const zoneCode = zone ? real(zone) : notReal(SRC_NSPD_ZONING);
|
||||||
const zoneName = zoning?.zone_name
|
const zoneName = zoning?.zone_name
|
||||||
? real(zoning.zone_name)
|
? real(zoning.zone_name)
|
||||||
: notReal("источник НСПД-зонирование");
|
: notReal(SRC_NSPD_ZONING);
|
||||||
|
|
||||||
|
const maxFar = zoning?.max_far;
|
||||||
|
const maxHeight = zoning?.max_height_m;
|
||||||
|
const buildingPct = zoning?.max_building_pct;
|
||||||
|
const areaM2 = parcelAreaM2(a);
|
||||||
|
const regSource = zoning?.regulation_source;
|
||||||
|
|
||||||
|
// КСИТ ёмкость (надземная GFA) = площадь × max_far.
|
||||||
|
const densityField: PticaField =
|
||||||
|
maxFar != null && areaM2 != null
|
||||||
|
? {
|
||||||
|
value: `${formatInt(areaM2 * maxFar)} м²`,
|
||||||
|
isReal: true,
|
||||||
|
caption: regSource
|
||||||
|
? `КСИТ ${formatFar(maxFar)} · ${regSource}`
|
||||||
|
: `КСИТ ${formatFar(maxFar)} · НСПД`,
|
||||||
|
}
|
||||||
|
: maxFar != null
|
||||||
|
? notReal("нужна геометрия участка")
|
||||||
|
: notReal("после расчёта потенциала");
|
||||||
|
|
||||||
|
const farReal = maxFar != null;
|
||||||
const regulation: DrawerKvRow[] = [
|
const regulation: DrawerKvRow[] = [
|
||||||
{ k: "Территориальная зона", field: zoneCode },
|
{ k: "Территориальная зона", field: zoneCode },
|
||||||
{ k: "Наименование зоны", field: zoneName },
|
{ k: "Наименование зоны", field: zoneName },
|
||||||
|
|
@ -669,19 +775,44 @@ export function adaptUrbanDrawer(a: ParcelAnalysis): PticaUrbanDrawer {
|
||||||
? real(a.egrn.permitted_use_text)
|
? real(a.egrn.permitted_use_text)
|
||||||
: notReal("нет данных ЕГРН"),
|
: notReal("нет данных ЕГРН"),
|
||||||
},
|
},
|
||||||
{ k: "КСИТ (max FAR)", field: notReal("нужен max_far (НСПД)") },
|
{
|
||||||
{ k: "Предельная высота", field: notReal("источник НСПД-зонирование") },
|
k: "КСИТ (max FAR)",
|
||||||
|
field: farReal
|
||||||
|
? real(formatFar(maxFar))
|
||||||
|
: notReal("нужен max_far (НСПД)"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
k: "Предельная высота",
|
||||||
|
field:
|
||||||
|
maxHeight != null
|
||||||
|
? real(`${formatInt(maxHeight)} м`)
|
||||||
|
: notReal(SRC_NSPD_ZONING),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
k: "Этажность (предельная)",
|
||||||
|
field:
|
||||||
|
zoning?.max_floors != null
|
||||||
|
? real(`${formatInt(zoning.max_floors)} эт.`)
|
||||||
|
: notReal(SRC_NSPD_ZONING),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
k: "Коэфф. застройки (пятно)",
|
k: "Коэфф. застройки (пятно)",
|
||||||
field: notReal("источник НСПД-зонирование"),
|
field:
|
||||||
|
buildingPct != null
|
||||||
|
? real(`${formatInt(buildingPct)} %`)
|
||||||
|
: notReal(SRC_NSPD_ZONING),
|
||||||
},
|
},
|
||||||
{ k: "Плотность (КСИТ)", field: notReal("после расчёта потенциала") },
|
{ k: "Плотность (КСИТ)", field: densityField },
|
||||||
];
|
];
|
||||||
|
|
||||||
const summary =
|
const summary = farReal
|
||||||
"Градостроительный регламент участка. Территориальная зона и ВРИ берутся из " +
|
? "Градостроительный регламент участка из НСПД-зонирования (ПЗЗ). " +
|
||||||
"НСПД-зонирования / ЕГРН; КСИТ (max_far), предельная высота и плотность пока " +
|
"Территориальная зона, КСИТ (max_far), предельная высота и коэффициент " +
|
||||||
"недоступны в выгрузке — отмечены «—» с источником.";
|
"застройки — реальные регламентные значения; плотность рассчитана от " +
|
||||||
|
"площади участка."
|
||||||
|
: "Градостроительный регламент участка. Территориальная зона и ВРИ берутся из " +
|
||||||
|
"НСПД-зонирования / ЕГРН; КСИТ (max_far), предельная высота и плотность пока " +
|
||||||
|
"недоступны в выгрузке — отмечены «—» с источником.";
|
||||||
|
|
||||||
return { summary, regulation, zoneCode, zoneName };
|
return { summary, regulation, zoneCode, zoneName };
|
||||||
}
|
}
|
||||||
|
|
@ -1298,21 +1429,76 @@ export function adaptPotentialDrawer(a: ParcelAnalysis): PticaPotentialDrawer {
|
||||||
? real(`${areaM2FromHa(areaHa)} (${formatHa(areaHa)})`)
|
? real(`${areaM2FromHa(areaHa)} (${formatHa(areaHa)})`)
|
||||||
: notReal("нет геометрии");
|
: notReal("нет геометрии");
|
||||||
|
|
||||||
|
const z = a.nspd_zoning;
|
||||||
|
const areaM2 = parcelAreaM2(a);
|
||||||
|
const maxFar = z?.max_far;
|
||||||
|
const farReal = maxFar != null;
|
||||||
|
const buildingPct = z?.max_building_pct;
|
||||||
|
const maxHeight = z?.max_height_m;
|
||||||
|
const maxFloors = z?.max_floors;
|
||||||
|
|
||||||
|
// КСИТ ёмкость (надземная GFA) = площадь × max_far — real with both inputs.
|
||||||
|
const capacityField: PticaField =
|
||||||
|
farReal && areaM2 != null
|
||||||
|
? {
|
||||||
|
value: `${formatInt(areaM2 * maxFar)} м²`,
|
||||||
|
isReal: true,
|
||||||
|
caption: `КСИТ ${formatFar(maxFar)} · площадь × КСИТ`,
|
||||||
|
}
|
||||||
|
: notReal("нужен max_far (НСПД)");
|
||||||
|
|
||||||
|
// Пятно застройки = площадь × коэф. застройки (max_building_pct).
|
||||||
|
const spotField: PticaField =
|
||||||
|
areaM2 != null && buildingPct != null
|
||||||
|
? {
|
||||||
|
value: `${formatInt(areaM2 * (buildingPct / 100))} м²`,
|
||||||
|
isReal: true,
|
||||||
|
caption: `${formatInt(buildingPct)} % площади · НСПД`,
|
||||||
|
}
|
||||||
|
: notReal("нужен % застройки (НСПД)");
|
||||||
|
|
||||||
|
// Расчётная этажность — прямо из регламента, либо высота / 3 м (этаж).
|
||||||
|
const floorsField: PticaField =
|
||||||
|
maxFloors != null
|
||||||
|
? real(`${formatInt(maxFloors)} эт.`)
|
||||||
|
: maxHeight != null
|
||||||
|
? {
|
||||||
|
value: `≈ ${formatInt(maxHeight / 3)} эт.`,
|
||||||
|
isReal: true,
|
||||||
|
caption: `от предельной высоты ${formatInt(maxHeight)} м`,
|
||||||
|
}
|
||||||
|
: notReal("нужна предельная высота (НСПД)");
|
||||||
|
|
||||||
const rows: DrawerKvRow[] = [
|
const rows: DrawerKvRow[] = [
|
||||||
{ k: "Площадь участка", field: areaField },
|
{ k: "Площадь участка", field: areaField },
|
||||||
{ k: "КСИТ (max_far)", field: notReal("нужен max_far (НСПД)") },
|
|
||||||
{ k: "Ёмкость по КСИТ", field: notReal("нужен max_far (НСПД)") },
|
|
||||||
{ k: "Пятно застройки", field: notReal("нужен max_far (НСПД)") },
|
|
||||||
{ k: "Продаваемая площадь", field: notReal("после расчёта потенциала") },
|
|
||||||
{
|
{
|
||||||
k: "Расчётная этажность",
|
k: "КСИТ (max_far)",
|
||||||
field: notReal("нужна предельная высота (НСПД)"),
|
field: farReal
|
||||||
|
? real(formatFar(maxFar))
|
||||||
|
: notReal("нужен max_far (НСПД)"),
|
||||||
},
|
},
|
||||||
|
{ k: "Ёмкость по КСИТ", field: capacityField },
|
||||||
|
{ k: "Пятно застройки", field: spotField },
|
||||||
|
{
|
||||||
|
k: "Продаваемая площадь",
|
||||||
|
field:
|
||||||
|
farReal && areaM2 != null
|
||||||
|
? {
|
||||||
|
value: `≈ ${formatInt(areaM2 * maxFar)} м²`,
|
||||||
|
isReal: true,
|
||||||
|
caption: "оценка по КСИТ-ёмкости · без вычета МОП/паркинга",
|
||||||
|
}
|
||||||
|
: notReal("после расчёта потенциала"),
|
||||||
|
},
|
||||||
|
{ k: "Расчётная этажность", field: floorsField },
|
||||||
];
|
];
|
||||||
|
|
||||||
const summary =
|
const summary = farReal
|
||||||
"Расчёт ёмкости (плотность, пятно, продаваемая площадь) требует предельного " +
|
? "Расчёт ёмкости по регламенту НСПД-зонирования: ёмкость по КСИТ = площадь × " +
|
||||||
"КСИТ (max_far) и высоты из НСПД-зонирования. Доступна только площадь участка.";
|
"max_far, пятно — по коэффициенту застройки. Продаваемая площадь — оценка " +
|
||||||
|
"(без вычета МОП и паркинга), уточняется финмоделью."
|
||||||
|
: "Расчёт ёмкости (плотность, пятно, продаваемая площадь) требует предельного " +
|
||||||
|
"КСИТ (max_far) и высоты из НСПД-зонирования. Доступна только площадь участка.";
|
||||||
|
|
||||||
return { summary, rows };
|
return { summary, rows };
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,15 @@ export interface NspdZoning {
|
||||||
zone_name: string | null;
|
zone_name: string | null;
|
||||||
source?: string;
|
source?: string;
|
||||||
raw_props?: Record<string, unknown>;
|
raw_props?: Record<string, unknown>;
|
||||||
|
// Real ПЗЗ regulation (PR #1847) — optional + nullable so the cockpit stays
|
||||||
|
// backward-compatible until #1847 deploys, then auto-lights-up.
|
||||||
|
max_far?: number | null; // КСИТ (предельный коэф. плотности застройки)
|
||||||
|
max_height_m?: number | null; // предельная высота, м
|
||||||
|
max_floors?: number | null; // предельная этажность
|
||||||
|
max_building_pct?: number | null; // коэф. застройки (процент пятна)
|
||||||
|
min_parcel_area_m2?: number | null; // мин. площадь участка, м²
|
||||||
|
regulation_zone_index?: string | null; // human zone index, e.g. "Ж-2"
|
||||||
|
regulation_source?: string | null; // источник регламента
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface NspdZouitOverlap {
|
export interface NspdZouitOverlap {
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue