Skip to content

Map Default View Settings — Implementation Plan

Decisions

Decision Choice Why
Storage scope Persist defaults on each MapRef inside the existing project payload The request is map-level, and the existing local JSON and Directus paths already persist the complete project model.
Settings location Existing settings mode in app/routes/project-map.tsx The route already exposes the map-level Settings entry point and currently renders a placeholder.
Persisted shape Optional map default-view value containing center.lat, center.lng, and zoom It matches the existing MapViewState shape and allows old maps to retain source defaults when absent.
Local override Existing per-map mapView.<mapId> localStorage state wins independently for center and zoom A user's browser-specific view is more specific than the shared saved default; one malformed value must not discard the other valid value.
Validation Reject invalid submissions with field-level errors Latitude/longitude and source-specific zoom bounds should be explicit rather than silently changed.
Reset behavior No in-app local-view reset in this task Reset was explicitly deferred; this task only defines default precedence.
Directus schema No schema field is assumed No directus/schema/*.json snapshot exists; the existing map_projects.data payload is the verified persistence boundary. Deployment schema/API compatibility must be checked during implementation.

Prerequisites

  • The protected route table maps project/:projectId/map/:mapId to app/routes/project-map.tsx; keep the existing route and Route.LoaderArgs/Route.ActionArgs types.
  • Preserve the existing server boundary: filesystem and Directus access stays in app/lib/projects.server.ts; the route action uses activeAccessToken(context) before calling persistence code.
  • Add optional default-view data so existing data/projects.json records and Directus project rows without the new value remain readable.
  • Confirm the Directus deployment accepts the existing nested MapProject payload. There is no local schema snapshot to verify a separate field against, so do not introduce a new top-level Directus collection or field without deployment confirmation.
  • Keep Leaflet and browser storage client-only. Settings form rendering may be server-rendered, but localStorage reads and writes remain inside useMapViewState effects.
  • Preserve the existing source bounds: tile sets use the tile set's minZoom/maxZoom; upload maps use the current -5/5 bounds and their current center/zoom fallback.

Step 1 — Extend the per-map settings contract

Goal: Represent optional persisted default center and zoom on an individual map without breaking existing projects.

Scope

  • In: app/lib/projects.server.ts, extend MapRef with the optional default-view value using the existing center/zoom shape; keep the field optional for backward compatibility.
  • In: app/lib/projects.server.ts, update readProjectsFile and normalizeProject only as needed to preserve valid settings and ignore or normalize malformed legacy values without crashing project loading.
  • In: app/lib/projects.server.ts, add a focused map-settings persistence function that loads the project, finds params.mapId, updates only that map's default view, writes through the existing local/Directus backend, and returns the updated map or an absent result.
  • In: data/projects.json and Directus data, do not perform a bulk migration; absent optional settings must resolve to source defaults.
  • Out: A project-wide settings field, a new collection, a URL representation, or changes to unrelated map mutations.

Acceptance criteria

  • Existing projects without map default settings load successfully.
  • A default view is stored on the targeted map only.
  • Updating a map's settings preserves its source, objects, overlays, and other map fields.
  • Missing project or map ids return an absent result that the route can convert to 404.
  • Both local JSON and Directus-backed persistence use the existing project-row write path.
  • pnpm typecheck passes.

Step 2 — Add server-side map-settings validation and mutation handling

Goal: Save only valid map defaults through the existing protected map route.

Scope

  • In: app/routes/project-map.tsx, add an action intent for map settings that parses latitude, longitude, and zoom from form data and calls the new server persistence function with activeAccessToken(context).
  • In: app/routes/project-map.tsx, validate finite latitude in -90..90, finite longitude in -180..180, and finite zoom within the current map source's bounds before writing.
  • In: app/routes/project-map.tsx, return structured field-level errors for invalid input and a success result for a completed save; return 404 when the project or map cannot be found.
  • In: app/routes/project-map.tsx, derive the current source bounds in one route-local path so validation and view-state fallback use the same limits.
  • Out: Client-side-only validation, silent clamping, unauthenticated writes, or changes to existing feature-editing intents.

Acceptance criteria

  • Invalid latitude, longitude, or zoom never reaches the persistence function.
  • Each invalid field receives an actionable error without discarding valid values in the other fields.
  • A valid submission updates only the selected map and reports success.
  • The action rejects an unknown or missing map with 404.
  • The action remains within the existing protected route and access-token flow.
  • pnpm typecheck passes.

Step 3 — Implement the map Settings form

Goal: Replace the Settings placeholder with a usable editor for the map's persisted default view.

Scope

  • In: app/routes/project-map.tsx, render controlled or initialized numeric fields for default latitude, longitude, and zoom using the map's persisted value or source-specific fallback.
  • In: app/routes/project-map.tsx, submit the fields with the new intent, display action errors beside the relevant fields, and show save success without navigating away from the map.
  • In: app/routes/project-map.tsx, keep the existing topbar and mode-switching behavior; settings mode may remain map-free if that is the current layout contract, but it must no longer show the placeholder.
  • In: existing UI primitives under app/components/ui/, reuse the repository's field, input, button, and error patterns rather than adding a parallel form system.
  • Out: Editing map name/source, project settings, a reset-local-view button, or a separate route.

Acceptance criteria

  • Settings shows the effective persisted/source default values when opened.
  • Users can edit latitude, longitude, and zoom and submit them explicitly.
  • Field errors remain visible after a rejected submission.
  • Successful saves update the displayed values and do not reset map objects or unrelated controls.
  • Existing view/edit/settings mode transitions continue to work.
  • The form is usable at desktop and narrow viewport widths without field/button overlap.
  • pnpm typecheck passes.

Step 4 — Apply persisted defaults beneath independent localStorage overrides

Goal: Make saved map defaults the fallback while preserving each browser's valid local view per coordinate category.

Scope

  • In: app/hooks/use-map-view-state.ts, retain the mapView.<mapId> storage key and update validation/hydration so valid saved center and valid saved zoom are applied independently to the supplied default view.
  • In: app/routes/project-map.tsx, compose the map's persisted default view with the source fallback before passing it to useMapViewState; do not use a localStorage value to mutate server data.
  • In: app/hooks/use-map-view-state.ts, ensure hydration completes before writing, and ensure malformed, stale, non-finite, or out-of-bounds local values fall back independently without a user-facing error.
  • In: app/routes/project-map.tsx, preserve hasSavedView semantics so image/tile fit-bounds behavior does not replace a valid restored local view; determine whether a partially restored local view should suppress fit-bounds and document that choice in implementation comments/types only if needed.
  • Out: New storage keys, local-view reset UI, persisting mode/layers/tabs, or changing Leaflet CRS and source bounds.

Acceptance criteria

  • A valid local center overrides the persisted center while the persisted zoom remains effective when local zoom is absent or invalid.
  • A valid local zoom overrides the persisted zoom while the persisted center remains effective when local center is absent or invalid.
  • A complete valid local view overrides both persisted values.
  • Invalid local values never overwrite persisted defaults or cause a render error.
  • The initial source defaults remain unchanged when no persisted map defaults or localStorage values exist.
  • Defaults are not written into localStorage before the saved local view has been read.
  • Pan/zoom updates continue to write the complete current view to the existing per-map key.
  • pnpm typecheck passes.

Step 5 — Reconcile the v0.0.8 local-storage plan

Goal: Keep the adjacent localStorage design document consistent with the new persisted-default fallback contract.

Scope

  • In: docs/features/versions/v0.0.8/local-storage-map-state-plan.md, remove or qualify the exclusion that says changing project map settings or adding server-side defaults is outside the map-view persistence boundary.
  • In: the same document, state that persisted map defaults are shared fallback configuration while localStorage remains browser-specific and authoritative when valid.
  • In: keep the existing guarantees about SSR safety, per-map keys, invalid-state handling, and hydration write guards.
  • Out: Expanding this task to implement the mode, layer, tab, or other localStorage items listed in that plan.

Acceptance criteria

  • The two v0.0.8 documents describe the same precedence order and persistence boundary.
  • The localStorage plan still clearly distinguishes server-persisted defaults from browser-local view state.
  • No implementation code is added to documentation.

Step 6 — Verify the complete behavior

Goal: Confirm persistence, validation, precedence, backward compatibility, and rendering behavior across map sources.

Acceptance criteria

  • pnpm typecheck passes.
  • pnpm format completes successfully.
  • pnpm build passes.
  • Manual check: open a tile-set map, save valid center and zoom, reload, and confirm the saved defaults are used with no localStorage entry.
  • Manual check: repeat with an upload-backed map and confirm its source-specific zoom bounds and fallback behavior remain correct.
  • Manual check: set a valid complete local view and confirm it overrides both saved defaults.
  • Manual check: set only a valid local center or only a valid local zoom, then confirm the other value comes from the persisted map default.
  • Manual check: place malformed, non-finite, and out-of-bounds values in the map's localStorage entry and confirm independent fallback without a user-facing error.
  • Manual check: submit latitude below -90, above 90, longitude outside -180..180, and zoom outside source bounds; confirm field errors and unchanged saved values.
  • Manual check: save settings for one map, open another map in the same project, and confirm its defaults are unaffected.
  • Manual check: verify an old map record with no default-view value still opens using the existing source defaults.
  • Confirm no new directus/schema/*.json field was assumed or introduced without deployment confirmation.

Risks / open questions

  • No Directus schema snapshot exists in the repository. The existing Directus adapter stores MapProject as the data field of map_projects; implementation should verify the deployed collection accepts the extended nested payload before release.
  • Existing localStorage currently validates a complete MapViewState. Independent override behavior may require a small compatibility adjustment so legacy complete entries remain valid while partial or malformed entries fall back per field.
  • Leaflet fit-bounds can run during initial layer setup. The implementation must preserve the existing hydration and hasSavedView guard so a restored or partially restored local view is not immediately replaced.
  • Updating persisted defaults while a map is already open must not reset the current local view; the new defaults should affect a subsequent hydration only when no valid local override exists.