|
| 1 | +# Master-file chooser for directory / multi-file Survex & Therion imports |
| 2 | + |
| 3 | +## Context |
| 4 | + |
| 5 | +A real Survex/Therion project folder often contains **several "master" files that describe the |
| 6 | +same caves multiple times** — e.g. the Migovec dataset |
| 7 | +(`/tmp/svx-test/migovecsurveydata/migovecsurveydata/`) has `system_migovec.svx`, `mig.svx`, |
| 8 | +`sistem_migovec_2017.svx`, `primadona_ubend_monatip.svx`, … each `*include`-ing overlapping |
| 9 | +sub-files, so the same caves appear ~4×. |
| 10 | + |
| 11 | +Because the browser cannot read the filesystem, the user can't select one master `.svx` and have |
| 12 | +the app follow its `*include`s (the included files aren't loaded). Their only options are: |
| 13 | +- **Directory pick** (`#caveDirInput`, `webkitdirectory`) — loads *all* files, but |
| 14 | + `findRootFile` (`src/io/cave-survey-helpers.js:140`) auto-picks the single highest-ranked master |
| 15 | + (`ranked[0]`), giving the user no say in *which* one. |
| 16 | +- **Multi-file pick** (`#caveInput`) — can't follow `*include`s across files the user didn't pick. |
| 17 | + |
| 18 | +**Goal:** after a directory (or multi-file) import, when more than one candidate master is |
| 19 | +detected, show a chooser so the user picks which master(s) to import; then import **only the |
| 20 | +chosen master('s) `*include`/`input` closure**, ignoring the duplicate masters. This bridges the |
| 21 | +browser limitation: pick the folder (all files in memory) → pick the master(s) → import its tree. |
| 22 | + |
| 23 | +Decisions (confirmed with user): |
| 24 | +- **Multi-select** chooser (checkboxes), top-ranked candidate pre-checked — lets the user import |
| 25 | + one variant *or* several genuinely-different caves in one action. |
| 26 | +- Chooser appears **only when ambiguous** (>1 candidate master). One master (or one master + its |
| 27 | + includes → 1 candidate) imports straight through, unchanged. |
| 28 | + |
| 29 | +## Approach |
| 30 | + |
| 31 | +`getCaves(textMap)` already parses from a single root via `#parseX(rootName, textMap)` → |
| 32 | +`flattenFile(...)` which follows `*include`/`input` and pulls in **only referenced files**. So |
| 33 | +importing "just the chosen master's tree" is simply `getCaves(textMap, chosenRoot)` — the other |
| 34 | +masters in `textMap` are never referenced, so never parsed. No duplicates. |
| 35 | + |
| 36 | +The chooser is a standard Promise-based modal (same pattern as |
| 37 | +`src/ui/encoding-selection-dialog.js` / `xyz-kind-dialog.js`, `dialog-overlay` CSS), shown from |
| 38 | +within `importFiles` (where `textMap` is built), so `getCaves`/`getCave` stay pure and |
| 39 | +deterministic for tests. |
| 40 | + |
| 41 | +## Changes |
| 42 | + |
| 43 | +### 1. `src/io/cave-survey-helpers.js` — expose all ranked candidates |
| 44 | +- Add `export function findRootFiles(textMap, opts)` that returns the **full ranked candidate |
| 45 | + list** as `[{ key, includeCount, title }]` (reuse the existing candidate/`ranked` logic from |
| 46 | + `findRootFile`, lines 149–187; `includeCount` = the `countPattern` match count already used for |
| 47 | + ranking; `title` = best-effort from `*title "…"` / `-title "…"` / first `*begin`/`survey` name, |
| 48 | + empty string if none). |
| 49 | +- Refactor `findRootFile` to `return findRootFiles(textMap, opts)[0]?.key ?? [...textMap.keys()][0]` |
| 50 | + so existing callers/behaviour are unchanged. |
| 51 | +- Add `export async function chooseRootImports(textMap, opts, dialog)`: |
| 52 | + - `const cands = findRootFiles(textMap, opts);` |
| 53 | + - `cands.length <= 1` → return `cands.map(c => c.key)` (0 ⇒ caller auto-detects; 1 ⇒ that key). |
| 54 | + - `> 1` → `const sel = await dialog.show(cands);` return `sel` (array of keys) or `null` if cancelled. |
| 55 | + |
| 56 | +### 2. `src/ui/root-file-selection-dialog.js` — new modal (template: `encoding-selection-dialog.js`) |
| 57 | +- `class RootFileSelectionDialog { async show(candidates) }` → `Promise<string[] | null>`. |
| 58 | +- Renders a checkbox list of candidates (label = relative `key` + `title` + `(N includes)`), the |
| 59 | + first (top-ranked) pre-checked; `dialog-overlay` / `dialog-container dialog-content` markup; |
| 60 | + "Import selected" (resolves selected keys) and "Cancel" / Escape / overlay-click (resolves |
| 61 | + `null`). Disable "Import selected" when nothing is checked. |
| 62 | + |
| 63 | +### 3. `src/io/therion-importer.js` & `src/io/survex-importer.js` — wire the chooser |
| 64 | +- Construct `this.rootFileDialog = new RootFileSelectionDialog();` (next to the existing |
| 65 | + `coordinateSystemDialog`). |
| 66 | +- Add optional `rootName` to `getCaves(textMap, rootName)`: if provided, parse from it |
| 67 | + (`#parseX(rootName, textMap)`); else `#findRootFile(textMap)` as today. `getCave` unchanged. |
| 68 | +- In `importFiles(filesMap, onCaveLoad)`, after building `textMap`: |
| 69 | + ``` |
| 70 | + const roots = await chooseRootImports(textMap, OPTS, this.rootFileDialog); |
| 71 | + if (roots === null) return; // user cancelled |
| 72 | + const targets = roots.length ? roots : [undefined]; // [] ⇒ auto-detect single tree |
| 73 | + for (const root of targets) |
| 74 | + for (const cave of await this.getCaves(textMap, root)) |
| 75 | + if (cave) await onCaveLoad(cave); |
| 76 | + ``` |
| 77 | + (`OPTS` = `THERION_OPTS` / `SURVEX_OPTS`.) `src/main.js` `#importCaveFiles` is unchanged. |
| 78 | + |
| 79 | +### 4. i18n — `src/i18n/translations/en.json` + `hu.json` |
| 80 | +- Add `ui.panels.rootFileSelection`: `title`, `message`, `importSelected`, `cancelled` |
| 81 | + (follow the `ui.panels.encodingSelection` convention, single-brace `{param}`). |
| 82 | + |
| 83 | +## Verification |
| 84 | + |
| 85 | +- **Unit** (`tests/unit/survex.test.js`, mirror in a therion test): |
| 86 | + - `findRootFiles` returns multiple ranked candidates for a textMap with two unconnected |
| 87 | + masters; returns one for a normal single-master map. |
| 88 | + - `getCaves(textMap, rootKey)` imports **only** the chosen master's tree: build a map with |
| 89 | + master A (includes a1/a2) + master B (includes b1/b2); assert `getCaves(map,'A.svx')` yields |
| 90 | + only A's caves and none of B's. |
| 91 | + - `importFiles` with a stub `rootFileDialog` (`show: () => ['A.svx']`) over a 2-master map calls |
| 92 | + `onCaveLoad` only for A's caves; a stub returning `null` imports nothing. |
| 93 | +- **Live (chrome-devtools)**: open the Migovec folder via *Open Cave Folder* → chooser lists the |
| 94 | + candidate masters (checkboxes, top pre-checked) → check only `system_migovec.svx` → Import → |
| 95 | + exactly one System Migovec tree appears (no 4× duplicates). Re-open and select two distinct |
| 96 | + masters → both import. Confirm a normal single-master folder imports with **no** dialog. |
| 97 | +- `npm test` green (esp. existing Survex/Therion importer + `findRootFile` behaviour). |
0 commit comments