User:Dudemanfellabra/UpdateNRHPProgress.js

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.
/*
   The following script places a button at the top of the NRHP Progress page [[WP:NRHPPROGRESS]]. When the button is clicked,
   the script begins to load each county list linked from the Progress page in the background, extract statistics about
   sites in each list, and updates the Progress page with the fetched data.
*/

var wikitext = 'error'
var ProgressStructure=[]; // ProgressStructure[table][row].StatName
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 ProgressButton() {
    if (mw.config.get('wgPageName')!="Wikipedia:WikiProject_National_Register_of_Historic_Places/Progress"||location.href.indexOf('action')!=-1) return;
    var button=document.createElement("input")
    button.setAttribute("type", "button");
    button.setAttribute("value", "Update Statistics");
    button.setAttribute("id", "button2");
    $(button).click( Click );
    var content=document.getElementById('mw-content-text')

    content.parentNode.insertBefore(button, content)
}

function Click() {   // after button is clicked, disable it and fetch wikitext of Progress page
    var button2 = document.getElementById('button2')
    button2.disabled = true
    var ProgressDiv = document.createElement("div")
    ProgressDiv.setAttribute("id", "ProgressDiv")
    ProgressDiv.setAttribute("style", "width:500px; border:1px solid black; padding:5px; background:#ffffff")
    button2.parentNode.insertBefore(ProgressDiv, button2)
    ProgressDiv.innerHTML = "Initializing..."

	mw.loader.using( ['mediawiki.util', 'user.options'] ).then( function () {
		getWikitext(mw.config.get('wgPageName')) // after wikitext fetched, SetupTables() is called
	} );
}

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

    // set up national totals
    var tr=table[0].getElementsByTagName("tr")
    ProgressStructure[0]=[];
    for (var j=1; j<tr.length-3; j++) {
        var td=tr[j].getElementsByTagName("td")
        ProgressStructure[0][j-1]={};
        ProgressStructure[0][j-1].ID=td[0].innerHTML // state name
        ProgressStructure[0][j-1].Total=0
        ProgressStructure[0][j-1].Illustrated=0
        ProgressStructure[0][j-1].Articled=0
        ProgressStructure[0][j-1].Stubs=0
        ProgressStructure[0][j-1].NRISonly=0
        ProgressStructure[0][j-1].StartPlus=0
        ProgressStructure[0][j-1].Unassessed=0
        ProgressStructure[0][j-1].Untagged=0
    }

    // special row for Tangier, Morocco
    var td=tr[tr.length-3].getElementsByTagName("td")
    ProgressStructure[0][tr.length-4]={};
    ProgressStructure[0][tr.length-4].ID="Tangier, Morocco"
    ProgressStructure[0][tr.length-4].Total=parseFloat(td[1].innerHTML.replace(",",""))
    ProgressStructure[0][tr.length-4].Illustrated=parseFloat(td[2].innerHTML.replace(",",""))
    ProgressStructure[0][tr.length-4].Articled=parseFloat(td[4].innerHTML.replace(",",""))
    ProgressStructure[0][tr.length-4].Stubs=parseFloat(td[6].innerHTML.replace(",",""))
    ProgressStructure[0][tr.length-4].NRISonly=parseFloat(td[7].innerHTML.replace(",",""))
    ProgressStructure[0][tr.length-4].StartPlus=parseFloat(td[8].innerHTML.replace(",",""))
    ProgressStructure[0][tr.length-4].Unassessed=parseFloat(td[10].innerHTML.replace(",",""))
    ProgressStructure[0][tr.length-4].Untagged=parseFloat(td[11].innerHTML.replace(",",""))

    // duplicates row
    var td=tr[tr.length-2].getElementsByTagName("td")
    ProgressStructure[0][tr.length-3]={};
    ProgressStructure[0][tr.length-3].ID="National Duplicates"
    ProgressStructure[0][tr.length-3].Total=parseFloat(td[0].innerHTML.replace(",",""))
    ProgressStructure[0][tr.length-3].Illustrated=parseFloat(td[1].innerHTML.replace(",",""))
    ProgressStructure[0][tr.length-3].Articled=parseFloat(td[3].innerHTML.replace(",",""))
    ProgressStructure[0][tr.length-3].Stubs=parseFloat(td[5].innerHTML.replace(",",""))
    ProgressStructure[0][tr.length-3].NRISonly=parseFloat(td[6].innerHTML.replace(",",""))
    ProgressStructure[0][tr.length-3].StartPlus=parseFloat(td[7].innerHTML.replace(",",""))
    ProgressStructure[0][tr.length-3].Unassessed=parseFloat(td[9].innerHTML.replace(",",""))
    ProgressStructure[0][tr.length-3].Untagged=parseFloat(td[10].innerHTML.replace(",",""))

    // national totals
    ProgressStructure[0][tr.length-2]={};
    ProgressStructure[0][tr.length-2].ID="National Totals"
    ProgressStructure[0][tr.length-2].Total=0
    ProgressStructure[0][tr.length-2].Illustrated=0
    ProgressStructure[0][tr.length-2].Articled=0
    ProgressStructure[0][tr.length-2].Stubs=0
    ProgressStructure[0][tr.length-2].NRISonly=0
    ProgressStructure[0][tr.length-2].StartPlus=0
    ProgressStructure[0][tr.length-2].Unassessed=0
    ProgressStructure[0][tr.length-2].Untagged=0

    // now data for each state
    for (var i=1; i<table.length; i++) {
        var tr=table[i].getElementsByTagName("tr")
        ProgressStructure[i]=[];
        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
            ProgressStructure[i][j-1]={};
            ProgressStructure[i][j-1].ID=td[0].innerHTML.substr(0,5)
            ProgressStructure[i][j-1].Total=parseFloat(td[3].innerHTML.replace(",",""))
            ProgressStructure[i][j-1].Illustrated=parseFloat(td[4].innerHTML.replace(",",""))
            ProgressStructure[i][j-1].Articled=parseFloat(td[6].innerHTML.replace(",",""))
            ProgressStructure[i][j-1].Stubs=parseFloat(td[8].innerHTML.replace(",",""))
            ProgressStructure[i][j-1].NRISonly=parseFloat(td[9].innerHTML.replace(",",""))
            ProgressStructure[i][j-1].StartPlus=parseFloat(td[10].innerHTML.replace(",",""))
            ProgressStructure[i][j-1].Unassessed=parseFloat(td[12].innerHTML.replace(",",""))
            ProgressStructure[i][j-1].Untagged=parseFloat(td[13].innerHTML.replace(",",""))
            var link=td[1].getElementsByTagName("a")
            if (link.length!=0 && link[0].href.search("#")==-1) {
                link=decodeURI(link[0].href).split("/")
                link=link[link.length-1].replace(/_/g," ")
                ProgressStructure[i][j-1].Link=link

                ProgressStructure[i][j-1].ArticleQueried=0 // for querying later
                ProgressStructure[i][j-1].TalkQueried=0
            } else {
                if (ProgressStructure[i][j-1].ID!="ddddd") { // if no link and not duplicate, must be totals row, so we can zero it
                    ProgressStructure[i][j-1].Total=0
                    ProgressStructure[i][j-1].Illustrated=0
                    ProgressStructure[i][j-1].Articled=0
                    ProgressStructure[i][j-1].Stubs=0
                    ProgressStructure[i][j-1].NRISonly=0
                    ProgressStructure[i][j-1].StartPlus=0
                    ProgressStructure[i][j-1].Unassessed=0
                    ProgressStructure[i][j-1].Untagged=0
                }
            }
        }

        // duplicates row
        var td=tr[tr.length-2].getElementsByTagName("td")
        ProgressStructure[i][tr.length-3]={};
        ProgressStructure[i][tr.length-3].ID=ProgressStructure[0][i-1].ID+" Duplicates"
        ProgressStructure[i][tr.length-3].Total=parseFloat(td[0].innerHTML.replace(",",""))
        ProgressStructure[i][tr.length-3].Illustrated=parseFloat(td[1].innerHTML.replace(",",""))
        ProgressStructure[i][tr.length-3].Articled=parseFloat(td[3].innerHTML.replace(",",""))
        ProgressStructure[i][tr.length-3].Stubs=parseFloat(td[5].innerHTML.replace(",",""))
        ProgressStructure[i][tr.length-3].NRISonly=parseFloat(td[6].innerHTML.replace(",",""))
        ProgressStructure[i][tr.length-3].StartPlus=parseFloat(td[7].innerHTML.replace(",",""))
        ProgressStructure[i][tr.length-3].Unassessed=parseFloat(td[9].innerHTML.replace(",",""))
        ProgressStructure[i][tr.length-3].Untagged=parseFloat(td[10].innerHTML.replace(",",""))

        // state totals
        ProgressStructure[i][tr.length-2]={};
        ProgressStructure[i][tr.length-2].ID=ProgressStructure[0][i-1].ID+" Totals"
        ProgressStructure[i][tr.length-2].Total=0
        ProgressStructure[i][tr.length-2].Illustrated=0
        ProgressStructure[i][tr.length-2].Articled=0
        ProgressStructure[i][tr.length-2].Stubs=0
        ProgressStructure[i][tr.length-2].NRISonly=0
        ProgressStructure[i][tr.length-2].StartPlus=0
        ProgressStructure[i][tr.length-2].Unassessed=0
        ProgressStructure[i][tr.length-2].Untagged=0
    }
    for (var i=1; i<ProgressStructure.length; i++) { // count total number of rows to check
        for (var j=0; j<ProgressStructure[i].length-2; j++) {
            if (typeof ProgressStructure[i][j].Link!="undefined") TotalToQuery++ // don't count duplicates and total rows
        }
    }
    TotalQueried=0;

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

    var ProgressSpan=document.createElement("span")
    ProgressSpan.setAttribute("id", "ProgressSpan")
    ProgressDiv.appendChild(ProgressSpan)
    ProgressSpan.innerHTML = "Querying county data... 0 (0%) of "+TotalToQuery+" lists checked."

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

    InitialTime=new Date() // record starting time
    UpdateProgressDiv();
    LoadList(1,0); // begin querying first page
}

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

    if (typeof ProgressStructure[currentTable][currentRow].Link=="undefined") { // skip duplicate and total rows
        LoadList(currentTable,currentRow+1)
        return;
    }

    var title=ProgressStructure[currentTable][currentRow].Link

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

function WikitextFetched(ajaxResponse,status,title,currentTable,currentRow) {
    if (status!="success") {
        NewWarning("Wikitext "+ajaxResponse.errorThrown)
        setTimeout(function(){ // try again after delay if rate limit reached
            getProgressListWikitext(title,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 = ProgressStructure[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
                ProgressStructure[currentTable][currentRow].Total=0
                ProgressStructure[currentTable][currentRow].Illustrated=0
                ProgressStructure[currentTable][currentRow].Articled=0
                ProgressStructure[currentTable][currentRow].Stubs=0
                ProgressStructure[currentTable][currentRow].NRISonly=0
                ProgressStructure[currentTable][currentRow].StartPlus=0
                ProgressStructure[currentTable][currentRow].Unassessed=0
                ProgressStructure[currentTable][currentRow].Untagged=0
                ProgressStructure[currentTable][currentRow].Link=title

                TotalQueried++
                if (TotalQueried==TotalToQuery) CalculateProgressTotals()
                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
                ProgressFatalError(0,title,currentTable,currentRow)
            }
        }
        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) {
                ProgressFatalError(2,title,currentTable,currentRow) // incorrectly formatted table
            } else { // must be an empty county
                ProgressStructure[currentTable][currentRow].Total=0
                ProgressStructure[currentTable][currentRow].Illustrated=0
                ProgressStructure[currentTable][currentRow].Articled=0
                ProgressStructure[currentTable][currentRow].Stubs=0
                ProgressStructure[currentTable][currentRow].NRISonly=0
                ProgressStructure[currentTable][currentRow].StartPlus=0
                ProgressStructure[currentTable][currentRow].Unassessed=0
                ProgressStructure[currentTable][currentRow].Untagged=0
                ProgressStructure[currentTable][currentRow].Link=title

                TotalQueried++
                if (TotalQueried==TotalToQuery) CalculateProgressTotals()
                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) {
            ProgressFatalError(1,title,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--}
    }

    // now begin querying statistics
    var Stats={"ID":ProgressStructure[currentTable][currentRow].ID, "Total":Rows.length, "Illustrated":0, "Articled":0, "Stubs":0, "NRISonly":0, "StartPlus":0, "Unassessed":0, "Untagged":0, "Link":title}

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

        // check for illustrated while we're cycling through anyway
        var test=ThisRow.match(/\|[ ]*?image[ ]*?=.*?(\n|\||}})/g)
        if (test!=null) {
            test=test[0].replace(/\|[ ]*?image[ ]*?=/g,"").replace(/(\n|\||}})/g,"").replace(/\<\!\-\-(.|[\r\n])*?\-\-\>/g,"").trim()
            if (test!="") {
                Stats.Illustrated++  // only true if image param there and non-blank
            }
        }

        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
        Titles.push(article)
    }

    var StartIndex=0
    LoadNextProgressQuery(Rows,Stats,Titles,StartIndex,title,currentTable,currentRow)
    return;
}

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

    setTimeout(function(){ // short delay to prevent API overload
        QueryProgressStats(Rows,Stats,Titles,TempTitles,StartIndex,title,currentTable,currentRow)
        LoadNextProgressQuery(Rows,Stats,Titles,StartIndex,title,currentTable,currentRow)
    }, DefaultQueryPause);
    return;
}

// query next batch of articles
function QueryProgressStats(Rows,Stats,Titles,TempTitles,StartIndex,title,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) {
                ProgressArticleChecked(ArticlejsonObject,status,Rows,Stats,Titles,TempTitles,StartIndex,title,currentTable,currentRow)
            }
    });
    return;
}

// parse API response for article query
function ProgressArticleChecked(ArticlejsonObject,status,Rows,Stats,Titles,TempTitles,StartIndex,title,currentTable,currentRow) {
    if (status!="success") {
        NewWarning("Articles "+ArticlejsonObject.errorThrown)

        setTimeout(function(){ // try again after delay if rate limit reached
            QueryProgressStats(Rows,Stats,Titles,TempTitles,StartIndex,title,currentTable,currentRow)
        }, 250);
        return;
    }
    // won't get here unless successful
    ProgressStructure[currentTable][currentRow].ArticleQueried+=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<Titles.length; i++) { // also update in Titles array to prepare to query talk pages
                if (Titles[i]==responseText.query.normalized[n].from) Titles[i]=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<Titles.length; i++) { // also update in Titles array to prepare to query talk pages
                if (Titles[i]==responseText.query.redirects[r].from) Titles[i]=responseText.query.redirects[r].to
            }
        }
    }

    // now determine the number of bluelinks and NRIS-only articles
    for (var page in responseText.query.pages) {
        var articled=true   // default to articled, not NRIS-only
        var NRISonly=false
        var pagetitle=responseText.query.pages[page].title
        if (typeof responseText.query.pages[page].missing!="undefined") {
            // redlink=unarticled
            articled=false
        }
        if (responseText.query.pages[page].categories) {
            for (var category in responseText.query.pages[page].categories) {
                if (responseText.query.pages[page].categories[category].title=="Category:All disambiguation pages") {
                    // dab=unarticled
                    articled=false
                }
                if (responseText.query.pages[page].categories[category].title.indexOf("sourced only to NRIS")!=-1) {
                    // mark as NRIS-only
                    NRISonly=true
                }
            }
        }
        for (var i=0; i<TempTitles.length; i++) { // if page is duplicated, count it multiple times
            if (TempTitles[i]==pagetitle) {
                if (articled) Stats.Articled++
                if (NRISonly) Stats.NRISonly++
            }
        }
    }

    if (ProgressStructure[currentTable][currentRow].ArticleQueried==Stats.Total) { // after querying all articles, query talk pages
        for (var i=0; i<Titles.length; i++) {
            Titles[i]="Talk:"+Titles[i]
        }
        StartIndex=0
        LoadNextProgressTalkQuery(Rows,Stats,Titles,StartIndex,title,currentTable,currentRow)
    }
}

// ready next batch of talk pages to query
function LoadNextProgressTalkQuery(Rows,Stats,Titles,StartIndex,title,currentTable,currentRow) {
    if (StartIndex==Stats.Total) { // all queries begun for this list
        return;
    }
    // must have some more rows to query
    if (Stats.Total-StartIndex>50) {
        var TempTitles=Titles.slice(StartIndex,StartIndex+50)
    } else {
        var TempTitles=Titles.slice(StartIndex)
    }
    StartIndex+=TempTitles.length

    setTimeout(function(){ // short delay to prevent API overload
        QueryProgressTalkStats(Rows,Stats,Titles,TempTitles,StartIndex,title,currentTable,currentRow)
        LoadNextProgressTalkQuery(Rows,Stats,Titles,StartIndex,title,currentTable,currentRow)
    }, DefaultQueryPause);
    return;
}

// query the next batch of talk pages
function QueryProgressTalkStats(Rows,Stats,Titles,TempTitles,StartIndex,title,currentTable,currentRow) {
    var catlist='Category:FA-Class National Register of Historic Places articles‎|Category:A-Class National Register of Historic Places '
    catlist+='articles‎|Category:GA-Class National Register of Historic Places articles‎|Category:B-Class National Register of Historic '
    catlist+='Places articles‎|Category:C-Class National Register of Historic Places articles‎|Category:Start-Class National Register of '
    catlist+='Historic Places articles‎|Category:Stub-Class National Register of Historic Places articles‎|Category:Unassessed National '
    catlist+='Register of Historic Places articles‎|Category:List-Class National Register of Historic Places articles|Category:Redirect-'
    catlist+='Class National Register of Historic Places articles'

    var TitleList=TempTitles.join("|")
    $.ajax({
        dataType: "json",
        url: mw.util.wikiScript('api'),
        data: {
            format: 'json',
            action: 'query',
            prop: 'categories',
            clcategories: catlist,
            cllimit: 'max',
            titles: TitleList,
            redirects: 'true'
        },
        error: function(ArticlejsonObject,status,errorThrown) {ArticlejsonObject.errorThrown=errorThrown},
        complete: function(ArticlejsonObject,status) {
                ProgressTalkChecked(ArticlejsonObject,status,Rows,Stats,Titles,TempTitles,StartIndex,title,currentTable,currentRow)
            }
    });
    return;
}

// parse API response for talk page query
function ProgressTalkChecked(ArticlejsonObject,status,Rows,Stats,Titles,TempTitles,StartIndex,title,currentTable,currentRow) {
    if (status!="success") {
        NewWarning("Talk "+ArticlejsonObject.errorThrown)

        setTimeout(function(){ // try again after delay if rate limit reached
            QueryProgressTalkStats(Rows,Stats,Titles,TempTitles,StartIndex,title,currentTable,currentRow)
        }, 250);
        return;
    }
    // won't get here unless successful
    ProgressStructure[currentTable][currentRow].TalkQueried+=TempTitles.length

    // now determine quality statistics
    var responseText=JSON.parse(ArticlejsonObject.responseText)
    for (var page in responseText.query.pages) {
        if (typeof responseText.query.pages[page].missing!="undefined") continue; // skip talk page if not articled

        var untagged=true // default to untagged
        var articled=true // assume articled to check for link to other county/MPS lists
        var stub=false
        var unassessed=false
        var startPlus=false
        var pagetitle=responseText.query.pages[page].title
        if (responseText.query.pages[page].categories) {
            untagged=false // if cat hit, mark as tagged
            for (var category in responseText.query.pages[page].categories) {
                var CatTitle=responseText.query.pages[page].categories[category].title
                if (CatTitle.indexOf("Stub")!=-1) {
                    stub=true // mark as stub
                }
                if (CatTitle.indexOf("Unassessed")!=-1||CatTitle.indexOf("Redirect")!=-1) {
                    unassessed=true // mark as unassessed
                }
                if  (CatTitle.indexOf("List")!=-1) { // count links to other county/MPS lists as unarticled; other list-class as stubs
                    if (responseText.query.pages[page].title.indexOf("National Register of Historic Places")!=-1){
                        articled=false
                    } else {
                        stub=true
                    }
                }
            }
        }
        if (articled&&!untagged&&!unassessed&&!stub) { // if articled, tagged, and assessed, but not stub, must be Start+
            startPlus=true
        }

        for (var i=0; i<TempTitles.length; i++) {
            if (TempTitles[i]==pagetitle) {
                if (!articled) Stats.Articled-- // reduce the count of articled if links to lists
                if (untagged) Stats.Untagged++
                if (stub) Stats.Stubs++
                if (unassessed) Stats.Unassessed++
                if (startPlus) Stats.StartPlus++
            }
        }
    }

    if (ProgressStructure[currentTable][currentRow].TalkQueried==Stats.Total) {
        ProgressStructure[currentTable][currentRow].Total=Stats.Total
        ProgressStructure[currentTable][currentRow].Illustrated=Stats.Illustrated
        ProgressStructure[currentTable][currentRow].Articled=Stats.Articled
        ProgressStructure[currentTable][currentRow].Stubs=Stats.Stubs
        ProgressStructure[currentTable][currentRow].NRISonly=Stats.NRISonly
        ProgressStructure[currentTable][currentRow].StartPlus=Stats.StartPlus
        ProgressStructure[currentTable][currentRow].Unassessed=Stats.Unassessed
        ProgressStructure[currentTable][currentRow].Untagged=Stats.Untagged
        ProgressStructure[currentTable][currentRow].Link=title

        TotalQueried++
        if (TotalQueried==TotalToQuery) CalculateProgressTotals()
    }
}

// keep track of warnings encountered while querying API
function NewWarning(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 ProgressFatalError(code,title,currentTable,currentRow) {
    var errorArray = ['No county section found for ','No list found for ','Incorrectly formatted list for ']
    var retry=confirm(errorArray[code]+title+"!\n\nCancel=Skip                   OK=Retry")
    if (retry) {
        getProgressListWikitext(title,currentTable,currentRow);
    } else { // if chose to skip, add one to error count
        TotalQueried++
        ErrorCount++
        if (TotalQueried==TotalToQuery) CalculateProgressTotals()
    }
    return;
}

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

    var PercentQueried=Math.round(TotalQueried/TotalToQuery*1000)/10
    ProgressSpan.innerHTML = "Querying county data... "+TotalQueried+" ("+PercentQueried+"%) of "+TotalToQuery+" lists checked."

    if (TotalQueried>100) {
        var CurrentTime=new Date()
        var SecondsElapsed = (CurrentTime-InitialTime)/1000
        var Average = SecondsElapsed/TotalQueried
        SecondsElapsed=Math.round(SecondsElapsed)
        var MinutesElapsed = 0
        while (SecondsElapsed>=60) {
            SecondsElapsed-=60
            MinutesElapsed++
        }
        var SecondsRemaining = Math.round(Average*(TotalToQuery-TotalQueried))
        var MinutesRemaining = 0
        while (SecondsRemaining>=60) {
            SecondsRemaining-=60
            MinutesRemaining++
        }

        var TimeRemainingStr = ""
        if (MinutesRemaining!=0) TimeRemainingStr=MinutesRemaining+" min "
        TimeRemainingStr+=SecondsRemaining+" sec"
        var TimeElapsedStr = ""
        if (MinutesElapsed!=0) TimeElapsedStr=MinutesElapsed+" min "
        TimeElapsedStr+=SecondsElapsed+" sec"
        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(){
            UpdateProgressDiv();
        }, 500);
    }
    return;
}

// 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 SecondsElapsed = (CurrentTime-InitialTime)/1000
    SecondsElapsed=Math.round(SecondsElapsed)
    var MinutesElapsed = 0
    while (SecondsElapsed>=60) {
        SecondsElapsed-=60
        MinutesElapsed++
    }
    TimeSpan.innerHTML=" Time elapsed: "
    if (MinutesElapsed!=0) TimeSpan.innerHTML+=MinutesElapsed+" min "
    TimeSpan.innerHTML+=SecondsElapsed+" sec"

    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( 'csrfToken' )
        },
        error: function(ajaxResponse,status,errorThrown) {ajaxResponse.errorThrown=errorThrown},
        complete: function(ajaxResponse,status) {PageEdited(ajaxResponse,status,text)}
    })
}

function getWikitext(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 {
                SetupTables()
            }
        }
    })
}

function getProgressListWikitext(title,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: title,
            indexpageids: true,
            redirects: 'true'
        },
        error: function(ajaxResponse,status,errorThrown) {ajaxResponse.errorThrown=errorThrown},
        complete: function(ajaxResponse,status) {WikitextFetched(ajaxResponse,status,title,currentTable,currentRow)}
    })
}

$(window).on('load', ProgressButton);