baldav/frontend/js/poi.js
4aiman 06cc77fdec Maps: trim tile retention, simplify big tracks by zoom, thin POIs when zoomed out
Canvas (prev commit) made the big-track map self-recover but the Chromium
compositor still hit 'tile memory limits exceeded' ~46x and 10k segments
still janked. Three cheap levers:
- tilecache: keepBuffer 2->1 and updateWhenZooming:false - fewer retained
  off-screen tiles in the compositor's raster budget on a high-DPI screen.
- track/roads polylines: smoothFactor 2.5 (Douglas-Peucker in pixels) - a
  10k-point track draws far fewer segments zoomed out, full detail zoomed in.
- POI: cap markers by zoom (z>=16:500, 15:200, 14:80, 13:30) instead of a
  flat 500 - a handful near min-zoom, everything only when zoomed in close.
2026-09-29 15:49:02 +03:00

112 lines
5.6 KiB
JavaScript

// Offline POI overlay for BalDav maps.
// Circular, contrasting icon markers (emoji-in-a-circle, readable on light AND dark
// tiles), culled to the visible bounds and shown only from a min zoom. Data is bundled
// (frontend/data/pois.json = [{la,lo,c,n}]) so it works fully offline. A Leaflet control
// toggles the layer and opens a per-category filter. Exposes window.POI.
(function () {
let MINZOOM = 13, CAP = 500; // don't render below this zoom; cap markers per view
// category -> emoji + Russian label (order defines the filter panel order)
let CATS = {
supermarket: { e: "🛒", l: "Супермаркет" }, bakery: { e: "🥖", l: "Пекарня" },
beverages: { e: "🥤", l: "Напитки" }, coffee: { e: "☕", l: "Кофе" },
cafe: { e: "☕", l: "Кафе" }, restaurant: { e: "🍴", l: "Ресторан" },
fast_food: { e: "🍔", l: "Фастфуд" }, food_court: { e: "🍽", l: "Фудкорт" },
pub: { e: "🍺", l: "Паб" }, bar: { e: "🍸", l: "Бар" }, bbq: { e: "🍖", l: "Мангал" },
drinking_water: { e: "🚰", l: "Вода" }, toilets: { e: "🚻", l: "Туалет" },
atm: { e: "🏧", l: "Банкомат" }, bank: { e: "🏦", l: "Банк" },
bench: { e: "🪑", l: "Скамейка" }, shelter: { e: "⛺", l: "Укрытие" },
picnic_site: { e: "🧺", l: "Пикник" }, viewpoint: { e: "🔭", l: "Смотровая" },
bicycle_parking: { e: "🚲", l: "Велопарк." }, bus_station: { e: "🚌", l: "Остановка" },
};
let POI = { CATS: CATS, _data: null, _loading: null };
function enabledSet() {
try { let s = localStorage.getItem("baldav_poi_cats"); if (s) return new Set(JSON.parse(s)); } catch (e) {}
return new Set(Object.keys(CATS)); // all on by default
}
function saveEnabled(set) { try { localStorage.setItem("baldav_poi_cats", JSON.stringify([...set])); } catch (e) {} }
POI.load = function () {
if (POI._data) return Promise.resolve(POI._data);
if (POI._loading) return POI._loading;
POI._loading = fetch("data/pois.json").then(r => r.json())
.then(d => { POI._data = d; return d; })
.catch(() => { POI._data = []; return []; });
return POI._loading;
};
function icon(cat) {
let c = CATS[cat] || { e: "•" };
return L.divIcon({ className: "poi-badge-wrap", html: '<div class="poi-badge">' + c.e + "</div>", iconSize: [26, 26], iconAnchor: [13, 13] });
}
// Attach the POI overlay + a control to a map. Renders on move/zoom, culled to view.
POI.attach = function (map) {
let group = L.layerGroup();
let shown = localStorage.getItem("baldav_poi_shown") === "1";
let enabled = enabledSet();
function render() {
group.clearLayers();
let z = map.getZoom();
if (!shown || z < MINZOOM || !POI._data) return;
// Fewer markers the further you zoom out: a wide view spans too many POIs to read (and to
// draw). Scale the cap with zoom - a handful near MINZOOM, the full CAP once zoomed in.
let cap = z >= 16 ? CAP : z >= 15 ? 200 : z >= 14 ? 80 : 30;
let b = map.getBounds(), n = 0;
for (let i = 0; i < POI._data.length; i++) {
let p = POI._data[i];
if (!enabled.has(p.c)) continue;
if (p.la < b.getSouth() || p.la > b.getNorth() || p.lo < b.getWest() || p.lo > b.getEast()) continue;
let m = L.marker([p.la, p.lo], { icon: icon(p.c) });
let c = CATS[p.c] || { l: p.c };
m.bindPopup((p.n ? "<b>" + p.n + "</b><br>" : "") + c.l);
group.addLayer(m);
if (++n >= cap) break;
}
}
map.on("moveend zoomend", render);
if (shown) group.addTo(map);
POI.load().then(render);
function setShown(v) {
shown = v;
try { localStorage.setItem("baldav_poi_shown", v ? "1" : "0"); } catch (e) {}
if (v) group.addTo(map); else map.removeLayer(group);
render();
}
// --- Leaflet control: 📍 button + collapsible category filter ---
let Ctl = L.Control.extend({
options: { position: "topright" },
onAdd: function () {
let wrap = L.DomUtil.create("div", "poi-ctl");
let btn = L.DomUtil.create("button", "rec-map-btn poi-btn" + (shown ? " active" : ""), wrap);
btn.textContent = "📍"; btn.title = "Точки интереса"; btn.style.position = "static";
let panel = L.DomUtil.create("div", "poi-panel", wrap); panel.hidden = true;
// master row
let head = L.DomUtil.create("label", "poi-row poi-head", panel);
let master = document.createElement("input"); master.type = "checkbox"; master.checked = shown;
head.appendChild(master); head.appendChild(document.createTextNode(" Показать POI"));
master.onchange = () => { setShown(master.checked); btn.classList.toggle("active", master.checked); };
// per-category rows
Object.keys(CATS).forEach(cat => {
let row = L.DomUtil.create("label", "poi-row", panel);
let cb = document.createElement("input"); cb.type = "checkbox"; cb.checked = enabled.has(cat);
cb.onchange = () => { if (cb.checked) enabled.add(cat); else enabled.delete(cat); saveEnabled(enabled); render(); };
row.appendChild(cb);
row.appendChild(document.createTextNode(" " + (CATS[cat].e) + " " + CATS[cat].l));
});
btn.onclick = (e) => { e.preventDefault(); e.stopPropagation(); panel.hidden = !panel.hidden; };
L.DomEvent.disableClickPropagation(wrap);
L.DomEvent.disableScrollPropagation(panel);
return wrap;
},
});
map.addControl(new Ctl());
return { layer: group, render: render, setShown: setShown };
};
window.POI = POI;
})();