Base layer visibility control
Base Layer Visibility Control¶
Goal: Users can hide or show a map's current base layer from the map view, whether that base is a tile layer or an uploaded image, while keeping the interaction ready for multiple future tile layers.
Decisions¶
| Decision | Choice | Why |
|---|---|---|
| Layer types shown | The control lists the active tile-set layer on tile-backed maps and the uploaded-image base on upload-backed maps | Every map type gets a meaningful base-layer toggle while the control remains focused on base imagery |
| Control label | Tile layers |
Matches the requested feature and leaves room for additional tile-layer entries; the uploaded-image entry is included in the same control for consistency |
| Initial state | Enabled when no saved preference exists | Existing maps must retain their current appearance by default |
| State ownership | Per-map client-side state persisted in localStorage |
Visibility is view state, not project configuration, and must not alter data/projects.json or Directus data |
| Leaflet lifecycle | Keep map bounds and the layer component mounted; add/remove only the imperative base layer | Unmounting the component that calls fitBounds could reset the user's current pan/zoom |
| Future layer model | The control consumes a list of layer entries, even though each current map supplies one | Additional tile layers can become additional checkbox rows without changing the UI contract |
| Directus schema | No schema field or Directus collection change | Visibility is deliberately local-only; no directus/schema/ snapshot exists in this workspace and no Directus field is required |
Prerequisites¶
- Confirm the current
MapRef.sourceremains the source of truth for the active base layer:tileSetmaps usesource.tileSetId, and upload maps usesource.path. - Preserve the existing localStorage selection conventions used by
image-overlay-control.tsxandavesmaps-layer-control.tsx: track disabled ids so newly introduced layers default to visible. - Keep
MapFitBoundsmounted for tile-backed maps and the upload image's existing map-fit behavior unchanged while only toggling the underlying Leaflet layer. - No route-table change, server action, persisted project-data migration, or Directus schema change is required.
Steps¶
1. Define the base-layer view-state and control contract¶
Goal: Add a reusable map-control module that represents base layers as labeled entries and exposes per-map enabled state and toggles, following the existing checkbox/popover/card conventions.
Scope
- In: app/features/map-controls/tile-layer-control.tsx with a layer-entry shape containing a stable id and human-readable label, a useTileLayerSelection(mapId, layers) hook, and a checkbox-based control component.
- In: Persist disabled layer ids under a tile-layer-specific, map-specific localStorage key; tolerate malformed or unavailable storage by using all-enabled state.
- In: Reconcile saved ids against the current entry list so stale ids do not affect future layers, while newly added entries remain enabled by default.
- Out: Any server persistence, action intent, project-data field, tile-set selection, layer creation, or layer ordering.
Acceptance criteria
- [ ] The hook defaults every supplied entry to enabled and exposes isEnabled plus a toggle operation.
- [ ] State is isolated by map id and persists across reloads without writing project data.
- [ ] A new layer entry is enabled when it is absent from the saved disabled-id set.
- [ ] The control renders one checkbox per supplied entry using the existing Popover, Card, Button, and Checkbox patterns.
- [ ] pnpm typecheck passes.
2. Make the tile-set base layer visibility-aware¶
Goal: The existing imperative tile layer can be removed from and restored to Leaflet without rebuilding the map or refitting its bounds.
Scope
- In: app/components/map/map-primitives.tsx, extend the tile-layer rendering contract with a visible prop defaulting to true.
- In: Keep MapFitBounds independent of visibility and change only the MapImperativeLayer lifecycle or equivalent Leaflet add/remove handling for the tile layer.
- In: Ensure changing visibility does not recreate the tile layer because of unrelated route renders or reset the current pan/zoom.
- Out: Changes to tile URL generation, CRS, bounds, zoom limits, or tile-set registry data.
Acceptance criteria
- [ ] visible={false} removes tile imagery from the map while map bounds, CRS, current center, and zoom remain unchanged.
- [ ] visible={true} restores the same tile layer and current view without calling fitBounds again.
- [ ] Existing tile rendering is unchanged when visible is omitted or true.
- [ ] pnpm typecheck passes.
3. Make the uploaded-image base layer visibility-aware¶
Goal: Upload-backed maps support the same hide/show behavior without changing their existing image sizing or map-fit behavior.
Scope
- In: app/features/map-controls/image-overlay-layer.tsx (the current owner of UploadImageLayer), add the same visibility contract used by the tile layer and remove/add only the image overlay instance when it changes.
- In: Keep image loading, natural-size bounds, map fitting, and the upload source URL behavior unchanged.
- In: Ensure the hidden image does not intercept clicks and restoring it does not refit or recreate the map container.
- Out: The separately configured image-overlay collection and its existing visibility control.
Acceptance criteria
- [ ] visible={false} hides the uploaded image without changing the current map view.
- [ ] visible={true} restores the uploaded image without refitting the map.
- [ ] Existing upload-backed map behavior is unchanged when visible is omitted or true.
- [ ] pnpm typecheck passes.
4. Wire base-layer entries, state, and rendering in the project-map route¶
Goal: The map route derives the correct base-layer entry from its loader data, connects selection state to the control, and passes visibility to the appropriate Leaflet layer.
Scope
- In: app/routes/project-map.tsx, derive a stable tile-set label from source.tileSet.label for tile-backed maps and a human-readable uploaded-image label for upload-backed maps.
- In: Initialize the new selection hook with map.id and the current entry list, render the control in MapControlContainer, and pass the corresponding enabled value to MapTileLayer or UploadImageLayer.
- In: Preserve the existing lazy-loading boundary and layer ordering: base imagery remains below AvesMaps, configured image overlays, map objects, and interaction handlers.
- Out: Changes to app/routes.ts, loader shape, route actions, MapRef, or Directus data.
Acceptance criteria
- [ ] Tile-set-backed maps show one checked base-layer checkbox with the tile set's human-readable label.
- [ ] Upload-backed maps show one checked checkbox for the uploaded-image base.
- [ ] Unchecking and rechecking each base type hides and restores only that base layer.
- [ ] The route does not add an action or loader field for visibility.
- [ ] Existing AvesMaps, image-overlay, POI, route, selection, and editing controls continue to work.
- [ ] pnpm typecheck passes.
5. Verify the feature and regression boundaries¶
Goal: Confirm the control works across both source kinds and that local-only visibility does not disturb map behavior.
Acceptance criteria
- [ ] pnpm typecheck passes.
- [ ] pnpm format completes successfully.
- [ ] pnpm build passes.
- [ ] With no saved preference, a tile-backed map loads with tiles visible and an upload-backed map loads with its image visible.
- [ ] Toggling each base layer preserves the current center and zoom, and toggling it back restores the imagery.
- [ ] Reloading the same map restores its saved visibility state; opening a different map does not inherit it.
- [ ] Hiding the base does not change POI placement, selection clearing, AvesMaps feature selection, route rendering, configured image overlays, or edit-mode interactions.
- [ ] Project JSON and Directus-backed project data remain unchanged by visibility toggles.
Risks / open questions¶
- Leaflet layer components currently use imperative effects, so an implementation that couples visibility to component unmounting may accidentally rerun
fitBounds; verify the map view before and after every toggle. - There is no
directus/schema/directory or JSON snapshot in the workspace. This plan intentionally adds no Directus field, so that absence does not block implementation.