// Button to add location from EXIF, script by Rkieferbaum (let me know of any bugs or suggestions):
// Load dependencies (MediaWiki API and EXIF.js)
mw.loader.using(['mediawiki.api', 'jquery'], function() {
mw.loader.load('https://cdnjs.cloudflare.com/ajax/libs/exif-js/2.3.0/exif.min.js');
(function() {
var filePage = mw.config.get('wgPageName');
var api = new mw.Api();
function addButton() {
console.log('Page Name:', filePage);
if (!/\.(jpg|jpeg|tiff|tif|heif|webp)$/i.test(filePage)) {
console.log('Page is not an image with EXIF data extensions.');
return;
}
api.get({
action: 'query',
prop: 'revisions',
titles: filePage,
rvprop: 'content',
formatversion: '2'
}).done(function(data) {
var page = data.query.pages[0];
if (page.missing) {
console.error('Page not found');
return;
}
var content = page.revisions[0].content;
console.log('Page Content:', content);
var fileUrl = 'https://commons.wikimedia.org/wiki/Special:FilePath/' + filePage.split(':')[1];
getExifData(fileUrl, function(data) {
if (!data) {
console.log('No EXIF data retrieved.');
return;
}
// If EXIF contains a date, add the "Update date parameter" button
if (data.date) {
var dateButtonContainer = $('<div>').css({ margin: '10px' });
var updateDateButton = $('<button>')
.text('Update date parameter from EXIF')
.css({
padding: '10px',
backgroundColor: '#17a2b8',
color: '#fff',
border: 'none',
borderRadius: '4px',
cursor: 'pointer'
})
.click(function() { updateDateInTemplateAndPost(content, data.date); });
dateButtonContainer.append(updateDateButton);
// Insert the date button just above the license header section
var licenseHeader = $('#content').find(":contains('=={{int:license-header}}==')").first();
if (licenseHeader.length) {
licenseHeader.before(dateButtonContainer);
} else {
$('#content').append(dateButtonContainer);
}
}
// Proceed with location buttons only if valid coordinates are present.
if (data.lat === null || data.lon === null ||
isNaN(data.lat) || isNaN(data.lon) ||
(Number(data.lat) === 0 || Number(data.lon) === 0) ||
(Number(data.lat) < -90 || Number(data.lat) > 90 || Number(data.lon) < -180 || Number(data.lon) > 180)) {
console.log('Invalid or missing EXIF coordinates. Location buttons will not be shown.');
return;
}
// Do not show location buttons if the file already has the "{{location withheld}}" template
if (/{{\s*location withheld\s*}}/i.test(content)) {
console.log('Location withheld template found. Location buttons will not be shown.');
return;
}
// Do not show location buttons if the category "Location not applicable" is already present
if (/\[\[Category:Location not applicable\]\]/i.test(content)) {
console.log('Location not applicable category found. Location buttons will not be shown.');
return;
}
// Do not show location buttons if the category "Ambiguous location in EXIF" is already present
if (/\[\[Category:Ambiguous location in EXIF\]\]/i.test(content)) {
console.log('Ambiguous location in EXIF category found. Location buttons will not be shown.');
return;
}
var hasLocationTag = /{{(?:location|camera location|location dec)\|/i.test(content);
if (hasLocationTag) {
console.log('Location tag already present. Location buttons will not be shown.');
return;
}
// Create container for location-related buttons
var container = $('<div>').css({ margin: '10px' });
var addLocationButton = $('<button>')
.text('Add location tag from EXIF data')
.css({
padding: '10px',
backgroundColor: '#007bff',
color: '#fff',
border: 'none',
borderRadius: '4px',
cursor: 'pointer',
marginRight: '10px'
})
.click(function() { insertLocationTemplate(data, content); });
var locationNotApplicableButton = $('<button>')
.text('Location not applicable')
.css({
padding: '10px',
backgroundColor: '#6c757d',
color: '#fff',
border: 'none',
borderRadius: '4px',
cursor: 'pointer',
marginRight: '10px'
})
.click(function() { insertLocationNotApplicable(content); });
var ambiguousCategoryButton = $('<button>')
.text('Add Ambiguous location category')
.css({
padding: '10px',
backgroundColor: '#28a745',
color: '#fff',
border: 'none',
borderRadius: '4px',
cursor: 'pointer'
})
.click(function() { insertAmbiguousCategory(content); });
container.append(addLocationButton, locationNotApplicableButton, ambiguousCategoryButton);
$('#content').prepend(container);
});
}).fail(function(err) {
console.error('API request failed', err);
});
}
function getExifData(url, callback) {
var xhr = new XMLHttpRequest();
xhr.open('GET', url, true);
xhr.responseType = 'blob';
xhr.onload = function() {
if (this.status === 200) {
EXIF.getData(this.response, function() {
var latArray = EXIF.getTag(this, "GPSLatitude");
var lonArray = EXIF.getTag(this, "GPSLongitude");
var latRef = EXIF.getTag(this, "GPSLatitudeRef") || "N";
var lonRef = EXIF.getTag(this, "GPSLongitudeRef") || "W";
var latNum = null, lonNum = null;
if (latArray && lonArray && latArray.length === 3 && lonArray.length === 3) {
latNum = (latArray[0] + latArray[1] / 60 + latArray[2] / 3600) *
(latRef.toUpperCase() === "N" ? 1 : -1);
lonNum = (lonArray[0] + lonArray[1] / 60 + lonArray[2] / 3600) *
(lonRef.toUpperCase() === "E" ? 1 : -1);
// Check if coordinates are in a plausible range before formatting
if (latNum < -90 || latNum > 90 || lonNum < -180 || lonNum > 180) {
console.log('EXIF coordinates out of valid range:', latNum, lonNum);
latNum = null;
lonNum = null;
} else {
latNum = latNum.toFixed(7);
lonNum = lonNum.toFixed(7);
}
}
// Additional EXIF data
var altitude = EXIF.getTag(this, "GPSAltitude");
var altRef = EXIF.getTag(this, "GPSAltitudeRef");
var heading = EXIF.getTag(this, "GPSImgDirection") || EXIF.getTag(this, "GPSTrack");
var altVal = null, headingVal = null;
if (altitude) {
altVal = parseFloat(altitude);
if (altRef === 1 || altRef === "1") {
altVal = -altVal;
}
altVal = altVal.toFixed(2);
}
if (heading) {
headingVal = parseFloat(heading).toFixed(2);
}
// Extract date information from EXIF (using DateTimeOriginal or fallback to DateTime)
var exifDate = EXIF.getTag(this, "DateTimeOriginal") || EXIF.getTag(this, "DateTime");
if (exifDate) {
// Convert format from "YYYY:MM:DD hh:mm:ss" to "YYYY-MM-DD hh:mm:ss"
exifDate = exifDate.replace(/^(\d{4}):(\d{2}):(\d{2})/, '$1-$2-$3');
}
callback({ lat: latNum, lon: lonNum, alt: altVal, heading: headingVal, date: exifDate });
});
} else {
callback(null);
}
};
xhr.send();
}
// Helper function to insert a template ensuring it starts on a new line without extra blank lines.
function insertAtPosition(content, index, template) {
var prefix = "";
var suffix = "";
if (index > 0 && content[index - 1] !== "\n") {
prefix = "\n";
}
if (index < content.length && content[index] !== "\n") {
suffix = "\n";
}
return content.slice(0, index) + prefix + template + suffix + content.slice(index);
}
function insertLocationTemplate(data, currentContent) {
// Validate coordinates including range check
if (!data.lat || !data.lon || isNaN(data.lat) || isNaN(data.lon) ||
(Number(data.lat) === 0 || Number(data.lon) === 0) ||
(Number(data.lat) < -90 || Number(data.lat) > 90 || Number(data.lon) < -180 || Number(data.lon) > 180)) {
console.log('Invalid coordinates, not inserting location tag.');
return;
}
var newContent = currentContent;
newContent = newContent.replace(/{{location\|.*?}}/gi, '');
newContent = newContent.replace(/{{camera location\|.*?}}/gi, '');
newContent = newContent.replace(/{{location dec\|.*?}}/gi, '');
newContent = newContent.replace(/{{GPS EXIF}}/gi, '');
newContent = newContent.replace(/{{GPS-EXIF}}/gi, '');
// Build extra parameters using underscores as separators
var extras = [];
if (data.alt) extras.push('alt:' + data.alt);
if (data.heading) extras.push('heading:' + data.heading);
extras.push('source:exif');
var extraParam = extras.join('_');
var template = '{{location|' + data.lat + '|' + data.lon + '|' + extraParam + '}}';
// Look for the main templates first (Information or Photograph)
var mainIndex = newContent.indexOf('{{Information');
if (mainIndex === -1) {
mainIndex = newContent.indexOf('{{Photograph');
}
if (mainIndex !== -1) {
var mainEndIndex = findClosingBrackets(newContent, mainIndex);
if (mainEndIndex !== -1) {
newContent = insertAtPosition(newContent, mainEndIndex, template);
} else {
newContent = newContent.trim() + "\n" + template;
}
} else {
// If no main template found, check for license header
var licenseIndex = newContent.indexOf('=={{int:license-header}}');
if (licenseIndex !== -1) {
newContent = insertAtPosition(newContent, licenseIndex, template);
} else {
newContent = newContent.trim() + "\n" + template;
}
}
var summary = 'Adding {{location}} template based on EXIF data';
api.postWithToken('csrf', {
action: 'edit',
title: filePage,
text: newContent,
summary: summary,
minor: true
}).done(function() {
console.log('Page updated successfully');
location.reload();
}).fail(function(err) {
console.error('Edit failed', err);
});
}
function insertLocationNotApplicable(currentContent) {
// Trim the content and append a single newline with the category line
var newContent = currentContent.trim() + "\n[[Category:Location not applicable]]";
var summary = 'Marking location as not applicable';
api.postWithToken('csrf', {
action: 'edit',
title: filePage,
text: newContent,
summary: summary,
minor: true
}).done(function() {
console.log('Page updated successfully');
location.reload();
}).fail(function(err) {
console.error('Edit failed', err);
});
}
function insertAmbiguousCategory(currentContent) {
// Trim the content and append a single newline with the ambiguous category
var newContent = currentContent.trim() + "\n[[Category:Ambiguous location in EXIF]]";
var summary = 'Marking location as ambiguous in EXIF';
api.postWithToken('csrf', {
action: 'edit',
title: filePage,
text: newContent,
summary: summary,
minor: true
}).done(function() {
console.log('Page updated successfully');
location.reload();
}).fail(function(err) {
console.error('Edit failed', err);
});
}
// Function to update the date parameter within the Information or Photograph template.
function updateDateInTemplate(currentContent, newDate) {
var newContent = currentContent;
var mainIndex = newContent.indexOf('{{Information');
if (mainIndex === -1) {
mainIndex = newContent.indexOf('{{Photograph');
}
if (mainIndex === -1) {
console.log('No Information or Photograph template found.');
return null;
}
var mainEndIndex = findClosingBrackets(newContent, mainIndex);
if (mainEndIndex === -1) {
console.log('Template not closed properly.');
return null;
}
var templateBlock = newContent.substring(mainIndex, mainEndIndex);
// Replace the date parameter (line starting with "|date")
var newTemplateBlock = templateBlock.replace(/(\|date\s*=)[^\n]*/i, '|date=' + newDate);
newContent = newContent.slice(0, mainIndex) + newTemplateBlock + newContent.slice(mainEndIndex);
return newContent;
}
// Function to update the date parameter and post the change.
function updateDateInTemplateAndPost(currentContent, newDate) {
var newContent = updateDateInTemplate(currentContent, newDate);
if (!newContent) {
console.log('Date update failed. Template not found or error in parsing.');
return;
}
var summary = 'Updating date parameter from EXIF data';
api.postWithToken('csrf', {
action: 'edit',
title: filePage,
text: newContent,
summary: summary,
minor: true
}).done(function() {
console.log('Page updated successfully with new date.');
location.reload();
}).fail(function(err) {
console.error('Edit failed', err);
});
}
function findClosingBrackets(content, startIndex) {
if (startIndex === -1) return -1;
var openBrackets = 0;
for (var i = startIndex; i < content.length; i++) {
if (content[i] === '{' && content[i + 1] === '{') {
openBrackets++;
i++;
} else if (content[i] === '}' && content[i + 1] === '}') {
openBrackets--;
i++;
if (openBrackets === 0) {
return i + 1;
}
}
}
return -1;
}
$(document).ready(addButton);
})();
});