fix(parcel-sync): fix ArcGIS 1000 server cap pagination + scan improvements
- eterra-client: detect server maxRecordCount cap in fetchAllLayerByWhere When server returns exactly 1000 (or other round cap) but we asked for 2000, recognize this as a server limit, adjust pageSize, and CONTINUE paginating. Previously: 1000 < 2000 -> break (lost all data beyond page 1). - no-geom-sync: count layers first, pass total to fetchAllLayer Belt-and-suspenders: even if cap detection misses, known total prevents early termination. Also use pageSize 1000 to match typical server cap. Clădiri count uses countLayer instead of fetching all OBJECTIDs. - UI: add include-no-geom checkbox in background sync section Users can toggle it independently of scan status. Shows '(scanare in curs)' hint when scan is still running.
This commit is contained in:
@@ -3068,6 +3068,30 @@ export function ParcelSyncModule() {
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Include no-geom toggle (works independently of scan) */}
|
||||||
|
{session.connected && (
|
||||||
|
<label className="flex items-center gap-2 cursor-pointer select-none ml-6">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={includeNoGeom}
|
||||||
|
onChange={(e) => setIncludeNoGeom(e.target.checked)}
|
||||||
|
disabled={
|
||||||
|
exporting ||
|
||||||
|
(!!bgJobId && bgProgress?.status === "running")
|
||||||
|
}
|
||||||
|
className="h-4 w-4 rounded border-muted-foreground/30 accent-amber-600"
|
||||||
|
/>
|
||||||
|
<span className="text-xs">
|
||||||
|
Include și parcelele fără geometrie
|
||||||
|
</span>
|
||||||
|
{noGeomScanning && (
|
||||||
|
<span className="text-[10px] text-muted-foreground">
|
||||||
|
(scanare în curs…)
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</label>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Row 2: Background sync buttons */}
|
{/* Row 2: Background sync buttons */}
|
||||||
{session.connected && (
|
{session.connected && (
|
||||||
<div className="grid gap-2 sm:grid-cols-2">
|
<div className="grid gap-2 sm:grid-cols-2">
|
||||||
|
|||||||
@@ -339,6 +339,11 @@ export class EterraClient {
|
|||||||
let offset = 0;
|
let offset = 0;
|
||||||
const all: EsriFeature[] = [];
|
const all: EsriFeature[] = [];
|
||||||
|
|
||||||
|
// ArcGIS servers have a maxRecordCount (typically 1000).
|
||||||
|
// If we request 2000 but get exactly 1000, we hit the server cap.
|
||||||
|
// Track this so we continue paginating instead of stopping.
|
||||||
|
let serverMaxRecordCount: number | null = null;
|
||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
const params = new URLSearchParams();
|
const params = new URLSearchParams();
|
||||||
params.set("f", "json");
|
params.set("f", "json");
|
||||||
@@ -375,11 +380,28 @@ export class EterraClient {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Detect server maxRecordCount cap:
|
||||||
|
// If we asked for more than we got AND the result is a round number
|
||||||
|
// (1000, 2000), the server likely capped us. Adjust pageSize to match.
|
||||||
|
if (
|
||||||
|
serverMaxRecordCount === null &&
|
||||||
|
features.length < pageSize &&
|
||||||
|
features.length > 0 &&
|
||||||
|
features.length % 500 === 0 // round cap: 500, 1000, 1500, 2000...
|
||||||
|
) {
|
||||||
|
serverMaxRecordCount = features.length;
|
||||||
|
pageSize = serverMaxRecordCount;
|
||||||
|
// Don't break — this is a full page at server's cap, continue
|
||||||
|
}
|
||||||
|
|
||||||
all.push(...features);
|
all.push(...features);
|
||||||
offset += features.length;
|
offset += features.length;
|
||||||
if (onProgress) onProgress(all.length, total);
|
if (onProgress) onProgress(all.length, total);
|
||||||
if (total && all.length >= total) break;
|
if (total && all.length >= total) break;
|
||||||
if (features.length < pageSize) {
|
|
||||||
|
// End of data: fewer features than the effective page size
|
||||||
|
const effectivePageSize = serverMaxRecordCount ?? pageSize;
|
||||||
|
if (features.length < effectivePageSize) {
|
||||||
const nextSize = PAGE_SIZE_FALLBACKS.find((s) => s < pageSize);
|
const nextSize = PAGE_SIZE_FALLBACKS.find((s) => s < pageSize);
|
||||||
if (total && all.length < total && nextSize) {
|
if (total && all.length < total && nextSize) {
|
||||||
pageSize = nextSize;
|
pageSize = nextSize;
|
||||||
|
|||||||
@@ -223,37 +223,54 @@ export async function scanNoGeometryParcels(
|
|||||||
|
|
||||||
// 2. Fetch remote GIS cadastral refs (lightweight — no geometry)
|
// 2. Fetch remote GIS cadastral refs (lightweight — no geometry)
|
||||||
// This is the source of truth for "has geometry" regardless of local DB state.
|
// This is the source of truth for "has geometry" regardless of local DB state.
|
||||||
// ~4 pages for 8k features with outFields only = very fast.
|
// Count first so pagination knows the total and doesn't stop early.
|
||||||
const terenuriLayer = {
|
const terenuriLayer = {
|
||||||
id: "TERENURI_ACTIVE",
|
id: "TERENURI_ACTIVE",
|
||||||
name: "TERENURI_ACTIVE",
|
name: "TERENURI_ACTIVE",
|
||||||
endpoint: "aut" as const,
|
endpoint: "aut" as const,
|
||||||
whereTemplate: "{{adminField}}={{siruta}} AND IS_ACTIVE=1",
|
whereTemplate: "{{adminField}}={{siruta}} AND IS_ACTIVE=1",
|
||||||
};
|
};
|
||||||
|
const [terenuriCount, cladiriCount] = await Promise.all([
|
||||||
|
client.countLayer(terenuriLayer, siruta).catch(() => 0),
|
||||||
|
client
|
||||||
|
.countLayer(
|
||||||
|
{
|
||||||
|
id: "CLADIRI_ACTIVE",
|
||||||
|
name: "CLADIRI_ACTIVE",
|
||||||
|
endpoint: "aut" as const,
|
||||||
|
whereTemplate: "{{adminField}}={{siruta}} AND IS_ACTIVE=1",
|
||||||
|
},
|
||||||
|
siruta,
|
||||||
|
)
|
||||||
|
.catch(() => 0),
|
||||||
|
]);
|
||||||
const remoteFeatures = await client.fetchAllLayer(terenuriLayer, siruta, {
|
const remoteFeatures = await client.fetchAllLayer(terenuriLayer, siruta, {
|
||||||
returnGeometry: false,
|
returnGeometry: false,
|
||||||
outFields: "OBJECTID,NATIONAL_CADASTRAL_REFERENCE,IMMOVABLE_ID",
|
outFields: "OBJECTID,NATIONAL_CADASTRAL_REFERENCE,IMMOVABLE_ID",
|
||||||
pageSize: 2000,
|
pageSize: 1000,
|
||||||
|
total: terenuriCount > 0 ? terenuriCount : undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
// 2b. Also fetch CLADIRI_ACTIVE count (lightweight, just OBJECTID)
|
// 2b. Also fetch CLADIRI_ACTIVE features (lightweight, just OBJECTID)
|
||||||
const cladiriLayer = {
|
const cladiriLayer = {
|
||||||
id: "CLADIRI_ACTIVE",
|
id: "CLADIRI_ACTIVE",
|
||||||
name: "CLADIRI_ACTIVE",
|
name: "CLADIRI_ACTIVE",
|
||||||
endpoint: "aut" as const,
|
endpoint: "aut" as const,
|
||||||
whereTemplate: "{{adminField}}={{siruta}} AND IS_ACTIVE=1",
|
whereTemplate: "{{adminField}}={{siruta}} AND IS_ACTIVE=1",
|
||||||
};
|
};
|
||||||
let remoteCladiriCount = 0;
|
let remoteCladiriCount = cladiriCount;
|
||||||
|
if (remoteCladiriCount === 0) {
|
||||||
try {
|
try {
|
||||||
const cladiriFeatures = await client.fetchAllLayer(cladiriLayer, siruta, {
|
const cladiriFeatures = await client.fetchAllLayer(cladiriLayer, siruta, {
|
||||||
returnGeometry: false,
|
returnGeometry: false,
|
||||||
outFields: "OBJECTID",
|
outFields: "OBJECTID",
|
||||||
pageSize: 2000,
|
pageSize: 1000,
|
||||||
});
|
});
|
||||||
remoteCladiriCount = cladiriFeatures.length;
|
remoteCladiriCount = cladiriFeatures.length;
|
||||||
} catch {
|
} catch {
|
||||||
// Non-fatal — just won't show clădiri count
|
// Non-fatal — just won't show clădiri count
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const remoteCadRefs = new Set<string>();
|
const remoteCadRefs = new Set<string>();
|
||||||
const remoteImmIds = new Set<string>();
|
const remoteImmIds = new Set<string>();
|
||||||
|
|||||||
Reference in New Issue
Block a user