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.
/* Landrace.wiki – Leaflet map w/ SMW CSV export (Styled) */
(function () {
var LVER = '1.9.4';
var CDN = 'https://unpkg.com/leaflet@' + LVER + '/dist/';
// Brand colors
var BRAND = {
primary: '#2d6a4f', // Deep forest green
primaryLight: '#40916c', // Lighter green for hover
accent: '#74c69d', // Soft green accent
dark: '#1b4332', // Dark green
neutral: '#495057', // Text gray
light: '#f8f9fa', // Light background
white: '#ffffff'
};
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) {
if (window.L && typeof window.L.map === 'function') return cb();
var s = document.querySelector('script[data-lw-leaflet="1"]');
if (s) { s.addEventListener('load', cb); return; }
s = document.createElement('script');
s.src = src;
s.setAttribute('data-lw-leaflet', '1');
s.onload = cb;
s.onerror = function () { console.error('[lw-map] Leaflet failed to load:', src); };
document.head.appendChild(s);
}
// Inject custom popup styles
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;',
'}'
].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>';
}
// Parse coordinates from various formats
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'] || '';
features.push({
type: 'Feature',
properties: {
id: accessionId,
name: name,
status: status,
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, '-5B')
.replace(/\]/g, '-5D')
.replace(/\?/g, '-3F')
.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);
});
}
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 // We'll add custom position
}).setView([26.4, 89.5], 8);
// Add zoom control to top-right
L.control.zoom({ position: 'topright' }).addTo(map);
// Muted, elegant base map (CartoDB Positron)
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
}).addTo(map);
var query = el.dataset.smwQuery;
if (!query) {
console.log('[lw-map] No data-smw-query attribute found');
return;
}
fetchCSV(query, 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;
}
var layer = L.geoJSON(geojson, {
pointToLayer: function (f, ll) {
return L.circleMarker(ll, {
radius: 8,
color: BRAND.white,
weight: 2.5,
fillColor: BRAND.primary,
fillOpacity: 0.9
});
},
onEachFeature: function (f, ly) {
var p = f.properties || {};
// Styled popup
ly.bindPopup(popupForAccession(p), {
className: 'lw-popup',
closeButton: true,
maxWidth: 300,
offset: [0, -5]
});
// Styled tooltip
var tip = p.name || p.id || '';
if (tip) {
ly.bindTooltip(esc(tip), {
direction: 'top',
offset: [0, -10],
opacity: 1,
className: 'lw-map-tooltip'
});
}
// Hover effect
ly.on('mouseover', function() {
this.setStyle({
fillColor: BRAND.primaryLight,
radius: 10
});
});
ly.on('mouseout', function() {
this.setStyle({
fillColor: BRAND.primary,
radius: 8
});
});
}
}).addTo(map);
var b = layer.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);
}
addCSS(CDN + 'leaflet.css', 'leaflet-css');
addJS(CDN + 'leaflet.js', function () {
init();
if (window.mw && mw.hook) {
mw.hook('wikipage.content').add(function ($c) {
init($c && $c[0] ? $c[0] : document);
});
}
});
})();