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/:mapIdtoapp/routes/project-map.tsx; keep the existing route andRoute.LoaderArgs/Route.ActionArgstypes. - Preserve the existing server boundary: filesystem and Directus access stays in
app/lib/projects.server.ts; the route action usesactiveAccessToken(context)before calling persistence code. - Add optional default-view data so existing
data/projects.jsonrecords and Directus project rows without the new value remain readable. - Confirm the Directus deployment accepts the existing nested
MapProjectpayload. 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
useMapViewStateeffects. - Preserve the existing source bounds: tile sets use the tile set's
minZoom/maxZoom; upload maps use the current-5/5bounds 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, extendMapRefwith the optional default-view value using the existing center/zoom shape; keep the field optional for backward compatibility. - In:
app/lib/projects.server.ts, updatereadProjectsFileandnormalizeProjectonly 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, findsparams.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.jsonand 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 typecheckpasses.
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 withactiveAccessToken(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; return404when 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 typecheckpasses.
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 typecheckpasses.
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 themapView.<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 touseMapViewState; 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, preservehasSavedViewsemantics 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 typecheckpasses.
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 typecheckpasses. -
pnpm formatcompletes successfully. -
pnpm buildpasses. - 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, above90, 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/*.jsonfield was assumed or introduced without deployment confirmation.
Risks / open questions¶
- No Directus schema snapshot exists in the repository. The existing Directus adapter stores
MapProjectas thedatafield ofmap_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
hasSavedViewguard 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.