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:
@@ -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,33 +60,96 @@ export const buildGpkg = async (options: GpkgBuildOptions): Promise<Buffer> => {
|
||||
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 {
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user