Improved rendering of custom features — implementation plan¶
Source idea (verbatim from the request):
- add a proper custom features control panel (like for images and avesmaps) that allows to filter the shown features by type. also group them by their "group" field, as done for the avesmaps features.
- add min- and max-zoomlevel fields to the custom features config in the project, so that we can show/hide them at certain zoomlevels.
- show labels on the map (e.g. city names) - but ensure to not clutter the map with too many labels, and only show them at certain zoomlevels. (e.g. show town names only when zoomed in enough, but not when zoomed out to the world map - which would only show metropoles/großstädte)
"Custom features" = points of interest, lines, and areas (the three
project-defined feature kinds in app/lib/poi.ts,
app/lib/line.ts and
app/lib/area.ts) — as opposed to the read-only
Avesmaps dataset layer, which already has its own filter panel and per-subtype
minimum zoom (AvesmapsLayerControl, getAvesmapsSubtypeMinZoom).
What's already there¶
PoiTypeDefinition(app/lib/poi.ts) already has an optionalgroupfield.LineTypeDefinition(app/lib/line.ts) andAreaTypeDefinition(app/lib/area.ts) do not havegroupyet — onlyslug,name,color.- None of the three type definitions have
minZoom/maxZoom.ImageOverlayLayeralready has aminZoom(nomaxZoom) and map.tsx'sImageOverlayLayerView(~line 292) already shows the exact pattern to reuse: track zoom viauseMap()+useMapEvent("zoomend", …), thenshouldShow = visible && zoom >= layer.minZoom. - Two existing per-map filter panels are the pattern to follow:
avesmaps-layer-control.tsx
(groups by
feature_type, tracks disabled layer keys, persists tolocalStorage) and image-overlay-control.tsx (same "track what's off" approach, flat list). Both are wired into project-map.tsx (~lines 648-650, 1093-1101) and passed into<Map>asisXEnabledpredicates. PoiLayer,LineLayer,AreaLayerin map.tsx currently render every POI / line / area unconditionally — no visibility or zoom filtering exists for them today.- The project settings type editors in
project.tsx (POI form ~line 273, POI
list ~line 786; line form/list ~line 888; area form/list ~line 980) are
where
group/zoom inputs need to be added. - app/lib/projects.server.ts has
matching CRUD pairs for all three:
add/updatePoiType,add/updateLineType,add/updateAreaType, plusadd/updatePointOfInterest,add/updateLine,add/updateAreafor instances — all currently take a fixed, explicit field list per function (no passthrough), so every new field needs a matching parameter added by hand. - There is no existing label-rendering or label-collision code anywhere in the app. docs/features/versions/v0.0.3/show-labels-on-map.md is an empty stub for the third bullet — step 7 below supersedes it.
Decisions taken¶
| Question | Decision |
|---|---|
| Scope | Points of interest, lines, and areas are all in scope (not POIs only) |
| Zoom field granularity | Lives on the type definition (poiTypes/lineTypes/areaTypes in the project); an individual POI/line/area may optionally override it |
| Override semantics | Field-by-field: an instance's minZoom (if set) replaces the type's minZoom; same independently for maxZoom. Unset instance fields fall back to the type's value |
| No range set anywhere | Always visible (matches today's unconditional rendering — backward compatible with existing data/projects.json) |
| Filter panel state | Per-map, localStorage only, "track what's off" — never written to data/projects.json, same as the two existing controls |
| Filter panel grouping | Grouped by each type's own group field, scoped within its own kind (POI types grouped among POI types, etc.) — mirrors group being defined separately per type collection today |
| Combining filter + zoom | AND: a feature renders only if its type/group is enabled in the panel and the current zoom is within its effective range |
| Labels | Reuse the same effective zoom range as visibility (a "metropole" type gets a low minZoom so its label survives zoomed out; a "dorf" type gets a high minZoom) — no separate label-zoom field |
| Label anchor | POI: its own position. Line/area: midpoint of the point list (first/last average for a line, centroid average for an area) — simplest anchor, not a true polygon centroid |
| Label collision handling | Priority + pixel-radius suppression (see step 7) — not a general layout algorithm, not a new dependency |
Prerequisites¶
None — builds directly on the existing POI/line/area type system, the
existing zoom-tracking pattern in ImageOverlayLayerView, and the two
existing filter-panel components as templates.
1. Add group/minZoom/maxZoom to the type & instance data models¶
Goal: The project's POI, line, and area type definitions can carry a group name and a zoom range, and an individual instance can optionally override that range, with one shared helper to resolve "is this visible at zoom Z" consistently everywhere it's needed.
Scope
- In:
group?: stringadded toLineTypeDefinitionandAreaTypeDefinitionin line.ts / area.ts (POI already has it). - In:
minZoom?: numberandmaxZoom?: numberadded toPoiTypeDefinition,LineTypeDefinition,AreaTypeDefinition. - In:
minZoom?: numberandmaxZoom?: numberadded toPointOfInterest,LineFeature,AreaFeatureas optional per-instance overrides. - In: A new shared module, e.g.
app/lib/feature-zoom.ts, exporting aZoomRange = { minZoom?: number; maxZoom?: number }type and two pure functions: one to resolve an instance's effective range against its type's range (field-by-field fallback, per the decision above), one to test whether a given zoom is within a resolved range (undefined bound = no limit on that side). - Out: No server or UI wiring yet — this step only changes types/shared logic.
Acceptance criteria
-
LineTypeDefinition/AreaTypeDefinitionhavegroup; all three type definitions haveminZoom/maxZoom. -
PointOfInterest/LineFeature/AreaFeaturehave optionalminZoom/maxZoomoverrides. -
app/lib/feature-zoom.tsexports the range-resolving and range-testing functions, with no existing behavior depending on them yet. -
pnpm typecheckpasses.
Affected areas
- app/lib/poi.ts
- app/lib/line.ts
- app/lib/area.ts
app/lib/feature-zoom.ts(new)
2. Thread the new fields through projects.server.ts¶
Goal: Creating or editing a POI/line/area type, or an individual
POI/line/area, can set group (types only) and minZoom/maxZoom, and
existing saved projects load unchanged (fields simply absent/undefined).
Scope
- In:
addPoiType/updatePoiType,addLineType/updateLineType,addAreaType/updateAreaTypeaccept and storeminZoom/maxZoom; the line/area pair also accepts and storesgroup(POI's already does). - In:
addPointOfInterest/updatePointOfInterest,addLine/updateLine,addArea/updateAreaaccept and store optionalminZoom/maxZoomoverrides. - Out: No change to
readProjectsFile's migration block — optional fields need no default-backfill (undefinedis already the correct "no range" state).
Acceptance criteria
- Every listed CRUD function's parameter type and body includes the new field(s); omitting them behaves exactly as before.
- Loading an existing
data/projects.jsonproject (with nogroup/ zoom fields anywhere) still works, with those fields simply absent. -
pnpm typecheckpasses.
Affected areas
3. Add group/zoom-range inputs to the project settings type editors¶
Goal: A project owner can set a group name (line/area types) and a min/max zoom (all three) on a type from the project settings page, and see the configured range in the type list.
Scope
- In: The line and area type forms (mirroring the POI type form's existing
groupinput, ~line 299) gain agrouptext input. - In: All three type forms gain
minZoom/maxZoomnumber inputs (both optional/blank-able). - In: The three type list views (~lines 786, 888, 980) show the configured range next to each type (e.g. "zoom 4–10") when set.
- Out: No change to the add-map / add-project flows, and no validation
beyond "optional number" (e.g. no enforcement that
minZoom <= maxZoom— out of scope, matches the codebase's existing light-touch form validation).
Acceptance criteria
- Creating or editing a line or area type can set a group name, stored and shown the same way the POI type's group already is.
- Creating or editing any of the three type kinds can set
minZoom/maxZoom, persisted via the actions from step 2. - The type list for all three kinds shows the configured range when set, and nothing extra when unset.
-
pnpm typecheckandpnpm formatpass.
Affected areas
4. Add per-instance zoom override inputs¶
Goal: An individual POI, line, or area can optionally override its
type's zoom range (e.g. one especially important dorf that should stay
visible zoomed out).
Scope
- In: The POI/line/area add and edit forms in
project-map.tsx gain optional
minZoom/maxZoominputs, submitted alongside the existing fields to the actions updated in step 2. - Out: No UI affordance beyond plain number inputs (no visual zoom-range slider).
Acceptance criteria
- Placing or editing a POI/line/area can set an override
minZoom/maxZoom, left blank by default (falls back to the type's range). -
pnpm typecheckpasses.
Affected areas
5. Filter POIs, lines, and areas on the map by effective zoom range¶
Goal: A POI/line/area outside its effective zoom range (resolved per step 1) is not rendered, and reappears as soon as the map's zoom re-enters the range — with no visible behavior change for features that have no range configured anywhere.
Scope
- In:
PoiLayer,LineLayer,AreaLayerin map.tsx each track the map's current zoom the same wayImageOverlayLayerViewalready does (useMap()+useMapEvent("zoomend", …)), and filter theirpois/lines/areasprop through step 1's range-testing function (matching type definition looked up by the instance'stype) before rendering. - Out: No change to editing/dragging/drawing interactions — a feature being
actively edited or drawn is unaffected by this filter (it's either not yet
in the
pois/lines/areasarray, or edited in place).
Acceptance criteria
- A POI/line/area whose effective range excludes the current zoom is not rendered; it reappears when zoom moves back into range.
- A feature with no range set anywhere (the common case today) renders at every zoom, unchanged from current behavior.
- Existing POI/line/area placement, dragging, and editing still work exactly as before.
-
pnpm typecheckpasses.
Affected areas
6. Add the custom features visibility control panel¶
Goal: A user can toggle which POI/line/area types (or whole groups) are shown on the map, from a panel matching the look and behavior of the Avesmaps and Image overlay controls, combined with the step 5 zoom filter.
Scope
- In: A new component (e.g.
app/components/feature-control.tsx) with a hook (e.g.useFeatureVisibilitySelection(mapId, { poiTypes, lineTypes, areaTypes })) that tracks disabled type slugs per kind, persisted tolocalStorageper map — same "track what's off" shape asuseAvesmapsLayerSelection/useImageOverlaySelection. - In: The panel UI shows three sections (Points, Lines, Areas), each listing
its project's types grouped by
group(ungrouped types listed outside any group), with group-level and type-level checkboxes — mirroringAvesmapsLayerControl's group/subtype checkbox structure. - In: Wired into project-map.tsx
alongside the existing two controls (~lines 1093-1101);
<Map>gainsisPoiTypeEnabled/isLineTypeEnabled/isAreaTypeEnabledpredicate props, ANDed with the step 5 zoom filter insidePoiLayer/LineLayer/AreaLayer. - Out: No change to the Avesmaps or Image overlay controls themselves.
Acceptance criteria
- A new control button opens a panel listing all POI/line/area types
grouped by their
groupfield. - Toggling a type or a whole group hides/shows matching features on the map immediately.
- The selection is restored from
localStorageon reload, scoped per map id, and never appears indata/projects.json. - A feature hidden by the panel stays hidden regardless of zoom; a feature shown by the panel is still subject to the step 5 zoom filter.
-
pnpm typecheckandpnpm formatpass.
Affected areas
app/components/feature-control.tsx(new)- app/routes/project-map.tsx
- app/components/map.tsx
7. Render feature name labels, scoped by zoom and thinned to avoid clutter¶
Goal: POIs/lines/areas show their name as a label on the map, visible only within their effective zoom range, with overlapping labels thinned so zoomed-out views (e.g. a world map showing only metropoles) stay readable.
Scope
- In: Each visible (post step ⅚ filtering) feature renders a permanent
label — a non-interactive
Tooltip permanent(same primitive asPendingPoiMarker's tooltip) anchored at the POI's position, or the line/area's point-list midpoint. - In: Before rendering, labels for the current zoom are sorted by priority
(ascending effective
minZoom— a lower/no minZoom, e.g. metropole, sorts first) and a label is skipped if its projected pixel position lands within a fixed radius of an already-placed label's position, recomputed onzoomend/moveend. - Out: No draggable/manually-positioned labels, no per-instance label text
override (always the feature's
name), no label styling beyond plain text (no leader lines, halos, or collision-avoiding offset). - Out: show-labels-on-map.md is superseded by this step; it is left as a pointer to this document rather than deleted.
Acceptance criteria
- A feature's name renders as a label when its effective zoom range includes the current zoom, and disappears outside it.
- Zoomed out enough that only "metropole"-type POIs are in range, only their labels show — no overlapping town/village labels.
- Zoomed in, labels for lower-priority types appear as they enter range, without needing a manual refresh.
- At a zoom where many same-priority labels would overlap, at most one per cluster renders (no overlapping text).
-
pnpm typecheckpasses.
Affected areas
8. Manual verification¶
Goal: Confirm the full feature end to end on a real project.
Scope
- In: Manual check only — no code change.
Acceptance criteria
- On an existing project (e.g. one already in
data/projects.json), every existing POI/line/area still renders exactly as before with no zoom range configured. - Setting a
dorf-style POI type'sminZoomhigh and ametropole-style type'sminZoomlow reproduces the "town names only when zoomed in, metropoles always" behavior from the original request. - The new control panel's group/type toggles work for all three feature kinds and persist across a reload.
-
pnpm buildsucceeds.