<!-- ==== Карта проектов (Leaflet) + фильтры + счётчик + попап с изображением ==== -->
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css">
<div class="projects-wrap">
<div class="filters">
<div class="filters__group" data-group="section">
<span class="filters__label">Разделы</span>
<button class="fbtn is-active" data-value="all">Все</button>
<button class="fbtn" data-value="private">Частная застройка</button>
<button class="fbtn" data-value="public">Общественные здания</button>
<button class="fbtn" data-value="multi">Многоквартирные комплексы</button>
<button class="fbtn" data-value="light">Некапитальная архитектура</button>
</div>
<div class="filters__group" data-group="region">
<span class="filters__label">Регион</span>
<button class="fbtn is-active" data-value="all">Все</button>
<button class="fbtn" data-value="nw">С-З/СПб</button>
<button class="fbtn" data-value="center">Центр</button>
<button class="fbtn" data-value="pv">Поволжье</button>
<button class="fbtn" data-value="south">Юг</button>
<button class="fbtn" data-value="ural">Урал</button>
<button class="fbtn" data-value="siberia">Сибирь</button>
<button class="fbtn" data-value="fareast">Дальний Восток</button>
</div>
<div class="filters__count">
<span id="shown">0</span> / <span id="total">0</span> показано
</div>
</div>
<div id="projects-map"></div>
</div>
<style>
:root{
--ink:#22211E; /* фирменный тёмный */
--paper:#FAFAFA; /* фирменный светлый */
--ink-20: rgba(34,33,30,.20);
--ink-10: rgba(34,33,30,.10);
}
.projects-wrap{max-width:1200px;margin:0 auto}
#projects-map{
height:520px;border-radius:16px;overflow:hidden;background:var(--paper);
border:1px solid var(--ink-10)
}
/* Фильтры — минимализм */
.filters{display:flex;flex-wrap:wrap;gap:10px 18px;align-items:center;margin:0 0 14px}
.filters__group{display:flex;gap:8px 8px;align-items:center;flex-wrap:wrap}
.filters__label{font:600 12px/1.2 Inter,system-ui;color:var(--ink);opacity:.7;margin-right:4px}
.fbtn{
appearance:none;background:transparent;color:var(--ink);
border:1px solid var(--ink); border-radius:999px; padding:6px 12px;
font:500 12px/1 Inter,system-ui; cursor:pointer; transition:background .15s ease
}
.fbtn.is-active{background:var(--ink);color:var(--paper)}
.fbtn:focus{outline:none;box-shadow:0 0 0 2px var(--ink-20)}
.filters__count{margin-left:auto;font:12px/1 Inter,system-ui;color:var(--ink);opacity:.7}
/* Кастомный круглый маркер (размер задаём инлайном) */
.proj-dot{
border-radius:50%; border:2px solid var(--paper); background:var(--ink);
box-shadow:0 0 0 1px var(--ink-20);
transition:transform .18s ease, box-shadow .18s ease;
transform:translate(-50%,-50%); position:absolute; left:0; top:0; pointer-events:auto
}
.proj-dot:hover{ transform:translate(-50%,-50%) scale(1.35); box-shadow:0 6px 14px var(--ink-20) }
/* Tooltip */
.leaflet-tooltip.my-tip{
background:var(--ink); color:var(--paper); border:none; border-radius:10px;
padding:8px 10px; font:500 12px/1.3 Inter,system-ui
}
.leaflet-tooltip.my-tip:before{display:none}
/* Попап-карточка */
.leaflet-popup-content{margin:0}
.popup-card{min-width:260px;max-width:300px;background:var(--paper);color:var(--ink)}
.popup-card__img{width:100%;height:160px;object-fit:cover;border-radius:10px;display:block}
.popup-card__inner{padding:10px 12px 12px}
.popup-card__head{font:600 14px/1.3 Inter,system-ui;margin:0 0 6px}
.popup-card__meta{font:12px/1.45 Inter,system-ui;opacity:.8}
.popup-card__tag{display:inline-block;margin-top:8px;font:11px/1 Inter,system-ui;
border:1px solid var(--ink);border-radius:999px;padding:4px 8px}
/* Атрибуция OSM (лаконично) */
.leaflet-control-attribution{
font:10px/1.2 Inter,system-ui;color:var(--ink);opacity:.5;background:rgba(250,250,250,.9);
border-radius:8px;padding:4px 6px;border:1px solid var(--ink-10)
}
@media (max-width:640px){
#projects-map{height:420px}
.filters__count{width:100%}
}
/* === Мобильная адаптация 480 и ниже === */
@media (max-width: 480px){
.filters{
flex-direction: column;
align-items: stretch;
gap: 12px;
margin-bottom: 12px;
}
.filters__group{
width: 100%;
overflow-x: auto;
-webkit-overflow-scrolling: touch;
gap: 8px;
padding-bottom: 4px;
scrollbar-width: none;
}
.filters__group::-webkit-scrollbar{ display:none; }
.filters__label{ flex:0 0 auto; margin-right:8px; }
.fbtn{
flex: 0 0 auto;
white-space: nowrap;
padding: 7px 10px;
font-size: 11px;
border-radius: 20px;
}
.filters__count{
margin-left: 0;
width: 100%;
text-align: right;
font-size: 11px;
}
#projects-map{ height: 360px; }
.leaflet-tooltip.my-tip{ max-width: calc(100vw - 40px); }
.leaflet-control-attribution{ font-size:9px; padding:3px 5px; }
.popup-card{
min-width: 0;
max-width: calc(100vw - 32px);
}
.popup-card__img{ height: 140px; }
.popup-card__inner{ padding: 10px; }
.popup-card__head{ font-size: 13px; }
.popup-card__meta{ font-size: 11px; }
}
/* — очень узкие экраны (320–360) — */
@media (max-width: 360px){
#projects-map{ height: 320px; }
.fbtn{ padding: 6px 8px; font-size: 10.5px; }
.popup-card__img{ height: 120px; }
}
</style>
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<script>
(function(){
const IMG_URL = 'https://static.tildacdn.com/tild3235-3361-4862-b832-373834653534/image.png';
const STYLE_TXT = 'Стиль: Ампир — величие и торжественность эпохи начала XIX века';
const STATUS23 = 'Статус: Реализован в 2023 году';
// Размеры маркеров по разделам (px)
const DOT_SIZES = { private:12, public:14, multi:16, light:10 };
// Данные
const projects = [
// Москва (оригиналы)
{ title:'«Московские ярмарки: Павильон на Коровинском шоссе» — Москва',
coords:[55.879,37.539], section:'light', region:'center', meta:STYLE_TXT+' • '+STATUS23 },
{ title:'«Московские сезоны: Ярмарка в Кузьминках» — Москва',
coords:[55.702,37.792], section:'public', region:'center', meta:STYLE_TXT },
{ title:'«Московские сезоны: Ярмарка в Митино» — Москва',
coords:[55.846,37.361], section:'public', region:'center', meta:STYLE_TXT },
// С-З
{ title:'«Московские ярмарки: Павильон на Коровинском шоссе» — Санкт-Петербург',
coords:[59.9386,30.3141], section:'light', region:'nw', meta:STYLE_TXT },
{ title:'«Московские сезоны: Ярмарка в Кузьминках» — Гатчина',
coords:[59.5707,30.1283], section:'public', region:'nw', meta:STYLE_TXT },
{ title:'«Московские сезоны: Ярмарка в Митино» — Всеволожск',
coords:[60.0160,30.6456], section:'public', region:'nw', meta:STYLE_TXT },
// Центр
{ title:'«Московские ярмарки: Павильон на Коровинском ш.» — Тверь',
coords:[56.8587,35.9176], section:'light', region:'center', meta:STYLE_TXТ() },
{ title:'«Московские сезоны: Ярмарка в Кузьминках» — Ярославль',
coords:[57.6266,39.8938], section:'public', region:'center', meta:STYLE_TXT },
{ title:'«Московские сезоны: Ярмарка в Митино» — Кострома',
coords:[57.7679,40.9269], section:'public', region:'center', meta:STYLE_TXТ },
{ title:'«Московские ярмарки: Павильон на Коровинском ш.» — Владимир',
coords:[56.1291,40.4066], section:'light', region:'center', meta:STYLE_TXТ },
{ title:'«Московские сезоны: Ярмарка в Кузьминках» — Рязань',
coords:[54.6290,39.7360], section:'public', region:'center', meta:STYLE_TXТ },
{ title:'«Московские сезоны: Ярмарка в Митино» — Тула',
coords:[54.1961,37.6182], section:'public', region:'center', meta:STYLE_TXТ },
{ title:'«Московские ярмарки: Павильон на Коровинском ш.» — Калуга',
coords:[54.5138,36.2612], section:'light', region:'center', meta:STYLE_TXТ },
{ title:'«Московские сезоны: Ярмарка в Кузьминках» — Воронеж',
coords:[51.6755,39.2089], section:'public', region:'center', meta:STYLE_TXТ },
// Поволжье
{ title:'«Московские сезоны: Ярмарка в Митино» — Нижний Новгород',
coords:[56.3269,44.0059], section:'public', region:'pv', meta:STYLE_TXТ },
{ title:'«Московские ярмарки: Павильон на Коровинском ш.» — Казань',
coords:[55.7963,49.1063], section:'light', region:'pv', meta:STYLE_TXТ },
{ title:'«Московские сезоны: Ярмарка в Кузьминках» — Самара',
coords:[53.1959,50.1002], section:'public', region:'pv', meta:STYLE_TXТ },
{ title:'«Московские сезоны: Ярмарка в Митино» — Саратов',
coords:[51.5331,46.0342], section:'public', region:'pv', meta:STYLE_TXТ },
{ title:'«Московские ярмарки: Павильон на Коровинском ш.» — Ульяновск',
coords:[54.3142,48.4031], section:'light', region:'pv', meta:STYLE_TXТ },
// Юг
{ title:'«Московские сезоны: Ярмарка в Кузьминках» — Краснодар',
coords:[45.0355,38.9753], section:'public', region:'south', meta:STYLE_TXТ },
{ title:'«Московские сезоны: Ярмарка в Митино» — Сочи',
coords:[43.6028,39.7342], section:'public', region:'south', meta:STYLE_TXТ },
{ title:'«Московские ярмарки: Павильон на Коровинском ш.» — Ростов-на-Дону',
coords:[47.2357,39.7015], section:'light', region:'south', meta:STYLE_TXТ },
{ title:'«Московские сезоны: Ярмарка в Кузьминках» — Волгоград',
coords:[48.7080,44.5133], section:'public', region:'south', meta:STYLE_TXТ },
// Урал
{ title:'«Московские сезоны: Ярмарка в Митино» — Екатеринбург',
coords:[56.8389,60.6057], section:'public', region:'ural', meta:STYLE_TXТ },
{ title:'«Московские ярмарки: Павильон на Коровинском ш.» — Челябинск',
coords:[55.1644,61.4368], section:'light', region:'ural', meta:STYLE_TXТ },
{ title:'«Московские сезоны: Ярмарка в Кузьминках» — Пермь',
coords:[58.0105,56.2294], section:'public', region:'ural', meta:STYLE_TXТ },
{ title:'«Московские сезоны: Ярмарка в Митино» — Тюмень',
coords:[57.1530,65.5343], section:'public', region:'ural', meta:STYLE_TXТ },
{ title:'«Московские ярмарки: Павильон на Коровинском ш.» — Уфа',
coords:[54.7388,55.9721], section:'light', region:'ural', meta:STYLE_TXТ },
// Сибирь
{ title:'«Московские сезоны: Ярмарка в Кузьминках» — Омск',
coords:[54.9885,73.3242], section:'public', region:'siberia', meta:STYLE_TXТ },
{ title:'«Московские сезоны: Ярмарка в Митино» — Новосибирск',
coords:[55.0084,82.9357], section:'public', region:'siberia', meta:STYLE_TXТ },
{ title:'«Московские ярмарки: Павильон на Коровинском ш.» — Красноярск',
coords:[56.0153,92.8932], section:'light', region:'siberia', meta:STYLE_TXТ },
{ title:'«Московские сезоны: Ярмарка в Кузьминках» — Иркутск',
coords:[52.2871,104.2810], section:'public', region:'siberia', meta:STYLE_TXТ },
// Дальний Восток
{ title:'«Московские сезоны: Ярмарка в Митино» — Хабаровск',
coords:[48.4827,135.0840], section:'public', region:'fareast', meta:STYLE_TXТ },
{ title:'«Московские ярмарки: Павильон на Коровинском ш.» — Владивосток',
coords:[43.1150,131.8850], section:'light', region:'fareast', meta:STYLE_TXТ }
];
// ==== карта ====
const map = L.map('projects-map', {
zoomControl: false,
scrollWheelZoom: false,
attributionControl: false,
maxBounds: [[15,-30],[85,190]],
maxBoundsViscosity: 0.6
});
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '© OpenStreetMap contributors', minZoom:2, maxZoom:18
}).addTo(map);
L.control.attribution({ prefix: '' }).addTo(map);
// иконка с размером по разделу
const makeIcon = (section) => {
const sz = DOT_SIZES[section] || 12;
const html = `<div class="proj-dot" style="width:${sz}px;height:${sz}px"></div>`;
return new L.DivIcon({ className:'', html, iconSize:[sz,sz], iconAnchor:[sz/2,sz/2] });
};
// маркеры
const markers = projects.map(p=>{
const m = L.marker(p.coords, {icon: makeIcon(p.section)})
.bindTooltip(p.title, {direction:'top', offset:[0,-12], className:'my-tip'})
.bindPopup(makePopup(p), {className:'popup-card'});
m.addTo(map);
return { marker:m, ...p };
});
// кадр по точкам
const bounds = L.latLngBounds(projects.map(p=>p.coords));
map.fitBounds(bounds.pad(0.03));
if (map.getZoom() < 4) map.setZoom(4);
// счётчик
const shownEl = document.getElementById('shown');
const totalEl = document.getElementById('total');
totalEl.textContent = String(projects.length);
// стейт фильтров (+ URL)
const state = {
section: getParam('section') || 'all',
region: getParam('region') || 'all'
};
setActiveBtn('section', state.section);
setActiveBtn('region', state.region);
applyFilters();
// обработчики фильтров
document.querySelectorAll('.filters__group').forEach(group=>{
group.addEventListener('click', e=>{
const btn = e.target.closest('.fbtn'); if(!btn) return;
group.querySelectorAll('.fbtn').forEach(b=>b.classList.remove('is-active'));
btn.classList.add('is-active');
state[group.dataset.group] = btn.dataset.value;
updateURL();
applyFilters();
});
});
function applyFilters(){
markers.forEach(o=>{
const okSec = state.section==='all' || o.section===state.section;
const okReg = state.region==='all' || o.region===state.region;
if (okSec && okReg){
if (!map.hasLayer(o.marker)) o.marker.addTo(map);
} else {
if (map.hasLayer(o.marker)) o.marker.remove();
}
});
const visible = markers.filter(o=>map.hasLayer(o.marker));
shownEl.textContent = String(visible.length);
if (visible.length){
const b = L.latLngBounds(visible.map(v=>v.marker.getLatLng()));
map.fitBounds(b.pad(0.05));
if (map.getZoom() < 4) map.setZoom(4);
}
}
function makePopup(p){
const tag = p.section==='private' ? 'Частная застройка'
: p.section==='public' ? 'Общественные здания'
: p.section==='multi' ? 'Многоквартирные комплексы'
: 'Некапитальная архитектура';
const imgSrc = p.img || IMG_URL;
return `
<div class="popup-card">
<img class="popup-card__img" src="${imgSrc}" alt="">
<div class="popup-card__inner">
<div class="popup-card__head">${p.title}</div>
<div class="popup-card__meta">${p.meta || ''}</div>
<div class="popup-card__tag">${tag}</div>
</div>
</div>`;
}
// helpers: URL-параметры и подсветка активных
function getParam(key){ const p=new URLSearchParams(location.search); return p.get(key); }
function updateURL(){
const p = new URLSearchParams(location.search);
p.set('section', state.section); p.set('region', state.region);
history.replaceState(null, '', `${location.pathname}?${p.toString()}`);
}
function setActiveBtn(group, val){
const root = document.querySelector(`.filters__group[data-group="${group}"]`);
if (!root) return;
let target = root.querySelector(`.fbtn[data-value="${val}"]`) || root.querySelector(`.fbtn[data-value="all"]`);
root.querySelectorAll('.fbtn').forEach(b=>b.classList.remove('is-active'));
target && target.classList.add('is-active');
}
map.on('click', ()=> map.scrollWheelZoom.enable());
// избегаем опечатки в одной записи
function STYLE_TXТ(){ return STYLE_TXT; }
})();
</script>