MediaWiki:Common.js
MediaWiki interface page
More actions
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'];
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 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 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 lat = dmsToDecimal(matches[0][1], matches[0][2], matches[0][3], matches[0][4]);
var lon = 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 = lat; lat = lon; lon = tmp;
}
if (lat !== null && lon !== null) return { lat: lat, lon: lon };
}
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 GPS coordinates'] || row['GPS coordinates'] || '';
var coords = parseCoords(coordStr);
if (!coords) {
console.log('[lw-map] Skipping row, no valid coords:', coordStr);
return;
}
var pageName = row[''] || Object.values(row)[0] || '';
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: '/wiki/' + encodeURIComponent(pageName.replace(/ /g, '_'))
},
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 convex 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);
})
);
// Try concave hull first for tighter fit
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');
}
}
// Fall back to convex hull
if (!hull) {
hull = turf.convex(points);
}
// Add buffer to prevent thin/linear hulls looking weird
if (hull) {
hull = turf.buffer(hull, 3, { units: 'kilometers' });
}
return hull;
} catch (e) {
console.error('[lw-map] Hull generation error:', e);
return null;
}
}
// Group features by a property
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;
}
// 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();
// Area hulls: show at zoom 6-12
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
var fullQuery = query;
if (!isInfoboxMap && query.indexOf('Category:Accessions') !== -1) {
fullQuery = '[[Category:Accessions]]|?Has GPS 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 accessions 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
if (!isInfoboxMap && window.turf) {
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; // Need at least 3 points for hull
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'
}
});
// Add popup for hull
hullLayer.bindPopup(
popupForArea(areaName, areaFeatures.length, worstStatus, '/wiki/' + encodeURIComponent(areaName.replace(/ /g, '_'))),
{ className: 'lw-popup', maxWidth: 300 }
);
// Add tooltip
hullLayer.bindTooltip(areaName + ' (' + areaFeatures.length + ')', {
className: 'lw-hull-tooltip',
direction: 'center',
permanent: false
});
// Hover effect
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);
});
// Set up zoom listener
map.on('zoomend', updateLayerVisibility);
updateLayerVisibility();
}
// Add markers (with or without clustering)
if (isInfoboxMap || !window.L.markerClusterGroup) {
var layer = L.geoJSON(geojson, {
pointToLayer: function (f, ll) {
var status = f.properties.status || '';
var color = STATUS_COLORS[status] || BRAND.primary;
return L.circleMarker(ll, {
radius: 8,
color: BRAND.white,
weight: 2.5,
fillColor: color,
fillOpacity: 0.9,
status: status
});
},
onEachFeature: function (f, ly) {
var p = f.properties || {};
ly.bindPopup(popupForAccession(p), {
className: 'lw-popup',
closeButton: true,
maxWidth: 300,
offset: [0, -5]
});
var tip = 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 status = f.properties.status || '';
var color = STATUS_COLORS[status] || BRAND.primary;
this.setStyle({
fillColor: color,
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 worstStatus = getWorstStatus(children);
var 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 color = 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
});
marker.bindPopup(popupForAccession(p), {
className: 'lw-popup',
closeButton: true,
maxWidth: 300,
offset: [0, -5]
});
var tip = 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 b = markers.getBounds();
if (b && b.isValid()) map.fitBounds(b.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['*'];
// Strip noinclude tags
content = content.replace(/<noinclude>[\s\S]*?<\/noinclude>/g, '');
content = content.trim();
$textbox.val(content);
}
}
}).fail(function() {
// Fallback if template doesn't exist
$textbox.val('{{TalkHeader}}\n\n== Discussion ==\n');
});
});
});
});
})();
// <nowiki>