Skip to content

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.tsx as the composition owner: it supplies map.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 zoom 0; tile-set maps start at [0, 0] and tileSet.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/*.json snapshot, 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 typecheck passes.

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.tsx or a focused map-view hook, load and validate the saved { center, zoom } for map.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 typecheck passes.

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 default view value 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, and settings values are accepted; invalid values fall back to view.
  • Switching modes persists the new mode and preserves the existing cleanup behavior.
  • Reopening in edit or settings does not resurrect an unfinished drawing, form, or selected object.
  • Storage failure does not prevent mode switching.
  • pnpm typecheck passes.

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, and avesmaps-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.tsx only where needed to ensure each control receives the stable map.id and 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 typecheck passes.

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 typecheck passes.
  • pnpm format completes successfully.
  • pnpm build passes.
  • 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.