102 lines
3.5 KiB
TypeScript
102 lines
3.5 KiB
TypeScript
/**
|
||
* 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();
|
||
});
|
||
});
|