Timeline Chart.fh_lua

--[[
@Title: Timeline Chart
@Author: Jane Taubman
@LastUpdated: 30 Nov 2020
@Version: 1.8
@Description: 
Creates a web page containing a timeline to display the events of persons life. 
Can either create a temporary page to see the currently selected person, or can select a group of people and create a whole set of pages for use on a web site.

Please note the exported pages may not display correctly until loaded on a web site, as local file restrictions on modern browsers prevent the data being loaded.

Uses SIMILE Widgets to display the time line.
V1.1 Fix issue with multiple Families with Parents Deaths
V1.2 Use 2.3.1 SIMILE Widgets
V1.3 Fix issue with year only births, better support for extended character sets.
V1.4 Use HTA files for the quick view to avoid IE and Chrome issues
V1.5 Add Sibling Births and Deaths in the Lifetime
V1.6 Adjust Timeline to Window Size and update page link to new code on Smilie (thanks Mike)
V1.7 Add Spouse Death to Timeline
V1.8 V7 Compatibility 
]]
------------------------------------------------------------------- Main Code
require "iuplua"
iup.SetGlobal("CUSTOMQUITMESSAGE","YES")

function main()
    settingsFile = fhGetPluginDataFileName()
    iAction = iup.Alarm('Select Timeline Action','Please Select your option from the items below:',
    'Quick View Current Record',
    'Select Records and Create Pages for All')
    if iAction == 2 then
        outputDir = getOutPutDir()
        if outputDir == nil then
            return
        end
        ptrList = fhPromptUserForRecordSel('INDI')
        if ptrList == nil then
            return
        end
    end
    if iAction == 1 then
        ptrList = fhGetCurrentRecordSel('INDI')
        if #ptrList >= 1 then
            ptrIndi = ptrList[1]:Clone()
            ptrList = {ptrIndi}
        else
            fhMessageBox('Please select a record before running this Plugin')
            return
        end
    end
    if iAction == 0 then
        return
    end
    local ptrEvent = fhNewItemPtr()
    local ptrFam = fhNewItemPtr()
    local ptrChild = fhNewItemPtr()
    local ptrDate = fhNewItemPtr()
    ---------------------------------------------------------- Loop all selected Records
    for i,ptrIndi in pairs(ptrList) do
        tblPlaces = {}
        tblEvents = {}
        strEndLabel = 'Death'
        birthdate = fhCallBuiltInFunction('EstimatedBirthDate',ptrIndi, 'EARLIEST')
        deathdate = fhCallBuiltInFunction('EstimatedDeathDate',ptrIndi, 'LATEST')
        if deathdate:IsNull() then
            deathdate = fhCallBuiltInFunction('Today')
            strEndLabel = 'Today'
        end
        xmlBirth = buildDate(birthdate)
        xmlDeath = buildDate(deathdate)
        ---------------------------------------------------------- Individual Events
        ptrEvent:MoveToFirstChildItem(ptrIndi)
        while ptrEvent:IsNotNull() do
            if fhIsFact(ptrEvent) then
                addFact(ptrEvent)
            end
            ptrEvent:MoveNext('ANY')
        end
        --------------------------------------------------------- Family Events
        i = 1
        ptrFam:MoveTo(ptrIndi,'~.FAMS['..i..']>')
        while ptrFam:IsNotNull() do
            --- Process Family Events
            ptrEvent:MoveToFirstChildItem(ptrFam)
            while ptrEvent:IsNotNull() do
                if fhIsFact(ptrEvent) then
                    addFact(ptrEvent,'with '..fhGetItemText(ptrIndi,'~.~SPOU['..i..']>NAME') ..': '..fhGetDisplayText(ptrEvent))
                end
                ptrEvent:MoveNext('ANY')
            end
            --- Process Spouses Death
            local ptrSpouse = fhGetItemPtr(ptrIndi,'~.~SPOU['..i..']>')
            if ptrSpouse:IsNotNull() then
                        ptrEvent:MoveTo(ptrSpouse,'~.DEAT')
              if fhIsFact(ptrEvent) then
                addFact(ptrEvent,'Spouse:'..fhGetItemText(ptrSpouse,'~.NAME') ..': '..fhGetDisplayText(ptrEvent))
              end
			 end
            --- Process Child Births
            c = 1
            ptrChild:MoveTo(ptrFam,'~.CHIL['..c..']>')
            while ptrChild:IsNotNull() do
                ptrEvent:MoveTo(ptrChild,'~.BIRT')
                if fhIsFact(ptrEvent) then
                    addFact(ptrEvent,'Child: '..fhGetItemText(ptrChild,'~.NAME') ..' '..fhGetDisplayText(ptrEvent))
                end
                -- Death of Child before Parent
                ptrEvent:MoveTo(ptrChild,'~.DEAT')
                if fhIsFact(ptrEvent) then
                    ptrDate:MoveTo(ptrEvent,'~.DATE')
                    ChildDeath = fhGetValueAsDate(ptrDate)
                    if deathdate:Compare(ChildDeath:GetDatePt1()) > 0 then
                        addFact(ptrEvent,'Child: '..fhGetItemText(ptrChild,'~.NAME') ..' '..fhGetDisplayText(ptrEvent))
                    end
                end
                c= c + 1
                ptrChild:MoveTo(ptrFam,'~.CHIL['..c..']>')
            end
            i = i + 1
            ptrFam:MoveTo(ptrIndi,'~.FAMS['..i..']>')
        end
        -- Get Parents Deaths
        i = 1
        ptrFam:MoveTo(ptrIndi,'~.FAMC['..i..']>')
        while ptrFam:IsNotNull() do
            ptrParent = fhGetItemPtr(ptrFam,'~.HUSB>')
            ptrEvent:MoveTo(ptrParent,'~.DEAT')
            if fhIsFact(ptrEvent) then
                addFact(ptrEvent,'Father:'..fhGetItemText(ptrParent,'~.NAME') ..': '..fhGetDisplayText(ptrEvent))
            end
            ptrParent = fhGetItemPtr(ptrFam,'~.WIFE>')
            ptrEvent:MoveTo(ptrParent,'~.DEAT')
            if fhIsFact(ptrEvent) then
                addFact(ptrEvent,'Mother:'..fhGetItemText(ptrParent,'~.NAME') ..': '..fhGetDisplayText(ptrEvent))
            end
            -- Sibling Births and Deaths in Lifetime
            c = 1
            ptrChild:MoveTo(ptrFam,'~.CHIL['..c..']>')
            while ptrChild:IsNotNull() do
                if not(ptrChild:IsSame(ptrIndi)) then
                    -- Birth of Younger Sibling
                    ptrEvent:MoveTo(ptrChild,'~.BIRT')
                    if fhIsFact(ptrEvent) then
                        ptrDate:MoveTo(ptrEvent,'~.DATE')
                        local sibBirthDate = fhGetValueAsDate(ptrDate)
                        if birthdate:Compare(sibBirthDate:GetDatePt1()) < 0 then
                            addFact(ptrEvent,'Sibling: '..fhGetItemText(ptrChild,'~.NAME') ..' '..fhGetDisplayText(ptrEvent))
                        end
                    end
                    -- Death of Sibling before Individual
                    ptrEvent:MoveTo(ptrChild,'~.DEAT')
                    if fhIsFact(ptrEvent) then
                        ptrDate:MoveTo(ptrEvent,'~.DATE')
                        ChildDeath = fhGetValueAsDate(ptrDate)
                        if deathdate:Compare(ChildDeath:GetDatePt1()) > 0 and birthdate:Compare(ChildDeath:GetDatePt1()) < 0 then
                            addFact(ptrEvent,'Sibling: '..fhGetItemText(ptrChild,'~.NAME') ..' '..fhGetDisplayText(ptrEvent))
                        end
                    end
                end
                c= c + 1
                ptrChild:MoveTo(ptrFam,'~.CHIL['..c..']>')
            end
            -- End Siblings
            i = i + 1
            ptrFam:MoveTo(ptrIndi,'~.FAMC['..i..']>')
        end
        strEndLabel = 'Death'
        birthdate = fhCallBuiltInFunction('EstimatedBirthDate',ptrIndi, 'EARLIEST')
        deathdate = fhCallBuiltInFunction('EstimatedDeathDate',ptrIndi, 'LATEST')
        if deathdate:IsNull() then
            deathdate = fhCallBuiltInFunction('Today')
            strEndLabel = 'Today'
        end
        xmlBirth = buildDate(birthdate,'min')
        xmlDeath = buildDate(deathdate,'max')
        ------------------------------------ Build Time Line XML for Html
        strEvents = ''
        xmlData = '\n'
        simpleSort2dDate(tblEvents,2)
        for i,event in pairs(tblEvents) do
            strEvents = strEvents..'
  • '..event[1]..'
  • \n' xmlLine = strEventTemplate if not(event[2]:IsNull()) then startDate = event[2]:GetDatePt1() endDate = event[2]:GetDatePt2() xmlStart = buildDate(startDate) xmlLine = xmlLine:gsubplain('{start}',xmlStart) if endDate:IsNull() then xmlLine = xmlLine:gsubplain('end="{end}"',' ') else xmlEnd = buildDate(endDate) xmlLine = xmlLine:gsubplain('{end}',xmlEnd) end xmlLine = xmlLine:gsubplain('{note}',StrHTML_Encode(event[4])) xmlLine = xmlLine:gsubplain('{title}',StrHTML_Encode(event[1])) xmlData = xmlData..xmlLine end end xmlData = xmlData..'
    ' ----------------------------------- Output Web Page and XML file if #tblEvents > 0 then htmlPage = htmlMasterPage htmlPage = htmlPage:gsubplain('{name}',fhGetDisplayText(ptrIndi)) htmlPage = htmlPage:gsubplain('{ref}',fhGetRecordId(ptrIndi)) htmlPage = htmlPage:gsubplain('{title}','The life of '..fhGetDisplayText(ptrIndi)) htmlPage = htmlPage:gsubplain('{startdate}',xmlBirth) htmlPage = htmlPage:gsubplain('{birthyear}',birthdate:GetYear()) htmlPage = htmlPage:gsubplain('{enddate}',xmlDeath) htmlPage = htmlPage:gsubplain('{endlabel}',strEndLabel) if outputDir == nil then -- Create Temp Filename filename = os.getenv('TEMP')..'\\timeline.hta' xmlfile = os.getenv('TEMP')..'\\timeline.xml' xmlshort = 'timeline.xml' else filename = outputDir..'\\timeI'..fhGetRecordId(ptrIndi)..'.html' xmlfile = outputDir..'\\timeI'..fhGetRecordId(ptrIndi)..'.xml' xmlshort = 'timeI'..fhGetRecordId(ptrIndi)..'.xml' end htmlPage = htmlPage:gsubplain('{xmlfilename}',xmlshort) savestringtofile(htmlPage,filename) savestringtofile(xmlData,xmlfile) if #ptrList == 1 then bOK, iErrorCode, strErrorText = fhShellExecute(filename) if not(bOK) then fhMessageBox(strErrorText..' ('..iErrorCode..')') end end else fhMessageBox('No Events with Places found for '..fhGetDisplayText(ptrIndi)..' No Map Created') end end if #ptrList > 1 then bOK, iErrorCode, strErrorText = fhShellExecute(outputDir) if not(bOK) then fhMessageBox(strErrorText..' ('..iErrorCode..')') end end end ------------------------------------------------------------- Functions function string.gsubplain (str,sSearch,sReplace) str = str:gsub(sSearch, function() return sReplace end) return str end function dumptable(tbl) -- Function dumptable -- Debug Dump Table to Output Pane -- params tbl for i,d in pairs(tbl) do print(i,d) end end function StrHTML_Encode(strHTML) -- Encode HTML symbols into &...; escape sequences -- -- "\n" newline is encoded as "
    " tag, which must be unencoded from "<br>" to "
    " local tblEncodings = { {"\n","
    "}, {"%c"," "}, {"&","&"}, {"<","<"}, {">",">"}, {"\'","'"},{"\"","""}, {"€","€"}, {"™","™"},{"¢","¢"}, {"¤","¤"},{"¥","¥"}, {"<br>","
    "}, {"["..string.char(127).."-"..string.char(255).."]", function (char) return "&#"..string.byte(char)..";" end }, } if strHTML then for i, tblEncode in ipairs(tblEncodings) do strHTML = strHTML:gsub(tblEncode[1],tblEncode[2]) end else strHTML = "" end return strHTML end -- function StrHTML_Encode function savestringtofile(str,filename) ------------------------------------------------ Save String To File local file = assert(io.open(filename,'w')) file:write(str) file:close() end function addFact(ptrEvent, strFact) ------------------------------------------------ Add Fact to List local dtDate = fhNewDate() local datePtr = fhNewItemPtr() datePtr:MoveTo(ptrEvent,'~.DATE') if datePtr:IsNotNull() then dtDate = fhGetValueAsDate(datePtr) else dtDate:SetSimpleDate(fhCallBuiltInFunction('Today')) end strPlace = fhGetItemText(ptrEvent,'~.PLAC') if strFact == nil then strFact = (fhGetDisplayText(ptrEvent)) end local strNote = (fhGetItemText(ptrEvent,'~.NOTE2')) if strNote == nil then strNote = ' ' end table.insert(tblEvents,{strFact,dtDate:Clone(),strPlace,strNote}) end function simpleSort2dDate(tbl,index) -------------------------------------------------------------------- Sort Event Table by Date table.sort(tbl,function(a, b) return a[index]:Compare(b[index]) == -1 end) end function getOutPutDir() -------------------------------------------------------------------- Select Directory local dir = fhGetContextInfo('CI_PROJECT_PUBLIC_FOLDER') filedlg = iup.filedlg{dialogtype = "DIR", title = "Please select destination directory", DIRECTORY=dir} -- Shows file dialog in the center of the screen filedlg:popup (iup.ANYWHERE, iup.ANYWHERE) -- Gets file dialog status status = filedlg.status if status == "-1" then iup.Message("Timeline Chart","Cancelled") return nil end return filedlg.value end function buildDate(datept,type) ----------------------------------------------------------- Build Date if type == nil then type = 'min' end local strDate = '' tblMonthName = {'Jan','Feb','Mar','Apr','May','June','Jul','Aug','Sep','Oct','Nov','Dec'} local iMonth = datept:GetMonth() local iDay = datept:GetDay() local iYear = datept:GetYear() if iMonth == 0 then if type == 'min' then iMonth = 1 else iMonth = 12 end end if iDay == 0 then if type == 'min' then iDay = 1 else iDay = 28 end end if iMonth > 0 and iDay > 0 then strDate = strDate..tblMonthName[iMonth] if iDay > 0 then strDate = strDate..' '..iDay end strDate = strDate..' '..iYear else strDate = iYear end return strDate end ------------------------------------------------ Templates htmlMasterPage = [[ {title}

    The Life of {name}

    ]] strEventTemplate = '{note}\n' ------------------------------------------------------------- MonthNames tblMonthName = {'Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'} --------------------------------------------------- Call Main Function main()

    Source:Timeline-Chart.fh_lua