Per-map local state persistence¶
Decisions¶
| Decision | Choice | Why |
|---|---|---|
| Storage scope | One client-only state namespace per map id | Returning to one map must not affect another map's view or controls |
| State restored | Map center and zoom, view/edit/settings mode, layer visibility, and active tabs in the Features and Avesmaps controls |
These are view preferences; editing drafts, selected objects, and open popovers remain transient |
| Initial values | Existing map defaults are used until valid saved state has been hydrated | Server rendering and the first client render must remain deterministic |
| Invalid state | Ignore malformed, out-of-range, unknown, or stale entries and retain valid current entries | Maps and layer/type definitions can change over time without making the map unusable |
| Storage failure | Fall back to in-memory/default state without showing an error | localStorage is an optional enhancement and can be blocked or full |
| Persistence boundary | Server-persisted map defaults provide shared fallback configuration; browser-local state remains the authoritative override when valid | Defaults are shared per map, while the current browser's view preferences remain browser-specific |
| Existing controls | Preserve the current per-map visibility keys and behavior where valid; add only the missing view-state persistence | Avoid breaking preferences already stored by existing users |
Prerequisites¶
- Keep Leaflet imports client-only. Any local-storage read must run after mount or through a client-only hook; no browser API may execute during SSR.
- Treat
app/routes/project-map.tsxas the composition owner: it suppliesmap.id, source-specific defaults, and the mode hook, while map-specific hooks/components own their local state. - Confirm the existing source defaults in
app/components/map.tsx: upload maps start at[0, 0]and zoom0; tile-set maps start at[0, 0]andtileSet.minZoom. - Preserve the current disabled-id strategy in the layer controls so newly added layers and object types default to visible.
- Reconcile stored layer ids against the currently supplied layers/types. Do not let removed ids affect rendering or prevent new entries from appearing.
- No Directus field is read or written. This workspace has no
directus/schema/*.jsonsnapshot, and the feature requires no schema change or authorization work.
Step 1 - Define the client-only persistence contract¶
Goal: Establish one consistent, typed approach for reading and writing per-map local state without changing server data contracts.
Scope
- In: Add the smallest shared browser-state utility or hooks needed by map view and mode persistence; keep existing control-specific APIs unless consolidation clearly reduces duplication.
- In: Define stable storage-key names scoped by
map.id, typed parsing/validation rules, and guarded read/write behavior for malformed or unavailable storage. - In: Ensure writes are suppressed until the corresponding saved value has been read, so initial defaults cannot overwrite a user's saved state.
- Out: Any server persistence, URL parameters, cross-device synchronization, storage migration UI, or changes to
MapProject/MapRef.
Acceptance criteria
- No local-storage access occurs during SSR.
- A storage read failure, parse failure, or write failure leaves the map usable with defaults or current in-memory state.
- State is isolated by map id and each state category has a stable, non-colliding key.
-
pnpm typecheckpasses.
Step 2 - Persist and restore the map view¶
Goal: Reopening a map restores its last valid center and zoom while preserving Leaflet's source-specific bounds and CRS.
Scope
- In:
app/components/map.tsx, accept an optional initial center/zoom and add a client-only view observer for Leaflet move/zoom changes. - In:
app/routes/project-map.tsxor a focused map-view hook, load and validate the saved{ center, zoom }formap.id, apply valid local center and zoom independently over the persisted map default and source fallback, and persist changes from Leaflet events. - In: Validate finite latitude/longitude and a zoom compatible with the current source's min/max bounds; discard invalid or stale values rather than forcing Leaflet into an invalid view.
- In: Avoid writing the source default before hydration and avoid fitting or resetting the map when the user changes mode or layer visibility.
- Out: Adding other server-side settings, changing project settings, or altering tile/image bounds behavior. Map default center/zoom is shared fallback configuration, not browser-local view state.
Acceptance criteria
- A valid saved center and zoom are applied when the same map is reopened.
- Pan and zoom changes update the saved view for that map.
- Upload and tile-set maps retain their existing CRS, min/max zoom, and fit behavior.
- A saved value from another map, malformed JSON, non-finite coordinates, or out-of-range zoom is ignored.
- Changing mode or layer visibility does not reset center or zoom.
-
pnpm typecheckpasses.
Step 3 - Persist and restore the map mode¶
Goal: Reopening a map restores its last valid view, edit, or settings mode without restoring transient editing state.
Scope
- In:
app/features/map-editor/use-map-editing-state.ts, allow the route to provide a validated initial mode or expose the mode state through a focused persistence boundary. - In:
app/routes/project-map.tsx, hydrate the mode from a map-scoped key, persist successful mode changes, and coordinate hydration so the defaultviewvalue is not written before the saved value is read. - In: Keep the hook's existing mode-transition cleanup: placing state, drafts, editing ids, and selections must still be cleared or transferred according to the current transition rules.
- Out: Persisting selected objects, drawing drafts, form contents, or mode in the URL.
Acceptance criteria
- Each map restores only its own last valid mode.
- Only
view,edit, andsettingsvalues are accepted; invalid values fall back toview. - Switching modes persists the new mode and preserves the existing cleanup behavior.
- Reopening in
editorsettingsdoes not resurrect an unfinished drawing, form, or selected object. - Storage failure does not prevent mode switching.
-
pnpm typecheckpasses.
Step 4 - Preserve and harden layer-control persistence¶
Goal: All current layer and object visibility preferences, plus the two existing control tabs, remain per-map, resilient, and compatible with changed map definitions.
Scope
- In:
app/features/map-controls/base-layer-control.tsx,image-layer-control.tsx,map-object-visibility-control.tsx, andavesmaps-layer-control.tsx, retain existing storage categories and validate saved disabled ids against currently available entries where the control has a current entry list. - In: Preserve the disabled-id model so absent ids do not disable future layers/types; remove or ignore stale ids when definitions no longer exist.
- In: Preserve active-tab persistence for Features and Avesmaps, accepting only their known tab values and falling back to the existing defaults.
- In: Ensure hydration guards prevent default enabled state or default tabs from overwriting saved values.
- In: Update
app/routes/project-map.tsxonly where needed to ensure each control receives the stablemap.idand current definitions. - Out: Persisting popover open/closed state, changing control layout, or adding a new combined layer-control abstraction.
Acceptance criteria
- Base-layer visibility, image-overlay visibility, custom POI/line/area type visibility, and Avesmaps subtype visibility persist per map.
- Features and Avesmaps active tabs persist per map and accept only valid current tab values.
- New layers/types default to visible; removed or unknown ids are ignored.
- Existing stored keys continue to work where their values are valid.
- Popover open/closed state remains transient.
-
pnpm typecheckpasses.
Step 5 - Verify the complete behavior¶
Goal: Confirm the feature works across map sources and does not regress map rendering, editing, or server persistence.
Acceptance criteria
-
pnpm typecheckpasses. -
pnpm formatcompletes successfully. -
pnpm buildpasses. - Manual check: open a tile-set map, pan and zoom, switch modes, change layer visibility, choose control tabs, reload, and confirm all saved state returns.
- Manual check: repeat with an upload-backed map and confirm its source-specific default zoom/bounds still behave correctly.
- Manual check: open a second map and confirm it does not inherit the first map's center, zoom, mode, visibility, or tabs.
- Manual check: introduce or remove a layer/type definition and confirm new entries default visible while stale saved ids are harmless.
- Manual check: use the map with local storage blocked or filled and confirm no user-facing error or broken map state.
- Manual check: confirm mode transitions still clear drafts and selections as before.
- Confirm no loader, action, route-table, project JSON, or Directus changes were introduced.
Risks / open questions¶
- Leaflet view events can fire during initial setup or fit-bounds logic. The implementation must distinguish hydration/setup from user changes so defaults do not overwrite saved state and fit logic does not immediately replace the restored view.
- The current controls use separate local-storage keys and hooks. A shared helper is useful only if it preserves those keys and avoids a broad refactor.
- There is no Directus schema snapshot in this workspace. This is not blocking because the plan intentionally reads and writes no Directus field.