POI image icons (proof of concept) — implementation plan¶
Source idea (verbatim from the request):
we want to replace some of the current icon markers with neat images (from public/images/map-icons).
Refined and scoped down (see prior clarification round) to: POI type markers
only — the avesmaps dataset's read-only feature layer (AvesmapsFeatureLayer)
is explicitly out of scope and deferred to a follow-up. This is a proof of
concept: existing POI types get an image option, no new POI types are added,
and no data/projects.json migration runs.
What's already there¶
- app/lib/poi.ts —
PoiTypeDefinition.iconis a plainstring, currently always a lucide icon name drawn fromPOI_TYPE_ICONS(12 names).POI_TYPE_ICON_COMPONENTSis the lookup table from icon name toLucideIconused both by the marker renderer and the type editor's icon picker.UNKNOWN_POI_TYPE({ color, icon }) is the fallback used whenever an icon name doesn't resolve. - app/components/map.tsx's
buildPoiIcon(color, iconName)(~line 346) builds anL.DivIcon: a 22px colored circle with a lucide icon rendered inside viarenderToStaticMarkup.usePoiIcons(~line 374) builds onePoiIconsobject ({ bySlug, unknown }) per distinct set of type definitions, andresolvePoiIconlooks a type slug up in it, falling back tounknown.PoiMarker(~line 389) is the only consumer. - app/routes/project.tsx's
PoiTypeForm(~line 170) rendersPOI_TYPE_ICONSas a row of buttons, each showing itsPOI_TYPE_ICON_COMPONENTS[name]glyph; the selected name is submitted as theiconhidden field. The POI type list (~line 500) separately rendersPOI_TYPE_ICON_COMPONENTS[type.icon]inside a small colored circle badge, with no fallback toUNKNOWN_POI_TYPEif the name doesn't resolve. - app/lib/projects.server.ts's
addPoiType/updatePoiTypestore whatevericonstring they're given — no allow-list validation, so no server-side change is needed to accept a new kind of value. data/projects.jsonalready has multiple projects with acityPOI type (e.g. lines 293, 483) using the current lucide icon.DEFAULT_POI_TYPESinpoi.tsis only read at project-creation time — it is copied into a new project'spoiTypes, so changing it does not touch already-saved projects.- public/images/map-icons has 4 PNGs
today:
castle.png,city.png,city_fortified.png,crossing.png. They are self-contained pictorial icons (own colors, drop shadow, transparent background) — not plain glyphs meant to sit inside a colored circle like the lucide icons do.
Decisions taken¶
| Question | Decision |
|---|---|
| Marker scope | POI types only. Avesmaps dataset features (AvesmapsFeatureLayer, crossing.png) are out of scope, deferred to a follow-up task |
| Image-to-type mapping | Map to existing POI types only, as a proof of concept — no new POI types (castle, city_fortified) are created in this task, though their images become pickable for any type |
| Icon value shape | Single icon: string field keeps both kinds — a image:<name> value addresses one of the map-icons PNGs (e.g. image:city → /images/map-icons/city.png), a bare name keeps meaning a lucide icon name, exactly as today. No new field, no schema/type change beyond the value convention, so old data needs no migration |
| Extensibility | Generic — the set of available images is one array (mirroring POI_TYPE_ICONS), so a future PNG dropped into public/images/map-icons becomes pickable by adding one entry, not by touching the marker or picker rendering logic |
| Color swatch on image icons | Not applied. The images already carry their own color/shading; wrapping them in the same colored circle used for lucide icons would clash. Image icons render standalone at marker size, lucide icons keep the existing colored-circle badge |
Default city type |
DEFAULT_POI_TYPES's city entry switches to image:city. Only affects newly created projects — no migration of data/projects.json |
| Fallback behavior | An icon value that resolves to neither a known image nor a known lucide name falls back to UNKNOWN_POI_TYPE's rendering, same as today |
Prerequisites¶
None — builds directly on the existing POI type system and the 4 PNGs
already in public/images/map-icons.
1. Add the image icon registry and value convention to app/lib/poi.ts¶
Goal: poi.ts exposes the set of available image icons and a way to tell
an image-icon value apart from a lucide-icon value, so both the picker and
the marker renderer can share one source of truth.
Scope
- In: A new exported list, e.g.
POI_TYPE_IMAGE_ICONS: string[], holding the base names of the 4 files (castle,city,city_fortified,crossing). - In: A small set of exported helpers to encode/decode the
image:<name>convention and resolve a base name to itspublic/images/map-iconspath (signatures only, e.g.isPoiImageIcon(icon: string): boolean,getPoiImageIconPath(name: string): string) — implementation is straightforward string handling, not designed here. - In:
DEFAULT_POI_TYPES'scityentry changes itsiconto theimage:-prefixed value forcity. - Out: No change to
PoiTypeDefinition's shape (iconstaysstring) and no change toPOI_TYPE_ICONS/POI_TYPE_ICON_COMPONENTS.
Acceptance criteria
-
poi.tsexports the image icon list and the encode/decode helpers. -
DEFAULT_POI_TYPES'scityentry uses the new image icon value. -
pnpm typecheckpasses.
2. Render image icons in app/components/map.tsx¶
Goal: A POI type whose icon is an image value renders that PNG as its
map marker, at a comparable footprint to the existing colored-circle lucide
markers, with the same click-to-select behavior.
Scope
- In:
buildPoiIcon(~line 346) or an equivalent branch reachable fromusePoiIconsbuilds a plain image-basedL.DivIcon(orL.icon) when the type's icon value is an image, instead of the colored circle + lucide glyph markup — no color swatch wrapper, aspect ratio preserved. - In:
resolvePoiIcon/usePoiIcons's fallback path still resolves toUNKNOWN_POI_TYPE's (lucide, colored-circle) rendering when an icon value is neither a known image nor a known lucide name. - Out: No change to
PoiMarker,MapClickHandler, or howeventHandlersare wired — only how theL.DivIconpassed toMarkeris constructed. - Out: No change to
PendingPoiMarker(icons.unknown) — the "placing a new POI" placeholder keeps its current lucide-based look regardless of the chosen type's icon, exactly as today.
Acceptance criteria
- A POI whose type has an image icon (e.g.
cityafter task 4) renders the corresponding PNG on the map, sized comparably to existing markers, not stretched or letterboxed. - Clicking an image-icon marker still calls
onSelectRequest(same as a lucide-icon marker). - A POI whose type has an unresolved icon value still renders
UNKNOWN_POI_TYPE's marker, not a broken image. -
pnpm typecheckpasses.
3. Extend the icon picker and type list preview in app/routes/project.tsx¶
Goal: The POI type editor lets a user pick any of the 4 map-icons images (in addition to the existing 12 lucide icons) for a new or edited type, and the type list shows the correct preview for either kind.
Scope
- In:
PoiTypeForm's icon picker (~line 231) adds a second row (or extends the existing row) of buttons forPOI_TYPE_IMAGE_ICONS, each showing the PNG instead of a lucide glyph; selecting one sets the hiddeniconfield to theimage:<name>value, same submission path as today. - In: The type list's icon badge (~line 500) branches on the icon value:
image values render the PNG standalone (no colored circle wrapper, per the
color-swatch decision above), lucide values keep the existing colored
circle + glyph, and an unresolved value falls back to
UNKNOWN_POI_TYPE's rendering (closing the existing gap where this preview has no fallback today). - Out: No change to
PoiTypeForm's color swatch picker — color continues to apply only to lucide-icon types; an image-icon type simply doesn't use its storedcolorfor the badge/marker (the field stays on the type for now, unused by image icons, rather than being conditionally hidden from the form — keeps this step small).
Acceptance criteria
- Creating or editing a POI type shows all 4 map-icons images as selectable options alongside the 12 lucide icons.
- Saving a type with an image icon persists the
image:<name>value via the existingadd-poi-type/update-poi-typeactions, unchanged. - The type list shows the PNG for an image-icon type and the existing colored-circle glyph for a lucide-icon type.
-
pnpm typecheckandpnpm formatpass.
4. Manual verification¶
Goal: Confirm the proof of concept end to end on a real project.
Scope
- In: Manual check only — no code change.
Acceptance criteria
- On a newly created project, the default
cityPOI type showscity.pngin the type list and on the map. - On an existing project (e.g. one of the ones in
data/projects.jsonwith acitytype), the type still shows its original lucide icon, confirming no migration occurred. - Editing an existing type to an image icon, and back to a lucide icon, updates the marker and type list preview correctly each time.
-
pnpm buildsucceeds.