mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-23 07:26:30 +00:00
feat(maps): show a chain's own logo on its map pin
A corridor full of petrol stations was a row of identical coloured dots, and the one thing that tells them apart is the brand. OSM carries brand:wikidata on most of them — 88% of the fuel stations in a Berlin box — Wikidata carries the logo as property P154, and Commons serves it. The bytes are proxied and cached rather than linked: one self-hosted instance asking Wikimedia for a handful of logos is a very different egress profile from every visitor's browser announcing which petrol stations they are looking at. The cache is in memory because a GET must not write to the database, and there are only so many fuel brands; a brand with no logo is remembered too, or every pan would ask again. A logo-less or unreachable brand answers 204 and the pin keeps its category icon, so nothing depends on Wikidata being reachable. Both renderers show it, since both read the same category table.
This commit is contained in:
@@ -395,16 +395,33 @@ function buildPluginMarkerPopup(mk: PluginMapMarker): HTMLDivElement {
|
||||
}
|
||||
|
||||
// Small coloured pin for an OSM "explore" POI (matches the pill category colour).
|
||||
function createPoiMarkerElement(category: string): HTMLDivElement {
|
||||
// A chain shows its logo instead of the category icon: on a corridor full of petrol
|
||||
// stations the brand is what the eye is looking for, and the server proxies it so the
|
||||
// browser never asks Wikimedia which ones are on screen.
|
||||
function createPoiMarkerElement(category: string, brandWikidata?: string | null): HTMLDivElement {
|
||||
const cat = POI_CATEGORY_BY_KEY[category]
|
||||
const color = cat?.color || '#6b7280'
|
||||
const svg = cat ? renderIconMarkup(createElement(cat.Icon, { size: 13, color: 'white', strokeWidth: 2.5 })) : ''
|
||||
const el = document.createElement('div')
|
||||
el.style.cssText = 'width:26px;height:26px;cursor:pointer;'
|
||||
el.innerHTML = `<div style="width:26px;height:26px;border-radius:50%;background:${color};border:2px solid #fff;box-shadow:0 1px 5px rgba(0,0,0,0.3);display:flex;align-items:center;justify-content:center;box-sizing:border-box;">${svg}</div>`
|
||||
el.style.cssText = 'width:26px;height:26px;cursor:pointer;will-change:transform;'
|
||||
const inner = brandLogoMarkup(brandWikidata) || svg
|
||||
const background = brandWikidata ? '#fff' : color
|
||||
el.innerHTML = `<div style="width:26px;height:26px;border-radius:50%;background:${background};border:2px solid ${color};box-shadow:0 1px 5px rgba(0,0,0,0.3);display:flex;align-items:center;justify-content:center;box-sizing:border-box;overflow:hidden;">${inner}</div>`
|
||||
return el
|
||||
}
|
||||
|
||||
/**
|
||||
* The <img> for a brand logo, or '' when there is no brand.
|
||||
*
|
||||
* `onerror` empties the wrapper rather than leaving a broken-image glyph: the route
|
||||
* answers 204 for a brand Wikidata has no logo for, and an empty circle in the
|
||||
* category colour is the right fallback.
|
||||
*/
|
||||
function brandLogoMarkup(brandWikidata?: string | null): string {
|
||||
if (!brandWikidata || !/^Q[1-9][0-9]{0,11}$/.test(brandWikidata)) return ''
|
||||
return `<img src="/api/maps/brand-logo/${brandWikidata}" alt="" width="18" height="18" style="display:block;object-fit:contain;" onerror="this.remove()" />`
|
||||
}
|
||||
|
||||
export function MapViewGL({
|
||||
places = NO_PLACES,
|
||||
dayPlaces = NO_PLACES,
|
||||
@@ -1234,7 +1251,7 @@ export function MapViewGL({
|
||||
poiMarkersRef.current.forEach(m => m.remove())
|
||||
poiMarkersRef.current = []
|
||||
for (const poi of (pois as Poi[])) {
|
||||
const el = createPoiMarkerElement(poi.category)
|
||||
const el = createPoiMarkerElement(poi.category, poi.brand_wikidata)
|
||||
el.addEventListener('mouseenter', () => {
|
||||
popupRef.current?.setLngLat([poi.lng, poi.lat]).setHTML(buildPoiPopupHtml(poi)).addTo(map)
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user