105 lines
4.8 KiB
Markdown
105 lines
4.8 KiB
Markdown
# Architecture
|
||
|
||
## Layer overview
|
||
|
||
```
|
||
src/
|
||
├── routes/ # SvelteKit pages (thin; they compose features)
|
||
├── app.html # plain SPA shell
|
||
├── app.css # global styles (imports Bootstrap + bootstrap-icons)
|
||
└── lib/
|
||
├── domain/ # pure types + pure functions (no Svelte, no fetch)
|
||
├── state/ # store factories (persisted, broadcast across tabs)
|
||
├── api/ # HTTP client + resource modules (points, scenarios…)
|
||
├── auth/ # auth store, login/logout, requireAuthenticated()
|
||
├── i18n/ # t() store, locale loading, message dictionaries
|
||
├── map/ # IMap interface + MapLibre impl + plot helpers + tools
|
||
├── ui/ # library-agnostic primitives (panel, tab, editor…)
|
||
└── features/ # vertical slices — one folder per feature
|
||
├── auth/ # Navbar, LoginForm
|
||
├── footer/
|
||
├── prediction/ # ControlPanel, ScenarioPanel, Point/Scenario/Curve editors
|
||
├── tracking/ # TelemetryPanel
|
||
├── workspaces/ # multi-layer workspace system
|
||
├── timeline/ # global playback clock
|
||
└── settings/ # schema-driven settings panel
|
||
mocks/ # dev-server API mock (enabled via VITE_USE_MOCK_API)
|
||
```
|
||
|
||
**Rules of thumb**
|
||
|
||
- `domain/` depends on nothing.
|
||
- `state/`, `i18n/`, `api/` depend only on `domain/`.
|
||
- `map/` depends on `domain/` (and Maplibre, internally).
|
||
- `ui/` depends on `domain/`, `state/`, `i18n/` at most.
|
||
- `features/*` depend on everything in `lib/` but never on each other directly
|
||
— cross-feature coordination happens through stores (e.g. `workspacesStore`
|
||
is shared by prediction + timeline).
|
||
- `routes/` compose features. Pages stay thin.
|
||
|
||
## Data flow
|
||
|
||
### Stores
|
||
`$state` → persisted via `persisted()` (localStorage + BroadcastChannel for
|
||
cross-tab sync). `workspaces`, `settings`, and `auth` state live here.
|
||
|
||
### API calls
|
||
All go through `$api` (`src/lib/api/client.ts`) which:
|
||
- prepends `VITE_API_BASE_URL`
|
||
- reads the `csrftoken` cookie (priming it via `/api/csrf/` if missing)
|
||
- serializes JSON bodies and parses structured DRF errors into `ApiError`
|
||
- delegates 401s to the auth layer (`setUnauthorizedHandler`)
|
||
|
||
### Auth
|
||
`authStore` is the single source of truth for who is logged in. Pages that
|
||
require auth call `requireAuthenticated()` from `$auth` in `onMount` — this
|
||
refreshes state and redirects to `/login` if anonymous. The API client triggers
|
||
the same redirect on 401s.
|
||
|
||
### Map
|
||
The map library is behind the `IMap` interface. Every plot operation (line,
|
||
marker, circle) runs through a **Scene** — a named collection of layers owned
|
||
by a feature. When a feature is done with its layers, `scene.clear()` or
|
||
`map.disposeScene(name)` removes them all at once. This is how each workspace
|
||
paints an independent trajectory without stepping on its neighbors.
|
||
|
||
Swapping MapLibre for another library means implementing `IMap` once (see
|
||
`src/lib/map/maplibre.ts`) and updating `createMapLibreMap()` at one
|
||
call-site inside `Map.svelte`.
|
||
|
||
### Workspaces
|
||
A workspace is an independently-configured prediction layer. The user creates
|
||
several, edits their flight parameters separately, runs each one, and sees all
|
||
their trajectories overlaid on the same map with their own colors and
|
||
opacities. `workspacesStore.run(id)` calls the prediction API and stores the
|
||
result in-memory on that workspace.
|
||
|
||
`WorkspaceRenderer.svelte` listens to the workspaces store and repaints layers
|
||
when workspaces change. It also drives the **global timeline**: whenever
|
||
workspaces change it recomputes the global `[min, max]` time range (the union
|
||
of every visible workspace’s trajectory span) and updates the timeline store.
|
||
|
||
### Timeline
|
||
`timelineStore` is a plain writable backed by a `requestAnimationFrame` loop.
|
||
It exposes `play`/`pause`/`seek`/`setSpeed`. `WorkspaceRenderer` subscribes to
|
||
`time` and draws a cursor marker on each visible workspace at that timestamp.
|
||
Workspaces stay in sync because they all sample the same clock.
|
||
|
||
### i18n
|
||
`initI18n()` (called from the root layout) loads the saved locale from
|
||
localStorage, imports the JSON dictionary, and stores it. Components use the
|
||
`$t` store from `$i18n`:
|
||
|
||
```svelte
|
||
<h5>{$t('panel.workspaces')}</h5>
|
||
```
|
||
|
||
Adding a locale: drop `xx.json` under `src/lib/i18n/locales/`, register it in
|
||
`loaders` and in the `Locale` type.
|
||
|
||
## Build & deploy
|
||
|
||
- `@sveltejs/adapter-static` with `fallback: 'index.html'` produces a pure SPA.
|
||
- SSR is disabled in the root `+layout.ts` (`ssr = false`, `prerender = false`).
|
||
- CSS is imported once in `+layout.svelte` (which loads `app.css`).
|
||
- No server-side routes. All data comes from the Django REST backend.
|