Section2 "Сети" map now shows the OSM engineering-networks layer
(osm_utility_infrastructure_ekb, endpoint /parcels/{cad}/utility-infrastructure)
alongside the NSPD connection-points + ЗОУИТ layers from #1751, on the same map.
- useUtilityInfrastructure hook (react-query, mirrors useConnectionPoints)
- UtilityInfrastructureLayer: Point→CircleMarker, LineString/Polygon(+Multi)→GeoJSON,
per-kind colour (power/water/gas/heat/communication/sewage), popups
- UtilityLayerControlPanel: collapsible legend, per-kind toggle + count
- SiteMap: optional utilityInfrastructure prop + visibleKinds state (CP/ЗОУИТ unchanged)
- 12 unit tests (geometry parsing + [lon,lat]→[lat,lon] swap + grouping)
Refs #1746
152 lines
5.1 KiB
TypeScript
152 lines
5.1 KiB
TypeScript
/**
|
|
* Unit tests for the #1746 OSM utility-infrastructure layer pure logic: kind
|
|
* classification, geometry_geojson parsing (the three-geometry-type contract +
|
|
* graceful-skip), and grouping.
|
|
*
|
|
* The layer renders Leaflet <CircleMarker>/<GeoJSON> (jsdom can't mount those
|
|
* meaningfully), so we test the data-shaping that decides WHAT gets drawn and
|
|
* HOW it's colored — that's where the bugs live (power-lines must NOT be
|
|
* skipped; [lon,lat]→[lat,lat] swap; bad parse → crash or ocean geometry).
|
|
*/
|
|
import type { UtilityInfrastructureFeature } from "@/hooks/useUtilityInfrastructure";
|
|
import {
|
|
classifyUtilityKind,
|
|
groupUtilityByKind,
|
|
parseUtilityGeometry,
|
|
} from "../UtilityInfrastructureLayer";
|
|
|
|
function feat(
|
|
partial: Partial<UtilityInfrastructureFeature>,
|
|
): UtilityInfrastructureFeature {
|
|
return {
|
|
osm_id: 1,
|
|
osm_type: "way",
|
|
infrastructure_kind: "power",
|
|
name: null,
|
|
source_tag: null,
|
|
distance_m: 0,
|
|
geometry_geojson: {},
|
|
...partial,
|
|
};
|
|
}
|
|
|
|
// geometry_geojson приходит уже распарсенным объектом (ST_AsGeoJSON на бэке).
|
|
const POINT_GEOM = { type: "Point", coordinates: [60.6, 56.83] };
|
|
const LINESTRING_GEOM = {
|
|
type: "LineString",
|
|
coordinates: [
|
|
[60.6, 56.83],
|
|
[60.61, 56.84],
|
|
],
|
|
};
|
|
const POLYGON_GEOM = {
|
|
type: "Polygon",
|
|
coordinates: [
|
|
[
|
|
[60.6, 56.83],
|
|
[60.61, 56.83],
|
|
[60.61, 56.84],
|
|
[60.6, 56.83],
|
|
],
|
|
],
|
|
};
|
|
|
|
describe("classifyUtilityKind", () => {
|
|
it("passes through known kinds", () => {
|
|
expect(classifyUtilityKind("power")).toBe("power");
|
|
expect(classifyUtilityKind("water")).toBe("water");
|
|
expect(classifyUtilityKind("gas")).toBe("gas");
|
|
expect(classifyUtilityKind("heat")).toBe("heat");
|
|
expect(classifyUtilityKind("communication")).toBe("communication");
|
|
expect(classifyUtilityKind("sewage")).toBe("sewage");
|
|
});
|
|
|
|
it("maps unknown / null kind to 'other' (graceful)", () => {
|
|
expect(classifyUtilityKind("telecom")).toBe("other");
|
|
expect(classifyUtilityKind(null)).toBe("other");
|
|
expect(classifyUtilityKind(undefined)).toBe("other");
|
|
});
|
|
});
|
|
|
|
describe("parseUtilityGeometry", () => {
|
|
it("parses Point geometry (towers / nodes)", () => {
|
|
expect(parseUtilityGeometry(POINT_GEOM)?.type).toBe("Point");
|
|
});
|
|
|
|
it("parses LineString geometry (power lines — the majority, must NOT skip)", () => {
|
|
expect(parseUtilityGeometry(LINESTRING_GEOM)?.type).toBe("LineString");
|
|
});
|
|
|
|
it("parses Polygon geometry (substations)", () => {
|
|
expect(parseUtilityGeometry(POLYGON_GEOM)?.type).toBe("Polygon");
|
|
});
|
|
|
|
it("parses MultiLineString / MultiPolygon geometry", () => {
|
|
expect(
|
|
parseUtilityGeometry({
|
|
type: "MultiLineString",
|
|
coordinates: [LINESTRING_GEOM.coordinates],
|
|
})?.type,
|
|
).toBe("MultiLineString");
|
|
expect(
|
|
parseUtilityGeometry({
|
|
type: "MultiPolygon",
|
|
coordinates: [POLYGON_GEOM.coordinates],
|
|
})?.type,
|
|
).toBe("MultiPolygon");
|
|
});
|
|
|
|
it("tolerates a GeoJSON string (future contract change)", () => {
|
|
expect(parseUtilityGeometry(JSON.stringify(POINT_GEOM))?.type).toBe(
|
|
"Point",
|
|
);
|
|
});
|
|
|
|
it("returns null for missing / empty / invalid input (graceful skip)", () => {
|
|
expect(parseUtilityGeometry(null)).toBeNull();
|
|
expect(parseUtilityGeometry(undefined)).toBeNull();
|
|
expect(parseUtilityGeometry("")).toBeNull();
|
|
expect(parseUtilityGeometry("{not json")).toBeNull();
|
|
expect(parseUtilityGeometry({})).toBeNull();
|
|
expect(parseUtilityGeometry({ type: "Point" })).toBeNull();
|
|
expect(
|
|
parseUtilityGeometry({ type: "GeometryCollection", geometries: [] }),
|
|
).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe("groupUtilityByKind", () => {
|
|
it("groups all three geometry types and keeps power lines", () => {
|
|
const grouped = groupUtilityByKind([
|
|
feat({ infrastructure_kind: "power", geometry_geojson: LINESTRING_GEOM }),
|
|
feat({ infrastructure_kind: "power", geometry_geojson: POINT_GEOM }),
|
|
feat({ infrastructure_kind: "power", geometry_geojson: POLYGON_GEOM }),
|
|
feat({ infrastructure_kind: "water", geometry_geojson: POINT_GEOM }),
|
|
feat({ infrastructure_kind: "gas", geometry_geojson: LINESTRING_GEOM }),
|
|
]);
|
|
expect(grouped.get("power")).toHaveLength(3);
|
|
expect(grouped.get("water")).toHaveLength(1);
|
|
expect(grouped.get("gas")).toHaveLength(1);
|
|
});
|
|
|
|
it("drops features without valid geometry", () => {
|
|
const grouped = groupUtilityByKind([
|
|
feat({ infrastructure_kind: "power", geometry_geojson: LINESTRING_GEOM }),
|
|
feat({ infrastructure_kind: "power", geometry_geojson: {} }),
|
|
]);
|
|
expect(grouped.get("power")).toHaveLength(1);
|
|
});
|
|
|
|
it("buckets unknown kinds under 'other'", () => {
|
|
const grouped = groupUtilityByKind([
|
|
feat({ infrastructure_kind: "telecom", geometry_geojson: POINT_GEOM }),
|
|
]);
|
|
expect(grouped.get("other")).toHaveLength(1);
|
|
});
|
|
|
|
it("returns empty buckets (not undefined) when features is empty", () => {
|
|
const grouped = groupUtilityByKind([]);
|
|
expect(grouped.get("power")).toEqual([]);
|
|
expect(grouped.get("other")).toEqual([]);
|
|
});
|
|
});
|