import { escapeHtml } from '@trek/shared'
import { getCached } from '../../services/photoService'
const PHOTO_PROXY_PREFIX = '/api/maps/place-photo/'
/** The fields of a place that decide which picture its marker gets. */
interface PhotoSource {
image_url?: string | null
google_place_id?: string | null
osm_id?: string | null
lat?: number | null
lng?: number | null
}
/**
* A place's image_url that is directly displayable on a map marker and should
* win over the auto-fetched provider thumbnail — a custom uploaded image (#1136)
* or an inline data URL. Provider proxy URLs (/api/maps/place-photo/…) are
* deliberately excluded: those still go through the downscale/thumb path so many
* markers don't lag on zoom.
*/
export function isCustomPlaceImage(url: string | null | undefined): boolean {
return !!url && (url.startsWith('/uploads/') || url.startsWith('data:'))
}
/**
* Cache key under which a place's auto-fetched thumbnail is stored: the provider
* photo the user picked when there is one, else the provider id, otherwise the
* coordinates. The picked photo comes first because picking another picture of the
* same place leaves its provider id alone, and the thumb cached under that id
* would go on answering for the new one. A place with none of these has no
* identity to cache under, so it gets the empty string: callers skip it rather
* than filing every coordinate-less place under one shared entry.
*/
export function photoCacheKey(place: PhotoSource): string {
if (place.image_url?.startsWith(PHOTO_PROXY_PREFIX)) return place.image_url
if (place.google_place_id) return place.google_place_id
if (place.osm_id) return place.osm_id
if (place.lat == null || place.lng == null) return ''
return `${place.lat},${place.lng}`
}
/**
* What the maps' thumb loading depends on, as one string to key the effect on.
* Place ids alone missed every change of picture: taking an upload off a place or
* picking another suggested photo changes no id, so the marker kept a category
* dot or the old thumb until the page was reloaded.
*/
export function photoSourcesKey(places: Array): string {
return places.map(p => `${p.id}:${isCustomPlaceImage(p.image_url) ? 'own' : photoCacheKey(p)}`).join('|')
}
/**
* The picture a place wears on the map: a custom uploaded image wins over the
* auto-fetched thumb, which wins over whatever image_url says. The marker and the
* hover card both ask this, so they never show two different pictures.
*/
export function placePhotoUrl(place: PhotoSource, thumbs: Record): string | null {
if (isCustomPlaceImage(place.image_url)) return place.image_url!
const key = photoCacheKey(place)
return (key && thumbs[key]) || place.image_url || null
}
/**
* The same picture at full size, for the hover card. The marker's thumb is a 48px
* circle cut for a round pin, which blurs and keeps its round cut-out when it is
* shown any larger, so the card asks the photo cache for the picture it came from.
*/
export function placePhotoFull(place: PhotoSource): string | null {
if (isCustomPlaceImage(place.image_url)) return place.image_url!
const key = photoCacheKey(place)
const full = key ? getCached(key)?.photoUrl : null
if (full) return full
return place.image_url?.startsWith(PHOTO_PROXY_PREFIX) ? place.image_url : null
}
/**
* The picture inside a round place marker, for both the Leaflet and the GL map.
*
* Sized by its style and not by width/height attributes, because stylesheet rules
* beat attributes: Tailwind's preflight gives every img `height: auto`, and
* Leaflet's marker pane adds `width: auto`. The 48px square thumbs got away with
* that. An uploaded photo kept its own proportions on the GL map and its full pixel
* size on Leaflet, where only the rounded-off corner of a 4000px image lands inside
* the circle, so the marker showed nothing but its category colour.
*/
export function markerPhotoHtml(url: string): string {
return `
`
}