gendesign/frontend/src/lib/__tests__/concept-api.test.ts
bot-backend 1fa99812af
All checks were successful
Deploy / changes (push) Successful in 8s
Deploy / build-backend (push) Has been skipped
Deploy / build-worker (push) Has been skipped
Deploy / build-frontend (push) Successful in 3m35s
Deploy / deploy (push) Successful in 1m23s
fix(concept): §7 massing-сцена осознаёт весь MultiPolygon (соседи уезжали до 1.6км) (#2180) (#2463)
2026-07-07 08:10:27 +00:00

102 lines
3.5 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.

/**
* Unit tests for the parcel-geometry narrowers in concept-api.ts.
*
* These guard the §7 «Размещение застройки» multi-part fix:
* • `extractPolygon` (the /concepts REQUEST geometry) must collapse a
* MultiPolygon to its LARGEST-area part, not `coordinates[0]` (which can be a
* sliver), so the generated corpus sits on the dominant part.
* • `extractParcelGeometry` (the SCENE geometry) must preserve EVERY part so the
* outline / projection origin / OSM tile cover the whole parcel and stay
* geo-aligned with backend-anchored neighbours.
*/
import type { MultiPolygon, Polygon, Position } from "geojson";
import { describe, expect, it } from "vitest";
import { extractParcelGeometry, extractPolygon } from "../concept-api";
// Part 0: a small sliver near the origin (≈55 m × 55 m).
const SLIVER: Position[] = [
[60.6, 56.83],
[60.6005, 56.83],
[60.6005, 56.8305],
[60.6, 56.8305],
[60.6, 56.83],
];
// Part 1: a much larger square ~1 km east (the dominant part).
const MAIN: Position[] = [
[60.616, 56.83],
[60.62, 56.83],
[60.62, 56.834],
[60.616, 56.834],
[60.616, 56.83],
];
const MULTI: MultiPolygon = {
type: "MultiPolygon",
coordinates: [[SLIVER], [MAIN]],
};
describe("extractPolygon — /concepts request geometry", () => {
it("returns a bare Polygon unchanged", () => {
const poly: Polygon = { type: "Polygon", coordinates: [MAIN] };
expect(extractPolygon(poly)).toEqual(poly);
});
it("unwraps a Feature wrapping a Polygon", () => {
const result = extractPolygon({
type: "Feature",
properties: {},
geometry: { type: "Polygon", coordinates: [MAIN] },
});
expect(result).toEqual({ type: "Polygon", coordinates: [MAIN] });
});
it("collapses a MultiPolygon to its LARGEST part, not coordinates[0]", () => {
const result = extractPolygon(MULTI);
expect(result).not.toBeNull();
expect(result?.type).toBe("Polygon");
// Must pick MAIN (part 1), NOT the SLIVER at index 0.
expect(result?.coordinates).toEqual([MAIN]);
expect(result?.coordinates).not.toEqual([SLIVER]);
});
it("returns null on a non-polygonal / malformed shape", () => {
expect(extractPolygon(null)).toBeNull();
expect(extractPolygon({ type: "Point", coordinates: [1, 2] })).toBeNull();
expect(extractPolygon("nope")).toBeNull();
});
});
describe("extractParcelGeometry — whole scene geometry", () => {
it("returns a bare Polygon unchanged", () => {
const poly: Polygon = { type: "Polygon", coordinates: [MAIN] };
expect(extractParcelGeometry(poly)).toEqual(poly);
});
it("preserves EVERY part of a MultiPolygon (both parts, not just part 0)", () => {
const result = extractParcelGeometry(MULTI);
expect(result).not.toBeNull();
expect(result?.type).toBe("MultiPolygon");
const mp = result as MultiPolygon;
expect(mp.coordinates).toHaveLength(2);
expect(mp.coordinates[0]).toEqual([SLIVER]);
expect(mp.coordinates[1]).toEqual([MAIN]);
});
it("unwraps a Feature wrapping a MultiPolygon", () => {
const result = extractParcelGeometry({
type: "Feature",
properties: {},
geometry: MULTI,
});
expect(result?.type).toBe("MultiPolygon");
expect((result as MultiPolygon).coordinates).toHaveLength(2);
});
it("returns null on a non-polygonal / malformed shape", () => {
expect(extractParcelGeometry(null)).toBeNull();
expect(
extractParcelGeometry({ type: "LineString", coordinates: [] }),
).toBeNull();
});
});