feat(site-finder): cad_parcels_geom migration + POI markers on map + bulk-job UX
This commit is contained in:
parent
f419900968
commit
81cd7499f6
4 changed files with 106 additions and 12 deletions
26
data/sql/83_cad_parcels_geom.sql
Normal file
26
data/sql/83_cad_parcels_geom.sql
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
-- 83_cad_parcels_geom.sql
|
||||
-- Context : хранение геометрии участков (4-part cad: XX:YY:ZZZZZZZ:NN) из NSPD (thematic_id=1).
|
||||
-- По аналогии с cad_quarters_geom (3-part cad).
|
||||
-- Dependency: PostGIS extension уже установлен (используется cad_quarters_geom, cad_buildings).
|
||||
-- Deploy order: standalone, нет FK-зависимостей от других таблиц.
|
||||
-- Идемпотентность: безопасно повторно запускать (IF NOT EXISTS).
|
||||
|
||||
BEGIN;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS cad_parcels_geom (
|
||||
cad_num TEXT PRIMARY KEY, -- 4-part cad: 66:41:0204016:10
|
||||
geom GEOMETRY(Geometry, 4326) NOT NULL, -- Polygon | MultiPolygon
|
||||
raw_props JSONB DEFAULT '{}'::jsonb,
|
||||
fetched_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
COMMENT ON TABLE cad_parcels_geom IS
|
||||
'Геометрия участков (4-part cad, thematic_id=1) из NSPD. '
|
||||
'Источник: nspd_geo task. '
|
||||
'Используется site-finder analyze endpoint как fallback для 4-part cad '
|
||||
'если в cad_quarters_geom нет.';
|
||||
|
||||
CREATE INDEX IF NOT EXISTS cad_parcels_geom_gist
|
||||
ON cad_parcels_geom USING GIST (geom);
|
||||
|
||||
COMMIT;
|
||||
|
|
@ -73,11 +73,14 @@ export default function GeoScrapeAdminPage() {
|
|||
|
||||
// Авто-синк thematic_id при смене job_kind. Пользователь может override через
|
||||
// селект ниже (например quarters + thematic_id=15 для комплексов).
|
||||
// Также переключаем source на manual_list когда job_kind != quarters, т.к.
|
||||
// rosreestr_pending содержит только кварталы (иначе backend вернёт 400).
|
||||
const handleJobKindChange = (kind: "quarters" | "parcels" | "buildings") => {
|
||||
setJobKind(kind);
|
||||
if (kind === "quarters") setThematicId(2);
|
||||
else if (kind === "parcels") setThematicId(1);
|
||||
else if (kind === "buildings") setThematicId(5);
|
||||
if (kind !== "quarters") setSourceKind("manual_list");
|
||||
};
|
||||
const [cadNumsText, setCadNumsText] = useState("");
|
||||
const [regionCodes, setRegionCodes] = useState("66,74,72,59");
|
||||
|
|
@ -306,6 +309,23 @@ export default function GeoScrapeAdminPage() {
|
|||
</label>
|
||||
</div>
|
||||
|
||||
{sourceKind === "rosreestr_pending" && jobKind !== "quarters" && (
|
||||
<div
|
||||
style={{
|
||||
background: "#fef3c7",
|
||||
border: "1px solid #f59e0b",
|
||||
borderRadius: 6,
|
||||
padding: "10px 14px",
|
||||
fontSize: 13,
|
||||
color: "#78350f",
|
||||
}}
|
||||
>
|
||||
rosreestr_pending пока работает только для кварталов. Для
|
||||
участков/зданий используй manual_list с cad-номерами через
|
||||
запятую.
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div style={{ display: "flex", gap: 12, alignItems: "center" }}>
|
||||
<button
|
||||
type="button"
|
||||
|
|
|
|||
|
|
@ -1,7 +1,13 @@
|
|||
"use client";
|
||||
|
||||
import { useEffect } from "react";
|
||||
import { MapContainer, TileLayer, GeoJSON } from "react-leaflet";
|
||||
import {
|
||||
MapContainer,
|
||||
TileLayer,
|
||||
GeoJSON,
|
||||
CircleMarker,
|
||||
Popup,
|
||||
} from "react-leaflet";
|
||||
import type { Geometry, Position } from "geojson";
|
||||
import "leaflet/dist/leaflet.css";
|
||||
|
||||
|
|
@ -14,20 +20,21 @@ import type { ParcelAnalysis } from "@/types/site-finder";
|
|||
interface CategoryStyle {
|
||||
color: string;
|
||||
label: string;
|
||||
radius: number;
|
||||
}
|
||||
|
||||
export const CATEGORY_STYLES: Record<string, CategoryStyle> = {
|
||||
school: { color: "#2563eb", label: "Школа" },
|
||||
kindergarten: { color: "#0891b2", label: "Детский сад" },
|
||||
pharmacy: { color: "#16a34a", label: "Аптека" },
|
||||
hospital: { color: "#059669", label: "Больница" },
|
||||
shop_mall: { color: "#f59e0b", label: "ТЦ" },
|
||||
shop_supermarket: { color: "#f97316", label: "Супермаркет" },
|
||||
shop_small: { color: "#fb923c", label: "Магазин" },
|
||||
park: { color: "#166534", label: "Парк" },
|
||||
tram_stop: { color: "#dc2626", label: "Трамвай" },
|
||||
bus_stop: { color: "#9ca3af", label: "Автобус" },
|
||||
metro_stop: { color: "#7c3aed", label: "Метро" },
|
||||
school: { color: "#1d4ed8", label: "Школа", radius: 8 },
|
||||
kindergarten: { color: "#1d4ed8", label: "Детский сад", radius: 8 },
|
||||
pharmacy: { color: "#10b981", label: "Аптека", radius: 6 },
|
||||
hospital: { color: "#f59e0b", label: "Больница", radius: 8 },
|
||||
shop_mall: { color: "#a855f7", label: "ТЦ", radius: 8 },
|
||||
shop_supermarket: { color: "#a855f7", label: "Супермаркет", radius: 6 },
|
||||
shop_small: { color: "#c084fc", label: "Магазин", radius: 6 },
|
||||
park: { color: "#16a34a", label: "Парк", radius: 6 },
|
||||
tram_stop: { color: "#dc2626", label: "Трамвай", radius: 6 },
|
||||
bus_stop: { color: "#6b7280", label: "Автобус", radius: 6 },
|
||||
metro_stop: { color: "#1e40af", label: "Метро", radius: 8 },
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
@ -135,6 +142,45 @@ export function SiteMap({ data }: Props) {
|
|||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* POI markers */}
|
||||
{Object.entries(data.score_breakdown).flatMap(([cat, pois]) => {
|
||||
const style = CATEGORY_STYLES[cat];
|
||||
if (!style) return [];
|
||||
return pois
|
||||
.filter((poi) => poi.lat !== 0 && poi.lon !== 0)
|
||||
.map((poi, idx) => (
|
||||
<CircleMarker
|
||||
key={`${cat}-${idx}`}
|
||||
center={[poi.lat, poi.lon]}
|
||||
radius={style.radius}
|
||||
pathOptions={{
|
||||
color: style.color,
|
||||
fillColor: style.color,
|
||||
fillOpacity: 0.85,
|
||||
weight: 1.5,
|
||||
}}
|
||||
>
|
||||
<Popup>
|
||||
<div style={{ fontSize: 13, lineHeight: 1.5 }}>
|
||||
<strong>{style.label}</strong>
|
||||
<br />
|
||||
{poi.name ?? <em>без названия</em>}
|
||||
<br />
|
||||
{poi.distance_m.toFixed(0)} м
|
||||
{poi.last_edit && (
|
||||
<>
|
||||
<br />
|
||||
<span style={{ color: "#6b7280", fontSize: 11 }}>
|
||||
OSM: {poi.last_edit}
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</Popup>
|
||||
</CircleMarker>
|
||||
));
|
||||
})}
|
||||
</MapContainer>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -20,6 +20,8 @@ export interface ParcelAnalysisPoi {
|
|||
name: string | null;
|
||||
distance_m: number;
|
||||
last_edit: string | null;
|
||||
lat: number;
|
||||
lon: number;
|
||||
}
|
||||
|
||||
export interface ParcelAnalysis {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue