← back to Rentv 2026
public/map.html
387 lines
<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>News Map — RENTV.com</title>
<meta name="description" content="Every RENTV commercial real estate story on the map. Turn on your location to see the news around you — green pins are from the last three months, red pins are older.">
<link rel="canonical" href="https://rentv.agentabrams.com/map">
<meta property="og:type" content="website">
<meta property="og:site_name" content="RENTV.com">
<meta property="og:title" content="RENTV News Map — CRE news around you">
<meta property="og:description" content="Every RENTV commercial real estate story on the map. Turn on location to see the news around you — green pins are the last 3 months, red are older.">
<meta property="og:url" content="https://rentv.agentabrams.com/map">
<meta property="og:image" content="https://rentv.agentabrams.com/img/map-og.jpg">
<meta property="og:image:width" content="1200">
<meta property="og:image:height" content="630">
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="RENTV News Map — CRE news around you">
<meta name="twitter:description" content="Every RENTV commercial real estate story on the map — green pins are the last 3 months, red are older.">
<meta name="twitter:image" content="https://rentv.agentabrams.com/img/map-og.jpg">
<link href="https://fonts.googleapis.com/css2?family=Playfair+Display:wght@400;700&family=Inter:wght@300;400;600;800&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css">
<link rel="stylesheet" href="https://unpkg.com/leaflet.markercluster@1.5.3/dist/MarkerCluster.css">
<link rel="stylesheet" href="https://unpkg.com/leaflet.markercluster@1.5.3/dist/MarkerCluster.Default.css">
<style>
:root{--red:#c8102e;--ink:#14171a;--sub:#5b636b;--line:#e4e7ea;--bg:#fff;--wash:#f5f6f8;--green:#1a9e4b}
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:'Inter',system-ui,sans-serif;color:var(--ink);background:var(--bg);line-height:1.5}
a{color:inherit;text-decoration:none}.wrap{max-width:1100px;margin:0 auto;padding:0 20px}
.util{background:var(--ink);color:#cfd4d9;font-size:12px}.util .wrap{display:flex;justify-content:space-between;align-items:center;height:34px}.util a{color:#cfd4d9}
.mast{border-bottom:2px solid var(--ink);padding:14px 0 10px;text-align:center}
.logo{font-family:'Playfair Display',serif;font-weight:700;font-size:30px;letter-spacing:-1px}.logo span{color:var(--red)}
.logo small{display:block;font-size:10px;letter-spacing:3px;text-transform:uppercase;color:var(--sub);margin-top:2px}
/* control bar */
.bar{border-bottom:1px solid var(--line);background:var(--wash)}
.bar .wrap{max-width:none;display:flex;align-items:center;gap:14px;flex-wrap:wrap;padding:9px 18px}
.bar h1{font-family:'Playfair Display',serif;font-size:19px;font-weight:700;margin-right:auto}
.bar h1 .n{color:var(--sub);font-family:'Inter';font-size:12px;font-weight:600}
.bar label{display:inline-flex;align-items:center;gap:7px;font-size:11px;font-weight:800;letter-spacing:.06em;text-transform:uppercase;color:var(--sub)}
.bar select{font:600 13px 'Inter';padding:6px 9px;border:1.5px solid var(--line);border-radius:8px;background:#fff;color:var(--ink);cursor:pointer}
.bar select:focus{outline:0;border-color:var(--red)}
.btn{font:800 12px 'Inter';letter-spacing:.04em;text-transform:uppercase;padding:8px 14px;border:0;border-radius:8px;background:var(--red);color:#fff;cursor:pointer}
.btn.ghost{background:#fff;color:var(--ink);border:1.5px solid var(--line)}
.btn:active{transform:translateY(1px)}
.legend{display:inline-flex;align-items:center;gap:12px;font-size:12px;color:var(--sub);font-weight:600}
.legend .dot{width:11px;height:11px;border-radius:50%;display:inline-block;margin-right:5px;vertical-align:-1px;border:1.5px solid #fff;box-shadow:0 0 0 1px rgba(0,0,0,.15)}
.legend .g{background:var(--green)}.legend .r{background:var(--red)}
/* stage: map + side list. Explicit viewport height so the map survives sendPage's
injected nav/footer (which would otherwise starve a flex:1 layout). */
.stage{display:flex;height:80vh;min-height:480px;position:relative}
#map{flex:1 1 auto;min-width:0;height:100%;background:#e8ecef}
aside{flex:0 0 340px;border-left:1px solid var(--line);display:flex;flex-direction:column;min-height:0;background:#fff}
aside .ah{padding:12px 16px;border-bottom:1px solid var(--line);font-size:12px;font-weight:800;text-transform:uppercase;letter-spacing:.05em;color:var(--sub);display:flex;justify-content:space-between;align-items:center}
.stats{padding:12px 16px;border-bottom:1px solid var(--line);background:var(--wash);flex:0 0 auto}
.stats .big{display:flex;gap:14px;margin-bottom:6px}
.stats .kpi{flex:1}
.stats .kpi .v{font-family:'Playfair Display',serif;font-size:22px;font-weight:700;line-height:1;color:var(--ink)}
.stats .kpi .l{font-size:10px;font-weight:800;text-transform:uppercase;letter-spacing:.05em;color:var(--sub);margin-top:3px}
.stats .mt{font-size:10px;font-weight:800;text-transform:uppercase;letter-spacing:.05em;color:var(--sub);margin:8px 0 3px}
.stats .mrow{display:flex;align-items:center;gap:8px;font-size:12px;padding:3px 5px;cursor:pointer;border-radius:5px}
.stats .mrow:hover{background:#fff}
.stats .mrow .nm{flex:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;font-weight:600}
.stats .mrow .bar{height:6px;background:var(--red);border-radius:3px;opacity:.85;flex:0 0 auto}
.stats .mrow .ct{color:var(--sub);font-weight:800;min-width:24px;text-align:right}
#list{overflow:auto;flex:1 1 auto}
.it{display:block;padding:12px 16px;border-bottom:1px solid var(--line);transition:background .12s}
.it:hover{background:var(--wash)}
.it .t{font-family:'Playfair Display',serif;font-size:15px;line-height:1.28;margin:3px 0 4px}
.it .m{font-size:12px;color:var(--sub);display:flex;gap:8px;flex-wrap:wrap;align-items:center}
.it .dot{width:9px;height:9px;border-radius:50%;display:inline-block;flex:0 0 auto}
.it .g{background:var(--green)}.it .r{background:var(--red)}
.it .amt{color:var(--ink);font-weight:700}
.it .dist{margin-left:auto;color:var(--red);font-weight:800;font-size:11px}
.empty{padding:26px 16px;color:var(--sub);font-size:14px}
/* leaflet popup */
.leaflet-popup-content{margin:12px 14px;font-family:'Inter',sans-serif}
.pp .pt{font-family:'Playfair Display',serif;font-size:16px;line-height:1.25;margin-bottom:6px}
.pp .pm{font-size:12px;color:var(--sub);margin-bottom:8px}
.pp a.open{display:inline-block;font:800 11px 'Inter';text-transform:uppercase;letter-spacing:.04em;color:#fff;background:var(--red);padding:6px 12px;border-radius:7px}
/* colored cluster badges */
.cl{border-radius:50%;display:flex;align-items:center;justify-content:center;font:800 13px 'Inter';color:#fff;border:2px solid #fff;box-shadow:0 2px 8px rgba(0,0,0,.3)}
.cl.g{background:rgba(26,158,75,.9)}.cl.r{background:rgba(200,16,46,.88)}
.me{background:#1467d6;border:3px solid #fff;border-radius:50%;width:18px;height:18px;box-shadow:0 0 0 4px rgba(20,103,214,.3)}
@media(max-width:760px){aside{position:absolute;right:0;top:auto;bottom:0;height:44%;width:100%;flex-basis:auto;border-left:0;border-top:1px solid var(--line);z-index:1000}
.stage{position:relative}.bar h1{font-size:15px}}
/* ── Hamburger + drawer: all the nav tabs collapse in here so the map gets the screen ── */
.rentvtabs{display:none!important} /* injected region/section strip → moved into the drawer */
/* Minimal top bar: hamburger UPPER-LEFT + UPPER-RIGHT, wordmark centered. All nav/tools live
in the two drawers (left = Explore+Regions, right = Sections+Filters). */
.mast{position:relative;display:flex;align-items:center;justify-content:center;padding:12px 56px;min-height:52px}
.mast .logo{position:absolute;left:50%;transform:translateX(-50%);font-size:26px;white-space:nowrap}
.cntpill{font-size:12px;font-weight:700;color:var(--sub);white-space:nowrap}
.ham{position:absolute;top:50%;transform:translateY(-50%);background:#fff;border:1.5px solid var(--line);border-radius:9px;font-size:17px;line-height:1;padding:8px 11px;cursor:pointer;color:var(--ink);z-index:5}
.ham.hamL{left:14px}.ham.hamR{right:14px}
.ham:hover{border-color:var(--red);color:var(--red)}
.drawer{position:fixed;inset:0;z-index:4000;display:none}
.drawer.open{display:block}
.drawer .bd{position:absolute;inset:0;background:rgba(0,0,0,.45)}
.drawer .pl{position:absolute;top:0;width:min(86vw,300px);height:100%;background:#fff;overflow:auto;padding:18px 16px 44px}
.drawer.left .pl{left:0;box-shadow:10px 0 44px rgba(0,0,0,.32)}
.drawer.right .pl{right:0;box-shadow:-10px 0 44px rgba(0,0,0,.32)}
.drawer .pl h3{font-family:'Playfair Display',serif;font-size:19px;margin:0 0 2px}
.drawer .x{position:absolute;top:12px;right:14px;font-size:24px;background:none;border:0;cursor:pointer;color:var(--sub);line-height:1}
.drawer .gt{font-size:10px;font-weight:800;text-transform:uppercase;letter-spacing:.08em;color:var(--sub);margin:16px 0 6px}
.drawer .legend{display:flex;gap:16px;font-size:12px;color:var(--sub);margin:2px 0 4px}
.drawer .fl{display:block;font-size:10px;font-weight:800;text-transform:uppercase;letter-spacing:.05em;color:var(--sub);margin:11px 0 0}
.drawer .fl select{display:block;width:100%;margin-top:5px;font:600 14px 'Inter';padding:8px 10px;border:1.5px solid var(--line);border-radius:8px;background:#fff;color:var(--ink);cursor:pointer}
.drawer .fl select:focus{outline:0;border-color:var(--red)}
.drawer .yrs{display:flex;gap:8px;align-items:center}.drawer .yrs .fl{flex:1;margin-top:0}
.drawer .btn{width:100%;margin-top:12px;font:800 12px 'Inter';letter-spacing:.04em;text-transform:uppercase;padding:11px 14px;border:0;border-radius:9px;background:var(--red);color:#fff;cursor:pointer}
.drawer .btn.ghost{background:#fff;color:var(--ink);border:1.5px solid var(--line)}
.drawer a{display:block;padding:9px 4px;border-bottom:1px solid var(--line);color:var(--ink);font-weight:600;font-size:14.5px;white-space:normal}
.drawer a:hover{color:var(--red)}
.drawer .clone{display:flex;flex-wrap:wrap;gap:2px 4px}
.drawer .clone a{border:0;display:inline-block;padding:5px 8px;font-size:12.5px;background:var(--wash);border-radius:6px;font-weight:600}
.drawer .clone .rt-k,.drawer .clone .rt-div{display:none}
@media(max-width:760px){.mast{padding:8px 12px 8px 54px}.mast .logo{font-size:22px}.util{display:none}}
</style></head><body>
<header class="mast">
<button class="ham hamL" id="hamL" aria-label="Explore & regions menu" aria-expanded="false">☰</button>
<a class="logo" href="/">REN<span>TV</span>.com<small>News Map</small></a>
<button class="ham hamR" id="hamR" aria-label="Sections & filters menu" aria-expanded="false">☰</button>
</header>
<!-- LEFT drawer: Explore + Regions -->
<div class="drawer left" id="drawerL" data-own-nav aria-hidden="true">
<div class="bd" data-close="L"></div>
<div class="pl">
<button class="x" data-close="L" aria-label="Close menu">×</button>
<h3>Explore</h3>
<a href="/">Front page</a>
<a href="/map">🗺 News Map</a>
<a href="/markets">Markets</a>
<a href="/market-pulse">📈 Market Pulse</a>
<a href="/deals-log">📋 Deals Log</a>
<a href="/review">The REview</a>
<a href="/cre-talk">CRE Talk</a>
<a href="/conferences">Conferences</a>
<a href="/blog">Blog</a>
<a href="/about">About</a>
<a href="/advertise">Advertise</a>
<a href="/subscribe">Subscribe</a>
<div class="gt">Regions</div>
<div id="drawerRegions"></div>
</div>
</div>
<!-- RIGHT drawer: Sections + map tools/filters -->
<div class="drawer right" id="drawerR" data-own-nav aria-hidden="true">
<div class="bd" data-close="R"></div>
<div class="pl">
<button class="x" data-close="R" aria-label="Close menu">×</button>
<h3>Filter the map <span class="cntpill" id="cnt"></span></h3>
<span class="legend"><span><span class="dot g"></span>Last 3 months</span><span><span class="dot r"></span>Older</span></span>
<label class="fl">Show
<select id="fresh">
<option value="all">All stories</option>
<option value="new">New only (green)</option>
<option value="old">Older only (red)</option>
</select>
</label>
<label class="fl">Type
<select id="txn"><option value="">All</option></select>
</label>
<div class="gt" style="margin-bottom:4px">Years</div>
<div class="yrs"><label class="fl" style="margin:0">From<select id="yfrom" title="From year"></select></label><label class="fl" style="margin:0">To<select id="yto" title="To year"></select></label></div>
<button class="btn" id="near">📍 Near me</button>
<button class="btn ghost" id="reset">Reset view</button>
<div class="gt">Sections</div>
<div id="drawerSections"></div>
</div>
</div>
<div class="stage">
<div id="map"></div>
<aside>
<div class="stats" id="stats"></div>
<div class="ah"><span id="listhd">Stories in view</span><span id="listn"></span></div>
<div id="list"><p class="empty">Loading the newsroom map…</p></div>
</aside>
</div>
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<script src="https://unpkg.com/leaflet.markercluster@1.5.3/dist/leaflet.markercluster.js"></script>
<script>
var esc=function(s){return (s||'').replace(/[&<>"]/g,function(c){return {'&':'&','<':'<','>':'>','"':'"'}[c];});};
var $=function(id){return document.getElementById(id);};
// Two drawers: LEFT (Explore + Regions), RIGHT (Sections + map filters). The region/section
// links come from the hidden injected .rentvtabs strip — .rt-geo → Regions, .rt-cat → Sections.
function cloneRows(hostId, rowSel){
var host=$(hostId); if(!host || host.getAttribute('data-filled')) return;
var row=document.querySelector('.rentvtabs '+rowSel);
if(row){ host.innerHTML='<div class="clone">'+row.innerHTML+'</div>'; host.setAttribute('data-filled','1'); }
}
function openL(){ cloneRows('drawerRegions','.rt-geo'); var d=$('drawerL'); d.classList.add('open'); d.setAttribute('aria-hidden','false'); $('hamL').setAttribute('aria-expanded','true'); }
function openR(){ cloneRows('drawerSections','.rt-cat'); var d=$('drawerR'); d.classList.add('open'); d.setAttribute('aria-hidden','false'); $('hamR').setAttribute('aria-expanded','true'); }
function closeDrawer(which){
var d=$('drawer'+which); if(!d) return; d.classList.remove('open'); d.setAttribute('aria-hidden','true');
$('ham'+which).setAttribute('aria-expanded','false');
}
function closeAll(){ closeDrawer('L'); closeDrawer('R'); }
$('hamL').onclick=openL; $('hamR').onclick=openR;
document.addEventListener('click',function(e){
var c=e.target.closest('[data-close]'); if(c){ closeDrawer(c.getAttribute('data-close')); return; }
// tap a link inside either drawer → close both
if(e.target.closest('.drawer a')) closeAll();
});
document.addEventListener('keydown',function(e){if(e.key==='Escape')closeAll();});
// "Last 3 months" is computed HERE, at load, so freshness is always relative to today.
var CUTOFF=(function(){var d=new Date();d.setMonth(d.getMonth()-3);return d;})();
function isFresh(a){ return a.date && new Date(a.date+'T00:00:00') >= CUTOFF; }
var map=L.map('map',{zoomControl:true,worldCopyJump:true}).setView([37.5,-119],5); // Western US default
// Two free base layers (no API key): OSM streets + Esri World Imagery satellite.
var streets=L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',{
maxZoom:18, attribution:'© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>'
}).addTo(map);
var satellite=L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}',{
maxZoom:19, attribution:'Imagery © <a href="https://www.esri.com/">Esri</a>, Maxar, Earthstar Geographics'
});
L.control.layers({'Streets':streets,'Satellite':satellite},null,{position:'topright',collapsed:false}).addTo(map);
// Cluster group whose badge turns GREEN if it holds any fresh story, else RED.
var cluster=L.markerClusterGroup({
maxClusterRadius:46, spiderfyOnMaxZoom:true, showCoverageOnHover:false, chunkedLoading:true,
iconCreateFunction:function(c){
var kids=c.getAllChildMarkers(), fresh=false;
for(var i=0;i<kids.length;i++){ if(kids[i].options._fresh){fresh=true;break;} }
var n=c.getChildCount(), s= n<10?34 : n<100?42 : 50;
return L.divIcon({html:'<div class="cl '+(fresh?'g':'r')+'" style="width:'+s+'px;height:'+s+'px">'+n+'</div>',
className:'', iconSize:[s,s]});
}
});
map.addLayer(cluster);
// Container height is set via CSS after Leaflet init; force a size recalc so tiles fill it.
setTimeout(function(){map.invalidateSize();},200);
window.addEventListener('resize',function(){map.invalidateSize();});
var ALL=[], markers=[], meMarker=null, meLatLng=null;
var fFresh='all', fTxn='', fYFrom=0, fYTo=9999;
function popupHtml(a){
var meta=[a.city+(a.state?', '+a.state:''), a.txn, a.ptype, a.amt].filter(Boolean).join(' · ');
return '<div class="pp"><div class="pt">'+esc(a.title)+'</div>'+
'<div class="pm">'+esc(meta)+' · '+esc((a.date||'').slice(0,10))+'</div>'+
'<a class="open" href="'+esc(a.url)+'">Open story →</a></div>';
}
function passFilter(a){
if(fFresh==='new' && !a._fresh) return false;
if(fFresh==='old' && a._fresh) return false;
if(fTxn && a.txn!==fTxn) return false;
var y=parseInt((a.date||'').slice(0,4),10)||0;
if(y && (y<fYFrom || y>fYTo)) return false;
return true;
}
function buildMarkers(){
cluster.clearLayers(); markers=[];
var shown=ALL.filter(passFilter);
shown.forEach(function(a){
var m=L.circleMarker([a.lat,a.lng],{
radius:6, weight:1.5, color:'#fff',
fillColor:a._fresh?getComputedStyle(document.documentElement).getPropertyValue('--green').trim()||'#1a9e4b':'#c8102e',
fillOpacity:.92, _fresh:a._fresh, _a:a
});
m.bindPopup(popupHtml(a));
markers.push(m);
});
cluster.addLayers(markers);
$('cnt').textContent='· '+shown.length.toLocaleString()+' stories';
renderStats(shown);
refreshList();
}
function fmtUSD(n){ return n>=1e12?'$'+(n/1e12).toFixed(2)+'T' : n>=1e9?'$'+(n/1e9).toFixed(1)+'B' : n>=1e6?'$'+Math.round(n/1e6)+'M' : '$'+Math.round(n).toLocaleString(); }
// Market-activity summary over the current filtered set — story count, total priced
// deal volume, and the most-active metros (click a metro to fly there).
function renderStats(arr){
var vol=0, priced=0, metro={};
arr.forEach(function(a){
if(a.amount){vol+=a.amount;priced++;}
var k=a.city+(a.state?', '+a.state:'');
if(!metro[k])metro[k]={n:0,vol:0,lat:a.lat,lng:a.lng};
metro[k].n++; metro[k].vol+=a.amount||0;
});
var tops=Object.keys(metro).map(function(k){return {k:k,n:metro[k].n,vol:metro[k].vol,lat:metro[k].lat,lng:metro[k].lng};})
.sort(function(x,y){return y.n-x.n;}).slice(0,6);
var max=tops.length?tops[0].n:1;
var h='<div class="big"><div class="kpi"><div class="v">'+arr.length.toLocaleString()+'</div><div class="l">Stories</div></div>'+
'<div class="kpi"><div class="v">'+(vol?fmtUSD(vol):'—')+'</div><div class="l">Deal volume · '+priced+' priced</div></div></div>'+
'<div class="mt">Most active metros</div>';
tops.forEach(function(t){
h+='<div class="mrow" data-lat="'+t.lat+'" data-lng="'+t.lng+'" title="'+esc(t.k)+(t.vol?' · '+fmtUSD(t.vol):'')+'">'+
'<span class="nm">'+esc(t.k)+'</span><span class="bar" style="width:'+Math.max(8,Math.round(64*t.n/max))+'px"></span><span class="ct">'+t.n+'</span></div>';
});
$('stats').innerHTML=h;
}
// Click a metro row → fly the map there.
$('stats').addEventListener('click',function(e){
var el=e.target.closest('.mrow'); if(!el)return;
var la=parseFloat(el.getAttribute('data-lat')), ln=parseFloat(el.getAttribute('data-lng'));
if(isFinite(la)&&isFinite(ln)) map.setView([la,ln],10);
});
// Haversine miles
function miles(a,b){var R=3959,dLat=(b[0]-a[0])*Math.PI/180,dLng=(b[1]-a[1])*Math.PI/180,
s=Math.sin(dLat/2)*Math.sin(dLat/2)+Math.cos(a[0]*Math.PI/180)*Math.cos(b[0]*Math.PI/180)*Math.sin(dLng/2)*Math.sin(dLng/2);
return R*2*Math.atan2(Math.sqrt(s),Math.sqrt(1-s));}
function itemHtml(a,dist){
var cls=a._fresh?'g':'r';
var meta=[a.city+(a.state?', '+a.state:''), (a.date||'').slice(0,10)].filter(Boolean).join(' · ');
return '<a class="it" href="'+esc(a.url)+'" data-id="'+esc(a.id)+'">'+
'<div class="t">'+esc(a.title)+'</div>'+
'<div class="m"><span class="dot '+cls+'"></span>'+esc(meta)+
(a.amt?'<span class="amt">'+esc(a.amt)+'</span>':'')+
(dist!=null?'<span class="dist">'+(dist<1?'<1':Math.round(dist))+' mi</span>':'')+
'</div></a>';
}
function refreshList(){
var L2=$('list');
var shown=ALL.filter(passFilter);
var rows;
if(meLatLng){ // NEAR ME → nearest first, within 150 mi
rows=shown.map(function(a){return {a:a,d:miles(meLatLng,[a.lat,a.lng])};})
.filter(function(x){return x.d<=150;}).sort(function(x,y){return x.d-y.d;}).slice(0,120);
$('listhd').textContent='Nearest to you';
$('listn').textContent=rows.length;
L2.innerHTML=rows.length?rows.map(function(x){return itemHtml(x.a,x.d);}).join(''):'<p class="empty">No stories within 150 miles. Zoom out or hit Reset view.</p>';
} else { // stories currently in the map viewport, newest first
var b=map.getBounds();
rows=shown.filter(function(a){return b.contains([a.lat,a.lng]);})
.sort(function(x,y){return (y.date||'').localeCompare(x.date||'');}).slice(0,120);
$('listhd').textContent='Stories in view';
$('listn').textContent=rows.length;
L2.innerHTML=rows.length?rows.map(function(a){return itemHtml(a,null);}).join(''):'<p class="empty">No stories in this area. Drag or zoom the map.</p>';
}
}
// Hover a list row → open its map popup
$('list').addEventListener('mouseover',function(e){
var el=e.target.closest('.it'); if(!el)return; var id=el.getAttribute('data-id');
for(var i=0;i<markers.length;i++){ if(markers[i].options._a.id===id){ var mk=markers[i];
cluster.zoomToShowLayer(mk,function(){mk.openPopup();}); break; } }
});
map.on('moveend',function(){ if(!meLatLng) refreshList(); });
// controls
$('fresh').onchange=function(e){fFresh=e.target.value;buildMarkers();};
$('txn').onchange=function(e){fTxn=e.target.value;buildMarkers();};
$('reset').onclick=function(){ closeDrawer('R'); meLatLng=null; if(meMarker){map.removeLayer(meMarker);meMarker=null;}
fFresh='all';$('fresh').value='all'; fTxn='';$('txn').value='';
if($('yfrom').options.length){fYFrom=parseInt($('yfrom').options[$('yfrom').options.length-1].value,10);$('yfrom').value=fYFrom;
fYTo=parseInt($('yto').options[0].value,10);$('yto').value=fYTo;}
map.setView([37.5,-119],5); buildMarkers(); };
$('near').onclick=function(){
if(!navigator.geolocation){alert('Location isn’t available in this browser.');return;}
closeDrawer('R');
$('near').textContent='📍 Locating…';
navigator.geolocation.getCurrentPosition(function(pos){
meLatLng=[pos.coords.latitude,pos.coords.longitude];
if(meMarker)map.removeLayer(meMarker);
meMarker=L.marker(meLatLng,{icon:L.divIcon({className:'',html:'<div class="me"></div>',iconSize:[18,18]}),zIndexOffset:1000})
.addTo(map).bindPopup('You are here');
map.setView(meLatLng,9);
$('near').textContent='📍 Near me';
refreshList();
},function(){ $('near').textContent='📍 Near me'; alert('Couldn’t get your location. Check browser permissions.'); },
{enableHighAccuracy:false,timeout:9000,maximumAge:600000});
};
// load data
fetch('/api/map-articles').then(function(r){return r.json();}).then(function(d){
ALL=(d.articles||[]).map(function(a){a._fresh=isFresh(a);return a;});
// populate txn filter from data
var txns={}; ALL.forEach(function(a){if(a.txn)txns[a.txn]=1;});
Object.keys(txns).sort().forEach(function(t){var o=document.createElement('option');o.value=t;o.textContent=t;$('txn').appendChild(o);});
// populate year range from data (spans ~2015..present)
var yrs=ALL.map(function(a){return parseInt((a.date||'').slice(0,4),10)||0;}).filter(Boolean);
var yMin=Math.min.apply(null,yrs), yMax=Math.max.apply(null,yrs);
fYFrom=yMin; fYTo=yMax;
for(var y=yMax;y>=yMin;y--){
var of=document.createElement('option'); of.value=y; of.textContent=y; $('yfrom').appendChild(of);
var ot=document.createElement('option'); ot.value=y; ot.textContent=y; $('yto').appendChild(ot);
}
$('yfrom').value=yMin; $('yto').value=yMax;
$('yfrom').onchange=function(e){fYFrom=parseInt(e.target.value,10); if(fYFrom>fYTo){fYTo=fYFrom;$('yto').value=fYTo;} buildMarkers();};
$('yto').onchange=function(e){fYTo=parseInt(e.target.value,10); if(fYTo<fYFrom){fYFrom=fYTo;$('yfrom').value=fYFrom;} buildMarkers();};
if(!ALL.length){ $('list').innerHTML='<p class="empty">The map data is still building. Check back shortly.</p>'; return; }
buildMarkers();
}).catch(function(){ $('list').innerHTML='<p class="empty">The map is temporarily unavailable.</p>'; });
</script></body></html>