Waypoint Pro V4.0 converts LandXML (Leica), Trimble JXL, GPX, and CSV waypoint files to GPX, Shapefile, CSV, and KML — with an integrated QAQC engine that detects survey grids, fits a geometric model, and flags mislabelled, displaced, or duplicate stations. All processing runs entirely in your browser; no data leaves your device. Multi-grid dataset processing capability.
L[n]S[n] convention (e.g. L13000S4550).L[n]S[n+1] convention. (e.g. Repeat of L7500S2400 → L7500S2401).xml, .jxl, .gpx, .csv. For multi-file uploads from a folder, use the 📁 Folder upload link.Click any stage to jump to its guide. The Auto Exports block opens the auto-export pathway.
Drag and drop a file onto the drop zone, or click to browse. Accepted formats: .xml (Leica LandXML), .jxl (Trimble JobXML), .gpx, and .csv. To merge multiple files from the same survey, use the 📁 Folder buttons — one per format type. All matching files in the selected folder are merged into a single dataset before processing.
As soon as a file loads, the app parses all waypoints, builds a working copy of the data, and generates export files in all formats. The dashboard, map, and data preview populate immediately. You can download at this point if you need no further processing.
Format notes:
CgPoint elements. Handles common Leica export quirks (bare ="" attributes, concatenated multi-file exports). Extracts CQ3D quality metric when present.<Reductions> section of Trimble JobXML files. Prefers WGS84 coordinates, falls back to Local if WGS84 is absent. Captures SurveyMethod, Classification, Grid coordinates, and QualityControl precision data — all available in CSV/SHP exports when Include all source fields is enabled.<wpt> waypoint elements only. Track points (<trkpt>) and route points (<rtept>) are ignored, as they lack the station names needed for QAQC.Points without names are assigned the sentinel UNKNOWN. During QAQC, unnamed points near a grid node are automatically assigned a suggested name via proximity matching (flagged as UNNAMED). Points far from any grid appear in the Non-Parsed section.
The Data Extraction Options panel controls the base content of your exports. Changes take effect immediately — no apply step needed.
Click ▶ Run QAQC Analysis. The engine analyses waypoints whose names follow the L[number]S[number] grid station pattern. It also recognises common field variants — spaces around the separator (L7400 S3750), alternative separator characters (L7400E3750, L7400-3750, L7400_3750), or a separator followed by a redundant letter (L7400-S3750). These non-standard forms are parsed, assigned to their grid, and flagged with FMT so they can be renamed to canonical form. Points with genuinely unrecognisable names — base stations, check shots, free-form labels — pass through to exports unchanged.
The engine detects survey grids automatically. It first identifies geographic clusters separated by gaps larger than 600 m, then within each cluster looks for label-namespace breaks and distinct inter-station bearing signatures to separate overlapping grids. For each grid it fits a robust affine model (GPS position as a linear function of L and S coordinates), rejects extreme outliers iteratively, and computes a per-grid RMSE. The final step assigns every parseable point to its best-fitting grid.
Two global controls appear in the QAQC panel header — changes take effect when you click Re-run Analysis:
max(this value, α × stationSpacing) with α = 0.20 for standard lines and 0.40 for tight infill (subSp < 50 m). EXTR is a separate, geometric threshold β × spacing (β = 0.50 / 0.65) representing "the label can no longer plausibly point to the closest node". The EXTR threshold itself is geometric (not directly user-set), but a residual at or below the DIST tolerance is silenced before classification — so raising this value suppresses EXTR flags as well as DIST. Raise this value to suppress noise on low-accuracy GPS; lower it for RTK-quality data.Each detected grid also has its own per-grid tolerance, line spacing, and station spacing controls inside its section of the QAQC panel. Changes to these take effect when you click ↵ Apply in that grid's section. Changes to any of these controls do not auto-trigger a re-run — use Re-run Analysis or ↵ Apply explicitly to avoid unintended re-analysis mid-entry. The ⊞ All Grids Map button opens a single overview map showing all detected grids simultaneously.
After flagging, the engine checks whether the pattern of LABEL flags on any survey line is consistent with a systematic S-coordinate origin offset — i.e. the operator started counting from a different S-value than the grid model expects. Two detection passes run:
Detected offset lines appear in an S-origin offset sub-panel inside the affected grid section. For each group you can Confirm (apply the corrected affine on re-run), Erroneous (treat as individual LABEL flags for manual review), or leave pending. If high variability is detected in per-line S-origins across a grid, a ⚠ S-VAR badge appears in the grid's header row as a warning — click it to jump to the S-origin sub-panel.
If the engine detects survey lines that share the same geographic grid orientation as an existing grid but use a discontinuous L-namespace (a large gap in L-values with no spatial gap), it flags them as an L-Origin Offset block. This typically means a second survey phase that continued from a different starting L-value. The affected lines appear in a sub-panel labelled L-ORIGIN OFFSET ← [parent grid name].
For each group — and for individual lines within it — choose one of three decisions:
If unsure, open the row map to compare GPS positions against the parent grid's node predictions — if the lines fit the parent model cleanly, use Merge; if they form a clearly separate block, use Confirm.
Decisions persist through re-runs. Ghost grids (confirmed sub-grids) are visible in the QAQC panel as dimmed sections and participate in flag detection but are excluded from the auto-merge pass.
When a survey line contains points at sub-spacing intervals (e.g. an extra station between S5000 and S5100 on a 100-station-spacing grid), the engine identifies these as infill stations. Infill points are stamped with an _isInfill marker and included in their own sub-panel within the grid section.
For each infill line you can Confirm (retain as infill in all exports — points are stamped with an _isInfill marker and surface in GPX [INFILL] tags, the SHP INFILL column, summary counts, and map markers) or Reject (treat as regular production). Click Confirm All / Reject All in the infill panel's grid-level row to act on all lines at once. Each line's row carries its own ✓ / ✕ per-line buttons for split decisions; clicking an already-active decision reverts that line to pending. Decisions take effect on the next QAQC re-run — commit via Apply Corrections in the main QAQC panel (which writes infill decisions alongside flag-row corrections and re-runs) or via Accept All ⚡ (which auto-confirms pending infill alongside OFST and NSD structural decisions). Sub-spacing controls and the No infill suppression toggle live in the Survey Spacing modal — opened via the Spacing button on each grid header — not in this panel.
Where infill stations densify points along a line, infill lines densify lines across the grid. When a survey adds a line at a fractional position of the base line spacing — e.g. L5300 on a 200 m line-spacing grid, halfway between L5200 and L5400 — the engine evaluates it as a candidate infill line rather than a mislabel. Candidates must pass bracketing, local-coverage, and per-point residual gates before they are offered for confirmation; their points are stamped with an _isInfillLine marker.
Detected infill lines appear in a dedicated Infill Lines subpanel (header ⚡ INFILL LINES — N lines, M pts) inside the grid section, directly below the Infill Stations subpanel. For each line, click ✓ to confirm — its points snap to their own fractional-L node and stop firing LABEL/DUPE on re-run — or ✕ to reject, treating the line as a labelling error so its points re-flag. Grid-level Confirm All / Reject All act on every line at once. Click a line row to expand it into per-point sub-rows; each sub-row has its own ✓ / ✕ buttons that override the line-level decision for that single point (useful when most of a line is good but one or two stations are typos). A point that is both a station-axis infill and on an L-axis infill line carries a grey ⥯ DUAL chip in the flag table. If the engine cannot decide between two equally consistent L-origin interpretations, a ⚠ AMBIGUOUS badge appears in the subpanel header — click it to set a Force Line Origin value in the Survey Spacing modal. Decisions commit on the next QAQC re-run via Apply Corrections or Accept All ⚡. The starting L-lattice (Force Line Origin) and a No L-axis infill opt-out live in the Survey Spacing modal, opened from the Spacing button on each grid header.
The Accept All ⚡ button in the QAQC panel header confirms all pending OFST groups, merges all pending NSD grids, and confirms all pending S-axis infill stations and L-axis infill lines in a single click — then immediately re-runs QAQC so those structural decisions are applied to the dataset. After re-run, the flag table refreshes and may still contain individual point flags (LABEL, DIST, DUPE, etc.) for your review. Use ✓ Accept All to bulk-accept the remaining rows, or review them individually, then click Apply Corrections when ready.
Flagged stations appear in the QAQC panel grouped by grid. Each row shows the current name, one or more flag tags, the residual distance from the predicted grid node, and — where the engine can identify a better name — a suggested correction. Colliding points appear as interactive sub-rows directly under the primary row; cross-grid collisions show a link to navigate to that point's own row in its home grid section.
For each row you can: Accept the suggestion (or your own edited name), Reject it (keep the original name), or type a custom name directly. Use ✓ Accept All / ✕ Reject All to handle all rows in one click. The Map button on each row opens a satellite view showing the GPS position, the predicted grid node, and any colliding points — useful for borderline cases. If a point appears to have been collected on the wrong grid, use the grid dropdown on that row to manually reassign it; the suggested name updates immediately to reflect the new grid's geometry.
Flag tags tell you why each point was flagged. Multiple tags can apply simultaneously:
L7400E3750, L7400-3750, L7400S 3750) instead of canonical L<n>S<n> form. The values are correct but the name must be standardised. May appear alongside LABEL if the station number is also wrong.Tags can combine. LABEL DUPE means the station number is wrong and the corrected name collides with another point. EXTR and TSCON are the highest priority — red, requiring immediate attention. UNNAMED, ORPH, and DUPE are moderate priority (orange). FMT is cosmetic only (yellow). DIST and XCOL shown with a green name indicate the label itself needs no change.
Repeat stations: S-values ending in 1–4 (or 6–9) are treated as repeat observations of the nearest standard station (S ending in 0 or 5). The engine orders same-node occupants by timestamp; the earliest gets the standard anchor name, subsequent arrivals get anchor+1, anchor+2, etc. Genuine repeat pairs are never flagged as duplicates.
Re-running QAQC: Adjust the grid count, tolerance, or per-grid spacing values and click Re-run Analysis or ↵ Apply to update results. No control auto-triggers a re-run. Accepted/rejected decisions are preserved when re-running as long as the algorithm's suggestion for that point hasn't changed. If a parameter change produces a different suggestion, that row resets to pending for fresh review.
Click Apply Corrections & Re-run to commit all accepted row decisions to the working dataset and immediately re-run QAQC against the updated data. Accepted renames are written into waypointData; deletions are committed to deletedPtKeys; rejected and pending rows keep their original names. Grid sections collapse after applying, the apply button label flips to ✓ Corrections Applied, and every export format regenerates as part of the re-run.
For a faster workflow, the Accept All ⚡ button (see Stage 3) handles all structural decisions and re-runs QAQC in one click. After re-run, use ✓ Accept All on remaining flag rows, then Apply Corrections here.
Applying corrections does not update Post-QC Processing outputs — changes in Stage 6 still require clicking ▶ Apply Settings.
The Post-QC Processing panel is fully static — nothing updates until you click ▶ Apply Settings. That single button triggers export regeneration, preview refresh, map redraw, and dashboard update.
-RTK) to all waypoint names in exports and display. Never stored in the working dataset.Enter comma-separated name prefixes in Include (only matching names shown) and/or Exclude (matching names hidden). Case-insensitive. Both default to blank (all points included). For gravity surveys, entering L in the include field filters out base stations and check points that use different prefixes.
The dashboard and cumulative chart count only points with valid L[n]S[n] grid names. Non-parseable waypoints (base stations, check points, free-form labels) are excluded from summary counts and the chart but remain visible in the data preview, map, and all exports.
PREV_NAME column of exports.All export formats reflect the current dataset state — including QAQC corrections, date filters, deduplication, grid toggles, the waypoint suffix, and any active filter rules.
PREV_NAME column preserves each point's original upload name.Prev_Name column (capitalisation matches the SHP PREV_NAME column) preserves each point's original upload name. When Include all source fields is on, all extra attributes from the source format (Trimble SurveyMethod, QC data, Leica CQ3D, etc.) are appended as additional columns. Can be re-imported for iterative workflows.The Auto Exports pathway runs the full processing pipeline — parse → QAQC → corrections → export — as a single guided run, with file selection, destination folders, and structural-decision defaults configured up front. Use it when survey files arrive in a known location, when destinations are stable across days, or when you want a one-button flow that lands corrected exports in pre-set folders without manual review at each stage.
Click the 📋 Run Auto Exports button at the top-right of the upload row. The Auto Exports popup opens.
Three modes, selected via the segmented control at the top of the popup:
The default folder-search extension is .xml. To change it, open More Options and switch the Folder search file type segmented control to .gpx, .jxl, or .csv.
Three independent export streams, each with its own segmented control to pick a destination mode:
.shp, .shx, .dbf, .prj, .cpg) in the chosen folder, matching the existing [prefix]-RTK-Completed_Stations.* filename so a live GIS layer pointed at this folder always reflects the latest data. Secondary destination available.Any section can be set to ✕ No export to skip that stream entirely.
Six toggles control how QAQC handles structural decisions and detected anomalies during the run. All default off.
L7400E3750, L7400 S3750) are renamed to canonical L[n]S[n] form. Only fires on grouped deviations (>10 points). Off → format suggestions surface as FMT flags.Click ▶ Generate. The app:
If unresolved flags remain after the auto-confirm pass, the popup minimises and the QAQC flag table appears for review. Accept suggestions, type corrections, or reject — the same review surface as the manual pathway. Your configured destinations are remembered while you work.
When you're done reviewing, click Apply + Auto Export in the QAQC apply row to continue.
The app applies your actioned corrections, re-runs QAQC against the corrected dataset, then writes the configured export files in sequence — GPX, then SHP zip, then GIS source overwrite. A summary appears in the popup listing each destination that was written.
If no flags remain after the auto-confirm pass, the run completes without surfacing the flag table. Exports write directly; the summary appears at the bottom of the popup.
End-to-end data flow from file input to export outputs. Each stage maps to a discrete processing phase in the app's JavaScript. waypointData[] is the single mutable array that carries points through every stage; pointsCache[] holds the immutable originals and is never modified after load.
Uses DOMParser to read LandXML documents, targeting CgPoint elements under the http://www.landxml.org/schema/LandXML-1.2 namespace with a fallback to namespace-less lookup. Common Leica export variants where the name appears as ="" or n="" are sanitised to name="" before parsing. Concatenated multi-file exports are recovered by wrapping in a <LandXMLCollection> envelope.
Extracted per point: name (name, n, or id attribute), lat/lon (WGS 84 decimal degrees), elevation (elevation, elev, or body text), timestamp (normalised to ISO 8601), role/description. The CQ3D quality metric and all non-core attributes are captured in _extra for optional export. Points with invalid coordinates trigger a warning modal.
Parses the <Reductions> section of Trimble JobXML files, extracting <Point> elements. Position is read from the <WGS84> child (Latitude/Longitude/Height in decimal degrees); if absent, falls back to <Local>. Points with only <Grid> or <ECEF> coordinates are skipped (no projection transform available in-browser). Unnamed points (no Name attribute) are assigned UNKNOWN.
All Trimble metadata is captured in _extra: SurveyMethod (Fix, Float, Autonomous, etc.), Classification (Normal, Control, Check, etc.), Code, Grid projected coordinates (North/East/Elevation), QualityControl1 (HorizontalPrecision, VerticalPrecision), and QualityControl2 (variance-covariance matrix: CovarianceXX through CovarianceZZ). These flow through to CSV and SHP exports when Include all source fields is enabled.
Imports <wpt> elements only — track points (<trkpt>) and route points (<rtept>) are silently ignored, as they lack station names. Namespace-aware lookup is used when the GPX declares an xmlns. Extracted per waypoint: lat/lon attributes, child elements <name>, <ele>, <time>, <desc>/<cmt>. Unnamed waypoints are assigned UNKNOWN.
Requires Name, Latitude, and Longitude columns (header matching is case-insensitive, trims whitespace, and recognises common aliases like Station, Easting, Northing). Elevation, Timestamp, and Description are optional. Any columns beyond the recognised set are captured in _extra. A CSV exported by this app can be re-imported — column structure is preserved for iterative field-office QA workflows.
pointsCache holds the original parsed records (including _extra) and is never modified after load. waypointData is rebuilt from pointsCache on every regeneration, with date filters, deduplication, and QAQC corrections reapplied in sequence. This ensures toggling any option always produces a deterministic result from the original source data.
When a dataset has fewer than 6 points with L[n]S[n] names but contains at least a dozen unnamed coordinate-bearing points, the engine attempts to infer the grid lattice from geometry alone and synthesise names before QAQC begins. This handles surveys collected without grid names in the field — typical of free-style RTK collection on a planned grid where the crew assigned generic point IDs. The bootstrap runs as the first action inside QAQC, before the parseable-points filter: it estimates the two grid axes (their bearings and spacings), assigns each point a line/station index, detects 1/2-fraction infill if present, resolves per-line station origin offsets, and writes a synthesised L#S# name onto each point that indexes cleanly. The result is a fully named dataset that flows into the normal pipeline — affine fit, flag generation, exports — with no special branching downstream.
If one or more grid-named anchors are already present, the cleanly-indexed anchors pin the labelspace by majority agreement so synthesised names match the existing convention — a single poor-geometry anchor is outvoted rather than trusted; otherwise a default origin of L5000S5000 is used. Points the engine cannot index cleanly remain nameless and surface in the standard non-parsed table. Non-grid datasets — track logs, scattered control points, anything where two coherent axes cannot be established — abort the bootstrap and fall through to the standard message that too few grid-named points were found for grid analysis to run. The current implementation handles a single grid per dataset and 1/2-fraction infill only; the original name is preserved on each point so the synthesis is reversible.
In order, on every regeneration: (1) date range filter removes points outside the selected window; (2) coordinate deduplication (pre-QAQC pass) — when the Remove duplicates (identical coordinates) toggle is on, removes points that share an exact latitude/longitude pair, retaining the earliest-timestamped record per coordinate; (3) QAQC corrections from the last Apply are re-applied as name substitutions; (4) preview renames from the current session are re-applied (keyed by stable _originalName|time so they survive the rebuild); (5) soft-deleted points are removed; (6) if QAQC has been run, it reruns on the cleaned dataset to keep grid models and flag lists in sync; (7) a second pass applies the Post-QC proximity deduplication (radius-based, configurable in the Post-QC Processing panel) and exact-name deduplication if those options are enabled.
Only points whose names match /^L(\d+)S(\d+)/i are analysed. The L and S numeric values are parsed directly from the name as floating-point numbers; zero-padding width is recorded separately and used when formatting suggested names. All other points pass through to exports unmodified.
Parseable points are projected to a local metre-scale coordinate system using a centroid-anchored cosine correction for longitude. Two independent 1D gap scans are then run — one along the northing axis, one along the easting axis — each looking for consecutive inter-point gaps exceeding 600 m. A new geographic cluster is declared only when both axes independently agree there is a gap at approximately the same location. This dual-axis requirement prevents false splits on highly rotated grids that span a wide latitudinal range but remain geographically connected.
Within each spatial cluster, the unique L-values (and separately S-values) are inspected for a bimodal gap. The mode of small consecutive gaps is computed as the typical label spacing; a split is accepted only if the largest gap exceeds 3× that typical spacing and the left subgroup contains at least 6 points and the right subgroup at least 2. This catches grids that share geographic space but use non-overlapping label ranges (e.g. Grid A uses L8000–L12000 and Grid B uses L18000–L20000). Namespace splitting is applied exactly once at the top level and deliberately not called recursively — recursive calls produce false S-axis splits when grids have different line lengths.
After spatial and namespace splitting, each remaining sub-cluster is examined for multiple grid orientations within the same geographic and label space. The approach exploits the fact that consecutive stations on the same survey line share a consistent bearing and distance that uniquely fingerprints their grid.
(cos θ, sin θ, log(dist) × 0.4). Mean-shift is run on this 3D feature space with bandwidth 0.10 (tuned to resolve grids separated by as little as 25° in azimuth). Converged seeds within 60% of the bandwidth are merged into modes. Modes with fewer than 15 contributing vectors are discarded as spurious.After structural clustering, candidate grid blocks whose bounding boxes overlap or are adjacent (within ~500 m) are checked for similarity. Two blocks are merged if their directed station-axis azimuth difference is less than 15° (mod-360, not mod-180 — the mod-180 form aliases opposite-facing grids) and their line-spacing ratio is less than 1.25. Merging uses iterative refinement until no further merges are possible.
If the user has entered a specific grid count, the pipeline attempts to reach that target by: splitting the highest-RMSE block (namespace gap split first, then k-means++ on affine residual error vectors as fallback) if too few grids exist; or merging the most similar adjacent pair (weighted by azimuth difference and spacing ratio) if too many exist. A warning is shown when the forced count differs substantially from the auto-detected count.
Once the grid set is stable, a final iterative reassignment pass runs across all grids simultaneously. Every parseable point — including those that were in small sub-clusters — is assigned to its lowest-residual grid, with transforms refit after each round.
After geometric assignment converges, points in an "uncertain bucket" (residual between 0.5× and 1.5× their current grid's tolerance) are tested against alternative grids using two signals. First, a timestamp score counts how many same-day neighbours the point shares on each candidate grid (points are grouped by predicted L-line bucket, rounded to the nearest 50 label units). Second, the point's current name is tested against both the current and the alternative grid's predicted node. Three outcomes are possible:
The affine transformation maps grid coordinates (L, S) to geographic coordinates (lat, lon):
The 6 parameters are solved by Gaussian elimination with partial pivoting on the normal equations. Grid azimuth is derived from the S-direction coefficients (b, e) — the direction perpendicular to survey lines — using atan2(e × mLonScale, b × 111320). Line spacing in metres is derived from the magnitude of the L-direction vector (a, d). Inlier RMSE is the root-mean-square of residuals after robust rejection. These values are reported in the per-grid section of the QAQC panel.
The robust fitting procedure runs up to 5 iterations: fit all points → compute residuals → exclude points with residual > mean + 3σ → refit → repeat. Iteration stops early if no points are excluded or if fewer than 6 inliers would remain. The inliers from the final pass drive the adaptive flagging threshold.
Line and station spacings used for node snapping are detected from the modal gap in the observed L- and S-value distributions, not from the affine parameters. This makes snapping robust to irregular label numbering (e.g. L values that increment by 50 rather than 1).
The standard 6-parameter affine requires at least two distinct L-values. When a grid presents only a single line of stations (degenerate L-axis), the fit is opt-in via opts.allowSingleLine: the engine collapses to a 4-parameter L-on-S affine, stamps the grid with _singleLine = true, and propagates that flag through the residual pipeline so DIST/EXTR thresholds and label suggestions remain geometrically meaningful. When the opt-in is off, single-line grids are detected, reported in the affine summary, but excluded from flagging.
The L-axis infill subsystem teaches the QAQC engine to recognise fractional-L lines — survey lines at a sub-multiple of the grid's base line spacing (e.g. L5300 on a 200 m grid) — as legitimate densification rather than mislabel candidates. It is the structural mirror of the S-axis infill-station subsystem.
Detection (Phase 12.1). _detectInfillLines(grid, gi) runs once per grid immediately after the S-axis _detectInfill, temporally after Phase 12.5 shift-table population (the phase number 12.1 is a name, not a sequence — it runs after 12.5 so the shift-aware residual check is available). The pass short-circuits when infillLineNoInfill[gi] is set. Otherwise it selects a base line spacing from observed-gap candidate modes — or from userSpacings[gi].line when the user has forced one — commits an L-origin into g._lineOrigin, and classifies each observed L-value as standard or candidate-infill. Candidates are gated through three checks: bracketing (which immediate-slot std-lattice lines are observed in the grid above and below the candidate), local coverage of those bracketers (T_local = 0.50 for two-bracketer cases; T_local_edge = 0.67 for single-bracketer cases), and a shift-aware per-point residual check via the node(gi, l, s) wrapper. The no-bracketer relaxation (V4.0005, Q-1): a candidate with neither immediate-slot std-lattice bracketer observed in the grid still passes when (a) ≥ 5 observed stations, (b) the candidate's offset matches a canonical fraction of the base spacing (1/2, 1/3, 2/3, 1/4, 3/4) within tolerance, and (c) every station's S-value lies on the grid's std S-lattice — boundary infill lines at the top or bottom of a grid (where the upper or lower bracketer wasn't surveyed) are now detectable; genuine off-lattice rows still fail. The residual check is OFST-interpolation-aware (V4.0005, Q-2): when bracketers carry non-equal OFST shifts, the candidate's expected position is computed via linear interpolation of those shifts across the candidate's mid-step L — so an L-axis infill line between a clean bracketer and a fractionally-OFST bracketer still stamps cleanly. Points on a PASS-classified line that clear the per-point residual gate are stamped _isInfillLine on the persistent waypointData object; per-grid results are written to qaqcInfillLineGroups[gi] and infillLineDetectedSpacing[gi]. PASS-line points that fail the residual gate are typos — they are not stamped, and fall through to LABEL.
Base line spacing (Phase 9). detectBaseLineSpacing(pts, options) returns { lineSpacing, lineOrigin } — the helper's earlier ambiguous return was removed at V&V C4 (V3.05206) because the authoritative ambiguity write happens at Phase 12.1's commit step, not here. Precedence at the Phase 9 call site: a user-forced Force Line Origin / Line spacing (from the Survey Spacing modal) wins; otherwise a project-wide cached default (qrDefaultLineSpacing) is written into userSpacings[gi] once per grid per file load via _qrCacheAppliedThisLoad before the helper is invoked; otherwise the helper derives lineSpacing from observed-gap candidate modes. Only lineSpacing is destructured at the Phase 9 site, but the same call retains _blsResult.lineOrigin for g._lSnapOffset construction (DR-L6 / V3.05214) — the std-lattice origin consumed by _snapL's fallback path.
Slot-engine integration (Phase 13). _snapL(val, g, labelL, anchorS) consults the runtime _confirmedInfillLineKeys Set: when the key ${gi}:${Math.round(labelL)}:${anchorS} is present, the function returns Math.round(labelL) directly — the point's own fractional-L position — bypassing the std-lattice snap. Otherwise it returns the nearest std-lattice node via g._lSnapOffset + g.lineSpacing. Phase 13's _nodeOccupantsMap consumes the result for bucket keying, so confirmed-infill-line points get their own buckets and stop firing LABEL/DUPE; rejected or unconfirmed candidates remain on the standard lattice and continue to flag.
State & persistence. The user's decisions persist across re-runs — infillLineConfirmedState (per-line gi:line:lineL and per-point gi:pt:idx shapes), userLineOrigins (per-grid Force Line Origin), and infillLineNoInfill (per-grid No L-axis infill opt-out). These reset only at file-load reset sites (currently 10 per V&V C2 / V3.05206). The engine's detections are recomputed on each runQAQC — qaqcInfillLineGroups, infillLineDetectedSpacing, qaqcInfillLineAmbiguous, and the per-grid derived g._lineOrigin. The runtime _confirmedInfillLineKeys Set is cleared once at the top of each runQAQC then rebuilt per-grid during Phase 12.1's pass from infillLineConfirmedState ∩ per-point gate survivors. applyQAQCCorrections clears _isInfillLine stamps on points belonging to ERRONEOUS decisions before exports rebuild (V3.05222 parity fix) — both the per-point gi:pt:idx and per-line gi:line:lineL ERRONEOUS shapes — so the brief Apply→re-run window does not surface stale state in summary tiles or the chart infill series.
L-origin ambiguity. When the observed line distribution is symmetric, two L-origin interpretations can be equally consistent. The engine commits one via a deterministic ordering — snap-consistency against g._lSnapOffset is the primary key (DR-L5 / V3.05213, introduced to keep detection-side origin consistent with the snap-path lattice), with descending std-line count and lex-min std-set comparison as secondary tie-breaks. When the top two eligible origins yield equal std-line counts, qaqcInfillLineAmbiguous[gi] is set, surfacing the ⚠ AMBIGUOUS badge in the Infill Lines subpanel header. Forcing a Line Origin in the Survey Spacing modal short-circuits the eligible-origins walk entirely (gated by the UI guard that disables the origin input until a Line spacing is also set) and removes the ambiguity.
Auto-exports. The Auto Exports pipeline confirms detected infill lines automatically when the Auto-confirm infill lines toggle (qrInfillLineAutoConfirm) is on — independent of the station-infill toggle (qrInfillAutoConfirm) per V3.05211, after the L-3 sub-batch's earlier unified gating was split. The post-run summary log line groups S-axis and L-axis confirmations into a single ${N} infill confirmed count via the shared _infillConfirmedCount counter.
A power-user toggle in the Auto Exports popup (More Options panel) that pre-densifies any grid with detected infill before the QAQC re-run, so every collected point — anchors and what would have been infill candidates — is resolved as a standard anchor of the densified lattice. Default OFF; setting is sticky (localStorage).
When enabled, the Auto Exports pipeline inserts a Pass 1e step between Run 1 and Run 2 of _qrRunPipeline. For each grid the pipeline runs both axes in parallel:
infillDetectedSpacing[gi]) and the user hasn't already forced a station spacing, the pipeline writes the detected sub-spacing into userSpacings[gi].station and sets infillNoInfill[gi] = true. Run 2's Phase 9 picks up the new station spacing; Phase 12's _detectInfill early-exits on the no-infill flag; every downstream station — including what would have been infill — is treated as a standard anchor of the densified station lattice.infillLineDetectedSpacing[gi]) and the user hasn't already forced a line spacing, the pipeline writes the detected L-sub-spacing into userSpacings[gi].line and sets infillLineNoInfill[gi] = true. Run 2's Phase 9 picks up the new line spacing; Phase 12.1's _detectInfillLines early-exits on the no-infill flag; every formerly-fractional-L line becomes std on the densified L-lattice and no _isInfillLine stamps survive. Closes the prior inconsistency where GPX/SHP [INFILL] tagging (which reads _isInfill only) treated the toggle as effective for S-axis only while chart/summary tile counts (which also read _isInfillLine) still surfaced L-axis-infill points in the infill series.Either axis can fire independently. The notice surfaces both as separate entries (e.g. L7050 (75m → 50m) for an S-axis densification, L7050 (150m → 75m, line) for an L-axis densification on the same grid). Skip rules apply per axis — see below.
The pipeline respects explicit user instructions and skips each axis's rewrite independently. The S-axis rewrite is skipped when (a) userSpacings[gi].station is already set (the user picked a station spacing in the Survey Spacing modal) or (b) infillUserSpacing[gi] is set (the user explicitly declared an S-axis sub-spacing — suppressing infill would silently override them). The L-axis rewrite is skipped when userSpacings[gi].line is already set (the user forced a line spacing). Both axes are skipped when (c) the grid is about to be NSD-merged-away (its stateKey is in _nsdMergedKeys) — writing to a soon-to-be-deleted gi is moot. User station origins (userStationOrigins[gi]) and L-origin (userLineOrigins[gi]) are intentionally not in the skip predicate — origins are base-invariant in physical metres (Inv-5), so densifying the spacing doesn't conflict with a user-set origin.
Side effects persist in-app after the run. The rewrites to userSpacings[gi].station and userSpacings[gi].line are normal Survey Spacing settings — they don't auto-clear when the toggle is later turned off. Revert by clearing the spacing in the Survey Spacing modal for the affected grid(s), or by reloading the file (which resets all user spacings). Each Pass-1e rewrite is recorded in _qrLastRunTreatedAsStandard[] (with an axis: 'S' | 'L' tag per entry) and surfaced as a notice line in the auto-exports completion summary.
This toggle is mutually exclusive with the Auto-confirm station infill toggle in the same More Options panel — enabling one disables the other. The two are complementary strategies for handling infill: auto-confirm accepts the infill detection and treats those points as confirmed infill; "Assume all primary" rewrites the spacing so infill detection doesn't trigger in the first place.
Use this mode when the operator knows ahead of time that all collected stations are intended as primary anchors on a densified grid — for example, a survey collected at 25 m station spacing reusing planned data that was generated at 50 m spacing. In this case the "infill" pattern is the intended grid, not an exception, and reviewing each infill grid through the standard subpanel adds no value.
Both gates are derived from the line's operative lattice spacing (g.stationSpacing for standard lines, infillDetectedSpacing[gi] for confirmed-infill lines). A residual at or below distThresh is silent. A residual above distThresh but at or below extrThresh fires DIST (label is plausibly correct, position is moderately off). A residual above extrThresh fires EXTR — the label can no longer plausibly point to the closest lattice node. α = 0.20, β = 0.50 on standard lines and infill at subSp ≥ 50 m; α = 0.40, β = 0.65 on tight infill (subSp < 50 m). The user-minimum is the DIST floor on standard lines (default 15 m); a per-grid override in the Survey Spacing modal replaces the DIST threshold for that grid only. The extrThresh threshold is geometric — β × spacing, a property of the lattice, not GPS noise, and not directly user-set. Note, however, that the Pass 1 gate silences any residual at or below distThresh before DIST/EXTR classification — so raising the Min DIST tolerance (or a per-grid override) above a point's residual suppresses its flag, EXTR included. Inlier-set membership remains the affine-fit gate (Phase 9 / Phase 12.6 RMSE) but is no longer an EXTR signal.
For each flagged point: (1) the affine inverse (Cramer's rule on the 2×2 system formed by the L and S columns) gives a continuous predicted L and S; (2) these are snapped to the nearest observed L- and S-values using the detected spacing, producing a canonical grid node; (3) the node's geographic position is back-projected through the forward affine; (4) if multiple points occupy the same node, they are sorted by timestamp and assigned the anchor name (S ending in 0 or 5) for the earliest, then anchor+1, anchor+2, etc. for each subsequent arrival. Zero-padding width is preserved from the original dataset.
A collision is detected when another parseable point either shares the suggested name or lies within 0.5× the minimum grid spacing of the predicted node. All parseable points are considered as potential collision partners — there is no residual restriction. Genuine repeat-station pairs — same L, same anchor-S (where anchor = ⌊S / 5⌋ × 5) — are excluded from collision detection regardless of proximity. If any collision partner is on the same grid, DUPE fires. If any partner is on a different grid, XCOL fires independently — both tags can appear together when a point collides with partners on both grids. XCOL is a structural observation (disabled by default) and does not imply an error on its own; TSCON separately handles cases where cross-grid overlap is accompanied by timestamp or label inconsistency.
Multiple tags can apply simultaneously to a single point:
| Tag | Condition | Typical action |
|---|---|---|
TSCON | Timestamp coherence conflict — collection date matches neighbours on a different grid, but the name does not fit that alternative grid's predicted node. GPS position, timestamp, and name are mutually inconsistent. Only fires when a timestamp is present; timestampless points are unaffected. | Human review required — check map, timestamp, and grid assignment |
EXTR | Residual exceeds the geometric extreme threshold (β × line spacing) — the label can no longer plausibly point to the closest lattice node. Suggested name is unreliable. | High priority — likely a gross GPS error or wrong node |
XCOL | Cross-grid node co-occupancy — a point on a different grid predicts to the same node. Structural observation only, not an error flag. Disabled by default; enable via the XCOL Flags toggle. TSCON handles cases where the overlap is accompanied by timestamp or label inconsistency. | Check row map for context; no rename needed unless TSCON or LABEL also present |
DUPE | Collision with a same-grid point. Independent of XCOL — both can fire. | Review both rows; delete the duplicate or assign a repeat slot |
FMT | Name is parseable but not in canonical L<n>S<n> form (e.g. uses E/W/N separator, dash, underscore, or spaces) | Accept to rewrite to standard form |
LABEL | Predicted station number differs from current name. May co-occur with FMT when both format and number are wrong. | Accept the suggestion or type a corrected name |
ORPH | Anchor missing (repeat-slot name with no S ending in 0/5 at this node), or a consecutive repeat slot is absent in the sequence (e.g. S5000 + S5002 with no S5001) | Accept to rename to the expected slot, or investigate the missing station |
UNNAMED | Point has no name; proximity-assigned to a grid node | Accept to apply suggested name; reject to leave unnamed |
DIST | Residual exceeds threshold, name appears correct, no other flag applies | Accept to note the poor GPS fix; name stays unchanged |
DIST is the fallback tag — assigned only when no other flag applies. DIST with a green name means the label is correct, only the GPS fix is suspect. XCOL alone (also green) is a structural note that two grids share a node — no rename needed and no action required unless TSCON or LABEL also appear. The current name text colour reflects the worst tag on that row: red for EXTR/TSCON/LABEL, orange for UNNAMED/ORPH/DUPE, yellow for FMT, green for DIST or XCOL only.
Clicking Apply Corrections writes all accepted renames into a qaqcCorrections map keyed by original name. On every subsequent regeneration this map is re-applied to waypointData after parsing, ensuring corrections persist through option changes. Rejected rows keep their original names and are not entered into the corrections map. Soft-deleted points (tracked by a set of unique GPS-coordinate + name keys) are filtered out at the same stage. The four export formats (GPX, Shapefile, CSV, KML) are rebuilt from the corrected, filtered dataset.
The Data Preview table supports two types of in-session edits that operate independently of the QAQC correction system:
Tracked in a deletedPtKeys Set keyed by ptDeleteKey(pt) — a stable key using the raw upload name (_originalName||name) and timestamp, so the entry survives QAQC renames. Deleted rows remain visible in the preview table (strikethrough, greyed) — they are not removed from the DOM. On every regenerateExports() call, deletedPtKeys is re-applied as a filter step after QAQC corrections, so the deletion persists through all option toggles. The GPX, Shapefile, CSV, and KML builders each independently filter with !isPtDeleted(pt) (buildGpxFromData, buildShapefile, buildCSV, buildKml), preserving the deletion across format-specific code paths.
Tracked in a previewRenames Map keyed by ptRenameKey(pt) — a stable key using _originalName|time rather than the current mutable pt.name. Each entry stores { original, newName }. On every regenerateExports() call, previewRenames is re-applied after qaqcCorrections, using the stable key to locate points even after waypointData is rebuilt from pointsCache. The revert target is always the post-QAQC name at the moment the first edit was committed — never the raw upload name. When the Include previous name toggle is on, the raw upload name is preserved as the PREV_NAME column in Shapefile exports and the Prev_Name column in CSV exports.
Both deletedPtKeys and previewRenames are cleared on every file load (all four load paths: single XML, folder XML, CSV, and QAQC-corrected re-export). They are also cleared in qrClose() so Auto Exports session state does not leak between pipeline runs.
The filter operates on the final waypointData array after all other processing. It is evaluated by the passesFilter(name) function, which applies two prefix lists:
The filter always controls what appears in the dashboard summary counts, the daily chart, the satellite map, and the data preview table. It does not affect export files unless the Remove filtered stations from export toggle (in the Post-QC Processing panel) is ON. When that toggle is enabled, passesFilter() is additionally called when building each export blob — points that fail the filter are omitted from GPX, Shapefile, CSV, and KML output.
In addition to the prefix filter, the Station Summary counts and the Cumulative Stations chart apply a parseLS() gate: only waypoints whose names resolve to a valid L[n]S[n] grid format are counted. Non-parseable waypoints pass through to the map, preview table, and exports unaffected, but are excluded from all summary statistics and the chart's new production / repeat / infill / cumulative series.
The isGridEnabled(pt) function provides a parallel gate for the per-grid export toggles. Non-grid points (those without a _gridIdx stamp, i.e. non-L[n]S[n] names) always pass this gate regardless of the toggle state.
| Limitation | Detail |
|---|---|
| Max 7 grids | The pipeline caps at 7 simultaneous grids. Datasets with more distinct grids will have some merged or dropped; use the grid-count override if you know the correct number. |
| Similar azimuths | Grids whose station-axis azimuths differ by less than ~15° in the same spatial area may be merged. The structural clustering resolves most cases, but very close azimuths remain the hardest scenario. |
| Overlapping label namespaces | When two overlapping grids share the same L/S number range, separation relies entirely on the structural (bearing-based) approach. Results are generally good but borderline points near shared margins may be misassigned. Manual reassignment is available via the grid dropdown on each flag row. |
| Sparse grids | A grid needs at least 6 L/S-named points to fit the 6-parameter affine model. Sparser grids are excluded from QAQC; their points pass through unchanged. |
| Non-standard name formats | Names using alternative separators (E/W/N, dash, underscore, spaces around S) are parsed and grid-assigned; they are flagged with FMT and suggested a canonical rename. Names that are genuinely unrecognisable (base stations, check shots, free-form labels) are not grid-assigned and pass through unchanged. |
| WGS 84 only | All coordinates are treated as WGS 84 decimal degree latitude/longitude. Projected coordinate systems (UTM, NAD83, etc.) will produce grossly incorrect results. |
| Missing timestamps | Files without timestamps skip the TSCON coherence check entirely. All geometric flag types (DIST, LABEL, DUPE, EXTR) remain fully active. XCOL is available via its toggle. Timestampless points whose label does not fit their geometrically assigned grid are passed through a label-aware reassignment pass — if the label fits an alternative grid and geometry does not strongly contradict it, the point is silently reassigned before flagging. |
| Large datasets | Files exceeding ~50,000 points may produce noticeable QAQC processing delay and elevated memory use during map rendering. Consider splitting very large multi-day datasets. |
ℹ By default, these rules affect station counts, the chart, the map, and the data preview only — all waypoints are written to every export format (GPX, Shapefile, CSV, KML) regardless. To exclude filtered points from exports as well, enable Remove filtered stations from export in the Post-QC Processing panel.
| Actions | # | Name | Latitude | Longitude | Elevation | Timestamp |
|---|