ba579d75c1
- Magic GPKG (terenuri_magic.gpkg) now contains ALL records: rows with geometry render as polygons, rows without have null geom but still carry all attribute/enrichment data (QGIS shows them fine) - Added HAS_GEOMETRY column to Magic GPKG fields (0 or 1) - GPKG builder now supports includeNullGeometry option: splits features into spatial-first (creates table), then appends null-geom rows - Base terenuri.gpkg / cladiri.gpkg unchanged (spatial only) - CSV still has all records as before - GeoJsonFeature type now allows null geometry - Reproject: null geometry guard added - UI text updated: no longer says 'Nu apar in GPKG'
83 lines
2.2 KiB
TypeScript
83 lines
2.2 KiB
TypeScript
/**
|
|
* ESRI → GeoJSON conversion for eTerra features.
|
|
*/
|
|
|
|
import type { EsriFeature } from "./eterra-client";
|
|
|
|
export type GeoJsonPolygon = { type: "Polygon"; coordinates: number[][][] };
|
|
export type GeoJsonMultiPolygon = {
|
|
type: "MultiPolygon";
|
|
coordinates: number[][][][];
|
|
};
|
|
|
|
export type GeoJsonFeature = {
|
|
type: "Feature";
|
|
properties: Record<string, unknown>;
|
|
geometry: GeoJsonPolygon | GeoJsonMultiPolygon | null;
|
|
};
|
|
|
|
export type GeoJsonFeatureCollection = {
|
|
type: "FeatureCollection";
|
|
features: GeoJsonFeature[];
|
|
};
|
|
|
|
const ringArea = (ring: number[][]) => {
|
|
let area = 0;
|
|
for (let i = 0; i < ring.length - 1; i++) {
|
|
const curr = ring[i]!;
|
|
const next = ring[i + 1]!;
|
|
area += curr[0]! * next[1]! - next[0]! * curr[1]!;
|
|
}
|
|
return area / 2;
|
|
};
|
|
|
|
const isClockwise = (ring: number[][]) => ringArea(ring) < 0;
|
|
|
|
const closeRing = (ring: number[][]) => {
|
|
if (ring.length === 0) return ring;
|
|
const first = ring[0]!;
|
|
const last = ring[ring.length - 1]!;
|
|
return first[0] !== last[0] || first[1] !== last[1]
|
|
? [...ring, [first[0]!, first[1]!]]
|
|
: ring;
|
|
};
|
|
|
|
const ringsToPolygons = (rings: number[][][]) => {
|
|
const polygons: number[][][][] = [];
|
|
let current: number[][][] | null = null;
|
|
for (const ring of rings) {
|
|
const closed = closeRing(ring);
|
|
if (closed.length < 4) continue;
|
|
if (isClockwise(closed) || !current) {
|
|
if (current) polygons.push(current);
|
|
current = [closed];
|
|
} else {
|
|
current.push(closed);
|
|
}
|
|
}
|
|
if (current) polygons.push(current);
|
|
return polygons;
|
|
};
|
|
|
|
export const esriToGeojson = (
|
|
features: EsriFeature[],
|
|
): GeoJsonFeatureCollection => {
|
|
const geoFeatures: GeoJsonFeature[] = [];
|
|
for (const feature of features) {
|
|
const rings = feature.geometry?.rings;
|
|
if (!rings?.length) continue;
|
|
const polygons = ringsToPolygons(rings);
|
|
if (!polygons.length) continue;
|
|
const geometry: GeoJsonPolygon | GeoJsonMultiPolygon =
|
|
polygons.length === 1
|
|
? { type: "Polygon", coordinates: polygons[0]! }
|
|
: { type: "MultiPolygon", coordinates: polygons };
|
|
geoFeatures.push({
|
|
type: "Feature",
|
|
properties: feature.attributes ?? {},
|
|
geometry,
|
|
});
|
|
}
|
|
return { type: "FeatureCollection", features: geoFeatures };
|
|
};
|