User:DarwIn/ScriptDoRaf.js

// 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);
    })();
});
Category:Ambiguous location in EXIF Category:Location not applicable Category:Pages with coordinates