Jump to content

User:Dudemanfellabra/Sandbox.js

From Wikipedia, the free encyclopedia
This is an old revision of this page, as edited by Dudemanfellabra (talk | contribs) at 02:54, 25 March 2016 (to distinguish between live code). The present address (URL) is a permanent link to this revision, which may differ significantly from the current revision.
Note: After saving, you have to bypass your browser's cache to see the changes. Google Chrome, Firefox, Microsoft Edge and Safari: Hold down the ⇧ Shift key and click the Reload toolbar button. For details and instructions about other browsers, see Wikipedia:Bypass your cache.
var wikitext = 'error'
var NRISOnlyStructure=[]; // NRISOnlyStructure[table][row]["Titles"][item].StatName to get info
var TotalToQuery=0;
var TotalQueried=0;
var ErrorCount=0;
var WarningCount=[["",0]]; // 0=status, 1=count
var InitialTime=0;
var ProgressDivTimer=0 // timer for updating ProgressDiv
var DefaultQueryPause=1 // number of milliseconds to wait between each API query; increased by code if rate limit reached

function CheckPermission() {
    if (mw.user.getName()!="NationalRegisterBot"&&mw.user.getName()!="Dudemanfellabra") {
        alert("Only NationalRegisterBot has been authorized to use the NRIS-only script!")
        return
    } else {
        NRISOnlyButtons()
    }
}

function NRISOnlyButtons() {
    var pageName=mw.config.get('wgPageName')
    var button=document.createElement("input")
    button.setAttribute("type", "button");
    button.setAttribute("id", "nrisonlybutton");
    var content=document.getElementById('mw-content-text')

    if (location.href.indexOf('action')==-1) {
        if (pageName=="Wikipedia:WikiProject_National_Register_of_Historic_Places/Progress") {
            button.setAttribute("value", "Update list for NationalRegisterBot (test)");
            button.setAttribute("onclick", "NRISOnlyClick('update')");
        } else if (pageName=="User:NationalRegisterBot/AllNRHPPages/Duplications") {
            button.setAttribute("value", "Gather duplicate stats");
            button.setAttribute("onclick", "NRISOnlyClick('duplicate')");
        } else if (pageName=="User:NationalRegisterBot/NRISOnly") {
            button.setAttribute("value", "Tag these articles");
            button.setAttribute("onclick", "NRISOnlyClick('tag')");
        } else {
            return;
        }
        content.parentNode.insertBefore(button, content)
    }
}

function NRISOnlyClick(action) { // after button is clicked, disable it and perform action
    var nrisonlybutton = document.getElementById('nrisonlybutton')
    nrisonlybutton.disabled = true
    var ProgressDiv = document.createElement("div")
    ProgressDiv.setAttribute("id", "ProgressDiv")
    ProgressDiv.setAttribute("style", "width:600px; border:5px solid black; padding:5px; background:#ffffff")
    nrisonlybutton.parentNode.insertBefore(ProgressDiv, nrisonlybutton)

    if (action=="update") {
        ProgressDiv.innerHTML = "Initializing..."
        getNRISOnlyWikitext(mw.config.get('wgPageName')) // after wikitext fetched, SetupNRISOnlyTables() is called
    } else if (action=="duplicate") {
        CheckDuplicates();
    } else if (action=="tag") {
        TagNRISOnly();
    }
}

// create array of table structure to be populated later
function SetupNRISOnlyTables() {
    var table=document.getElementsByClassName('wikitable sortable');

    // extract list names from Progress page
    for (var i=1; i<table.length; i++) { // skip national table
        var tr=table[i].getElementsByTagName("tr")
        NRISOnlyStructure[i-1]=[];
        var skipOffset=1 // number of duplicate/skippable rows encountered so far
        for (var j=1; j<tr.length-2; j++) { // skip title row, statewide duplicates, and totals row
            var td=tr[j].getElementsByTagName("td") // fill in existing data in case error
            var link=td[1].getElementsByTagName("a")
            if (link.length!=0 && link[0].href.search("#")==-1) {
                NRISOnlyStructure[i-1][j-skipOffset]={};
                NRISOnlyStructure[i-1][j-skipOffset].Titles=[]; // Placeholder for article names

                link=decodeURI(link[0].href).split("/")
                link=link[link.length-1].replace(/_/g," ")
                NRISOnlyStructure[i-1][j-skipOffset].Link=link

                NRISOnlyStructure[i-1][j-skipOffset].ID=td[0].innerHTML.substr(0,5) // for finding empty counties

                NRISOnlyStructure[i-1][j-1].CatsQueried=0 // for querying later
                NRISOnlyStructure[i-1][j-1].TotalToCheck=0
                NRISOnlyStructure[i-1][j-1].TotalChecked=0
            } else { // must be a duplicate row
                skipOffset++
            }
        }
    }
    for (var i=0; i<NRISOnlyStructure.length; i++) { // count total number of lists to check
        TotalToQuery+=NRISOnlyStructure[i].length
    }

    var ProgressDiv=document.getElementById("ProgressDiv")
    ProgressDiv.innerHTML+=" Done!<br>"

    var ProgressSpan=document.createElement("span")
    ProgressSpan.setAttribute("id", "ProgressSpan")
    ProgressDiv.appendChild(ProgressSpan)
    ProgressSpan.innerHTML = "Querying articles in each list... 0 (0%) of "+TotalToQuery+" lists complete."

    var TimeSpan=document.createElement("span")
    TimeSpan.setAttribute("id", "TimeSpan")
    ProgressDiv.appendChild(TimeSpan)
    TimeSpan.innerHTML = ""

    InitialTime=new Date() // record starting time
    UpdateNRISOnlyProgressDiv();
    LoadNRISOnlyList(0,0); // begin querying first page
}

// load next list to query
function LoadNRISOnlyList(currentTable,currentRow) {
    // check if we need to go to the next table
    if (currentRow>NRISOnlyStructure[currentTable].length-1) {
        currentRow=0
        currentTable++
    }
    // check if there are no more tables
    if (currentTable>NRISOnlyStructure.length-1) return;

    setTimeout(function(){ // short delay to prevent API overload
        getNRISOnlyListWikitext(currentTable,currentRow);
        LoadNRISOnlyList(currentTable,currentRow+1);
    }, DefaultQueryPause);
    return;
}

function NRISOnlyWikitextFetched(ajaxResponse,status,currentTable,currentRow) {
    if (status!="success") {
        NewNRISOnlyWarning("Wikitext "+ajaxResponse.errorThrown)
        setTimeout(function(){ // try again after delay if rate limit reached
            getNRISOnlyListWikitext(currentTable,currentRow);
        }, 250);
        return;
    }
    // won't get here unless successful
    var responseText=JSON.parse(ajaxResponse.responseText)
    var pagetext=responseText.query.pages[responseText.query.pageids[0]].revisions[0]["*"]
    if (responseText.query.redirects) { // if redirect, find section
        var SectionName="Undefined"
        for (var r in responseText.query.redirects) {
            if (typeof responseText.query.redirects[r].tofragment!="undefined") SectionName=responseText.query.redirects[r].tofragment.replace(/.27/g,"'")
        }

        var regex = new RegExp("=[ ]*(\\[\\[(.*?\\|)?[ ]*)?"+SectionName+"([ ]*\\]\\])?[ ]*=", "g")
        var sectionheader=pagetext.match(regex)
        if (sectionheader == null) { // if no section found, check if one of known empty counties
            var EmptyCounties=["02270", "12067", "42023", "48017", "48023", "48033", "48069", "48075", "48079", "48083", "48103", "48107", "48119", "48131", "48155", "48165", "48195", "48207", "48219", "48247", "48269", "48279", "48341", "48369", "48389", "48415", "48421", "48431", "48433", "48437", "48445", "48461", "48475", "48501", "51735"]

            var ID = NRISOnlyStructure[currentTable][currentRow].ID
            var errorcode = 0
            for (var k=0; k<EmptyCounties.length; k++) {
                if (ID==EmptyCounties[k]) {errorcode=-1}
            }
            if (errorcode!=0) { // must be an empty county
                TotalQueried++
                if (TotalQueried==TotalToQuery) CalculateNRISOnlyTotals()
                return;
            }
            // if we're here, must have been a redirect with no section, and not a known empty county
            sectionheader=pagetext.match(/{{NRHP header/g) // then look for tables without a section
            if (sectionheader==null||sectionheader.length>1) { // if still can't find a table or find multiple tables, fatal error
                NRISOnlyFatalError(0,currentTable,currentRow)
                return;
            }
        }
        var StartIndex=pagetext.indexOf(sectionheader[0])
        var sectiontext=pagetext.substr(StartIndex,pagetext.indexOf("\n==",StartIndex)-StartIndex) // only look at relevant section

        StartIndex=sectiontext.indexOf("{{NRHP header")
        if (StartIndex==-1) {
            if (sectiontext.indexOf("{{NRHP row")!=-1) {
                NRISOnlyFatalError(2,currentTable,currentRow) // incorrectly formatted table
                return;
            } else { // must be an empty county
                TotalQueried++
                if (TotalQueried==TotalToQuery) CalculateNRISOnlyTotals()
                return;
            }
        }
        var tabletext=sectiontext.substr(StartIndex,sectiontext.indexOf("\n|}",StartIndex)-StartIndex)
    } else { // if not a redirect, default to first table on page
        var StartIndex=pagetext.indexOf("{{NRHP header")
        if (StartIndex==-1) {
            NRISOnlyFatalError(1,currentTable,currentRow) // no list found
            return;
        }
        var tabletext=pagetext.substr(StartIndex,pagetext.indexOf("\n|}",StartIndex)-StartIndex)
    }

    // now that tabletext has only relevant table, extract rows
    var Rows=[]
    var str = "{{"
    var start=0
    var commentstart=0
    while (true) {
        commentstart=tabletext.indexOf("<!--",start)
        start=tabletext.indexOf(str,start)
        if (start==-1) break
        while (commentstart<start&&commentstart!=-1) { // skip any commented out rows
            start=tabletext.indexOf("-->",commentstart)
            commentstart=tabletext.indexOf("<!--",start)
            start=tabletext.indexOf(str,start)
        }
        if (start==-1) break
        var open=1
        var index=start+str.length
        while (open!=0 && index<tabletext.length) { // make sure to find correct matching close brackets for row template
            if (tabletext.substr(index,2)=="}}") {
                open--
                index++
            } else if (tabletext.substr(index,2)=="{{") {
                open++
                index++
            }
            index++
        }
        var template=tabletext.substr(start,index-start)
        var regex = new RegExp("{{[\\s]*NRHP row(\\s)*\\|", "g")
        if (template.match(regex)!=null) Rows.push(template) // make sure it's the row template and not some other one
        start++
    }
    for (var i=0; i<Rows.length; i++) { // get rid of false positives inside nowiki or pre tags
        var regex=new RegExp("<[ ]*?(nowiki|pre)[ ]*?>((?!<[ ]*?/[ ]*?(nowiki|pre)[ ]*?>)(.|\\n))*?"+Rows[i].replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&")+"(.|\\n)*?<[ ]*?/[ ]*?(nowiki|pre)[ ]*?>", "g")
        if (tabletext.match(regex)!=null) {Rows.splice(i,1); i--}
    }

    for (var i=0; i<Rows.length; i++) { // extract titles for querying
        var ThisRow=Rows[i]

        var article=ThisRow.match(/\|[ ]*?article[ ]*?=[ ]*?.*?[\n|\|]/g)
        var blank=ThisRow.match(/\|[ ]*?article[ ]*?=[ ]*?[\n|\|]/g)                               // default to name param if article
        if (article==null||blank!=null) article=ThisRow.match(/\|[ ]*?name[ ]*?=[ ]*?.*?[\n|\|]/g) // blank or missing
        // strip param name, final line break
        article=article[0].replace(/\|[ ]*?(article|name)[ ]*?=[ ]*?/g,"").replace(/[\n|\|]/g,"").replace(/\<\!\-\-(.|[\r\n])*?\-\-\>/g,"").trim()
        article=decodeURIComponent(article.split("#")[0].trim())     // corrections for weird titles

        var refnum=ThisRow.match(/[0-9]{8}/g) // also extract refnums
        if (refnum!=null) {
            if (refnum[0]=="98000562"||refnum[0]=="98000563") refnum[0]="98000562,98000563"   // hard-code troublesome
            if (refnum[0]=="01000363"||refnum[0]=="01000364") refnum[0]="01000363,01000364"   // multistate duplicates
            if (refnum[0]=="02000529"||refnum[0]=="02000530") refnum[0]="02000529,02000530"   // ...
            refnum=refnum[0]
        } else {
            refnum="missing"
        }

        var temp=NRISOnlyStructure[currentTable][currentRow].Titles
        temp[temp.length]={"name":article, "refnum":refnum, "exists":true, "isDab":false, "isTaggedNRISOnly":false}
        NRISOnlyStructure[currentTable][currentRow].Titles=temp
    }

    var StartIndex=0
    LoadNextNRISOnlyQuery(StartIndex,currentTable,currentRow)
    return;
}

// ready next batch of articles to query
function LoadNextNRISOnlyQuery(StartIndex,currentTable,currentRow) {
    if (StartIndex==NRISOnlyStructure[currentTable][currentRow].Titles.length) { // all queries begun for this list
        return;
    }
    // must have some more rows to query
    if (NRISOnlyStructure[currentTable][currentRow].Titles.length-StartIndex>50) {
        var TempTitles=NRISOnlyStructure[currentTable][currentRow].Titles.slice(StartIndex,StartIndex+50)
    } else {
        var TempTitles=NRISOnlyStructure[currentTable][currentRow].Titles.slice(StartIndex)
    }

    for (var i=0; i<TempTitles.length; i++) { // only extract article names
        TempTitles[i]=TempTitles[i].name
    }

    StartIndex+=TempTitles.length
    setTimeout(function(){ // short delay to prevent API overload
        QueryNRISOnlyCats(TempTitles,StartIndex,currentTable,currentRow)
        LoadNextNRISOnlyQuery(StartIndex,currentTable,currentRow)
    }, DefaultQueryPause);
    return;
}

// query next batch of articles
function QueryNRISOnlyCats(TempTitles,StartIndex,currentTable,currentRow) {
    var TitleList=TempTitles.join("|")
    $.ajax({
        dataType: "json",
        url: mw.util.wikiScript('api'),
        data: {
            format: 'json',
            action: 'query',
            prop: 'categories',
            clcategories: 'Category:All disambiguation pages|Category:All articles sourced only to NRIS',
            cllimit: 'max',
            titles: TitleList,
            redirects: 'true'
        },
        error: function(ArticlejsonObject,status,errorThrown) {ArticlejsonObject.errorThrown=errorThrown},
        complete: function(ArticlejsonObject,status) {
                NRISOnlyCatsChecked(ArticlejsonObject,status,TempTitles,StartIndex,currentTable,currentRow)
            }
    });
    return;
}

// parse API response for article query
function NRISOnlyCatsChecked(ArticlejsonObject,status,TempTitles,StartIndex,currentTable,currentRow) {
    if (status!="success") {
        NewNRISOnlyWarning("Articles "+ArticlejsonObject.errorThrown)

        setTimeout(function(){ // try again after delay if rate limit reached
            QueryNRISOnlyCats(TempTitles,StartIndex,currentTable,currentRow)
        }, 250);
        return;
    }
    // won't get here unless successful
    NRISOnlyStructure[currentTable][currentRow].CatsQueried+=TempTitles.length

    var responseText=JSON.parse(ArticlejsonObject.responseText)
    if (responseText.query.normalized) { // normalize any weird titles
        for (var n in responseText.query.normalized) {
            for (var i=0; i<TempTitles.length; i++) {
                if (TempTitles[i]==responseText.query.normalized[n].from) TempTitles[i]=responseText.query.normalized[n].to
            }
            for (var i=0; i<NRISOnlyStructure[currentTable][currentRow].Titles.length; i++) { // also update in main data array
                if (NRISOnlyStructure[currentTable][currentRow].Titles[i].name==responseText.query.normalized[n].from) {
                    NRISOnlyStructure[currentTable][currentRow].Titles[i].name=responseText.query.normalized[n].to
                }
            }
        }
    }
    if (responseText.query.redirects) { // resolve any redirects also
        for (var r in responseText.query.redirects) {
            for (var i=0; i<TempTitles.length; i++) {
                if (TempTitles[i]==responseText.query.redirects[r].from) TempTitles[i]=responseText.query.redirects[r].to
            }
            for (var i=0; i<NRISOnlyStructure[currentTable][currentRow].Titles.length; i++) { // also update in main data array
                if (NRISOnlyStructure[currentTable][currentRow].Titles[i].name==responseText.query.redirects[r].from) {
                    NRISOnlyStructure[currentTable][currentRow].Titles[i].name=responseText.query.redirects[r].to
                }
            }
        }
    }

    // now determine if dab/NRIS-only
    for (var page in responseText.query.pages) {
        var pagetitle=responseText.query.pages[page].title
        // mark as redlink
        if (typeof responseText.query.pages[page].missing!="undefined") {
            for (var i=0; i<NRISOnlyStructure[currentTable][currentRow].Titles.length; i++) {
                if (NRISOnlyStructure[currentTable][currentRow].Titles[i].name==pagetitle) {
                    NRISOnlyStructure[currentTable][currentRow].Titles[i].exists=false
                }
            }
        }
        if (responseText.query.pages[page].categories) {
            for (var category in responseText.query.pages[page].categories) {
                // mark as link to dab
                if (responseText.query.pages[page].categories[category].title=="Category:All disambiguation pages") {
                    for (var i=0; i<NRISOnlyStructure[currentTable][currentRow].Titles.length; i++) {
                        if (NRISOnlyStructure[currentTable][currentRow].Titles[i].name==pagetitle) {
                            NRISOnlyStructure[currentTable][currentRow].Titles[i].exists=false // dab=redlink
                            NRISOnlyStructure[currentTable][currentRow].Titles[i].isDab=true
                        }
                    }
                }
                // mark already tagged NRIS-only
                if (responseText.query.pages[page].categories[category].title.indexOf("sourced only to NRIS")!=-1) {
                    for (var i=0; i<NRISOnlyStructure[currentTable][currentRow].Titles.length; i++) {
                        if (NRISOnlyStructure[currentTable][currentRow].Titles[i].name==pagetitle) {
                            NRISOnlyStructure[currentTable][currentRow].Titles[i].isTaggedNRISOnly=true
                        }
                    }
                }
            }
        }
    }

    if (NRISOnlyStructure[currentTable][currentRow].CatsQueried==NRISOnlyStructure[currentTable][currentRow].Titles.length) {
        for (var i=0; i<NRISOnlyStructure[currentTable][currentRow].Titles.length; i++) { // tally up articles to query
            if (NRISOnlyStructure[currentTable][currentRow].Titles[i].exists) {
                NRISOnlyStructure[currentTable][currentRow].TotalToCheck++
            }
        }
        // now begin querying article content and checking if NRIS-only
        TotalQueried++
        if (TotalQueried==TotalToQuery) CalculateNRISOnlyTotals()
        return;
    }
}

// keep track of warnings encountered while querying API
function NewNRISOnlyWarning(warning) {
    var NewWarning=true
    for (var i=0; i<WarningCount.length; i++) { // check if already encountered error
        if (warning==WarningCount[i][0]||WarningCount[i][0]=="") {WarningCount[i][0]=warning; WarningCount[i][1]++; NewWarning=false;}
    }
    if (NewWarning) WarningCount[WarningCount.length]=[warning,1] // if new warning, make new entry

    var test=0
    for (var i=0; i<WarningCount.length; i++) {
        test+=WarningCount[i][1]
    }
    if (test%50==0) DefaultQueryPause++ // for every 50 errors encountered, increase time between each query to throttle speed
}

// these errors require user input; can't just be ignored
function NRISOnlyFatalError(code,currentTable,currentRow) {
    var errorArray = ['No county section found for ','No list found for ','Incorrectly formatted list for ']
    var retry=confirm(errorArray[code]+NRISOnlyStructure[currentTable][currentRow].Link+"!\n\nCancel=Skip                   OK=Retry")
    if (retry) {
        getNRISOnlyListWikitext(currentTable,currentRow);
    } else { // if chose to skip, add one to error count
        TotalQueried++
        ErrorCount++
        if (TotalQueried==TotalToQuery) CalculateNRISOnlyTotals()
    }
    return;
}

// update ProgressDiv to let user know what's going on
function UpdateNRISOnlyProgressDiv() {
    var ProgressSpan=document.getElementById("ProgressSpan")
    var TimeSpan=document.getElementById("TimeSpan")

    var PercentQueried=Math.round(TotalQueried/TotalToQuery*1000)/10
    ProgressSpan.innerHTML = "Querying articles in each list... "+TotalQueried+" ("+PercentQueried+"%) of "+TotalToQuery+" lists complete."

    if (TotalQueried>10) {
        var CurrentTime=new Date()
        var MinutesElapsed = (CurrentTime-InitialTime)/60000
        var Average = MinutesElapsed/TotalQueried
        MinutesElapsed = Math.round(MinutesElapsed)
        var MinutesRemaining = Math.round(Average*(TotalToQuery-TotalQueried))
        if (MinutesRemaining == 0) MinutesRemaining="Less than one"

        var TimeRemainingStr = MinutesRemaining+" min"
        var TimeElapsedStr = ""
        if (MinutesElapsed!=0) TimeElapsedStr=MinutesElapsed+" min"
        if (TimeElapsedStr!="") TimeRemainingStr+=" ("+TimeElapsedStr+" elapsed)"
    } else {
        var TimeRemainingStr="Calculating..."
    }
    TimeSpan.innerHTML="<br>Estimated time remaining: "+TimeRemainingStr

    if (TotalQueried!=TotalToQuery) { // update ProgressDiv only at regular intervals to prevent CPU overload; stop once done
        ProgressDivTimer=setTimeout(function(){
            UpdateNRISOnlyProgressDiv();
        }, 500);
    }
    return;
}

// temp function to see if extracting cat info works
function CalculateNRISOnlyTotals() {
    var popup = open("");
    var pre = popup.document.createElement("pre");
    pre.innerHTML=JSON.stringify(NRISOnlyStructure,null,4)
    popup.document.body.appendChild(pre);
}




// after all querying complete, calculate totals for states and counties with multiple sublists
function CalculateProgressTotals() {
    clearTimeout(ProgressDivTimer)
    var ProgressSpan=document.getElementById("ProgressSpan")
    var TimeSpan=document.getElementById("TimeSpan")

    ProgressSpan.innerHTML = "Querying county data... Done! "+TotalToQuery+" lists checked."

    var CurrentTime=new Date()
    var MinutesElapsed = Math.round((CurrentTime-InitialTime)/60000)
    TimeSpan.innerHTML=" Time elapsed: "+MinutesElapsed+" min"

    for (var i=1; i<ProgressStructure.length; i++) { // i=table number; skip national table until end
        for (var j=0; j<ProgressStructure[i].length-2; j++) {
            var TotalsIndex=ProgressStructure[i].length-1
            if (!isNaN(parseFloat(ProgressStructure[i][j].ID))) { // if regular county without sublists, add to totals
                ProgressStructure[i][TotalsIndex].Total+=ProgressStructure[i][j].Total
                ProgressStructure[i][TotalsIndex].Illustrated+=ProgressStructure[i][j].Illustrated
                ProgressStructure[i][TotalsIndex].Articled+=ProgressStructure[i][j].Articled
                ProgressStructure[i][TotalsIndex].Stubs+=ProgressStructure[i][j].Stubs
                ProgressStructure[i][TotalsIndex].NRISonly+=ProgressStructure[i][j].NRISonly
                ProgressStructure[i][TotalsIndex].StartPlus+=ProgressStructure[i][j].StartPlus
                ProgressStructure[i][TotalsIndex].Unassessed+=ProgressStructure[i][j].Unassessed
                ProgressStructure[i][TotalsIndex].Untagged+=ProgressStructure[i][j].Untagged
            } else if (ProgressStructure[i][j].ID=="-----") { // if county sublist, find total county row and add there
                var CountyTotalsIndex=j+1
                while (CountyTotalsIndex<ProgressStructure[i].length-2) {
                    if (!isNaN(parseFloat(ProgressStructure[i][CountyTotalsIndex].ID))) break;
                    CountyTotalsIndex++
                }
                ProgressStructure[i][CountyTotalsIndex].Total+=ProgressStructure[i][j].Total
                ProgressStructure[i][CountyTotalsIndex].Illustrated+=ProgressStructure[i][j].Illustrated
                ProgressStructure[i][CountyTotalsIndex].Articled+=ProgressStructure[i][j].Articled
                ProgressStructure[i][CountyTotalsIndex].Stubs+=ProgressStructure[i][j].Stubs
                ProgressStructure[i][CountyTotalsIndex].NRISonly+=ProgressStructure[i][j].NRISonly
                ProgressStructure[i][CountyTotalsIndex].StartPlus+=ProgressStructure[i][j].StartPlus
                ProgressStructure[i][CountyTotalsIndex].Unassessed+=ProgressStructure[i][j].Unassessed
                ProgressStructure[i][CountyTotalsIndex].Untagged+=ProgressStructure[i][j].Untagged
            } else if (ProgressStructure[i][j].ID=="ddddd") { // if county duplicate row, subtract from county total
                ProgressStructure[i][j+1].Total-=ProgressStructure[i][j].Total
                ProgressStructure[i][j+1].Illustrated-=ProgressStructure[i][j].Illustrated
                ProgressStructure[i][j+1].Articled-=ProgressStructure[i][j].Articled
                ProgressStructure[i][j+1].Stubs-=ProgressStructure[i][j].Stubs
                ProgressStructure[i][j+1].NRISonly-=ProgressStructure[i][j].NRISonly
                ProgressStructure[i][j+1].StartPlus-=ProgressStructure[i][j].StartPlus
                ProgressStructure[i][j+1].Unassessed-=ProgressStructure[i][j].Unassessed
                ProgressStructure[i][j+1].Untagged-=ProgressStructure[i][j].Untagged
            } else { // unknown ID; skip it
                alert("Error! Unknown ID="+ProgressStructure[i][j].ID+" in Table "+i+", Row "+j+". Skipping this list in totals.")
            }
        }
        // subtract state duplicates
        ProgressStructure[i][TotalsIndex].Total-=ProgressStructure[i][TotalsIndex-1].Total
        ProgressStructure[i][TotalsIndex].Illustrated-=ProgressStructure[i][TotalsIndex-1].Illustrated
        ProgressStructure[i][TotalsIndex].Articled-=ProgressStructure[i][TotalsIndex-1].Articled
        ProgressStructure[i][TotalsIndex].Stubs-=ProgressStructure[i][TotalsIndex-1].Stubs
        ProgressStructure[i][TotalsIndex].NRISonly-=ProgressStructure[i][TotalsIndex-1].NRISonly
        ProgressStructure[i][TotalsIndex].StartPlus-=ProgressStructure[i][TotalsIndex-1].StartPlus
        ProgressStructure[i][TotalsIndex].Unassessed-=ProgressStructure[i][TotalsIndex-1].Unassessed
        ProgressStructure[i][TotalsIndex].Untagged-=ProgressStructure[i][TotalsIndex-1].Untagged

        // record state totals in national table
        ProgressStructure[0][i-1].Total=ProgressStructure[i][TotalsIndex].Total
        ProgressStructure[0][i-1].Illustrated=ProgressStructure[i][TotalsIndex].Illustrated
        ProgressStructure[0][i-1].Articled=ProgressStructure[i][TotalsIndex].Articled
        ProgressStructure[0][i-1].Stubs=ProgressStructure[i][TotalsIndex].Stubs
        ProgressStructure[0][i-1].NRISonly=ProgressStructure[i][TotalsIndex].NRISonly
        ProgressStructure[0][i-1].StartPlus=ProgressStructure[i][TotalsIndex].StartPlus
        ProgressStructure[0][i-1].Unassessed=ProgressStructure[i][TotalsIndex].Unassessed
        ProgressStructure[0][i-1].Untagged=ProgressStructure[i][TotalsIndex].Untagged

        // add state totals to national totals
        var NationalTotalsIndex=ProgressStructure[0].length-1
        ProgressStructure[0][NationalTotalsIndex].Total+=ProgressStructure[0][i-1].Total
        ProgressStructure[0][NationalTotalsIndex].Illustrated+=ProgressStructure[0][i-1].Illustrated
        ProgressStructure[0][NationalTotalsIndex].Articled+=ProgressStructure[0][i-1].Articled
        ProgressStructure[0][NationalTotalsIndex].Stubs+=ProgressStructure[0][i-1].Stubs
        ProgressStructure[0][NationalTotalsIndex].NRISonly+=ProgressStructure[0][i-1].NRISonly
        ProgressStructure[0][NationalTotalsIndex].StartPlus+=ProgressStructure[0][i-1].StartPlus
        ProgressStructure[0][NationalTotalsIndex].Unassessed+=ProgressStructure[0][i-1].Unassessed
        ProgressStructure[0][NationalTotalsIndex].Untagged+=ProgressStructure[0][i-1].Untagged
    }
    // special row for Tangier, Morocco
    ProgressStructure[0][NationalTotalsIndex].Total+=ProgressStructure[0][NationalTotalsIndex-2].Total
    ProgressStructure[0][NationalTotalsIndex].Illustrated+=ProgressStructure[0][NationalTotalsIndex-2].Illustrated
    ProgressStructure[0][NationalTotalsIndex].Articled+=ProgressStructure[0][NationalTotalsIndex-2].Articled
    ProgressStructure[0][NationalTotalsIndex].Stubs+=ProgressStructure[0][NationalTotalsIndex-2].Stubs
    ProgressStructure[0][NationalTotalsIndex].NRISonly+=ProgressStructure[0][NationalTotalsIndex-2].NRISonly
    ProgressStructure[0][NationalTotalsIndex].StartPlus+=ProgressStructure[0][NationalTotalsIndex-2].StartPlus
    ProgressStructure[0][NationalTotalsIndex].Unassessed+=ProgressStructure[0][NationalTotalsIndex-2].Unassessed
    ProgressStructure[0][NationalTotalsIndex].Untagged+=ProgressStructure[0][NationalTotalsIndex-2].Untagged

    // subtract national duplicates
    ProgressStructure[0][NationalTotalsIndex].Total-=ProgressStructure[0][NationalTotalsIndex-1].Total
    ProgressStructure[0][NationalTotalsIndex].Illustrated-=ProgressStructure[0][NationalTotalsIndex-1].Illustrated
    ProgressStructure[0][NationalTotalsIndex].Articled-=ProgressStructure[0][NationalTotalsIndex-1].Articled
    ProgressStructure[0][NationalTotalsIndex].Stubs-=ProgressStructure[0][NationalTotalsIndex-1].Stubs
    ProgressStructure[0][NationalTotalsIndex].NRISonly-=ProgressStructure[0][NationalTotalsIndex-1].NRISonly
    ProgressStructure[0][NationalTotalsIndex].StartPlus-=ProgressStructure[0][NationalTotalsIndex-1].StartPlus
    ProgressStructure[0][NationalTotalsIndex].Unassessed-=ProgressStructure[0][NationalTotalsIndex-1].Unassessed
    ProgressStructure[0][NationalTotalsIndex].Untagged-=ProgressStructure[0][NationalTotalsIndex-1].Untagged

    setTimeout(function() {ParseProgressWikitext()},1); // small delay for non-Firefox browsers to update screen
}

// update wikitext with queried totals
function ParseProgressWikitext() {
    var newwikitext=wikitext
    var TableStartIndex=wikitext.indexOf("==State totals")
    for (var i=0; i<ProgressStructure.length; i++) {
        var TableStartIndex=wikitext.indexOf("{|",TableStartIndex+1)    // find next table in old wikitext
        var TableEndIndex=wikitext.indexOf("|}",TableStartIndex)+2
        var oldTable=wikitext.substr(TableStartIndex,TableEndIndex-TableStartIndex)
        var newTable=oldTable

        var RowStartIndex=0
        for (var j=0; j<ProgressStructure[i].length-2; j++) {
            RowStartIndex=oldTable.indexOf("\n|-",RowStartIndex+1) // find next row in old table
            if (ProgressStructure[i][j].ID=="ddddd") continue;   // skip duplicate rows
            var RowEndIndex=oldTable.indexOf("\n|-",RowStartIndex+1)
            var oldRow=oldTable.substr(RowStartIndex,RowEndIndex-RowStartIndex)
            var firstColumn=oldRow.indexOf("\n|")
            var stop=4        // skip one cell for national table, three for states
            if (i==0) stop=2
            for (var inc=0; inc<stop; inc++) {
                firstColumn=oldRow.indexOf("\n|",firstColumn+1)  // find next cell
            }

            // build up new row
            var newRow=oldRow.substr(0,firstColumn)
            var temp=ProgressStructure[i][j]
            // total
            var str=temp.Total.toString()
            if (temp.Total>999) str=str.replace(/\B(?=(\d{3})+(?!\d))/g, ","); // add thousands separators
            newRow+="\n| "+str
            // illustrated
            str=temp.Illustrated.toString()
            if (temp.Illustrated>999) str=str.replace(/\B(?=(\d{3})+(?!\d))/g, ",");
            newRow+="\n| "+str
            // illustrated percent
            if (temp.Total==0) {
                str="-"
            } else {
                str = Math.round(temp.Illustrated/temp.Total*1000)/10
                var test = str.toString().indexOf(".")
                if (test==-1 && str!=100 && str!=0) str+=".0" // force decimal
                str+="%"
            }
            newRow+="\n| "+str
            // articled
            str=temp.Articled.toString()
            if (temp.Articled>999) str=str.replace(/\B(?=(\d{3})+(?!\d))/g, ",");
            newRow+="\n| "+str
            // articled percent
            if (temp.Total==0) {
                str="-"
            } else {
                str = Math.round(temp.Articled/temp.Total*1000)/10
                var test = str.toString().indexOf(".")
                if (test==-1 && str!=100 && str!=0) str+=".0"
                str+="%"
            }
            newRow+="\n| "+str
            // stubs
            str=temp.Stubs.toString()
            if (temp.Stubs>999) str=str.replace(/\B(?=(\d{3})+(?!\d))/g, ",");
            newRow+="\n| "+str
            // NRIS-only
            str=temp.NRISonly.toString()
            if (temp.NRISonly>999) str=str.replace(/\B(?=(\d{3})+(?!\d))/g, ",");
            newRow+="\n| "+str
            // Start+
            str=temp.StartPlus.toString()
            if (temp.StartPlus>999) str=str.replace(/\B(?=(\d{3})+(?!\d))/g, ",");
            newRow+="\n| "+str
            // Start+ percent
            if (temp.Total==0) {
                str="-"
            } else {
                str = Math.round(temp.StartPlus/temp.Total*1000)/10
                var test = str.toString().indexOf(".")
                if (test==-1 && str!=100 && str!=0) str+=".0" // force decimal
                str+="%"
            }
            newRow+="\n| "+str
            // unassessed
            str=temp.Unassessed.toString()
            if (temp.Unassessed>999) str=str.replace(/\B(?=(\d{3})+(?!\d))/g, ",");
            newRow+="\n| "+str
            // untagged
            str=temp.Untagged.toString()
            if (temp.Untagged>999) str=str.replace(/\B(?=(\d{3})+(?!\d))/g, ",");
            newRow+="\n| "+str
            // net quality
            if (temp.Total==0) {
                str="-"
            } else {
                str=temp.StartPlus+0.5*temp.Stubs+0.5*temp.Unassessed-0.5*temp.Untagged-0.75*temp.NRISonly
                str=Math.round((0.75*str/temp.Total+0.25*temp.Illustrated/temp.Total)*1000)/10
                if (str<0) str=0
                var test=str.toString().indexOf(".")
                if (test==-1 && str!=100 && str!=0) str+=".0"
                str+="%"
            }
            newRow+="\n| "+str

            // update new table with new row
            newTable=newTable.replace(oldRow,newRow)
        }
        RowStartIndex=oldTable.indexOf("\n|-",RowStartIndex+1) // skip duplicate row
        RowStartIndex=oldTable.indexOf("\n|-",RowStartIndex+1)
        RowEndIndex=oldTable.indexOf("\n|}",RowStartIndex+1)
        oldRow=oldTable.substr(RowStartIndex,RowEndIndex-RowStartIndex)
        firstColumn=oldRow.indexOf("\n!") // totals row uses ! instead of |
        firstColumn=oldRow.indexOf("\n!",firstColumn+1) // skip first cell

        // build up new totals row
        var newRow=oldRow.substr(0,firstColumn)
        var temp=ProgressStructure[i][ProgressStructure[i].length-1]
        // total
        var str=temp.Total.toString()
        if (temp.Total>999) str=str.replace(/\B(?=(\d{3})+(?!\d))/g, ","); // add thousands separators
        newRow+="\n! "+str
        // illustrated
        str=temp.Illustrated.toString()
        if (temp.Illustrated>999) str=str.replace(/\B(?=(\d{3})+(?!\d))/g, ",");
        newRow+="\n! "+str
        // illustrated percent
        if (temp.Total==0) {
            str="-"
        } else {
            str = Math.round(temp.Illustrated/temp.Total*1000)/10
            var test = str.toString().indexOf(".")
            if (test==-1 && str!=100 && str!=0) str+=".0" // force decimal
            str+="%"
        }
        newRow+="\n! "+str
        // articled
        str=temp.Articled.toString()
        if (temp.Articled>999) str=str.replace(/\B(?=(\d{3})+(?!\d))/g, ",");
        newRow+="\n! "+str
        // articled percent
        if (temp.Total==0) {
            str="-"
        } else {
            str = Math.round(temp.Articled/temp.Total*1000)/10
            var test = str.toString().indexOf(".")
            if (test==-1 && str!=100 && str!=0) str+=".0" // force decimal
            str+="%"
        }
        newRow+="\n! "+str
        // stubs
        str=temp.Stubs.toString()
        if (temp.Stubs>999) str=str.replace(/\B(?=(\d{3})+(?!\d))/g, ",");
        newRow+="\n! "+str
        // NRIS-only
        str=temp.NRISonly.toString()
        if (temp.NRISonly>999) str=str.replace(/\B(?=(\d{3})+(?!\d))/g, ",");
        newRow+="\n! "+str
        // Start+
        str=temp.StartPlus.toString()
        if (temp.StartPlus>999) str=str.replace(/\B(?=(\d{3})+(?!\d))/g, ",");
        newRow+="\n! "+str
        // Start+ percent
        if (temp.Total==0) {
            str="-"
        } else {
            str = Math.round(temp.StartPlus/temp.Total*1000)/10
            var test = str.toString().indexOf(".")
            if (test==-1 && str!=100 && str!=0) str+=".0" // force decimal
            str+="%"
        }
        newRow+="\n! "+str
        // unassessed
        str=temp.Unassessed.toString()
        if (temp.Unassessed>999) str=str.replace(/\B(?=(\d{3})+(?!\d))/g, ",");
        newRow+="\n! "+str
        // untagged
        str=temp.Untagged.toString()
        if (temp.Untagged>999) str=str.replace(/\B(?=(\d{3})+(?!\d))/g, ",");
        newRow+="\n! "+str
        // net quality
        if (temp.Total==0) {
            str="-"
        } else {
            str=temp.StartPlus+0.5*temp.Stubs+0.5*temp.Unassessed-0.5*temp.Untagged-0.75*temp.NRISonly
            str=Math.round((0.75*str/temp.Total+0.25*temp.Illustrated/temp.Total)*1000)/10
            if (str<0) str=0
            var test=str.toString().indexOf(".")
            if (test==-1 && str!=100 && str!=0) str+=".0"
            str+="%"
        }
        newRow+="\n! "+str

        // update new wikitext with new table
        newTable=newTable.replace(oldRow,newRow)
        newwikitext=newwikitext.replace(oldTable,newTable)
    }

    // now edit page with new wikitext
    var ProgressDiv=document.getElementById("ProgressDiv")
    ProgressDiv.innerHTML+="<br>Editing page (this might take up to one minute)... "
    InitializeEdit(newwikitext)
}

// initialize edit
function InitializeEdit(newwikitext) {
    var d=new Date();
    var months=['January','February','March','April','May','June','July','August','September','October','November','December'];
    var year=d.getYear();
    if (year < 1000) year += 1900
    var DateStr=months[d.getMonth()]+" "+d.getDate()+", "+year

    regex=/(January|February|March|April|May|June|July|August|September|October|November|December) [0-9]{1,2}, [0-9]{4}/g
    var tempstring=newwikitext.split('==County totals==') // ignore dates in lead (e.g. date of last map update)
    newwikitext=tempstring[0]+'==County totals=='+tempstring[1].replace(regex,DateStr) // update date strings above tables

    var ErrorStr = ''
    if (ErrorCount>0) ErrorStr = " Errors encountered for "+ErrorCount+" counties, which were skipped. Human attention needed."
    var summary='Updating county data as of '+DateStr+' using [[User:Dudemanfellabra/UpdateNRHPProgress|script]].'+ErrorStr
    editPage(newwikitext,mw.config.get('wgPageName'),summary)
}

function PageEdited(ajaxResponse,status,newwikitext) {
    var ProgressDiv=document.getElementById("ProgressDiv")
    if (status!="success") {
        var retry=confirm("Error: "+ajaxResponse.errorThrown+" while editing page!\n\nCancel=Abort                   OK=Retry")
        if (retry) {
            ProgressDiv.innerHTML+="Retrying... "
            InitializeEdit(newwikitext) // try again
        } else {
            ProgressDiv.innerHTML+="Edit failure! Script aborted!"
        }
        return;
    }
    var responseText=JSON.parse(ajaxResponse.responseText)
    var diff=responseText.edit.newrevid
    var linkStr="//en.wikipedia.org/w/index.php?diff="+diff
    ProgressDiv.innerHTML+="Page edited! Click <a href='"+linkStr+"'>here</a> for diff."

    // output technical information to console
    var WarningText="NRHP Progress Warnings: "
    for (var i=0; i<WarningCount.length; i++) {
        WarningText+=WarningCount[i][0]+" ("+WarningCount[i][1]+"), "
    }
    if (WarningCount[0][0]!="") {
        WarningText=WarningText.substr(0,WarningText.length-2)
    } else {
        WarningText="NRHP Progress Warnings: none"
    }
    console.log(WarningText)
}

function editPage(text,title,summary) { // edit page when done
    $.ajax({
        dataType: 'json',
        url: mw.util.wikiScript( 'api' ),
        type: 'POST',
        data: {
            format: 'json',
            action: 'edit',
            title: title,
            text: text,
            summary: summary,
            token: mw.user.tokens.get( 'editToken' )
        },
        error: function(ajaxResponse,status,errorThrown) {ajaxResponse.errorThrown=errorThrown},
        complete: function(ajaxResponse,status) {PageEdited(ajaxResponse,status,text)}
    })
}

function getNRISOnlyWikitext(title) {    // asynchronous fetch of Progress page wikitext
    $.ajax({
        dataType: "json",
        url: mw.util.wikiScript('api'),
        data: {
            format: 'json',
            action: 'query',
            prop: 'revisions',
            rvprop: 'content',
            titles: title,
            indexpageids: true,
            redirects: 'true'
        },
        error: function() {wikitext="error"},
        success: function(output) {
            for (page in output.query.pages) {
                wikitext=output.query.pages[page].revisions[0]['*'];
            }
        },
        complete: function() {
            if (wikitext=="error") {
                var ProgressDiv=document.getElementById("ProgressDiv")
                ProgressDiv.innerHTML+=" Unable to fetch wikitext! Script aborted."
            } else {
                SetupNRISOnlyTables()
            }
        }
    })
}

function getNRISOnlyListWikitext(currentTable,currentRow) {   // asynchronous fetch of each list's wikitext
    $.ajax({
        dataType: "json",
        url: mw.util.wikiScript('api'),
        data: {
            format: 'json',
            action: 'query',
            prop: 'revisions',
            rvprop: 'content',
            titles: NRISOnlyStructure[currentTable][currentRow].Link,
            indexpageids: true,
            redirects: 'true'
        },
        error: function(ajaxResponse,status,errorThrown) {ajaxResponse.errorThrown=errorThrown},
        complete: function(ajaxResponse,status) {NRISOnlyWikitextFetched(ajaxResponse,status,currentTable,currentRow)}
    })
}

$(window).load(CheckPermission);