Toggle menu
604
110
57
4.3K
Landrace.Wiki - The Landrace Cannabis Wiki
Toggle preferences menu
Toggle personal menu
Not logged in
Your IP address will be publicly visible if you make any edits.

MediaWiki:Common.js

MediaWiki interface page
Revision as of 23:39, 13 March 2026 by Eloise Zomia (talk | contribs)

Note: After publishing, you may have to bypass your browser's cache to see the changes.

  • Firefox / Safari: Hold Shift while clicking Reload, or press either Ctrl-F5 or Ctrl-R (⌘-R on a Mac)
  • Google Chrome: Press Ctrl-Shift-R (⌘-Shift-R on a Mac)
  • Edge: Hold Ctrl while clicking Refresh, or press Ctrl-F5.
// <nowiki>
/* Infobox map expand toggle */
$(function(){
var $overlay=$('<div class="lw-map-overlay"></div>').appendTo('body');

$('.lw-infobox__map-expand').on('click',function(){
var $mapContainer=$(this).closest('.lw-infobox__map-container');
var $map=$mapContainer.find('.lw-infobox__map');

if($overlay.hasClass('lw-map-overlay--active')){
$map.appendTo($mapContainer);
$overlay.removeClass('lw-map-overlay--active');
$('body').css('overflow','');
}else{
$map.appendTo($overlay);
$overlay.addClass('lw-map-overlay--active');
$('body').css('overflow','hidden');
}

setTimeout(function(){
$map.each(function(){
if(this._leaflet_map){
this._leaflet_map.invalidateSize();
}
});
},100);
});

$(document).on('keydown',function(e){
if(e.key==='Escape' && $overlay.hasClass('lw-map-overlay--active')){
$('.lw-infobox__map-expand').trigger('click');
}
});

$overlay.on('click',function(e){
if(e.target===this){
$('.lw-infobox__map-expand').trigger('click');
}
});
});

/* Landrace.wiki – Leaflet map w/ SMW CSV export, Clustering, Labels & Hulls */
(function(){
var LVER='1.9.4';
var CDN='https://unpkg.com/leaflet@'+LVER+'/dist/';
var CLUSTER_CDN='https://unpkg.com/leaflet.markercluster@1.4.1/dist/';
var TURF_CDN='https://unpkg.com/@turf/turf@6/turf.min.js';

var BRAND={
primary:'#2d6a4f',
primaryLight:'#40916c',
accent:'#74c69d',
dark:'#1b4332',
neutral:'#495057',
light:'#f8f9fa',
white:'#ffffff'
};

var STATUS_COLORS={
'Critical':'#c92a2a',
'High':'#d9480f',
'Medium':'#e67700',
'Low':'#2b8a3e'
};

var STATUS_PRIORITY=['Critical','High','Medium','Low'];

var EVENT_COLORS={
'Enforcement':'#c92a2a',
'Trafficking':'#d9480f',
'Policy':'#1c7ed6',
'Research':BRAND.primary,
'Fieldwork':BRAND.primaryLight,
'Community':BRAND.accent,
'Report':BRAND.neutral
};

var EVENT_PRIORITY=['Enforcement','Trafficking','Policy','Research','Fieldwork','Community','Report'];

function addCSS(href,id){if(id&&document.getElementById(id))return;var l=document.createElement('link');l.rel='stylesheet';l.href=href;if(id)l.id=id;document.head.appendChild(l);}

function addJS(src,cb,id){var existing=id?document.getElementById(id):document.querySelector('script[src="'+src+'"]');if(existing){if(existing.dataset.loaded==='1')return cb();existing.addEventListener('load',cb);return;}var s=document.createElement('script');s.src=src;if(id)s.id=id;s.onload=function(){s.dataset.loaded='1';cb();};s.onerror=function(){console.error('[lw-map] Script failed to load:',src);};document.head.appendChild(s);}

function getWorstStatus(markers){var worst='Low';var worstIndex=STATUS_PRIORITY.indexOf(worst);markers.forEach(function(marker){var status=marker.options.status||'Low';var index=STATUS_PRIORITY.indexOf(status);if(index!==-1&&index<worstIndex){worst=status;worstIndex=index;}});return worst;}

function getWorstStatusFromFeatures(features){var worst='Low';var worstIndex=STATUS_PRIORITY.indexOf(worst);features.forEach(function(f){var status=f.properties.status||'Low';var index=STATUS_PRIORITY.indexOf(status);if(index!==-1&&index<worstIndex){worst=status;worstIndex=index;}});return worst;}

function getWorstCategory(markers){var worst=EVENT_PRIORITY[EVENT_PRIORITY.length-1];var worstIndex=EVENT_PRIORITY.indexOf(worst);markers.forEach(function(marker){var cat=marker.options.event_category||'';var idx=EVENT_PRIORITY.indexOf(cat);if(idx!==-1&&idx<worstIndex){worst=cat;worstIndex=idx;}});return worst;}

function injectStyles(){
if(document.getElementById('lw-map-styles')) return;
var style=document.createElement('style');
style.id='lw-map-styles';
style.textContent=[
'.lw-popup .leaflet-popup-content-wrapper{background:'+BRAND.white+';border-radius:12px;box-shadow:0 4px 20px rgba(0,0,0,0.12),0 2px 6px rgba(0,0,0,0.08);padding:0;overflow:hidden}',
'.lw-popup .leaflet-popup-content{margin:0;min-width:220px;max-width:280px}',
'.lw-popup .leaflet-popup-tip{background:'+BRAND.white+';box-shadow:0 2px 6px rgba(0,0,0,0.1)}',
'.lw-popup .leaflet-popup-close-button{color:'+BRAND.neutral+' !important;font-size:20px !important;padding:8px 10px !important;right:4px !important;top:4px !important;transition:color 0.2s ease}',
'.lw-popup .leaflet-popup-close-button:hover{color:'+BRAND.dark+' !important}',
'.lw-popup-inner{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif}',
'.lw-popup-header{background:linear-gradient(135deg,'+BRAND.primary+' 0%,'+BRAND.dark+' 100%);padding:16px 18px;color:'+BRAND.white+'}',
'.lw-popup-id{font-size:10px;font-weight:600;letter-spacing:0.5px;text-transform:uppercase;opacity:0.85;margin-bottom:4px}',
'.lw-popup-name{font-size:15px;font-weight:600;line-height:1.3}',
'.lw-popup-body{padding:14px 18px}',
'.lw-popup-meta{display:flex;flex-direction:column;align-items:flex-start;gap:6px;margin-bottom:8px}',
'.lw-popup-label{font-size:11px;color:'+BRAND.neutral+';font-weight:500;text-transform:uppercase;letter-spacing:0.3px}',
'.lw-popup-status{display:inline-flex;align-items:center;padding:4px 10px;border-radius:20px;font-size:11px;font-weight:600;background:'+BRAND.light+';color:'+BRAND.neutral+'}',
'.lw-popup-status::before{content:"";width:6px;height:6px;border-radius:50%;margin-right:6px;background:currentColor}',
'.lw-popup-status--critical{color:#c92a2a;background:#fff5f5}',
'.lw-popup-status--high{color:#d9480f;background:#fff4e6}',
'.lw-popup-status--medium{color:#e67700;background:#fff9db}',
'.lw-popup-status--low{color:#2b8a3e;background:#ebfbee}',
'.lw-popup-link{display:block;text-align:center;padding:10px 18px;background:'+BRAND.light+';color:'+BRAND.primary+';text-decoration:none;font-size:13px;font-weight:600;transition:background 0.2s ease,color 0.2s ease;border-top:1px solid #e9ecef}',
'.lw-popup-link:hover{background:'+BRAND.primary+';color:'+BRAND.white+'}',
'.lw-map-tooltip{background:'+BRAND.dark+' !important;border:none !important;border-radius:6px !important;color:'+BRAND.white+' !important;font-size:12px !important;font-weight:500 !important;padding:6px 10px !important;box-shadow:0 2px 8px rgba(0,0,0,0.2) !important}',
'.lw-map-tooltip::before{border-top-color:'+BRAND.dark+' !important}',
'.lw-layer-toggle{display:flex;background:'+BRAND.white+';border-radius:8px;box-shadow:0 2px 8px rgba(0,0,0,0.12);overflow:hidden}',
'.lw-layer-btn{display:flex;align-items:center;justify-content:center;width:36px;height:36px;border:none;background:'+BRAND.white+';color:'+BRAND.neutral+';cursor:pointer;transition:all 0.2s ease}',
'.lw-layer-btn:hover{background:'+BRAND.light+';color:'+BRAND.primary+'}',
'.lw-layer-btn--active{background:'+BRAND.primary+' !important;color:'+BRAND.white+' !important}',
'.lw-layer-btn:not(:last-child){border-right:1px solid #e9ecef}',
'.lw-layer-btn svg{pointer-events:none}',
'.lw-cluster-icon{background:transparent !important}',
'.lw-cluster{width:40px;height:40px;border-radius:50%;color:#fff;font-weight:700;font-size:14px;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;display:flex;align-items:center;justify-content:center;box-shadow:0 3px 10px rgba(0,0,0,0.25);border:3px solid rgba(255,255,255,0.9);transition:transform 0.2s ease}',
'.lw-cluster:hover{transform:scale(1.1)}',
'.lw-cluster--small{width:36px;height:36px;font-size:13px}',
'.lw-cluster--large{width:48px;height:48px;font-size:15px}',
'.marker-cluster{background:transparent !important}',
'.marker-cluster div{background:transparent !important}',
'.lw-hull-tooltip{background:'+BRAND.white+' !important;border:1px solid '+BRAND.primary+' !important;border-radius:6px !important;color:'+BRAND.dark+' !important;font-size:12px !important;font-weight:600 !important;padding:6px 10px !important;box-shadow:0 2px 8px rgba(0,0,0,0.15) !important}',
'.lw-yearctl{background:'+BRAND.white+';border-radius:10px;box-shadow:0 2px 12px rgba(0,0,0,0.15);padding:10px 14px;display:flex;align-items:center;gap:10px;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif}',
'.lw-yearctl button{border:none;background:'+BRAND.primary+';color:'+BRAND.white+';border-radius:6px;padding:5px 12px;font-size:11px;font-weight:700;cursor:pointer;letter-spacing:0.3px;text-transform:uppercase;transition:all 0.2s ease;opacity:0.5}',
'.lw-yearctl button:hover{opacity:0.75}',
'.lw-yearctl button.lw-yearctl--active{opacity:1;box-shadow:0 1px 4px rgba(45,106,79,0.3)}',
'.lw-yearctl input[type="range"]{-webkit-appearance:none;appearance:none;width:160px;height:4px;background:#e9ecef;border-radius:2px;outline:none;cursor:pointer}',
'.lw-yearctl input[type="range"]::-webkit-slider-thumb{-webkit-appearance:none;width:16px;height:16px;border-radius:50%;background:'+BRAND.primary+';border:3px solid '+BRAND.white+';box-shadow:0 1px 4px rgba(0,0,0,0.2);cursor:pointer;transition:transform 0.15s ease}',
'.lw-yearctl input[type="range"]::-webkit-slider-thumb:hover{transform:scale(1.2)}',
'.lw-yearctl input[type="range"]::-moz-range-thumb{width:12px;height:12px;border-radius:50%;background:'+BRAND.primary+';border:3px solid '+BRAND.white+';box-shadow:0 1px 4px rgba(0,0,0,0.2);cursor:pointer}',
'.lw-yearctl input[type="range"]::-moz-range-track{height:4px;background:#e9ecef;border-radius:2px;border:none}',
'.lw-yearctl .lw-yearctl__label{font-size:15px;font-weight:800;color:'+BRAND.dark+';min-width:36px;text-align:center;font-variant-numeric:tabular-nums}'
].join('\n');
document.head.appendChild(style);
}

function esc(s){return String(s==null?'':s).replace(/[&<>"']/g,function(c){return({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]);});}

function statusClass(s){var status=String(s||'').toLowerCase();if(status==='critical')return'lw-popup-status--critical';if(status==='high')return'lw-popup-status--high';if(status==='medium')return'lw-popup-status--medium';if(status==='low')return'lw-popup-status--low';return'';}

function popupForAccession(p){
var id=p.id||'';var name=p.name||'Unknown';var status=p.status||'';var url=p.page_url||'#';
var statusHtml='';
if(status){statusHtml='<div class="lw-popup-meta"><span class="lw-popup-label">Conservation Priority</span><span class="lw-popup-status '+statusClass(status)+'">'+esc(status)+'</span></div>';}
return '<div class="lw-popup-inner"><div class="lw-popup-header">'+(id?'<div class="lw-popup-id">'+esc(id)+'</div>':'')+'<div class="lw-popup-name">'+esc(name)+'</div></div><div class="lw-popup-body">'+statusHtml+'</div><a href="'+esc(url)+'" class="lw-popup-link">View Accession →</a></div>';
}

function popupForNewsItem(p){
var title=p.name||'News Item';var date=p.event_date||'';var cat=p.event_category||'';
var loc=[p.locality,p.admin2,p.admin1].filter(Boolean).join(', ');
var url=p.page_url||'#';var src=p.source_url||'';var blocks='';
if(date){blocks+='<div class="lw-popup-meta"><span class="lw-popup-label">Date</span><span style="font-size:14px;font-weight:700;color:'+BRAND.dark+'">'+esc(date)+'</span></div>';}
if(loc){blocks+='<div class="lw-popup-meta"><span class="lw-popup-label">Location</span><span style="font-size:13px;color:'+BRAND.neutral+'">'+esc(loc)+'</span></div>';}
if(cat){blocks+='<div class="lw-popup-meta"><span class="lw-popup-label">Category</span><span style="font-size:13px;color:'+BRAND.neutral+'">'+esc(cat)+'</span></div>';}
var sourceBtn=src?('<a href="'+esc(src)+'" target="_blank" rel="noreferrer" class="lw-popup-link" style="border-top:0">Source ↗</a>'):'';
return '<div class="lw-popup-inner"><div class="lw-popup-header"><div class="lw-popup-id">News Item</div><div class="lw-popup-name">'+esc(title)+'</div></div><div class="lw-popup-body">'+blocks+'</div><a href="'+esc(url)+'" class="lw-popup-link">View Article →</a>'+sourceBtn+'</div>';
}

function popupForArea(name,count,worstStatus,url){
var statusHtml='';
if(worstStatus){statusHtml='<div class="lw-popup-meta"><span class="lw-popup-label">Worst Status in Area</span><span class="lw-popup-status '+statusClass(worstStatus)+'">'+esc(worstStatus)+'</span></div>';}
return '<div class="lw-popup-inner"><div class="lw-popup-header"><div class="lw-popup-id">Growing Area</div><div class="lw-popup-name">'+esc(name)+'</div></div><div class="lw-popup-body"><div class="lw-popup-meta"><span class="lw-popup-label">Accessions</span><span style="font-size:18px;font-weight:700;color:'+BRAND.dark+'">'+count+'</span></div>'+statusHtml+'</div><a href="'+esc(url)+'" class="lw-popup-link">View Growing Area →</a></div>';
}

function parseCoords(coordStr){
if(!coordStr) return null;
var decimalMatch=coordStr.match(/(-?\d+\.?\d*)\s*,\s*(-?\d+\.?\d*)/);
if(decimalMatch){var lat=parseFloat(decimalMatch[1]);var lon=parseFloat(decimalMatch[2]);if(!isNaN(lat)&&!isNaN(lon))return{lat:lat,lon:lon};}
var dmsRegex=/(\d+)[°]\s*(\d+)[′']\s*(\d+\.?\d*)[″"]?\s*([NSEW])/gi;
var matches=[];var m;
while((m=dmsRegex.exec(coordStr))!==null){matches.push(m);if(matches.length>=2)break;}
if(matches.length>=2){var lat2=dmsToDecimal(matches[0][1],matches[0][2],matches[0][3],matches[0][4]);var lon2=dmsToDecimal(matches[1][1],matches[1][2],matches[1][3],matches[1][4]);var h0=matches[0][4].toUpperCase();if(h0==='E'||h0==='W'){var tmp=lat2;lat2=lon2;lon2=tmp;}if(lat2!==null&&lon2!==null)return{lat:lat2,lon:lon2};}
return null;
}

function dmsToDecimal(deg,min,sec,hemi){var d=parseFloat(deg);var m=parseFloat(min)||0;var s=parseFloat(sec)||0;if(isNaN(d))return null;var dec=Math.abs(d)+(m/60)+(s/3600);hemi=String(hemi||'').toUpperCase();if(hemi==='S'||hemi==='W')dec=-dec;return dec;}

function parseCSV(text){
var lines=text.trim().split('\n');if(lines.length<2)return[];
var headers=parseCSVLine(lines[0]);var results=[];
for(var i=1;i<lines.length;i++){var values=parseCSVLine(lines[i]);var obj={};for(var j=0;j<headers.length;j++){obj[headers[j]]=values[j]||'';}results.push(obj);}
return results;
}

function parseCSVLine(line){
var result=[];var current='';var inQuotes=false;
for(var i=0;i<line.length;i++){var c=line[i];var next=line[i+1];
if(inQuotes){if(c==='"'&&next==='"'){current+='"';i++;}else if(c==='"'){inQuotes=false;}else{current+=c;}}
else{if(c==='"'){inQuotes=true;}else if(c===','){result.push(current.trim());current='';}else{current+=c;}}}
result.push(current.trim());return result;
}

function csvToGeoJSON(data){
var features=[];
data.forEach(function(row){
var coordStr=row['Has coordinates']||row['Has GPS coordinates']||row['GPS coordinates']||'';
var coords=parseCoords(coordStr);if(!coords)return;
var pageName=row['']||Object.values(row)[0]||'';
var pageUrl='/wiki/'+encodeURIComponent(pageName.replace(/ /g,'_'));
var isNews=!!(row['Has event headline']||row['Has event date']||row['Has event category']);
if(isNews){
var headline=row['Has event headline']||pageName;
var eventDate=row['Has event date']||'';
var eventCat=row['Has event category']||'';
var admin1=row['Has admin subdivision 1']||'';
var admin2=row['Has admin subdivision 2']||'';
var locality=row['Has locality']||'';
var sourceUrl=row['Has source']||'';
features.push({type:'Feature',properties:{name:headline,event_date:eventDate,event_category:eventCat,admin1:admin1,admin2:admin2,locality:locality,source_url:sourceUrl,page_url:pageUrl},geometry:{type:'Point',coordinates:[coords.lon,coords.lat]}});
return;
}
var name=row['Has descriptive name']||row['descriptive name']||pageName;
var status=row['Has conservation priority']||row['conservation priority']||'';
var accessionId=row['Has accession ID']||row['accession ID']||'';
var growingArea=row['Has growing area']||row['growing area']||'';
var growingRegion=row['Has growing region']||row['growing region']||'';
features.push({type:'Feature',properties:{id:accessionId,name:name,status:status,growing_area:growingArea,growing_region:growingRegion,page_url:pageUrl},geometry:{type:'Point',coordinates:[coords.lon,coords.lat]}});
});
return{type:'FeatureCollection',features:features};
}

/* FIX 1: Handle newlines from template #if blocks, encode + for LiteSpeed */
function buildCSVUrl(query){
query=query.replace(/[\r\n]+/g,' ').replace(/\s+/g,' ').trim();
var parts=query.split('|');
var encodedParts=parts.map(function(part){
return part.trim()
.replace(/-/g,'-2D')
.replace(/\[/g,'-5B')
.replace(/\]/g,'-5D')
.replace(/\?/g,'-3F')
.replace(/:/g,'-3A')
.replace(/\+/g,'-2B')
.replace(/ /g,'-20');
});
var baseUrl=mw.config.get('wgScriptPath')||'';
return baseUrl+'/wiki/Special:Ask/'+encodedParts.join('/')+'/format=csv';
}

function fetchCSV(query,cb){
var url=buildCSVUrl(query);
fetch(url).then(function(r){if(!r.ok)throw new Error('CSV fetch error: '+r.status);return r.text();}).then(function(text){var data=parseCSV(text);var geojson=csvToGeoJSON(data);cb(null,geojson);}).catch(function(err){console.error('[lw-map] CSV fetch error:',err);cb(err,null);});
}

function generateHull(features){
if(!window.turf||features.length<3)return null;
try{var points=turf.featureCollection(features.map(function(f){return turf.point(f.geometry.coordinates);}));var hull;if(features.length>=4){try{hull=turf.concave(points,{maxEdge:100,units:'kilometers'});}catch(e){}}if(!hull){hull=turf.convex(points);}if(hull){hull=turf.buffer(hull,3,{units:'kilometers'});}return hull;}catch(e){console.error('[lw-map] Hull generation error:',e);return null;}
}

function groupBy(features,prop){var groups={};features.forEach(function(f){var key=f.properties[prop]||'Unknown';if(!groups[key])groups[key]=[];groups[key].push(f);});return groups;}

function initOne(el){
if(el.dataset.init)return;
el.dataset.init='1';
if(el.clientHeight<100)el.style.height='70vh';
injectStyles();

var map=L.map(el,{minZoom:2,maxZoom:17,zoomControl:false}).setView([26.4,89.5],8);
el._leaflet_map=map;
L.control.zoom({position:'topright'}).addTo(map);

var baseLayers={
basic:L.tileLayer('https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png',{attribution:'© <a href="https://www.openstreetmap.org/copyright">OSM</a> © <a href="https://carto.com/attributions">CARTO</a>',subdomains:'abcd',maxZoom:19}),
terrain:L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/World_Topo_Map/MapServer/tile/{z}/{y}/{x}',{attribution:'© <a href="https://www.esri.com">Esri</a>',maxZoom:18}),
satellite:L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}',{attribution:'© <a href="https://www.esri.com">Esri</a>',maxZoom:18})
};
baseLayers.basic.addTo(map);
var currentLayer='basic';

var layerToggle=L.control({position:'topleft'});
layerToggle.onAdd=function(){
var container=L.DomUtil.create('div','lw-layer-toggle');
container.innerHTML='<button class="lw-layer-btn lw-layer-btn--active" data-layer="basic" title="Basic view"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="18" height="18" rx="2"/></svg></button><button class="lw-layer-btn" data-layer="terrain" title="Terrain view"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M8 21l4-10 4 10M12 11V3M4 21l4.5-9L12 17l3.5-5L20 21"/></svg></button><button class="lw-layer-btn" data-layer="satellite" title="Satellite view"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><path d="M12 2a14.5 14.5 0 0 0 0 20 14.5 14.5 0 0 0 0-20M2 12h20"/></svg></button>';
L.DomEvent.disableClickPropagation(container);
container.querySelectorAll('.lw-layer-btn').forEach(function(btn){
btn.addEventListener('click',function(){
var layer=this.dataset.layer;if(layer===currentLayer)return;
map.removeLayer(baseLayers[currentLayer]);baseLayers[layer].addTo(map);currentLayer=layer;
container.querySelectorAll('.lw-layer-btn').forEach(function(b){b.classList.remove('lw-layer-btn--active');});
this.classList.add('lw-layer-btn--active');
});
});
return container;
};
layerToggle.addTo(map);

var query=el.dataset.smwQuery;
if(!query)return;
var mapType=el.dataset.mapType||'';
var isInfoboxMap=el.closest('.lw-infobox')!==null;
var areaHullsLayer=L.layerGroup();

function updateLayerVisibility(){var zoom=map.getZoom();if(zoom>=6&&zoom<=12){if(!map.hasLayer(areaHullsLayer))map.addLayer(areaHullsLayer);}else{if(map.hasLayer(areaHullsLayer))map.removeLayer(areaHullsLayer);}}

var fullQuery=query;
if(!isInfoboxMap&&query.indexOf('Category:Accessions')!==-1){
fullQuery='[[Category:Accessions]]|?Has coordinates|?Has descriptive name|?Has conservation priority|?Has accession ID|?Has growing area|?Has growing region|limit=500';
}

fetchCSV(fullQuery,function(err,geojson){
if(err||!geojson||!geojson.features||!geojson.features.length){
var msg=L.control({position:'bottomleft'});
msg.onAdd=function(){var d=L.DomUtil.create('div');d.style.cssText='background:white;padding:12px 16px;border-radius:8px;font-size:13px;color:#495057;box-shadow:0 2px 8px rgba(0,0,0,0.1);font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;';d.innerHTML='<strong>No results found</strong><br><span style="opacity:0.7;">No mappable results for this query.</span>';return d;};
msg.addTo(map);return;
}

if(!isInfoboxMap&&window.turf&&mapType!=='eradication'){
var areaGroups=groupBy(geojson.features,'growing_area');
Object.keys(areaGroups).forEach(function(areaName){
if(areaName==='Unknown'||areaName==='')return;
var areaFeatures=areaGroups[areaName];if(areaFeatures.length<3)return;
var hull=generateHull(areaFeatures);if(!hull)return;
var worstStatus=getWorstStatusFromFeatures(areaFeatures);
var color=STATUS_COLORS[worstStatus]||BRAND.primary;
var hullLayer=L.geoJSON(hull,{style:{fillColor:color,fillOpacity:0.12,color:color,weight:2,dashArray:'6, 4',lineCap:'round'}});
hullLayer.bindPopup(popupForArea(areaName,areaFeatures.length,worstStatus,'/wiki/'+encodeURIComponent(areaName.replace(/ /g,'_'))),{className:'lw-popup',maxWidth:300});
hullLayer.bindTooltip(areaName+' ('+areaFeatures.length+')',{className:'lw-hull-tooltip',direction:'center',permanent:false});
hullLayer.on('mouseover',function(e){e.target.setStyle({fillOpacity:0.25,weight:3});});
hullLayer.on('mouseout',function(e){e.target.setStyle({fillOpacity:0.12,weight:2});});
areaHullsLayer.addLayer(hullLayer);
});
map.on('zoomend',updateLayerVisibility);
updateLayerVisibility();
}

if(isInfoboxMap||!window.L.markerClusterGroup){
var layer=L.geoJSON(geojson,{
pointToLayer:function(f,ll){
var p=f.properties||{};var status=p.status||'';var cat=p.event_category||'';
var color=(mapType==='eradication')?(EVENT_COLORS[cat]||BRAND.primary):(STATUS_COLORS[status]||BRAND.primary);
if(mapType==='eradication'){return L.circleMarker(ll,{radius:7,color:color,weight:2.6,fillColor:color,fillOpacity:0.10,dashArray:'3 3',status:status,event_category:cat});}
return L.circleMarker(ll,{radius:8,color:BRAND.white,weight:2.5,fillColor:color,fillOpacity:0.9,status:status,event_category:cat});
},
onEachFeature:function(f,ly){
var p=f.properties||{};
ly.bindPopup((mapType==='eradication')?popupForNewsItem(p):popupForAccession(p),{className:'lw-popup',closeButton:true,maxWidth:300,offset:[0,-5]});
var tip=(mapType==='eradication')?(p.name||''):(p.name||p.id||'');
if(tip){ly.bindTooltip(esc(tip),{direction:'top',offset:[0,-10],opacity:1,className:'lw-map-tooltip'});}
ly.on('mouseover',function(){if(mapType==='eradication'){this.setStyle({fillOpacity:0.22,weight:3});}else{this.setStyle({fillColor:BRAND.primaryLight,radius:10});}});
ly.on('mouseout',function(){var pp=f.properties||{};var status2=pp.status||'';var cat2=pp.event_category||'';var color2=(mapType==='eradication')?(EVENT_COLORS[cat2]||BRAND.primary):(STATUS_COLORS[status2]||BRAND.primary);if(mapType==='eradication'){this.setStyle({color:color2,fillColor:color2,fillOpacity:0.10,weight:2.6});}else{this.setStyle({fillColor:color2,radius:8});}});
}
}).addTo(map);
var b=layer.getBounds();if(b&&b.isValid())map.fitBounds(b.pad(0.3),{maxZoom:11});

}else{

var markers=L.markerClusterGroup({
maxClusterRadius:50,spiderfyOnMaxZoom:true,showCoverageOnHover:false,zoomToBoundsOnClick:true,
iconCreateFunction:function(cluster){
var children=cluster.getAllChildMarkers();var count=children.length;var color;
if(mapType==='eradication'){var worstCat=getWorstCategory(children);color=EVENT_COLORS[worstCat]||BRAND.primary;}
else{var worstStatus=getWorstStatus(children);color=STATUS_COLORS[worstStatus]||BRAND.primary;}
var sizeClass='lw-cluster';if(count<10)sizeClass+=' lw-cluster--small';else if(count>=50)sizeClass+=' lw-cluster--large';
return L.divIcon({html:'<div class="'+sizeClass+'" style="background:'+color+'">'+count+'</div>',className:'lw-cluster-icon',iconSize:[40,40]});
}
});

var allMarkers=[];
var byYear={};

/* FIX 2: Match year anywhere in string, not just start.
   SMW CSV returns dates as "5 February 2000" not "2000-02-05" */
function yearFromDate(s){
var m=String(s||'').match(/(\d{4})/);
return m?m[1]:'';
}

function fitTo(list){if(!list||!list.length)return;var fg=L.featureGroup(list);var bb=fg.getBounds();if(bb&&bb.isValid())map.fitBounds(bb.pad(0.3),{maxZoom:11});}

var initialFitDone=false;
function applyYear(y){markers.clearLayers();var list=(y==='all')?allMarkers:(byYear[y]||[]);if(list.length)markers.addLayers(list);if(!initialFitDone){fitTo(allMarkers);initialFitDone=true;}}

geojson.features.forEach(function(f){
var coords=f.geometry.coordinates;var p=f.properties||{};
var status=p.status||'';var cat=p.event_category||'';
var color=(mapType==='eradication')?(EVENT_COLORS[cat]||BRAND.primary):(STATUS_COLORS[status]||BRAND.primary);
var marker=L.circleMarker([coords[1],coords[0]],
(mapType==='eradication')
?{radius:7,color:color,weight:2.6,fillColor:color,fillOpacity:0.10,dashArray:'3 3',status:status,event_category:cat}
:{radius:8,color:BRAND.white,weight:2.5,fillColor:color,fillOpacity:0.9,status:status,event_category:cat}
);
marker.bindPopup((mapType==='eradication')?popupForNewsItem(p):popupForAccession(p),{className:'lw-popup',closeButton:true,maxWidth:300,offset:[0,-5]});
var tip=(mapType==='eradication')?(p.name||''):(p.name||p.id||'');
if(tip){marker.bindTooltip(esc(tip),{direction:'top',offset:[0,-10],opacity:1,className:'lw-map-tooltip'});}
marker.on('mouseover',function(){if(mapType==='eradication'){this.setStyle({fillOpacity:0.22,weight:3});}else{this.setStyle({fillColor:BRAND.primaryLight,radius:10});}});
marker.on('mouseout',function(){if(mapType==='eradication'){this.setStyle({fillOpacity:0.10,weight:2.6});}else{this.setStyle({fillColor:color,radius:8});}});
markers.addLayer(marker);allMarkers.push(marker);
if(mapType==='eradication'){var y=yearFromDate(p.event_date);if(y){if(!byYear[y])byYear[y]=[];byYear[y].push(marker);}}
});

map.addLayer(markers);

if(mapType==='eradication'){
var years=Object.keys(byYear).sort();
if(years.length>1){
var yearCtl=L.control({position:'bottomleft'});
yearCtl.onAdd=function(){
var d=L.DomUtil.create('div','lw-yearctl');
d.innerHTML='<button type="button" class="lw-yearctl__all lw-yearctl--active">All</button><input class="lw-yearctl__range" type="range" min="0" max="'+(years.length-1)+'" step="1" value="'+(years.length-1)+'"><span class="lw-yearctl__label">'+years[0]+'–'+years[years.length-1]+'</span>';
L.DomEvent.disableClickPropagation(d);
var btn=d.querySelector('.lw-yearctl__all');
var rng=d.querySelector('.lw-yearctl__range');
var lab=d.querySelector('.lw-yearctl__label');
btn.addEventListener('click',function(){btn.classList.add('lw-yearctl--active');lab.textContent=years[0]+'–'+years[years.length-1];applyYear('all');});
rng.addEventListener('input',function(){btn.classList.remove('lw-yearctl--active');var y=years[parseInt(rng.value,10)];lab.textContent=y;applyYear(y);});
return d;
};
yearCtl.addTo(map);
}
applyYear('all');
}else{
var b2=markers.getBounds();if(b2&&b2.isValid())map.fitBounds(b2.pad(0.3),{maxZoom:11});
}
}

setTimeout(function(){map.invalidateSize(true);},100);
});
}

function init(root){(root||document).querySelectorAll('.lw-map').forEach(initOne);}

addCSS(CDN+'leaflet.css','leaflet-css');
addCSS(CLUSTER_CDN+'MarkerCluster.css','markercluster-css');
addCSS(CLUSTER_CDN+'MarkerCluster.Default.css','markercluster-default-css');

addJS(CDN+'leaflet.js',function(){
addJS(CLUSTER_CDN+'leaflet.markercluster.js',function(){
addJS(TURF_CDN,function(){
init();
if(window.mw&&mw.hook){mw.hook('wikipage.content').add(function($c){init($c&&$c[0]?$c[0]:document);});}
},'turf-js');
},'markercluster-js');
},'leaflet-js');
})();

/**
 * Auto-preload templates for new talk pages
 */
(function(){
'use strict';
if(mw.config.get('wgAction')!=='edit')return;
if(mw.config.get('wgArticleId')!==0)return;
if(mw.config.get('wgNamespaceNumber')!==1)return;
var mainPageTitle=mw.config.get('wgTitle');
var categoryToPreload={'Category:Countries':'Template:Talk preload/Country','Category:States':'Template:Talk preload/State','Category:Growing Regions':'Template:Talk preload/Growing region','Category:Growing Areas':'Template:Talk preload/Growing area','Category:Accessions':'Template:Talk preload/Accession'};
var defaultPreload='Template:Talk preload/Default';
mw.loader.using(['mediawiki.api'],function(){
var api=new mw.Api();
$(function(){
var $textbox=$('#wpTextbox1');if($textbox.val().trim())return;
api.get({action:'query',titles:mainPageTitle,prop:'categories',cllimit:50,format:'json'}).done(function(data){
var pages=data.query.pages;var categories=[];
for(var id in pages){if(pages[id].categories){categories=pages[id].categories.map(function(c){return c.title;});}}
var preloadTemplate=defaultPreload;
for(var cat in categoryToPreload){if(categories.indexOf(cat)!==-1){preloadTemplate=categoryToPreload[cat];break;}}
api.get({action:'query',titles:preloadTemplate,prop:'revisions',rvprop:'content',rvslots:'main',format:'json'}).done(function(data){
var pages=data.query.pages;
for(var id in pages){if(pages[id].revisions){var content=pages[id].revisions[0].slots.main['*'];content=content.replace(/<noinclude>[\s\S]*?<\/noinclude>/g,'');content=content.trim();$textbox.val(content);}}
}).fail(function(){$textbox.val('{{TalkHeader}}\n\n== Discussion ==\n');});
});
});
});
})();
/* ═══════════════════════════════════════════════════════════════════
 *  GLOBE VISUALIZATION MODULE — v3.0 (dynamic tile LOD)
 *  Append to MediaWiki:Common.js
 *  <div class="lw-globe" style="height:620px;"></div>
 * ═══════════════════════════════════════════════════════════════════ */
(function(){
'use strict';
var globeEls=document.querySelectorAll('.lw-globe');
if(!globeEls.length)return;

/* ── Config ── */
var GR=1.5, TILE_PX=256, ML=85.051129;
var BASE_Z=4, BASE_N=16, BASE_MERC=BASE_N*TILE_PX, BASE_EQ_W=4096, BASE_EQ_H=2048;
var HI_Z=6, HI_N=64, HI_MERC=HI_N*TILE_PX, HI_EQ_W=8192, HI_EQ_H=4096;
var DETAIL_CAM_THRESHOLD=3.4; /* start loading detail when camZ < this */
var ZOOM_MIN=1.8, ZOOM_MAX=6.0, ZOOM_DEFAULT=4.2;
var CLUSTER_PX=45, IDLE_MS=6000;
var SMW_Q='[[Category:Accessions]]|?Has coordinates|?Has descriptive name|?Has conservation priority|?Has accession ID|?Has growing area|?Has growing region|?Has country';

var PC={Critical:'#c92a2a',High:'#d9480f',Medium:'#e67700',Low:'#2b8a3e'};
var PO=['Critical','High','Medium','Low'];
var TILE_URLS={
  basic:function(z,x,y){return'https://'+'abc'[Math.abs(x+y)%3]+'.basemaps.cartocdn.com/dark_all/'+z+'/'+x+'/'+y+'.png';},
  terrain:function(z,x,y){return'https://server.arcgisonline.com/ArcGIS/rest/services/World_Topo_Map/MapServer/tile/'+z+'/'+y+'/'+x;},
  satellite:function(z,x,y){return'https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/'+z+'/'+y+'/'+x;}
};

/* ── CSS (unchanged from v2) ── */
var css=document.createElement('style');
css.textContent='.lw-globe-wrap{display:flex;border-radius:8px;overflow:hidden;background:#0d2818;position:relative;font-family:Georgia,serif}.lw-globe-sidebar{width:250px;min-width:250px;background:linear-gradient(180deg,#0d2818,#0f2e1e);border-right:1px solid rgba(116,198,157,.18);display:flex;flex-direction:column;overflow:hidden}.lw-globe-sidebar-hd{padding:18px 14px 12px;border-bottom:1px solid rgba(116,198,157,.12)}.lw-globe-label{font-family:monospace;font-size:9px;letter-spacing:2.5px;text-transform:uppercase;color:rgba(248,249,250,.28)}.lw-globe-sidebar-hd .lw-globe-label{color:#74c69d;margin-bottom:4px}.lw-globe-sidebar-hd h3{font-size:17px;font-weight:600;color:#f8f9fa;line-height:1.25;margin:0}.lw-globe-sidebar-hd .meta{font-size:11px;color:rgba(248,249,250,.4);margin-top:3px}.lw-globe-countries{flex:1;overflow-y:auto;padding:6px 0}.lw-globe-countries::-webkit-scrollbar{width:4px}.lw-globe-countries::-webkit-scrollbar-thumb{background:rgba(116,198,157,.2);border-radius:2px}.lw-globe-cbtn{display:flex;align-items:center;width:100%;padding:9px 14px;border:none;background:0 0;cursor:pointer;text-align:left;border-left:3px solid transparent;transition:background .15s;font-family:Georgia,serif}.lw-globe-cbtn:hover{background:rgba(116,198,157,.06)}.lw-globe-cbtn.active{background:rgba(116,198,157,.12);border-left-color:#74c69d}.lw-globe-cdot{width:8px;height:8px;border-radius:50%;margin-right:10px;flex-shrink:0}.lw-globe-cinfo{flex:1}.lw-globe-cname{font-size:13px;color:#f8f9fa;font-weight:400}.lw-globe-cbtn.active .lw-globe-cname{font-weight:600}.lw-globe-csub{font-size:10.5px;color:rgba(248,249,250,.38)}.lw-globe-ccount{font-size:15px;font-weight:700;color:rgba(248,249,250,.18);font-family:monospace}.lw-globe-legend{padding:10px 14px;border-top:1px solid rgba(116,198,157,.12)}.lw-globe-litem{display:flex;align-items:center;margin-bottom:3px}.lw-globe-ldot{width:6px;height:6px;border-radius:50%;margin-right:8px}.lw-globe-llabel{font-size:11px;color:rgba(248,249,250,.55)}.lw-globe-main{flex:1;position:relative;overflow:hidden}.lw-globe-canvas{width:100%;height:100%;cursor:grab;display:block}.lw-globe-canvas.dragging{cursor:grabbing}.lw-globe-vsw{position:absolute;top:12px;left:12px;display:flex;background:rgba(13,40,24,.88);backdrop-filter:blur(10px);border:1px solid rgba(116,198,157,.18);border-radius:6px;overflow:hidden;z-index:5}.lw-globe-vbtn{padding:7px 13px;border:none;font-size:11px;cursor:pointer;font-family:monospace;letter-spacing:.3px;background:0 0;color:rgba(248,249,250,.4);transition:all .15s;border-right:1px solid rgba(116,198,157,.08)}.lw-globe-vbtn:last-child{border-right:none}.lw-globe-vbtn:hover{color:rgba(248,249,250,.6)}.lw-globe-vbtn.active{background:rgba(116,198,157,.18);color:#74c69d;font-weight:600}.lw-globe-zoom{position:absolute;bottom:50px;right:12px;display:flex;flex-direction:column;background:rgba(13,40,24,.88);backdrop-filter:blur(10px);border:1px solid rgba(116,198,157,.18);border-radius:6px;overflow:hidden;z-index:5}.lw-globe-zbtn{width:34px;height:34px;border:none;font-size:18px;font-weight:600;cursor:pointer;background:0 0;color:rgba(248,249,250,.5);transition:all .15s;font-family:monospace;display:flex;align-items:center;justify-content:center}.lw-globe-zbtn:first-child{border-bottom:1px solid rgba(116,198,157,.1)}.lw-globe-zbtn:hover{color:#74c69d;background:rgba(116,198,157,.12)}.lw-globe-zbtn:active{background:rgba(116,198,157,.22)}.lw-globe-loader{position:absolute;top:50px;left:12px;font-size:10px;color:#74c69d;font-family:monospace;display:flex;align-items:center;gap:6px;opacity:0;transition:opacity .3s;z-index:5}.lw-globe-loader.visible{opacity:1}.lw-globe-spinner{width:8px;height:8px;border-radius:50%;border:2px solid rgba(116,198,157,.25);border-top-color:#74c69d;animation:lwGlobeSpin .7s linear infinite}@keyframes lwGlobeSpin{to{transform:rotate(360deg)}}.lw-globe-intro{position:absolute;bottom:55px;left:50%;transform:translateX(-50%);background:rgba(13,40,24,.88);backdrop-filter:blur(10px);border:1px solid rgba(116,198,157,.18);border-radius:8px;padding:10px 18px;max-width:340px;text-align:center;z-index:5;transition:opacity .5s}.lw-globe-intro.hidden{opacity:0;pointer-events:none}.lw-globe-intro p{font-size:13px;color:#f8f9fa;line-height:1.5;margin:0}.lw-globe-intro .sub{font-size:11px;color:rgba(248,249,250,.4);margin-top:3px}.lw-globe-detail{position:absolute;top:12px;right:12px;width:270px;background:rgba(13,40,24,.92);backdrop-filter:blur(14px);border:1px solid rgba(116,198,157,.18);border-radius:10px;padding:14px;max-height:70%;overflow-y:auto;z-index:5;display:none}.lw-globe-detail.open{display:block}.lw-globe-detail::-webkit-scrollbar{width:4px}.lw-globe-detail::-webkit-scrollbar-thumb{background:rgba(116,198,157,.2);border-radius:2px}.lw-globe-dhead{display:flex;justify-content:space-between;align-items:flex-start;margin-bottom:10px}.lw-globe-dcountry{font-size:16px;font-weight:600;color:#f8f9fa}.lw-globe-dcount{font-size:11px;color:rgba(248,249,250,.45);margin-top:2px}.lw-globe-dback{background:rgba(116,198,157,.1);border:1px solid rgba(116,198,157,.2);border-radius:4px;color:#74c69d;cursor:pointer;padding:2px 9px;font-size:11px;font-family:monospace}.lw-globe-dback:hover{background:rgba(116,198,157,.2)}.lw-globe-pbar{display:flex;gap:5px;margin-bottom:12px}.lw-globe-pchip{flex:1;background:rgba(0,0,0,.3);border-radius:6px;padding:5px 6px;text-align:center;border-top:2px solid}.lw-globe-pnum{font-size:15px;font-weight:700;font-family:monospace}.lw-globe-plbl{font-size:8.5px;color:rgba(248,249,250,.45);text-transform:uppercase;letter-spacing:.4px}.lw-globe-aitem{padding:7px 9px;border-radius:5px;margin-bottom:2px;cursor:default;transition:background .12s;border-left:2px solid}.lw-globe-aitem:hover{background:rgba(116,198,157,.07)}.lw-globe-aname{font-size:12px;color:#f8f9fa;font-weight:500;line-height:1.3}.lw-globe-ameta{font-size:10px;color:rgba(248,249,250,.38);margin-top:2px;font-family:monospace}.lw-globe-alink{font-size:10px;color:#74c69d;margin-top:3px;text-decoration:none;display:inline-block;font-family:monospace}.lw-globe-alink:hover{text-decoration:underline}.lw-globe-stats{position:absolute;bottom:0;left:0;right:0;display:flex;justify-content:center;gap:22px;padding:10px 14px;background:linear-gradient(transparent,rgba(13,40,24,.75));z-index:5}.lw-globe-stat{text-align:center}.lw-globe-sval{font-size:17px;font-weight:700;font-family:monospace;color:#74c69d}.lw-globe-slbl{font-size:8.5px;letter-spacing:1.5px;color:rgba(248,249,250,.35);text-transform:uppercase}.lw-globe-tt{position:absolute;pointer-events:none;z-index:20;background:rgba(13,40,24,.88);backdrop-filter:blur(10px);border:1px solid rgba(116,198,157,.18);border-radius:7px;padding:8px 12px;display:none;max-width:260px}.lw-globe-tt.visible{display:block}.lw-globe-tt-name{font-size:12px;font-weight:600;color:#f8f9fa}.lw-globe-tt-id{font-size:10px;color:rgba(248,249,250,.4);font-family:monospace;margin-top:2px}.lw-globe-tt-badge{display:inline-block;font-size:9px;font-weight:600;padding:1px 6px;border-radius:3px;margin-top:4px;text-transform:uppercase;letter-spacing:.5px}@media(max-width:768px){.lw-globe-sidebar{width:200px;min-width:200px}.lw-globe-detail{width:220px}}@media(max-width:600px){.lw-globe-wrap{flex-direction:column}.lw-globe-sidebar{width:100%;min-width:unset;max-height:160px;flex-direction:row;border-right:none;border-bottom:1px solid rgba(116,198,157,.12)}.lw-globe-sidebar-hd{display:none}.lw-globe-countries{display:flex;overflow-x:auto;overflow-y:hidden;padding:4px 8px}.lw-globe-cbtn{min-width:120px;border-left:none!important;border-bottom:2px solid transparent;padding:6px 10px}.lw-globe-cbtn.active{border-bottom-color:#74c69d}.lw-globe-legend{display:none}.lw-globe-detail{width:88%;right:6%;top:auto;bottom:48px;max-height:38%}}';
document.head.appendChild(css);

function loadScript(u,cb){var s=document.createElement('script');s.src=u;s.onload=cb;s.onerror=function(){console.warn('[lw-globe] Failed: '+u);};document.head.appendChild(s);}
loadScript('https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js',function(){globeEls.forEach(function(el){initGlobe(el);});});

/* ── Coord parser ── */
function dms2d(d,m,s,h){var r=Math.abs(parseFloat(d))+(parseFloat(m)||0)/60+(parseFloat(s)||0)/3600;if(isNaN(r))return null;h=String(h||'').toUpperCase();if(h==='S'||h==='W')r=-r;return r;}
function parseGC(cs){
  if(!cs)return null;
  var dm=cs.match(/(-?\d+\.?\d*)\s*,\s*(-?\d+\.?\d*)/);
  if(dm){var a=parseFloat(dm[1]),b=parseFloat(dm[2]);if(!isNaN(a)&&!isNaN(b))return{lat:a,lng:b};}
  var re=/(\d+)[°]\s*(\d+)[′']\s*(\d+\.?\d*)[″""]?\s*([NSEW])/gi,ms=[],m;
  while((m=re.exec(cs))!==null){ms.push(m);if(ms.length>=2)break;}
  if(ms.length>=2){var a2=dms2d(ms[0][1],ms[0][2],ms[0][3],ms[0][4]),b2=dms2d(ms[1][1],ms[1][2],ms[1][3],ms[1][4]);var h0=ms[0][4].toUpperCase();if(h0==='E'||h0==='W'){var t=a2;a2=b2;b2=t;}if(a2!==null&&b2!==null)return{lat:a2,lng:b2};}
  return null;
}

/* ── CSV ── */
function csvL(l){var r=[],c='',q=false;for(var i=0;i<l.length;i++){var ch=l[i],nx=l[i+1];if(q){if(ch==='"'&&nx==='"'){c+='"';i++;}else if(ch==='"'){q=false;}else{c+=ch;}}else{if(ch==='"'){q=true;}else if(ch===','){r.push(c.trim());c='';}else{c+=ch;}}}r.push(c.trim());return r;}
function parseCSV(t){var ls=t.split('\n');if(ls.length<2)return[];var hd=csvL(ls[0]),o=[];for(var i=1;i<ls.length;i++){if(!ls[i].trim())continue;var v=csvL(ls[i]),row={};hd.forEach(function(h,j){row[h]=v[j]||'';});o.push(row);}return o;}

/* ── SMW ── */
function buildUrl(q){q=q.replace(/[\r\n]+/g,' ').replace(/\s+/g,' ').trim();var ps=q.split('|');var e=ps.map(function(p){return p.trim().replace(/-/g,'-2D').replace(/\[/g,'-5B').replace(/\]/g,'-5D').replace(/\?/g,'-3F').replace(/:/g,'-3A').replace(/\+/g,'-2B').replace(/ /g,'-20');});var b=(typeof mw!=='undefined'&&mw.config)?mw.config.get('wgScriptPath')||'':'';return b+'/wiki/Special:Ask/'+e.join('/')+'/format=csv';}
function fetchData(){return fetch(buildUrl(SMW_Q)).then(function(r){if(!r.ok)throw new Error('HTTP '+r.status);return r.text();}).then(function(csv){var rows=parseCSV(csv),res=[];rows.forEach(function(row){var c=parseGC(row['Has coordinates']);if(!c)return;res.push({id:row['Has accession ID']||row['Page']||'',name:row['Has descriptive name']||row['Page']||'',country:row['Has country']||'',region:row['Has growing region']||'',area:row['Has growing area']||'',lat:c.lat,lng:c.lng,priority:row['Has conservation priority']||'Medium',page:row['Page']||''});});if(!res.length)throw new Error('No mappable accessions');return res;}).catch(function(e){console.warn('[lw-globe] '+e.message);return[];});}

/* ── Mercator math ── */
function lat2mn(lat){var lr=Math.max(-ML,Math.min(ML,lat))*Math.PI/180;return(1-Math.log(Math.tan(lr)+1/Math.cos(lr))/Math.PI)/2;}
function mn2lat(mn){return Math.atan(Math.sinh(Math.PI*(1-2*mn)))*180/Math.PI;}

/* ── Base tile loader (zoom 4 → 4096×2048 equirectangular) ── */
function loadBase(mode,cache){
  var key='base_'+mode;if(cache[key])return Promise.resolve(cache[key]);
  var urlFn=TILE_URLS[mode],proms=[],tiles=[];
  for(var ty=0;ty<BASE_N;ty++)for(var tx=0;tx<BASE_N;tx++){(function(tx_,ty_){tiles.push({tx:tx_,ty:ty_});proms.push(new Promise(function(res){var img=new Image();img.crossOrigin='anonymous';img.onload=function(){res(img);};img.onerror=function(){res(null);};img.src=urlFn(BASE_Z,tx_,ty_);}));})(tx,ty);}
  return Promise.all(proms).then(function(images){
    var mc=document.createElement('canvas');mc.width=BASE_MERC;mc.height=BASE_MERC;var mx=mc.getContext('2d');
    mx.fillStyle=mode==='basic'?'#1a1a2e':mode==='satellite'?'#0a1520':'#d4dadc';mx.fillRect(0,0,BASE_MERC,BASE_MERC);
    tiles.forEach(function(t,i){if(images[i])mx.drawImage(images[i],t.tx*TILE_PX,t.ty*TILE_PX,TILE_PX,TILE_PX);});
    var ec=document.createElement('canvas');ec.width=BASE_EQ_W;ec.height=BASE_EQ_H;var ex=ec.getContext('2d');
    ex.fillStyle=mode==='basic'?'#1a1a2e':mode==='satellite'?'#c8d8e8':'#d4dadc';ex.fillRect(0,0,BASE_EQ_W,BASE_EQ_H);
    for(var y=0;y<BASE_EQ_H;y++){var lat=90-(y/BASE_EQ_H)*180;if(Math.abs(lat)>ML)continue;var my=lat2mn(lat),sY=Math.floor(my*BASE_MERC);if(sY>=0&&sY<BASE_MERC)ex.drawImage(mc,0,sY,BASE_MERC,1,0,y,BASE_EQ_W,1);}
    if(mode==='terrain'){ex.fillStyle='rgba(0,20,10,0.3)';ex.fillRect(0,0,BASE_EQ_W,BASE_EQ_H);}
    else if(mode==='satellite'){ex.fillStyle='rgba(0,10,5,0.15)';ex.fillRect(0,0,BASE_EQ_W,BASE_EQ_H);}
    cache[key]={canvas:ec};return cache[key];
  });
}

/* ═════════════════════════════════════════════════════
 *  DETAIL TILE MANAGER — loads zoom-6 tiles on demand
 *  and paints them onto an 8192×4096 equirect canvas
 * ═════════════════════════════════════════════════════ */
function createDetailMgr(){
  var hiCanvas=document.createElement('canvas');hiCanvas.width=HI_EQ_W;hiCanvas.height=HI_EQ_H;
  var hiCtx=hiCanvas.getContext('2d');
  var tileImgCache={};  /* 'mode:tx:ty' → Image */
  var painted={};        /* 'mode:tx:ty' → true */
  var loading={};        /* 'mode:tx:ty' → true */
  var curMode='basic';
  var hiTexture=null;
  var dirty=false;
  var debounceTimer=null;

  /* Paint a single zoom-6 tile onto the hi-res equirect canvas */
  function paintTile(img,tx,ty){
    var dstX=tx*(HI_EQ_W/HI_N); /* each tile gets HI_EQ_W/64 = 128px wide */
    var dstW=HI_EQ_W/HI_N;
    /* Vertical: find equirect rows that map into this tile's Mercator range */
    var mercTop=ty*TILE_PX, mercBot=(ty+1)*TILE_PX;
    for(var y=0;y<HI_EQ_H;y++){
      var lat=90-(y/HI_EQ_H)*180;
      if(Math.abs(lat)>ML)continue;
      var mercY=lat2mn(lat)*HI_MERC;
      if(mercY>=mercTop&&mercY<mercBot){
        var srcY=Math.floor(mercY-mercTop);
        if(srcY>=0&&srcY<TILE_PX){
          hiCtx.drawImage(img,0,srcY,TILE_PX,1,dstX,y,dstW,1);
        }
      }
    }
  }

  /* Apply mode overlay (darken terrain/satellite) for a tile region */
  function overlayTile(tx,ty,mode){
    if(mode==='terrain'||mode==='satellite'){
      var dstX=tx*(HI_EQ_W/HI_N),dstW=HI_EQ_W/HI_N;
      var mercTop=ty*TILE_PX,mercBot=(ty+1)*TILE_PX;
      /* Find Y range */
      var yMin=HI_EQ_H,yMax=0;
      for(var y=0;y<HI_EQ_H;y++){
        var lat=90-(y/HI_EQ_H)*180;if(Math.abs(lat)>ML)continue;
        var my=lat2mn(lat)*HI_MERC;
        if(my>=mercTop&&my<mercBot){if(y<yMin)yMin=y;if(y>yMax)yMax=y;}
      }
      if(yMin<yMax){
        hiCtx.fillStyle=mode==='terrain'?'rgba(0,20,10,0.3)':'rgba(0,10,5,0.15)';
        hiCtx.fillRect(dstX,yMin,dstW,yMax-yMin+1);
      }
    }
  }

  return {
    init: function(baseCanvas, mode){
      curMode=mode;
      painted={};loading={};
      /* Upscale base (4096×2048) to hi-res (8192×4096) */
      hiCtx.drawImage(baseCanvas,0,0,HI_EQ_W,HI_EQ_H);
      dirty=true;
    },

    setMode: function(baseCanvas, mode){
      curMode=mode;painted={};loading={};
      hiCtx.drawImage(baseCanvas,0,0,HI_EQ_W,HI_EQ_H);
      dirty=true;
    },

    /* Called from animation loop — loads tiles for visible region */
    update: function(centerLat, centerLng, camZ){
      if(camZ>=DETAIL_CAM_THRESHOLD)return; /* too far out */

      /* Visible window: wider when less zoomed */
      var halfSpan=Math.max(15, (camZ/ZOOM_MIN)*25);
      var latMin=centerLat-halfSpan, latMax=centerLat+halfSpan;
      var lngMin=centerLng-halfSpan*1.5, lngMax=centerLng+halfSpan*1.5;
      latMin=Math.max(-ML,latMin);latMax=Math.min(ML,latMax);

      /* Convert to tile coords at HI_Z */
      function lng2tx(lng){return Math.floor(((lng+180)/360)*HI_N);}
      function lat2ty(lat){return Math.floor(lat2mn(lat)*HI_N);}

      var txMin=lng2tx(lngMin), txMax=lng2tx(lngMax);
      var tyMin=lat2ty(latMax), tyMax=lat2ty(latMin); /* reversed because Mercator Y increases downward */
      txMin=Math.max(0,txMin);txMax=Math.min(HI_N-1,txMax);
      tyMin=Math.max(0,tyMin);tyMax=Math.min(HI_N-1,tyMax);

      /* Handle wraparound */
      var txList=[];
      if(txMin<=txMax){for(var x=txMin;x<=txMax;x++)txList.push(x);}
      else{for(var x=txMin;x<HI_N;x++)txList.push(x);for(var x=0;x<=txMax;x++)txList.push(x);}

      var urlFn=TILE_URLS[curMode];
      var newLoads=0;
      for(var yi=tyMin;yi<=tyMax;yi++){
        for(var xi=0;xi<txList.length;xi++){
          var txx=txList[xi];
          var key=curMode+':'+txx+':'+yi;
          if(painted[key]||loading[key])continue;

          /* Check cache first */
          if(tileImgCache[key]){
            paintTile(tileImgCache[key],txx,yi);
            overlayTile(txx,yi,curMode);
            painted[key]=true;dirty=true;
            continue;
          }

          /* Load tile */
          loading[key]=true;newLoads++;
          (function(k,tx_,ty_){
            var img=new Image();img.crossOrigin='anonymous';
            img.onload=function(){
              tileImgCache[k]=img;
              paintTile(img,tx_,ty_);
              overlayTile(tx_,ty_,curMode);
              painted[k]=true;delete loading[k];dirty=true;
            };
            img.onerror=function(){delete loading[k];};
            img.src=urlFn(HI_Z,tx_,ty_);
          })(key,txx,yi);

          if(newLoads>=12)return; /* throttle: max 12 new loads per frame batch */
        }
      }
    },

    /* Returns texture if dirty, null if no update needed */
    getTexture: function(renderer){
      if(!dirty)return hiTexture;
      clearTimeout(debounceTimer);
      debounceTimer=setTimeout(function(){
        if(hiTexture){hiTexture.dispose();}
        hiTexture=new THREE.CanvasTexture(hiCanvas);
        var ma=renderer.capabilities.getMaxAnisotropy();
        hiTexture.anisotropy=ma;
        hiTexture.minFilter=THREE.LinearMipmapLinearFilter;
        hiTexture.magFilter=THREE.LinearFilter;
        hiTexture.generateMipmaps=true;
        hiTexture.needsUpdate=true;
        dirty=false;
      },150); /* debounce 150ms to batch tile paints */
      return hiTexture;
    },

    getActiveTexture: function(){ return hiTexture; },
    isDirty: function(){ return dirty; }
  };
}

/* ── Helpers ── */
function ll2v(lat,lng,r){var p=(90-lat)*Math.PI/180,t=(lng+180)*Math.PI/180;return new THREE.Vector3(-r*Math.sin(p)*Math.cos(t),r*Math.cos(p),r*Math.sin(p)*Math.sin(t));}
function worstP(list){for(var i=0;i<PO.length;i++)if(list.some(function(a){return a.priority===PO[i];}))return PO[i];return'Low';}
function makeClusterTex(n,color){var s=64,c=document.createElement('canvas');c.width=s;c.height=s;var x=c.getContext('2d');x.beginPath();x.arc(s/2,s/2,s/2-2,0,Math.PI*2);x.fillStyle=color;x.fill();x.lineWidth=3;x.strokeStyle='rgba(255,255,255,0.9)';x.stroke();x.fillStyle='#fff';x.font='bold '+(n>99?18:n>9?22:24)+'px monospace';x.textAlign='center';x.textBaseline='middle';x.fillText(String(n),s/2,s/2+1);return new THREE.CanvasTexture(c);}

/* Derive view center lat/lng from globe rotation */
function viewCenter(rotX,rotY){
  var front=new THREE.Vector3(0,0,GR);
  var q=new THREE.Quaternion().setFromEuler(new THREE.Euler(rotX,rotY,0,'XYZ'));
  q.invert();front.applyQuaternion(q);
  var lat=Math.asin(Math.max(-1,Math.min(1,front.y/GR)))*180/Math.PI;
  var lng=Math.atan2(-front.x,front.z)*180/Math.PI-180;
  if(lng<-180)lng+=360;if(lng>180)lng-=360;
  return{lat:lat,lng:lng};
}



/* Compute globe rotation to face a lat/lng toward camera.
   At rot.x=0, rot.y=0 the camera sees lat=0, lng=-90.
   rot.y controls longitude (around Y axis), rot.x controls latitude tilt.
   curY = current rot.y, needed to find shortest rotation path. */
function rotToFace(lat,lng,curY){
  var targetY=-(lng+90)*Math.PI/180;
  /* Normalize to shortest path from current rotation (handles auto-rotate accumulation) */
  var diff=targetY-curY;
  diff=diff-Math.round(diff/(2*Math.PI))*2*Math.PI;
  return{x:lat*Math.PI/180, y:curY+diff};
}

/* ═══ MAIN INIT ═══ */
function initGlobe(hostEl){
  var accessions=[],countryData={},selectedCountry=null,mapMode='basic';
  var tCache={},isDragging=false,autoRotate=true;
  var lastMouse={x:0,y:0},rot={x:0.3,y:-1.5},tgt={x:0.3,y:-1.5};
  var markerDots=[],markerRings=[];
  var camZ=ZOOM_DEFAULT,camZTgt=ZOOM_DEFAULT,pinchDist=0,idleTimer=null;
  var detailMgr=createDetailMgr();
  var baseCanvasForMode=null; /* store current base canvas for detail init */

  function userInteracted(){autoRotate=false;clearTimeout(idleTimer);idleTimer=setTimeout(function(){autoRotate=true;},IDLE_MS);}

  /* DOM */
  var h=hostEl.style.height||'620px';hostEl.innerHTML='';
  var wrap=document.createElement('div');wrap.className='lw-globe-wrap';wrap.style.height=h;
  wrap.innerHTML='<div class="lw-globe-sidebar"><div class="lw-globe-sidebar-hd"><div class="lw-globe-label">Landrace.Wiki</div><h3>Global Accession Map</h3><div class="meta" data-ref="meta"></div></div><div class="lw-globe-countries" data-ref="countries"><div class="lw-globe-label" style="padding:8px 14px 5px">Countries</div></div><div class="lw-globe-legend"><div class="lw-globe-label" style="padding:0 0 7px">Conservation Priority</div><div class="lw-globe-litem"><div class="lw-globe-ldot" style="background:#c92a2a;box-shadow:0 0 4px #c92a2a88"></div><span class="lw-globe-llabel">Critical</span></div><div class="lw-globe-litem"><div class="lw-globe-ldot" style="background:#d9480f;box-shadow:0 0 4px #d9480f88"></div><span class="lw-globe-llabel">High</span></div><div class="lw-globe-litem"><div class="lw-globe-ldot" style="background:#e67700;box-shadow:0 0 4px #e6770088"></div><span class="lw-globe-llabel">Medium</span></div><div class="lw-globe-litem"><div class="lw-globe-ldot" style="background:#2b8a3e;box-shadow:0 0 4px #2b8a3e88"></div><span class="lw-globe-llabel">Low</span></div></div></div><div class="lw-globe-main" data-ref="main"><canvas class="lw-globe-canvas" data-ref="canvas"></canvas><div class="lw-globe-vsw" data-ref="vsw"><button class="lw-globe-vbtn active" data-mode="basic">Basic</button><button class="lw-globe-vbtn" data-mode="terrain">Terrain</button><button class="lw-globe-vbtn" data-mode="satellite">Satellite</button></div><div class="lw-globe-zoom" data-ref="zoomctl"><button class="lw-globe-zbtn" data-dir="in">+</button><button class="lw-globe-zbtn" data-dir="out">\u2212</button></div><div class="lw-globe-loader" data-ref="loader"><div class="lw-globe-spinner"></div><span>Loading tiles\u2026</span></div><div class="lw-globe-intro" data-ref="intro"><p>Drag to rotate \u00b7 Scroll to zoom \u00b7 Select a country</p><div class="sub">Each marker represents a documented landrace accession</div></div><div class="lw-globe-detail" data-ref="detail"><div class="lw-globe-dhead"><div><div class="lw-globe-dcountry" data-ref="dcountry"></div><div class="lw-globe-dcount" data-ref="dcount"></div></div><button class="lw-globe-dback" data-ref="dback">\u2190 All</button></div><div class="lw-globe-pbar" data-ref="pbar"></div><div class="lw-globe-label">Accessions</div><div data-ref="dlist"></div></div><div class="lw-globe-stats" data-ref="stats"></div><div class="lw-globe-tt" data-ref="tt"><div class="lw-globe-tt-name"></div><div class="lw-globe-tt-id"></div><div class="lw-globe-tt-badge"></div></div></div>';
  hostEl.appendChild(wrap);
  var R={};wrap.querySelectorAll('[data-ref]').forEach(function(el){R[el.getAttribute('data-ref')]=el;});

  /* Three.js scene */
  var mainEl=R.main,canvas=R.canvas;
  var w=mainEl.clientWidth,ht=mainEl.clientHeight;
  var scene=new THREE.Scene();scene.background=new THREE.Color('#0d2818');
  var camera=new THREE.PerspectiveCamera(45,w/ht,0.1,1000);camera.position.z=ZOOM_DEFAULT;
  var renderer=new THREE.WebGLRenderer({canvas:canvas,antialias:true});
  renderer.setSize(w,ht);renderer.setPixelRatio(Math.min(window.devicePixelRatio,2));

  var globeMesh=new THREE.Mesh(new THREE.SphereGeometry(GR,96,96),new THREE.MeshPhongMaterial({color:0x112211,shininess:12}));
  scene.add(globeMesh);
  scene.add(new THREE.Mesh(new THREE.SphereGeometry(GR*1.06,96,96),new THREE.ShaderMaterial({vertexShader:'varying vec3 vN;void main(){vN=normalize(normalMatrix*normal);gl_Position=projectionMatrix*modelViewMatrix*vec4(position,1.);}',fragmentShader:'varying vec3 vN;void main(){float i=pow(0.62-dot(vN,vec3(0,0,1)),3.);gl_FragColor=vec4(0.18,0.56,0.34,1.)*i*0.5;}',blending:THREE.AdditiveBlending,side:THREE.BackSide,transparent:true})));
  scene.add(new THREE.AmbientLight(0x445544,1.0));
  var dl=new THREE.DirectionalLight(0xffffff,0.8);dl.position.set(5,3,5);scene.add(dl);
  var bl=new THREE.DirectionalLight(0x2d6a4f,0.25);bl.position.set(-5,-3,-5);scene.add(bl);
  var sg=new THREE.BufferGeometry(),sp=[];for(var i=0;i<500;i++)sp.push((Math.random()-.5)*50,(Math.random()-.5)*50,(Math.random()-.5)*50);
  sg.setAttribute('position',new THREE.Float32BufferAttribute(sp,3));
  scene.add(new THREE.Points(sg,new THREE.PointsMaterial({color:0x446644,size:0.02,transparent:true,opacity:0.35})));

  var markersGroup=new THREE.Group();scene.add(markersGroup);
  var clustersGroup=new THREE.Group();scene.add(clustersGroup);
  var clock=new THREE.Clock();

  /* Texture application */
  function applyTex(mode){
    R.loader.classList.add('visible');
    loadBase(mode,tCache).then(function(obj){
      baseCanvasForMode=obj.canvas;
      /* Apply base texture initially */
      var tex=new THREE.CanvasTexture(obj.canvas);
      var ma=renderer.capabilities.getMaxAnisotropy();
      tex.anisotropy=ma;tex.minFilter=THREE.LinearMipmapLinearFilter;tex.magFilter=THREE.LinearFilter;tex.generateMipmaps=true;
      globeMesh.material.map=tex;globeMesh.material.color=new THREE.Color(0xffffff);globeMesh.material.needsUpdate=true;
      R.loader.classList.remove('visible');
      /* Init detail manager with upscaled base */
      detailMgr.setMode(obj.canvas,mode);
    }).catch(function(){R.loader.classList.remove('visible');});
  }

  function buildMarkers(){
    while(markersGroup.children.length)markersGroup.remove(markersGroup.children[0]);
    markerDots=[];markerRings=[];
    accessions.forEach(function(acc,idx){
      var pos=ll2v(acc.lat,acc.lng,GR*1.005);
      var col=new THREE.Color(PC[acc.priority]||PC.Medium);
      var dot=new THREE.Mesh(new THREE.SphereGeometry(0.013,8,8),new THREE.MeshBasicMaterial({color:col}));
      dot.position.copy(pos);dot.userData={acc:acc,idx:idx};markersGroup.add(dot);markerDots.push(dot);
      var ring=new THREE.Mesh(new THREE.RingGeometry(0.016,0.025,16),new THREE.MeshBasicMaterial({color:col,transparent:true,opacity:acc.priority==='Critical'?0.7:0.35,side:THREE.DoubleSide}));
      ring.position.copy(pos);ring.lookAt(new THREE.Vector3(0,0,0));ring.userData={type:'ring',priority:acc.priority,idx:idx};markersGroup.add(ring);markerRings.push(ring);
      if(acc.priority==='Critical'){var dir=pos.clone().normalize(),end=pos.clone().add(dir.multiplyScalar(0.07));var spike=new THREE.Line(new THREE.BufferGeometry().setFromPoints([pos.clone(),end]),new THREE.LineBasicMaterial({color:col,transparent:true,opacity:0.8}));spike.userData={type:'spike',idx:idx};markersGroup.add(spike);}
    });
  }

  /* Clustering */
  function updateClusters(){
    while(clustersGroup.children.length)clustersGroup.remove(clustersGroup.children[0]);
    var n=markerDots.length;if(!n)return;
    var camDir=camera.position.clone().normalize();
    var items=[];
    for(var i=0;i<n;i++){var wp=markerDots[i].position.clone().applyMatrix4(markersGroup.matrixWorld);var front=wp.clone().normalize().dot(camDir)>-0.05;var scr=null;if(front){var v=wp.clone().project(camera);scr={x:(v.x*0.5+0.5)*mainEl.clientWidth,y:(-v.y*0.5+0.5)*mainEl.clientHeight};}items.push({idx:i,front:front,sx:scr?scr.x:0,sy:scr?scr.y:0,used:false});}
    var groups=[];for(var i=0;i<n;i++){if(items[i].used||!items[i].front)continue;var grp=[i];items[i].used=true;for(var j=i+1;j<n;j++){if(items[j].used||!items[j].front)continue;var dx=items[i].sx-items[j].sx,dy=items[i].sy-items[j].sy;if(dx*dx+dy*dy<CLUSTER_PX*CLUSTER_PX){grp.push(j);items[j].used=true;}}groups.push(grp);}
    for(var i=0;i<n;i++){markerDots[i].visible=false;markerRings[i].visible=false;}
    markersGroup.children.forEach(function(c){if(c.userData&&c.userData.type==='spike')c.visible=false;});
    groups.forEach(function(grp){
      if(grp.length===1){var idx=grp[0];markerDots[idx].visible=true;markerRings[idx].visible=true;markersGroup.children.forEach(function(c){if(c.userData&&c.userData.type==='spike'&&c.userData.idx===idx)c.visible=true;});}
      else{var cx=0,cy=0,cz=0,ga=[];grp.forEach(function(idx){var p=markerDots[idx].position;cx+=p.x;cy+=p.y;cz+=p.z;ga.push(markerDots[idx].userData.acc);});cx/=grp.length;cy/=grp.length;cz/=grp.length;var centroid=new THREE.Vector3(cx,cy,cz).normalize().multiplyScalar(GR*1.01);var worst=worstP(ga),color=PC[worst]||PC.Medium;var tex=makeClusterTex(grp.length,color);var sprite=new THREE.Sprite(new THREE.SpriteMaterial({map:tex,transparent:true,depthTest:true}));sprite.position.copy(centroid);var sc=0.06+camZ*0.012;sprite.scale.set(sc,sc,1);sprite.userData={type:'cluster',accessions:ga};clustersGroup.add(sprite);}
    });
    for(var i=0;i<n;i++){if(!items[i].front){markerDots[i].visible=false;markerRings[i].visible=false;}}
  }

  /* Animation */
  var fc=0;
  function animate(){
    requestAnimationFrame(animate);var t=clock.getElapsedTime();fc++;
    if(autoRotate&&!isDragging)tgt.y-=0.0008;
    rot.x+=(tgt.x-rot.x)*0.07;rot.y+=(tgt.y-rot.y)*0.07;
    globeMesh.rotation.x=rot.x;globeMesh.rotation.y=rot.y;
    markersGroup.rotation.x=rot.x;markersGroup.rotation.y=rot.y;
    clustersGroup.rotation.x=rot.x;clustersGroup.rotation.y=rot.y;
    camZ+=(camZTgt-camZ)*0.1;camera.position.z=camZ;
    markerRings.forEach(function(ring){if(ring.visible&&ring.userData.priority==='Critical'){var s=1+Math.sin(t*2.5)*0.35;ring.scale.set(s,s,s);ring.material.opacity=0.35+Math.sin(t*2.5)*0.35;}});
    if(accessions.length&&fc%4===0)updateClusters();

    /* Dynamic detail tile loading */
    if(fc%8===0&&camZ<DETAIL_CAM_THRESHOLD){
      var vc=viewCenter(rot.x,rot.y);
      detailMgr.update(vc.lat,vc.lng,camZ);
    }
    /* Apply hi-res texture if detail manager has updates */
    var hiTex=detailMgr.getActiveTexture();
    if(hiTex&&globeMesh.material.map!==hiTex&&camZ<DETAIL_CAM_THRESHOLD){
      globeMesh.material.map=hiTex;globeMesh.material.needsUpdate=true;
    }
    /* Switch back to base when zoomed out */
    if(camZ>=DETAIL_CAM_THRESHOLD&&baseCanvasForMode){
      var curMap=globeMesh.material.map;
      if(curMap&&curMap.image&&curMap.image.width===HI_EQ_W){
        /* Currently on hi-res, switch to base */
        var baseTex=new THREE.CanvasTexture(baseCanvasForMode);
        var ma=renderer.capabilities.getMaxAnisotropy();baseTex.anisotropy=ma;
        baseTex.minFilter=THREE.LinearMipmapLinearFilter;baseTex.magFilter=THREE.LinearFilter;baseTex.generateMipmaps=true;
        globeMesh.material.map=baseTex;globeMesh.material.needsUpdate=true;
      }
    }
    detailMgr.getTexture(renderer); /* process dirty flag */

    renderer.render(scene,camera);
  }

  /* Interaction */
  var raycaster=new THREE.Raycaster(),mouse=new THREE.Vector2();
  function onDown(cx,cy){isDragging=true;userInteracted();lastMouse={x:cx,y:cy};canvas.classList.add('dragging');}
  function onMove(cx,cy){
    if(isDragging){tgt.y+=(cx-lastMouse.x)*0.005;tgt.x+=(cy-lastMouse.y)*0.005;tgt.x=Math.max(-1.2,Math.min(1.2,tgt.x));lastMouse={x:cx,y:cy};}
    var rect=canvas.getBoundingClientRect();mouse.x=((cx-rect.left)/rect.width)*2-1;mouse.y=-((cy-rect.top)/rect.height)*2+1;
    raycaster.setFromCamera(mouse,camera);var tt=R.tt;
    var hits=raycaster.intersectObjects(markerDots.filter(function(d){return d.visible;}));
    if(hits.length>0&&hits[0].object.userData.acc){
      var a=hits[0].object.userData.acc;tt.querySelector('.lw-globe-tt-name').textContent=a.name;tt.querySelector('.lw-globe-tt-id').textContent=a.id+' \u00b7 '+a.area;
      var badge=tt.querySelector('.lw-globe-tt-badge');badge.textContent=a.priority;badge.style.background=(PC[a.priority]||'#666')+'33';badge.style.color=PC[a.priority]||'#666';
      tt.style.left=(cx-rect.left+14)+'px';tt.style.top=(cy-rect.top-10)+'px';tt.classList.add('visible');if(!isDragging)canvas.style.cursor='pointer';
    }else{
      var ch=raycaster.intersectObjects(clustersGroup.children);
      if(ch.length>0&&ch[0].object.userData.accessions){var accs=ch[0].object.userData.accessions,worst=worstP(accs);tt.querySelector('.lw-globe-tt-name').textContent=accs.length+' accessions';tt.querySelector('.lw-globe-tt-id').textContent=accs.map(function(a){return a.area;}).filter(function(v,i,a){return a.indexOf(v)===i;}).join(', ');var badge=tt.querySelector('.lw-globe-tt-badge');badge.textContent=worst;badge.style.background=(PC[worst]||'#666')+'33';badge.style.color=PC[worst]||'#666';tt.style.left=(cx-rect.left+14)+'px';tt.style.top=(cy-rect.top-10)+'px';tt.classList.add('visible');if(!isDragging)canvas.style.cursor='pointer';}
      else{tt.classList.remove('visible');if(!isDragging)canvas.style.cursor='grab';}
    }
  }
  function onUp(){isDragging=false;canvas.classList.remove('dragging');}

  canvas.addEventListener('mousedown',function(e){onDown(e.clientX,e.clientY);});
  canvas.addEventListener('mousemove',function(e){onMove(e.clientX,e.clientY);});
  canvas.addEventListener('mouseup',onUp);
  canvas.addEventListener('mouseleave',function(){onUp();R.tt.classList.remove('visible');});
  canvas.addEventListener('wheel',function(e){e.preventDefault();userInteracted();camZTgt=Math.max(ZOOM_MIN,Math.min(ZOOM_MAX,camZTgt+e.deltaY*0.003));},{passive:false});
  canvas.addEventListener('touchstart',function(e){if(e.touches.length===1){onDown(e.touches[0].clientX,e.touches[0].clientY);}else if(e.touches.length===2){isDragging=false;var dx=e.touches[0].clientX-e.touches[1].clientX,dy=e.touches[0].clientY-e.touches[1].clientY;pinchDist=Math.sqrt(dx*dx+dy*dy);}},{passive:true});
  canvas.addEventListener('touchmove',function(e){if(e.touches.length===1&&isDragging){onMove(e.touches[0].clientX,e.touches[0].clientY);}else if(e.touches.length===2){userInteracted();var dx=e.touches[0].clientX-e.touches[1].clientX,dy=e.touches[0].clientY-e.touches[1].clientY;var dist=Math.sqrt(dx*dx+dy*dy);if(pinchDist>0)camZTgt=Math.max(ZOOM_MIN,Math.min(ZOOM_MAX,camZTgt+(pinchDist-dist)*0.01));pinchDist=dist;}},{passive:true});
  canvas.addEventListener('touchend',function(){pinchDist=0;onUp();});
  canvas.addEventListener('click',function(e){var rect=canvas.getBoundingClientRect();mouse.x=((e.clientX-rect.left)/rect.width)*2-1;mouse.y=-((e.clientY-rect.top)/rect.height)*2+1;raycaster.setFromCamera(mouse,camera);var hits=raycaster.intersectObjects(markerDots.filter(function(d){return d.visible;}));if(hits.length>0&&hits[0].object.userData.acc){selectCountry(hits[0].object.userData.acc.country);return;}var ch=raycaster.intersectObjects(clustersGroup.children);if(ch.length>0&&ch[0].object.userData.accessions){selectCountry(ch[0].object.userData.accessions[0].country);}});
  window.addEventListener('resize',function(){var nw=mainEl.clientWidth,nh=mainEl.clientHeight;if(nw<1||nh<1)return;camera.aspect=nw/nh;camera.updateProjectionMatrix();renderer.setSize(nw,nh);});

  R.vsw.querySelectorAll('.lw-globe-vbtn').forEach(function(btn){btn.addEventListener('click',function(){R.vsw.querySelectorAll('.lw-globe-vbtn').forEach(function(b){b.classList.remove('active');});btn.classList.add('active');mapMode=btn.getAttribute('data-mode');applyTex(mapMode);});});
  R.zoomctl.querySelectorAll('.lw-globe-zbtn').forEach(function(btn){btn.addEventListener('click',function(){userInteracted();camZTgt=Math.max(ZOOM_MIN,Math.min(ZOOM_MAX,camZTgt+(btn.getAttribute('data-dir')==='in'?-0.4:0.4)));});});

  /* UI */
  function buildCD(){countryData={};accessions.forEach(function(a){if(!countryData[a.country])countryData[a.country]={count:0,regions:{},priorities:{}};countryData[a.country].count++;countryData[a.country].regions[a.region]=true;countryData[a.country].priorities[a.priority]=(countryData[a.country].priorities[a.priority]||0)+1;});}
  function renderSB(){
    R.meta.textContent=accessions.length+' accessions \u00b7 '+Object.keys(countryData).length+' countries';
    var list=R.countries;list.innerHTML='<div class="lw-globe-label" style="padding:8px 14px 5px">Countries</div>';
    Object.keys(countryData).sort(function(a,b){return countryData[b].count-countryData[a].count;}).forEach(function(c){
      var d=countryData[c],rc=Object.keys(d.regions).length,worst=worstP(accessions.filter(function(a){return a.country===c;}));
      var btn=document.createElement('button');btn.className='lw-globe-cbtn'+(selectedCountry===c?' active':'');
      btn.innerHTML='<div class="lw-globe-cdot" style="background:'+PC[worst]+';box-shadow:0 0 6px '+PC[worst]+'66"></div><div class="lw-globe-cinfo"><div class="lw-globe-cname">'+c+'</div><div class="lw-globe-csub">'+d.count+' accession'+(d.count!==1?'s':'')+' \u00b7 '+rc+' region'+(rc!==1?'s':'')+'</div></div><div class="lw-globe-ccount">'+d.count+'</div>';
      btn.addEventListener('click',function(){selectCountry(c);});list.appendChild(btn);
    });
  }
  function renderStats(){var crit=accessions.filter(function(a){return a.priority==='Critical';}).length;var rm={};accessions.forEach(function(a){rm[a.region]=true;});R.stats.innerHTML=[{l:'Accessions',v:accessions.length},{l:'Countries',v:Object.keys(countryData).length},{l:'Regions',v:Object.keys(rm).length},{l:'Critical',v:crit,c:'#c92a2a'}].map(function(s){return'<div class="lw-globe-stat"><div class="lw-globe-sval" style="color:'+(s.c||'#74c69d')+'">'+s.v+'</div><div class="lw-globe-slbl">'+s.l+'</div></div>';}).join('');}
  function selectCountry(c){if(c===selectedCountry){deselectCountry();return;}selectedCountry=c;R.intro.classList.add('hidden');var ca=accessions.filter(function(a){return a.country===c;}),clat=0,clng=0;if(ca.length){ca.forEach(function(a){clat+=a.lat;clng+=a.lng;});clat/=ca.length;clng/=ca.length;var rv=rotToFace(clat,clng,rot.y);tgt.x=rv.x;tgt.y=rv.y;}camZTgt=2.8;userInteracted();renderSB();renderDetail();}
  function deselectCountry(){selectedCountry=null;R.detail.classList.remove('open');camZTgt=ZOOM_DEFAULT;renderSB();}
  function renderDetail(){
    if(!selectedCountry){R.detail.classList.remove('open');return;}var d=countryData[selectedCountry];
    R.dcountry.textContent=selectedCountry;R.dcount.textContent=d.count+' accession'+(d.count!==1?'s':'');
    R.pbar.innerHTML='';PO.forEach(function(p){var cnt=d.priorities[p];if(!cnt)return;R.pbar.innerHTML+='<div class="lw-globe-pchip" style="border-top-color:'+PC[p]+'"><div class="lw-globe-pnum" style="color:'+PC[p]+'">'+cnt+'</div><div class="lw-globe-plbl">'+p+'</div></div>';});
    var filtered=accessions.filter(function(a){return a.country===selectedCountry;}).sort(function(a,b){return PO.indexOf(a.priority)-PO.indexOf(b.priority);});
    R.dlist.innerHTML='';filtered.forEach(function(acc){
      var div=document.createElement('div');div.className='lw-globe-aitem';div.style.borderLeftColor=PC[acc.priority]||PC.Medium;
      var pu=acc.page?'/wiki/'+encodeURIComponent(acc.page.replace(/ /g,'_')):'';
      div.innerHTML='<div class="lw-globe-aname">'+acc.name+'</div><div class="lw-globe-ameta">'+acc.id+' \u00b7 '+acc.area+'</div>'+(pu?'<a class="lw-globe-alink" href="'+pu+'">View Accession \u2192</a>':'');
      R.dlist.appendChild(div);
    });R.detail.classList.add('open');
  }
  R.dback.addEventListener('click',deselectCountry);

  fetchData().then(function(data){accessions=data;buildCD();buildMarkers();renderSB();renderStats();applyTex('basic');animate();});
}
})();
// </nowiki>