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.
112 lines
5.6 KiB
JavaScript
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;
|
|
})();
|