feat(parcel-sync): include no-geometry rows in Magic GPKG + HAS_GEOMETRY column

- 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'
This commit is contained in:
AI Assistant
2026-03-07 18:06:28 +02:00
parent 96859dde4f
commit ba579d75c1
5 changed files with 106 additions and 38 deletions
+5 -5
View File
@@ -469,7 +469,7 @@ export async function POST(req: Request) {
const enrichment =
(record.enrichment as FeatureEnrichment | null) ??
({} as Partial<FeatureEnrichment>);
const geom = record.geometry as GeoJsonFeature["geometry"] | null;
const geom = record.geometry as GeoJsonFeature["geometry"];
const geomSource = (
record as unknown as { geometrySource: string | null }
).geometrySource;
@@ -504,14 +504,13 @@ export async function POST(req: Request) {
];
csvRows.push(row.map(csvEscape).join(","));
if (geom) {
// ALL records go into magic GPKG — with or without geometry
magicFeatures.push({
type: "Feature",
geometry: geom,
properties: { ...attrs, ...e },
properties: { ...attrs, ...e, HAS_GEOMETRY: hasGeometry },
});
}
}
csvContent = csvRows.join("\n");
@@ -522,8 +521,9 @@ export async function POST(req: Request) {
layers: [
{
name: "TERENURI_MAGIC",
fields: magicFields,
fields: [...magicFields, "HAS_GEOMETRY"],
features: magicFeatures,
includeNullGeometry: true,
},
],
}),
@@ -2566,8 +2566,9 @@ export function ParcelSyncModule() {
</label>
{includeNoGeom && (
<p className="text-[11px] text-muted-foreground ml-7">
Vor fi importate în DB și incluse în CSV (coloana
HAS_GEOMETRY=0). Nu apar în GPKG.
Vor fi importate în DB și incluse în CSV + Magic GPKG
(coloana HAS_GEOMETRY=0/1). În GPKG de bază apar doar
cele cu geometrie.
</p>
)}
{workflowPreview}
@@ -13,7 +13,7 @@ export type GeoJsonMultiPolygon = {
export type GeoJsonFeature = {
type: "Feature";
properties: Record<string, unknown>;
geometry: GeoJsonPolygon | GeoJsonMultiPolygon;
geometry: GeoJsonPolygon | GeoJsonMultiPolygon | null;
};
export type GeoJsonFeatureCollection = {
@@ -18,6 +18,8 @@ type GpkgLayerInput = {
name: string;
fields: string[];
features: GeoJsonFeatureCollection["features"];
/** If true, also include features with null geometry (attribute-only rows) */
includeNullGeometry?: boolean;
};
type GpkgBuildOptions = {
@@ -58,10 +60,19 @@ export const buildGpkg = async (options: GpkgBuildOptions): Promise<Buffer> => {
try {
let first = true;
for (const layer of options.layers) {
// Split: spatial features go first (define the geometry column),
// then null-geometry features are appended as rows without geom.
const spatialFeatures = layer.features.filter((f) => f.geometry != null);
const nullGeomFeatures = layer.includeNullGeometry
? layer.features.filter((f) => f.geometry == null)
: [];
// Write spatial features
if (spatialFeatures.length > 0) {
const geojsonPath = path.join(tmpDir, `${layer.name}.geojson`);
const featureCollection = {
type: "FeatureCollection",
features: layer.features,
features: spatialFeatures,
};
await fs.writeFile(geojsonPath, JSON.stringify(featureCollection));
const args = [
@@ -86,6 +97,60 @@ export const buildGpkg = async (options: GpkgBuildOptions): Promise<Buffer> => {
await runOgr(args, ogrEnv);
first = false;
}
// Append null-geometry features as additional rows
if (nullGeomFeatures.length > 0) {
// Create a GeoJSON with null geometries — ogr2ogr handles these
// by inserting rows with empty/null geom into the existing layer.
const nullGeoPath = path.join(
tmpDir,
`${layer.name}_nullgeom.geojson`,
);
const nullCollection = {
type: "FeatureCollection",
features: nullGeomFeatures.map((f) => ({
...f,
geometry: null,
})),
};
await fs.writeFile(nullGeoPath, JSON.stringify(nullCollection));
if (spatialFeatures.length === 0 && first) {
// No spatial features yet — create the layer from null-geom
const args = [
"-f",
"GPKG",
outputPath,
nullGeoPath,
"-nln",
layer.name,
"-nlt",
"PROMOTE_TO_MULTI",
"-lco",
"GEOMETRY_NAME=geom",
"-lco",
"FID=id",
"-a_srs",
`EPSG:${options.srsId}`,
];
await runOgr(args, ogrEnv);
first = false;
} else {
// Layer exists — append null-geom rows
const args = [
"-f",
"GPKG",
outputPath,
nullGeoPath,
"-nln",
layer.name,
"-update",
"-append",
];
await runOgr(args, ogrEnv);
}
}
}
usedOgr = true;
} catch {
usedOgr = false;
@@ -36,6 +36,8 @@ export const reprojectFeatureCollection = (
if (from === to) return collection;
const features = collection.features.map((feature) => {
if (!feature.geometry) return feature;
if (feature.geometry.type === "Polygon") {
const geometry: GeoJsonPolygon = {
type: "Polygon",