diff --git a/src/app/api/eterra/export-bundle/route.ts b/src/app/api/eterra/export-bundle/route.ts index ff4962c..b78ad72 100644 --- a/src/app/api/eterra/export-bundle/route.ts +++ b/src/app/api/eterra/export-bundle/route.ts @@ -469,7 +469,7 @@ export async function POST(req: Request) { const enrichment = (record.enrichment as FeatureEnrichment | null) ?? ({} as Partial); - 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,13 +504,12 @@ export async function POST(req: Request) { ]; csvRows.push(row.map(csvEscape).join(",")); - if (geom) { - magicFeatures.push({ - type: "Feature", - geometry: geom, - properties: { ...attrs, ...e }, - }); - } + // ALL records go into magic GPKG — with or without geometry + magicFeatures.push({ + type: "Feature", + geometry: geom, + 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, }, ], }), diff --git a/src/modules/parcel-sync/components/parcel-sync-module.tsx b/src/modules/parcel-sync/components/parcel-sync-module.tsx index fa11cf7..e0d9c73 100644 --- a/src/modules/parcel-sync/components/parcel-sync-module.tsx +++ b/src/modules/parcel-sync/components/parcel-sync-module.tsx @@ -2566,8 +2566,9 @@ export function ParcelSyncModule() { {includeNoGeom && (

- 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.

)} {workflowPreview} diff --git a/src/modules/parcel-sync/services/esri-geojson.ts b/src/modules/parcel-sync/services/esri-geojson.ts index 820b0f9..d97c217 100644 --- a/src/modules/parcel-sync/services/esri-geojson.ts +++ b/src/modules/parcel-sync/services/esri-geojson.ts @@ -13,7 +13,7 @@ export type GeoJsonMultiPolygon = { export type GeoJsonFeature = { type: "Feature"; properties: Record; - geometry: GeoJsonPolygon | GeoJsonMultiPolygon; + geometry: GeoJsonPolygon | GeoJsonMultiPolygon | null; }; export type GeoJsonFeatureCollection = { diff --git a/src/modules/parcel-sync/services/gpkg-export.ts b/src/modules/parcel-sync/services/gpkg-export.ts index 9ea6a75..2668631 100644 --- a/src/modules/parcel-sync/services/gpkg-export.ts +++ b/src/modules/parcel-sync/services/gpkg-export.ts @@ -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,33 +60,96 @@ export const buildGpkg = async (options: GpkgBuildOptions): Promise => { try { let first = true; for (const layer of options.layers) { - const geojsonPath = path.join(tmpDir, `${layer.name}.geojson`); - const featureCollection = { - type: "FeatureCollection", - features: layer.features, - }; - await fs.writeFile(geojsonPath, JSON.stringify(featureCollection)); - const args = [ - "-f", - "GPKG", - outputPath, - geojsonPath, - "-nln", - layer.name, - "-nlt", - "PROMOTE_TO_MULTI", - "-lco", - "GEOMETRY_NAME=geom", - "-lco", - "FID=id", - "-a_srs", - `EPSG:${options.srsId}`, - ]; - if (!first) { - args.push("-update", "-append"); + // 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: spatialFeatures, + }; + await fs.writeFile(geojsonPath, JSON.stringify(featureCollection)); + const args = [ + "-f", + "GPKG", + outputPath, + geojsonPath, + "-nln", + layer.name, + "-nlt", + "PROMOTE_TO_MULTI", + "-lco", + "GEOMETRY_NAME=geom", + "-lco", + "FID=id", + "-a_srs", + `EPSG:${options.srsId}`, + ]; + if (!first) { + args.push("-update", "-append"); + } + 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); + } } - await runOgr(args, ogrEnv); - first = false; } usedOgr = true; } catch { diff --git a/src/modules/parcel-sync/services/reproject.ts b/src/modules/parcel-sync/services/reproject.ts index a6c2dfb..20a3a8e 100644 --- a/src/modules/parcel-sync/services/reproject.ts +++ b/src/modules/parcel-sync/services/reproject.ts @@ -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",