Anatomy of a Plugin in Family Historian.

On the FHU mailing list Kath asked

is there yet any way to export a GEDCOM file contain only Name, Birth and Death?

I want to include Birth and Death places without any qualification like
Address, Age, Cause of Death, also Name without Known As etc. ONLY basic basic basic.


[FHU] GEDCOM basic export 28th Nov 2018

The answer is of course yes,  you need to just to write a plugin,  or use the “Clean Unwanted Fields” plugin already available in the plugin store.  As a test I spent an hour writing a simple plugin to perform the task and thought it would be a good idea to do a walk through of how the plugin works, in case anyone needs to write something similar.

Rather than writing a plugin to export a cut down file,  it’s much easier to delete everything you don’t need from a copy of the project.  So the plugin needs to delete all events other than those required. Delete all unneeded record types. Delete all subfields from the NAME and remove flags.

When planning any program I try to use functions when ever possible to prevent repeating code.  With this plugin it’s not completely optimised,  but I have used both functions from the Code Snippets on FHUG as well as a couple functions written direct for this plugin.

The first thing to do is to set up a basic plugin using the Insert Header option on the plugin editor and then adding a main function and a call to it.

1
2
3
4
5
6
7
8
9
10
11
12
13
--[[
@Title: BMD Only Gedcom
@Author: Jane Taubman  
@Version: 0.1
@LastUpdated: Nov 2018
@Description: Deletes all Facts other than Birth, Marriage and Death, and all Sources, Media, Repositories and Submitters
]]

function main()

end
--------------------------------------------------- Additional Functions
--------------------------------------------------- Call Main
main()

The Main Function

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
function main()
    local sMsg = "Warning:\n This plugin is designed to delete all data other than BMD Dates and Places from the current file.\n"
   
    if fhGetContextInfo('CI_APP_MODE') == 'Project Mode' then
        sMsg = sMsg.."/nYou have a Project open. \n\nPlease confirm you want to delete the data on"
        ..fhGetContextInfo('CI_PROJECT_NAME')
    else
        sMsg = sMsg.."\n\nPlease confirm you want to delete the data on "
        ..fhGetContextInfo('CI_GEDCOM_FILE')
    end
    ret = fhMessageBox(sMsg,'MB_OKCANCEL','MB_ICONEXCLAMATION')
    if ret ~= 'OK' then
        return
    end
    for pi in records('INDI') do
        clnFacts(pi,{'BIRT','DEAT'})
    end
    for pf in records('FAM') do
        clnFacts(pf,{'MARR'})
    end
    -- Delete all unused Place records
    dltRecords('_PLAC',false)
    -- Delete All Unneeded record types
    dltRecords('NOTE',true)
    dltRecords('SOUR',true)
    dltRecords('OBJE',true)
    dltRecords('REPO',true)
    dltRecords('SUBM',true)
    fhMessageBox('Process Complete','MB_OK','MB_ICONINFORMATION')
end

The main function actions the deletes which Kath requested, after prompting the user to confirm they want to delete the data.

  • Lines 1-14 Prompt the User
  • Lines 15-17 Loops through all the Individual Records deleting all Events other than Birth and Death
  • Lines 18-20 Loop through the Family Records deleteing everything other than Marriage.
  • Line 22 Removes all unused Place Records using dltRecords
  • Lines 25-28 Remove all Note, Source, Media, Repository and Submitter Records using dltRecords.

 I prefer to use a function for the core logic and add a simple

1
main()

to the end of the source.  The reasons for this are:

  1. Functions used must have already been defined in the source
  2. Using a function for main means you can put the core logic at the top of the source code.

There are quite a few standard calls in the main function, such as

  • fhMessageBox
  • fhContextInfo

Built in Family Historian functions normally start with fh and are documented in the plugin help.

There are also several functions, which are included in the source of the plugin which I will explain below, the linked functions come from the Code snippets section of the Knowledge base.

Functions

clnFacts

clnFacts takes two parameters, a pointer to a record and a table containing the Facts to keep.  When called from main, the table contains either BIRT and DEAT (16) or MARR (19).

It takes a two pass approach to deleting the extra facts. It first creates a local table to contain all the items to be deleted, then uses the table to remove them.  Note the use of Clone when adding the pointer to the table, if you don’t do this, you will end up with a table all of the entries point to the last fact returned.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function clnFacts(p,tblKeep)
    local tblFacts = {}
    for px in facts(p) do
        table.insert(tblFacts,px:Clone())
    end
    -- Delete un-needed facts
    for _,pFact in ipairs(tblFacts) do
        local sType = fhGetTag(pFact)
        if not(inList(tblKeep,sType)) then
            fhDeleteItem(pFact)
        else
            stripFact(pFact,{'DATE','PLAC'})
        end
    end
    -- Clean up other items
    stripItem(p,'NAME',{})
    dltItem(p,'_FLGS',{})
    dltItem(p,'NOTE2',{})
end

dltItem

The dltItem takes two parameters a Record level pointer and a tag to remove, it works in the same way as clnFacts building a list and then deleting all the items in the list.

1
2
3
4
5
6
7
8
9
10
11
12
13
function dltItem(p,sTag)
    local tblLine = {}
    local pc = fhNewItemPtr() -- declare pointer
    local sTag2 = "~."..sTag
    pc:MoveTo(p,sTag2)
    while pc:IsNotNull() do
        table.insert(tblLine,pc:Clone())
        pc:MoveNext("SAME_TAG")
    end
    for _,pLine in ipairs(tblLine) do
        fhDeleteItem(pLine)
    end
end

stripFact

The stripFact function takes two parameters,  the first is a pointer to a Fact and the second a list of child items to retain.   When called from clnFact it will have DATE and PLAC as the child items to retain. Again it builds the list and then deletes the listed items.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function stripFact(pFact,tblKeep)
    local tblLine = {}
    local pc = fhNewItemPtr() -- declare pointer
    pc:MoveToFirstChildItem(pFact)
    while pc:IsNotNull() do
        local sType = fhGetTag(pc)
        if not(inList(tblKeep,sType)) then
            table.insert(tblLine,pc:Clone())
        end
        pc:MoveNext("SAME_TAG")
    end
    for _,pLine in ipairs(tblLine) do
        fhDeleteItem(pLine)
    end
end

stripItem

stripItem takes two parameters a record pointer and a tag name,  it’s similar to dltItem, but it only removes the child items of the item provided,  so for example called with a Tag of NAME it will remove fields such as Given name used, but leave the NAME value intact.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function stripItem(pi,sTag)
    local tblLine = {}
    local pc = fhNewItemPtr() -- declare pointer
    local sTag2 = "~."..sTag
    pc:MoveTo(pi,sTag2)
    pc:MoveToFirstChildItem(pc)
    while pc:IsNotNull() do
        table.insert(tblLine,pc:Clone())
        pc:MoveNext("SAME_TAG")
    end
    for _,pLine in ipairs(tblLine) do
        fhDeleteItem(pLine)
    end
end

dltRecords

dltRecords takes two parameters a record type value and a boolean (true, false) which if set to true deletes all records, where as false only deletes unused records.

It uses a slightly different method of looping through the list as it uses two pointers one to step through the list and one to delete the previous record,  if you just deleted the pointer directly you would lose your place in the records list.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function dltRecords(sType,bAll)
    local ptr = fhNewItemPtr()
    local dltptr = fhNewItemPtr()
    ptr:MoveToFirstRecord(sType)
    while ptr:IsNotNull() do
        lnks = fhCallBuiltInFunction('LinksTo',ptr)
        if lnks == 0 or bAll then
            dltptr:MoveTo(ptr)
            ptr:MoveNext()
            fhDeleteItem(dltptr)
        else
            ptr:MoveNext()
        end
    end
end

You can download the full source code below


Leave a Reply

Your email address will not be published. Required fields are marked *