MediaWiki:Common.js: Difference between revisions
MediaWiki interface page
More actions
Eloise Zomia (talk | contribs) No edit summary Tag: Reverted |
Eloise Zomia (talk | contribs) Undo revision 3490 by Eloise Zomia (talk) Tag: Undo |
||
| Line 3: | Line 3: | ||
$(function(){ | $(function(){ | ||
var $overlay=$('<div class="lw-map-overlay"></div>').appendTo('body'); | var $overlay=$('<div class="lw-map-overlay"></div>').appendTo('body'); | ||
$('.lw-infobox__map-expand').on('click',function(){ | $('.lw-infobox__map-expand').on('click',function(){ | ||
var $mapContainer=$(this).closest('.lw-infobox__map-container'); | var $mapContainer=$(this).closest('.lw-infobox__map-container'); | ||
var $map=$mapContainer.find('.lw-infobox__map'); | var $map=$mapContainer.find('.lw-infobox__map'); | ||
if($overlay.hasClass('lw-map-overlay--active')){ | if($overlay.hasClass('lw-map-overlay--active')){ | ||
$map.appendTo($mapContainer); | $map.appendTo($mapContainer); | ||
| Line 15: | Line 17: | ||
$('body').css('overflow','hidden'); | $('body').css('overflow','hidden'); | ||
} | } | ||
setTimeout(function(){ | setTimeout(function(){ | ||
$map.each(function(){ | $map.each(function(){ | ||
if(this._leaflet_map){this._leaflet_map.invalidateSize();} | if(this._leaflet_map){ | ||
this._leaflet_map.invalidateSize(); | |||
} | |||
}); | }); | ||
},100); | },100); | ||
}); | }); | ||
$(document).on('keydown',function(e){ | $(document).on('keydown',function(e){ | ||
if(e.key==='Escape' && $overlay.hasClass('lw-map-overlay--active')){$('.lw-infobox__map-expand').trigger('click');} | if(e.key==='Escape' && $overlay.hasClass('lw-map-overlay--active')){ | ||
$('.lw-infobox__map-expand').trigger('click'); | |||
} | |||
}); | }); | ||
$overlay.on('click',function(e){ | $overlay.on('click',function(e){ | ||
if(e.target===this){$('.lw-infobox__map-expand').trigger('click');} | if(e.target===this){ | ||
$('.lw-infobox__map-expand').trigger('click'); | |||
} | |||
}); | }); | ||
}); | }); | ||
/* Landrace.wiki – Leaflet map w/ SMW CSV export, Clustering, Labels & Hulls | /* Landrace.wiki – Leaflet map w/ SMW CSV export, Clustering, Labels & Hulls */ | ||
(function(){ | (function(){ | ||
var LVER='1.9.4'; | var LVER='1.9.4'; | ||
| Line 36: | Line 47: | ||
var TURF_CDN='https://unpkg.com/@turf/turf@6/turf.min.js'; | 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 BRAND={ | ||
var STATUS_COLORS={'Critical':'#c92a2a','High':'#d9480f','Medium':'#e67700','Low':'#2b8a3e'}; | 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 STATUS_PRIORITY=['Critical','High','Medium','Low']; | ||
/* Event maps (News Items / eradications) */ | /* Event maps (News Items / eradications) */ | ||
var EVENT_COLORS={'Enforcement':'#c92a2a','Trafficking':'#d9480f','Policy':'#1c7ed6','Research':BRAND.primary,'Fieldwork':BRAND.primaryLight,'Community':BRAND.accent,'Report':BRAND.neutral}; | 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']; | var EVENT_PRIORITY=['Enforcement','Trafficking','Policy','Research','Fieldwork','Community','Report']; | ||
| Line 63: | Line 98: | ||
s.src=src; | s.src=src; | ||
if(id) s.id=id; | if(id) s.id=id; | ||
s.onload=function(){s.dataset.loaded='1';cb();}; | s.onload=function(){ | ||
s.dataset.loaded='1'; | |||
cb(); | |||
}; | |||
s.onerror=function(){console.error('[lw-map] Script failed to load:',src);}; | s.onerror=function(){console.error('[lw-map] Script failed to load:',src);}; | ||
document.head.appendChild(s); | document.head.appendChild(s); | ||
| Line 74: | Line 112: | ||
var status=marker.options.status || 'Low'; | var status=marker.options.status || 'Low'; | ||
var index=STATUS_PRIORITY.indexOf(status); | var index=STATUS_PRIORITY.indexOf(status); | ||
if(index!==-1 && index<worstIndex){worst=status;worstIndex=index;} | if(index!==-1 && index<worstIndex){ | ||
worst=status; | |||
worstIndex=index; | |||
} | |||
}); | }); | ||
return worst; | return worst; | ||
| Line 85: | Line 126: | ||
var status=f.properties.status || 'Low'; | var status=f.properties.status || 'Low'; | ||
var index=STATUS_PRIORITY.indexOf(status); | var index=STATUS_PRIORITY.indexOf(status); | ||
if(index!==-1 && index<worstIndex){worst=status;worstIndex=index;} | if(index!==-1 && index<worstIndex){ | ||
worst=status; | |||
worstIndex=index; | |||
} | |||
}); | }); | ||
return worst; | return worst; | ||
| Line 96: | Line 140: | ||
var cat=marker.options.event_category || ''; | var cat=marker.options.event_category || ''; | ||
var idx=EVENT_PRIORITY.indexOf(cat); | var idx=EVENT_PRIORITY.indexOf(cat); | ||
if(idx!==-1 && idx<worstIndex){worst=cat;worstIndex=idx;} | if(idx!==-1 && idx<worstIndex){ | ||
worst=cat; | |||
worstIndex=idx; | |||
} | |||
}); | }); | ||
return worst; | return worst; | ||
| Line 106: | Line 153: | ||
style.id='lw-map-styles'; | style.id='lw-map-styles'; | ||
style.textContent=[ | 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-wrapper {', | ||
'.lw-popup .leaflet-popup-content{margin:0;min-width:220px;max-width:280px}', | ' background: '+BRAND.white+';', | ||
'.lw-popup .leaflet-popup-tip{background:'+BRAND.white+';box-shadow:0 2px 6px rgba(0,0,0,0.1)}', | ' border-radius: 12px;', | ||
'.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}', | ' box-shadow: 0 4px 20px rgba(0,0,0,0.12), 0 2px 6px rgba(0,0,0,0.08);', | ||
'.lw-popup .leaflet-popup-close-button:hover{color:'+BRAND.dark+' !important | ' padding: 0;', | ||
'.lw-popup-inner{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif}', | ' overflow: hidden;', | ||
'.lw-popup-header{background:linear-gradient(135deg,'+BRAND.primary+' 0%,'+BRAND.dark+' 100%);padding:16px 18px;color:'+BRAND.white+' | '}', | ||
' | '.lw-popup .leaflet-popup-content {', | ||
'.lw-popup-name{font-size:15px;font-weight:600;line-height:1.3}', | ' margin: 0;', | ||
'.lw-popup-body{padding:14px 18px}', | ' min-width: 220px;', | ||
'.lw-popup-meta{display:flex;flex-direction:column;align-items:flex-start;gap:6px;margin-bottom:8px}', | ' max-width: 280px;', | ||
'.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; | '.lw-popup .leaflet-popup-tip {', | ||
'.lw-popup-status::before{content:"";width:6px;height:6px;border-radius:50%;margin-right:6px;background:currentColor}', | ' background: '+BRAND.white+';', | ||
'.lw-popup-status--critical{color:#c92a2a;background:#fff5f5}', | ' box-shadow: 0 2px 6px rgba(0,0,0,0.1);', | ||
'.lw-popup-status--high{color:#d9480f;background:#fff4e6}', | '}', | ||
'.lw-popup-status--medium{color:#e67700;background:#fff9db}', | '.lw-popup .leaflet-popup-close-button {', | ||
'.lw-popup-status--low{color:#2b8a3e;background:#ebfbee}', | ' color: '+BRAND.neutral+' !important;', | ||
'.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}', | ' font-size: 20px !important;', | ||
'.lw-popup-link:hover{background:'+BRAND.primary+';color:'+BRAND.white+'}', | ' padding: 8px 10px !important;', | ||
'.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; | ' right: 4px !important;', | ||
'.lw-map-tooltip::before{border-top-color:'+BRAND.dark+' !important}', | ' top: 4px !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}', | ' transition: color 0.2s ease;', | ||
'.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{ | '.lw-popup .leaflet-popup-close-button:hover {', | ||
'.lw-layer-btn--active{background:'+BRAND.primary+' !important;color:'+BRAND.white+' !important}', | ' color: '+BRAND.dark+' !important;', | ||
'.lw-layer-btn:not(:last-child){ | '}', | ||
'.lw-layer-btn svg{pointer-events:none}', | '.lw-popup-inner {', | ||
'.lw-cluster-icon{background:transparent !important}', | ' font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;', | ||
'.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-popup-header {', | ||
'.lw-cluster--small{width:36px;height:36px;font-size:13px}', | ' background: linear-gradient(135deg, '+BRAND.primary+' 0%, '+BRAND.dark+' 100%);', | ||
'.lw-cluster--large{width:48px;height:48px;font-size:15px}', | ' padding: 16px 18px;', | ||
'.marker-cluster{background:transparent !important}', | ' color: '+BRAND.white+';', | ||
'.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-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;', | |||
'}', | |||
'/* Cluster styles */', | |||
'.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;', | |||
'}', | |||
'/* Hull polygons */', | |||
'.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;', | |||
'}' | |||
].join('\n'); | ].join('\n'); | ||
document.head.appendChild(style); | document.head.appendChild(style); | ||
| Line 153: | Line 356: | ||
function esc(s){ | function esc(s){ | ||
return String(s==null ? '' : s).replace(/[&<>"']/g,function(c){return({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]);}); | return String(s==null ? '' : s).replace(/[&<>"']/g,function(c){ | ||
return({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]); | |||
}); | |||
} | } | ||
| Line 170: | Line 375: | ||
var status=p.status || ''; | var status=p.status || ''; | ||
var url=p.page_url || '#'; | var url=p.page_url || '#'; | ||
var statusHtml=''; | var statusHtml=''; | ||
if(status){ | 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>'; | 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>'; | |||
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>'; | |||
} | } | ||
| Line 184: | Line 403: | ||
var url=p.page_url || '#'; | var url=p.page_url || '#'; | ||
var src=p.source_url || ''; | var src=p.source_url || ''; | ||
var blocks=''; | 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(date){ | ||
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>';} | 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(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>';} | } | ||
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>') : ''; | 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>'; | |||
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>'; | |||
} | } | ||
| Line 195: | Line 433: | ||
var statusHtml=''; | var statusHtml=''; | ||
if(worstStatus){ | 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>'; | 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>'; | |||
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){ | function parseCoords(coordStr){ | ||
if(!coordStr) return null; | if(!coordStr) return null; | ||
var decimalMatch=coordStr.match(/(-?\d+\.?\d*)\s*,\s*(-?\d+\.?\d*)/); | var decimalMatch=coordStr.match(/(-?\d+\.?\d*)\s*,\s*(-?\d+\.?\d*)/); | ||
if(decimalMatch){ | if(decimalMatch){ | ||
| Line 208: | Line 464: | ||
if(!isNaN(lat) && !isNaN(lon)) return {lat:lat,lon:lon}; | if(!isNaN(lat) && !isNaN(lon)) return {lat:lat,lon:lon}; | ||
} | } | ||
var dmsRegex=/(\d+)[°]\s*(\d+)[′']\s*(\d+\.?\d*)[″"]?\s*([NSEW])/gi; | var dmsRegex=/(\d+)[°]\s*(\d+)[′']\s*(\d+\.?\d*)[″"]?\s*([NSEW])/gi; | ||
var matches=[]; | var matches=[]; | ||
var m; | var m; | ||
while((m=dmsRegex.exec(coordStr))!==null){matches.push(m);if(matches.length>=2) break;} | while((m=dmsRegex.exec(coordStr))!==null){ | ||
matches.push(m); | |||
if(matches.length>=2) break; | |||
} | |||
if(matches.length>=2){ | if(matches.length>=2){ | ||
var lat2=dmsToDecimal(matches[0][1],matches[0][2],matches[0][3],matches[0][4]); | 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 lon2=dmsToDecimal(matches[1][1],matches[1][2],matches[1][3],matches[1][4]); | ||
var h0=matches[0][4].toUpperCase(); | var h0=matches[0][4].toUpperCase(); | ||
if(h0==='E' || h0==='W'){var tmp=lat2;lat2=lon2;lon2=tmp;} | if(h0==='E' || h0==='W'){ | ||
var tmp=lat2; lat2=lon2; lon2=tmp; | |||
} | |||
if(lat2!==null && lon2!==null) return {lat:lat2,lon:lon2}; | if(lat2!==null && lon2!==null) return {lat:lat2,lon:lon2}; | ||
} | } | ||
return null; | return null; | ||
} | } | ||
| Line 237: | Line 503: | ||
if(lines.length<2) return []; | if(lines.length<2) return []; | ||
var headers=parseCSVLine(lines[0]); | var headers=parseCSVLine(lines[0]); | ||
var results=[]; | var results=[]; | ||
for(var i=1;i<lines.length;i++){ | for(var i=1;i<lines.length;i++){ | ||
var values=parseCSVLine(lines[i]); | var values=parseCSVLine(lines[i]); | ||
var obj={}; | var obj={}; | ||
for(var j=0;j<headers.length;j++){obj[headers[j]]=values[j] || '';} | for(var j=0;j<headers.length;j++){ | ||
obj[headers[j]]=values[j] || ''; | |||
} | |||
results.push(obj); | results.push(obj); | ||
} | } | ||
| Line 251: | Line 520: | ||
var current=''; | var current=''; | ||
var inQuotes=false; | var inQuotes=false; | ||
for(var i=0;i<line.length;i++){ | for(var i=0;i<line.length;i++){ | ||
var c=line[i]; | var c=line[i]; | ||
var next=line[i+1]; | var next=line[i+1]; | ||
if(inQuotes){ | if(inQuotes){ | ||
if(c === '"' && next === '"'){current+='"';i++;} | if(c === '"' && next === '"'){ | ||
else if(c === '"'){inQuotes=false;} | current+='"'; | ||
else{current+=c;} | i++; | ||
}else if(c === '"'){ | |||
inQuotes=false; | |||
}else{ | |||
current+=c; | |||
} | |||
}else{ | |||
if(c === '"'){ | |||
inQuotes=true; | |||
}else if(c === ','){ | |||
result.push(current.trim()); | |||
current=''; | |||
}else{ | }else{ | ||
current+=c; | |||
} | |||
} | } | ||
} | } | ||
| Line 270: | Line 551: | ||
function csvToGeoJSON(data){ | function csvToGeoJSON(data){ | ||
var features=[]; | var features=[]; | ||
data.forEach(function(row){ | data.forEach(function(row){ | ||
var coordStr=row['Has coordinates'] || row['Has GPS coordinates'] || row['GPS coordinates'] || ''; | var coordStr=row['Has coordinates'] || row['Has GPS coordinates'] || row['GPS coordinates'] || ''; | ||
var coords=parseCoords(coordStr); | var coords=parseCoords(coordStr); | ||
if(!coords) return; | if(!coords) return; | ||
var pageName=row[''] || Object.values(row)[0] || ''; | var pageName=row[''] || Object.values(row)[0] || ''; | ||
var pageUrl='/wiki/'+encodeURIComponent(pageName.replace(/ /g,'_')); | var pageUrl='/wiki/' + encodeURIComponent(pageName.replace(/ /g,'_')); | ||
var isNews=!!(row['Has event headline'] || row['Has event date'] || row['Has event category']); | var isNews=!!(row['Has event headline'] || row['Has event date'] || row['Has event category']); | ||
if(isNews){ | if(isNews){ | ||
var headline=row['Has event headline'] || pageName; | var headline=row['Has event headline'] || pageName; | ||
| Line 285: | Line 570: | ||
var locality=row['Has locality'] || ''; | var locality=row['Has locality'] || ''; | ||
var sourceUrl=row['Has source'] || ''; | 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]}}); | |||
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; | return; | ||
} | } | ||
/* Default: Accessions */ | |||
var name=row['Has descriptive name'] || row['descriptive name'] || pageName; | var name=row['Has descriptive name'] || row['descriptive name'] || pageName; | ||
var status=row['Has conservation priority'] || row['conservation priority'] || ''; | var status=row['Has conservation priority'] || row['conservation priority'] || ''; | ||
| Line 293: | Line 594: | ||
var growingArea=row['Has growing area'] || row['growing area'] || ''; | var growingArea=row['Has growing area'] || row['growing area'] || ''; | ||
var growingRegion=row['Has growing region'] || row['growing region'] || ''; | 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]}}); | |||
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]} | |||
}); | |||
}); | }); | ||
console.log('[lw-map] Converted',features.length,'features from CSV'); | |||
return {type:'FeatureCollection',features:features}; | return {type:'FeatureCollection',features:features}; | ||
} | } | ||
| Line 301: | Line 616: | ||
var parts=query.split('|'); | var parts=query.split('|'); | ||
var encodedParts=parts.map(function(part){ | var encodedParts=parts.map(function(part){ | ||
return part.replace(/-/g,'-2D').replace(/\[/g,'-5B').replace(/\]/g,'-5D').replace(/\?/g,'-3F').replace(/:/g,'-3A').replace(/ /g,'-20'); | return part | ||
.replace(/-/g,'-2D') | |||
.replace(/\[/g,'-5B') | |||
.replace(/\]/g,'-5D') | |||
.replace(/\?/g,'-3F') | |||
.replace(/:/g,'-3A') | |||
.replace(/ /g,'-20'); | |||
}); | }); | ||
var baseUrl=mw.config.get('wgScriptPath') || ''; | var baseUrl=mw.config.get('wgScriptPath') || ''; | ||
return baseUrl+'/wiki/Special:Ask/'+encodedParts.join('/')+'/format=csv'; | return baseUrl+'/wiki/Special:Ask/'+encodedParts.join('/')+'/format=csv'; | ||
| Line 310: | Line 632: | ||
var url=buildCSVUrl(query); | var url=buildCSVUrl(query); | ||
console.log('[lw-map] Fetching CSV from:',url); | console.log('[lw-map] Fetching CSV from:',url); | ||
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);}) | fetch(url) | ||
.catch(function(err){console.error('[lw-map] CSV fetch error:',err);cb(err,null);}); | .then(function(r){ | ||
if(!r.ok) throw new Error('CSV fetch error: '+r.status); | |||
return r.text(); | |||
}) | |||
.then(function(text){ | |||
console.log('[lw-map] CSV response length:',text.length); | |||
var data=parseCSV(text); | |||
console.log('[lw-map] Parsed',data.length,'rows'); | |||
var geojson=csvToGeoJSON(data); | |||
cb(null,geojson); | |||
}) | |||
.catch(function(err){ | |||
console.error('[lw-map] CSV fetch error:',err); | |||
cb(err,null); | |||
}); | |||
} | } | ||
/* Generate hull for a set of points using Turf.js */ | |||
function generateHull(features){ | function generateHull(features){ | ||
if(!window.turf || features.length<3) return null; | if(!window.turf || features.length<3) return null; | ||
try{ | try{ | ||
var points=turf.featureCollection(features.map(function(f){return turf.point(f.geometry.coordinates);})); | var points=turf.featureCollection( | ||
features.map(function(f){ return turf.point(f.geometry.coordinates); }) | |||
); | |||
var hull; | var hull; | ||
if(features.length>=4){ | if(features.length>=4){ | ||
try{hull=turf.concave(points,{maxEdge:100,units:'kilometers'});}catch(e){} | try{ | ||
hull=turf.concave(points,{maxEdge:100,units:'kilometers'}); | |||
}catch(e){ | |||
console.log('[lw-map] Concave hull failed, falling back to convex'); | |||
} | |||
} | } | ||
if(!hull){hull=turf.convex(points);} | |||
if(hull){hull=turf.buffer(hull,3,{units:'kilometers'});} | if(!hull){ | ||
hull=turf.convex(points); | |||
} | |||
if(hull){ | |||
hull=turf.buffer(hull,3,{units:'kilometers'}); | |||
} | |||
return hull; | return hull; | ||
}catch(e){console.error('[lw-map] Hull generation error:',e);return null;} | }catch(e){ | ||
console.error('[lw-map] Hull generation error:',e); | |||
return null; | |||
} | |||
} | } | ||
| Line 343: | Line 698: | ||
el.dataset.init='1'; | el.dataset.init='1'; | ||
if(el.clientHeight<100) el.style.height='70vh'; | if(el.clientHeight<100) el.style.height='70vh'; | ||
injectStyles(); | injectStyles(); | ||
var map=L.map(el,{minZoom:2,maxZoom:17,zoomControl:false}).setView([26.4,89.5],8); | var map=L.map(el,{ | ||
minZoom:2, | |||
maxZoom:17, | |||
zoomControl:false | |||
}).setView([26.4,89.5],8); | |||
el._leaflet_map=map; | el._leaflet_map=map; | ||
L.control.zoom({position:'topright'}).addTo(map); | L.control.zoom({position:'topright'}).addTo(map); | ||
var baseLayers={ | 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}), | basic:L.tileLayer('https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png',{ | ||
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}), | attribution:'© <a href="https://www.openstreetmap.org/copyright">OSM</a> © <a href="https://carto.com/attributions">CARTO</a>', | ||
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}) | 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 | |||
}) | |||
}; | }; | ||
| Line 361: | Line 733: | ||
layerToggle.onAdd=function(){ | layerToggle.onAdd=function(){ | ||
var container=L.DomUtil.create('div','lw-layer-toggle'); | 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>'; | 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); | L.DomEvent.disableClickPropagation(container); | ||
container.querySelectorAll('.lw-layer-btn').forEach(function(btn){ | container.querySelectorAll('.lw-layer-btn').forEach(function(btn){ | ||
btn.addEventListener('click',function(){ | btn.addEventListener('click',function(){ | ||
var layer=this.dataset.layer; | var layer=this.dataset.layer; | ||
if(layer===currentLayer) return; | if(layer===currentLayer) return; | ||
map.removeLayer(baseLayers[currentLayer]); | map.removeLayer(baseLayers[currentLayer]); | ||
baseLayers[layer].addTo(map); | baseLayers[layer].addTo(map); | ||
currentLayer=layer; | currentLayer=layer; | ||
container.querySelectorAll('.lw-layer-btn').forEach(function(b){b.classList.remove('lw-layer-btn--active');}); | |||
container.querySelectorAll('.lw-layer-btn').forEach(function(b){ | |||
b.classList.remove('lw-layer-btn--active'); | |||
}); | |||
this.classList.add('lw-layer-btn--active'); | this.classList.add('lw-layer-btn--active'); | ||
}); | }); | ||
}); | }); | ||
return container; | return container; | ||
}; | }; | ||
| Line 379: | Line 767: | ||
var query=el.dataset.smwQuery; | var query=el.dataset.smwQuery; | ||
if(!query){console.log('[lw-map] No data-smw-query attribute found');return;} | if(!query){ | ||
console.log('[lw-map] No data-smw-query attribute found'); | |||
return; | |||
} | |||
var mapType=el.dataset.mapType || ''; | var mapType=el.dataset.mapType || ''; | ||
/* Check if this is an infobox map */ | |||
var isInfoboxMap=el.closest('.lw-infobox') !== null; | var isInfoboxMap=el.closest('.lw-infobox') !== null; | ||
/* Layer group for area hulls */ | |||
var areaHullsLayer=L.layerGroup(); | var areaHullsLayer=L.layerGroup(); | ||
/* Update visibility based on zoom */ | |||
function updateLayerVisibility(){ | function updateLayerVisibility(){ | ||
var zoom=map.getZoom(); | var zoom=map.getZoom(); | ||
if(zoom>=6 && zoom<=12){if(!map.hasLayer(areaHullsLayer)) map.addLayer(areaHullsLayer);} | if(zoom>=6 && zoom<=12){ | ||
else{if(map.hasLayer(areaHullsLayer)) map.removeLayer(areaHullsLayer);} | if(!map.hasLayer(areaHullsLayer)) map.addLayer(areaHullsLayer); | ||
}else{ | |||
if(map.hasLayer(areaHullsLayer)) map.removeLayer(areaHullsLayer); | |||
} | |||
} | } | ||
/* For main map, add growing area to query to enable grouping (accessions only) */ | |||
var fullQuery=query; | var fullQuery=query; | ||
if(!isInfoboxMap && query.indexOf('Category:Accessions') !== -1){ | if(!isInfoboxMap && query.indexOf('Category:Accessions') !== -1){ | ||
| Line 408: | Line 809: | ||
} | } | ||
/* | /* Generate area hulls for main map (accessions only) */ | ||
if(!isInfoboxMap && window.turf && mapType!=='eradication'){ | if(!isInfoboxMap && window.turf && mapType!=='eradication'){ | ||
var areaGroups=groupBy(geojson.features,'growing_area'); | var areaGroups=groupBy(geojson.features,'growing_area'); | ||
Object.keys(areaGroups).forEach(function(areaName){ | Object.keys(areaGroups).forEach(function(areaName){ | ||
if(areaName==='Unknown' || areaName==='') return; | if(areaName==='Unknown' || areaName==='') return; | ||
var areaFeatures=areaGroups[areaName]; | var areaFeatures=areaGroups[areaName]; | ||
if(areaFeatures.length<3) return; | if(areaFeatures.length<3) return; | ||
var hull=generateHull(areaFeatures); | var hull=generateHull(areaFeatures); | ||
if(!hull) return; | if(!hull) return; | ||
var worstStatus=getWorstStatusFromFeatures(areaFeatures); | var worstStatus=getWorstStatusFromFeatures(areaFeatures); | ||
var color=STATUS_COLORS[worstStatus] || BRAND.primary; | 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}); | var hullLayer=L.geoJSON(hull,{ | ||
hullLayer.bindTooltip(areaName+' ('+areaFeatures.length+')',{className:'lw-hull-tooltip',direction:'center',permanent:false}); | style:{ | ||
hullLayer.on('mouseover',function(e){e.target.setStyle({fillOpacity:0.25,weight:3});}); | fillColor:color, | ||
hullLayer.on('mouseout',function(e){e.target.setStyle({fillOpacity:0.12,weight:2});}); | 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); | areaHullsLayer.addLayer(hullLayer); | ||
}); | }); | ||
map.on('zoomend',updateLayerVisibility); | map.on('zoomend',updateLayerVisibility); | ||
updateLayerVisibility(); | updateLayerVisibility(); | ||
} | } | ||
/* | /* Add markers */ | ||
if(isInfoboxMap || !window.L.markerClusterGroup){ | if(isInfoboxMap || !window.L.markerClusterGroup){ | ||
var layer=L.geoJSON(geojson,{ | var layer=L.geoJSON(geojson,{ | ||
| Line 438: | Line 869: | ||
var cat=p.event_category || ''; | var cat=p.event_category || ''; | ||
var color=(mapType==='eradication') ? (EVENT_COLORS[cat] || BRAND.primary) : (STATUS_COLORS[status] || BRAND.primary); | var color=(mapType==='eradication') ? (EVENT_COLORS[cat] || BRAND.primary) : (STATUS_COLORS[status] || BRAND.primary); | ||
return L.circleMarker(ll,{ | |||
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){ | onEachFeature:function(f,ly){ | ||
var p=f.properties || {}; | var p=f.properties || {}; | ||
ly.bindPopup((mapType==='eradication') ? popupForNewsItem(p) : popupForAccession(p),{className:'lw-popup',closeButton:true,maxWidth:300,offset:[0,-5]}); | |||
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 || ''); | 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'});} | if(tip){ | ||
ly.bindTooltip(esc(tip),{ | |||
direction:'top', | |||
offset:[0,-10], | |||
opacity:1, | |||
className:'lw-map-tooltip' | |||
}); | |||
} | |||
ly.on('mouseover',function(){ | ly.on('mouseover',function(){ | ||
this.setStyle({fillColor:BRAND.primaryLight,radius:10}); | |||
}); | }); | ||
ly.on('mouseout',function(){ | ly.on('mouseout',function(){ | ||
| Line 457: | Line 907: | ||
var cat2=pp.event_category || ''; | var cat2=pp.event_category || ''; | ||
var color2=(mapType==='eradication') ? (EVENT_COLORS[cat2] || BRAND.primary) : (STATUS_COLORS[status2] || BRAND.primary); | var color2=(mapType==='eradication') ? (EVENT_COLORS[cat2] || BRAND.primary) : (STATUS_COLORS[status2] || BRAND.primary); | ||
this.setStyle({fillColor:color2,radius:8}); | |||
}); | }); | ||
} | } | ||
}).addTo(map); | }).addTo(map); | ||
var b=layer.getBounds(); | var b=layer.getBounds(); | ||
if(b && b.isValid()) map.fitBounds(b.pad(0.3),{maxZoom:11}); | if(b && b.isValid()) map.fitBounds(b.pad(0.3),{maxZoom:11}); | ||
| Line 474: | Line 924: | ||
var count=children.length; | var count=children.length; | ||
var color; | var color; | ||
if(mapType==='eradication'){var worstCat=getWorstCategory(children);color=EVENT_COLORS[worstCat] || BRAND.primary;} | if(mapType==='eradication'){ | ||
else{var worstStatus=getWorstStatus(children);color=STATUS_COLORS[worstStatus] || BRAND.primary;} | 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'; | var sizeClass='lw-cluster'; | ||
if(count<10) sizeClass+=' lw-cluster--small'; | if(count<10) sizeClass+=' lw-cluster--small'; | ||
else if(count>=50) sizeClass+=' lw-cluster--large'; | 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]}); | |||
return L.divIcon({ | |||
html:'<div class="'+sizeClass+'" style="background:'+color+'">'+count+'</div>', | |||
className:'lw-cluster-icon', | |||
iconSize:[40,40] | |||
}); | |||
} | } | ||
}); | }); | ||
geojson.features.forEach(function(f){ | geojson.features.forEach(function(f){ | ||
| Line 512: | Line 951: | ||
var color=(mapType==='eradication') ? (EVENT_COLORS[cat] || BRAND.primary) : (STATUS_COLORS[status] || BRAND.primary); | var color=(mapType==='eradication') ? (EVENT_COLORS[cat] || BRAND.primary) : (STATUS_COLORS[status] || BRAND.primary); | ||
var marker=L.circleMarker([coords[1],coords[0]], | var marker=L.circleMarker([coords[1],coords[0]],{ | ||
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 || ''); | 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'});} | if(tip){ | ||
marker.bindTooltip(esc(tip),{ | |||
direction:'top', | |||
offset:[0,-10], | |||
opacity:1, | |||
className:'lw-map-tooltip' | |||
}); | |||
} | |||
marker.on('mouseover',function(){ | marker.on('mouseover',function(){ | ||
this.setStyle({fillColor:BRAND.primaryLight,radius:10}); | |||
}); | }); | ||
marker.on('mouseout',function(){ | marker.on('mouseout',function(){ | ||
this.setStyle({fillColor:color,radius:8}); | |||
}); | }); | ||
markers.addLayer(marker); | |||
}); | }); | ||
map.addLayer(markers); | map.addLayer(markers); | ||
var b2=markers.getBounds(); | var b2=markers.getBounds(); | ||
if(b2 && b2.isValid()) map.fitBounds(b2.pad(0.3),{maxZoom:11}); | if(b2 && b2.isValid()) map.fitBounds(b2.pad(0.3),{maxZoom:11}); | ||
} | } | ||
setTimeout(function(){map.invalidateSize(true);},100); | setTimeout(function(){ map.invalidateSize(true); },100); | ||
}); | }); | ||
} | } | ||
| Line 579: | Line 1,011: | ||
addJS(CLUSTER_CDN+'leaflet.markercluster.js',function(){ | addJS(CLUSTER_CDN+'leaflet.markercluster.js',function(){ | ||
addJS(TURF_CDN,function(){ | addJS(TURF_CDN,function(){ | ||
console.log('[lw-map] All libraries loaded, initializing maps'); | |||
init(); | init(); | ||
if(window.mw && mw.hook){ | if(window.mw && mw.hook){ | ||
mw.hook('wikipage.content').add(function($c){init($c && $c[0] ? $c[0] : document);}); | mw.hook('wikipage.content').add(function($c){ | ||
init($c && $c[0] ? $c[0] : document); | |||
}); | |||
} | } | ||
},'turf-js'); | },'turf-js'); | ||
},'markercluster-js'); | },'markercluster-js'); | ||
},'leaflet-js'); | },'leaflet-js'); | ||
})(); | })(); | ||
/* Auto-preload templates for new talk pages */ | /** | ||
* Auto-preload templates for new talk pages | |||
* Detects the category of the subject page and loads the appropriate talk template | |||
*/ | |||
(function(){ | (function(){ | ||
'use strict'; | 'use strict'; | ||
/* Only run on edit action for new pages in Talk namespace */ | |||
if(mw.config.get('wgAction')!=='edit') return; | if(mw.config.get('wgAction')!=='edit') return; | ||
if(mw.config.get('wgArticleId')!==0) return; | if(mw.config.get('wgArticleId')!==0) return; /* Page already exists */ | ||
if(mw.config.get('wgNamespaceNumber')!==1) return; | if(mw.config.get('wgNamespaceNumber')!==1) return; /* Not Talk namespace */ | ||
var mainPageTitle=mw.config.get('wgTitle'); | var mainPageTitle=mw.config.get('wgTitle'); /* Subject page name */ | ||
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'}; | |||
/* Map categories to preload templates (checked in order) */ | |||
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' | |||
}; | |||
/* Default fallback template */ | |||
var defaultPreload='Template:Talk preload/Default'; | var defaultPreload='Template:Talk preload/Default'; | ||
mw.loader.using(['mediawiki.api'],function(){ | mw.loader.using(['mediawiki.api'],function(){ | ||
var api=new mw.Api(); | var api=new mw.Api(); | ||
$(function(){ | $(function(){ | ||
var $textbox=$('#wpTextbox1'); | var $textbox=$('#wpTextbox1'); | ||
if($textbox.val().trim()) return; | if($textbox.val().trim()) return; /* Already has content */ | ||
api.get({action:'query',titles:mainPageTitle,prop:'categories',cllimit:50,format:'json'}).done(function(data){ | /* Get categories of the subject page */ | ||
api.get({ | |||
action:'query', | |||
titles:mainPageTitle, | |||
prop:'categories', | |||
cllimit:50, | |||
format:'json' | |||
}).done(function(data){ | |||
var pages=data.query.pages; | var pages=data.query.pages; | ||
var categories=[]; | var categories=[]; | ||
for(var id in pages){ | |||
if(pages[id].categories){ | |||
categories=pages[id].categories.map(function(c){ return c.title; }); | |||
} | |||
} | |||
/* Find matching preload template */ | |||
var preloadTemplate=defaultPreload; | var preloadTemplate=defaultPreload; | ||
for(var cat in categoryToPreload){if(categories.indexOf(cat)!==-1){preloadTemplate=categoryToPreload[cat];break;}} | 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){ | /* Fetch and insert the preload template content */ | ||
api.get({ | |||
action:'query', | |||
titles:preloadTemplate, | |||
prop:'revisions', | |||
rvprop:'content', | |||
rvslots:'main', | |||
format:'json' | |||
}).done(function(data){ | |||
var pages=data.query.pages; | var pages=data.query.pages; | ||
for(var id in pages){ | for(var id in pages){ | ||
| Line 624: | Line 1,100: | ||
} | } | ||
} | } | ||
}).fail(function(){$textbox.val('{{TalkHeader}}\n\n== Discussion ==\n');}); | }).fail(function(){ | ||
$textbox.val('{{TalkHeader}}\n\n== Discussion ==\n'); | |||
}); | |||
}); | }); | ||
}); | }); | ||
Revision as of 14:57, 22 February 2026
// <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'];
/* Event maps (News Items / eradications) */
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;',
'}',
'/* Cluster styles */',
'.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;',
'}',
'/* Hull polygons */',
'.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;',
'}'
].join('\n');
document.head.appendChild(style);
}
function esc(s){
return String(s==null ? '' : s).replace(/[&<>"']/g,function(c){
return({'&':'&','<':'<','>':'>','"':'"',"'":'''}[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;
}
/* Default: Accessions */
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]}
});
});
console.log('[lw-map] Converted',features.length,'features from CSV');
return {type:'FeatureCollection',features:features};
}
function buildCSVUrl(query){
var parts=query.split('|');
var encodedParts=parts.map(function(part){
return part
.replace(/-/g,'-2D')
.replace(/\[/g,'-5B')
.replace(/\]/g,'-5D')
.replace(/\?/g,'-3F')
.replace(/:/g,'-3A')
.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);
console.log('[lw-map] Fetching CSV from:',url);
fetch(url)
.then(function(r){
if(!r.ok) throw new Error('CSV fetch error: '+r.status);
return r.text();
})
.then(function(text){
console.log('[lw-map] CSV response length:',text.length);
var data=parseCSV(text);
console.log('[lw-map] Parsed',data.length,'rows');
var geojson=csvToGeoJSON(data);
cb(null,geojson);
})
.catch(function(err){
console.error('[lw-map] CSV fetch error:',err);
cb(err,null);
});
}
/* Generate hull for a set of points using Turf.js */
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){
console.log('[lw-map] Concave hull failed, falling back to convex');
}
}
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){
console.log('[lw-map] No data-smw-query attribute found');
return;
}
var mapType=el.dataset.mapType || '';
/* Check if this is an infobox map */
var isInfoboxMap=el.closest('.lw-infobox') !== null;
/* Layer group for area hulls */
var areaHullsLayer=L.layerGroup();
/* Update visibility based on zoom */
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);
}
}
/* For main map, add growing area to query to enable grouping (accessions only) */
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';
}
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;
}
/* Generate area hulls for main map (accessions only) */
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();
}
/* Add markers */
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);
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(){
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);
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]
});
}
});
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]],{
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(){
this.setStyle({fillColor:BRAND.primaryLight,radius:10});
});
marker.on('mouseout',function(){
this.setStyle({fillColor:color,radius:8});
});
markers.addLayer(marker);
});
map.addLayer(markers);
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);
}
/* Load CSS */
addCSS(CDN+'leaflet.css','leaflet-css');
addCSS(CLUSTER_CDN+'MarkerCluster.css','markercluster-css');
addCSS(CLUSTER_CDN+'MarkerCluster.Default.css','markercluster-default-css');
/* Load JS: Leaflet → MarkerCluster → Turf → init */
addJS(CDN+'leaflet.js',function(){
addJS(CLUSTER_CDN+'leaflet.markercluster.js',function(){
addJS(TURF_CDN,function(){
console.log('[lw-map] All libraries loaded, initializing maps');
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
* Detects the category of the subject page and loads the appropriate talk template
*/
(function(){
'use strict';
/* Only run on edit action for new pages in Talk namespace */
if(mw.config.get('wgAction')!=='edit') return;
if(mw.config.get('wgArticleId')!==0) return; /* Page already exists */
if(mw.config.get('wgNamespaceNumber')!==1) return; /* Not Talk namespace */
var mainPageTitle=mw.config.get('wgTitle'); /* Subject page name */
/* Map categories to preload templates (checked in order) */
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'
};
/* Default fallback template */
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; /* Already has content */
/* Get categories of the subject page */
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; });
}
}
/* Find matching preload template */
var preloadTemplate=defaultPreload;
for(var cat in categoryToPreload){
if(categories.indexOf(cat)!==-1){
preloadTemplate=categoryToPreload[cat];
break;
}
}
/* Fetch and insert the preload template content */
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');
});
});
});
});
})();
// </nowiki>