Plugins and Language Packs for Family Historian

Map Life Facts.fh_lua

--[[
@Title:			Map Life Facts
@Type:				Standard
@Author:			Mike Tate
@Contributors:	
@Version:			6.1
@Keywords:		
@LastUpdated:		08 Oct 2026
@Licence:			This plugin is copyright (c) 2026 Mike Tate & contributors and is licensed under the MIT License which is hereby incorporated by reference (see https://pluginstore.family-historian.co.uk/fh-plugin-licence)
@Description:		Creates maps for web pages by using online mapping tools to display Locations for each persons Facts.
@Version Log:		See end of file.
]]
 
if fhGetAppVersion() > 5 then fhSetStringEncoding("UTF-8") end

--[[
@Title:			aa Library Functions Preamble
@Author:			Mike Tate
@Version:			4.1
@LastUpdated:		18 Feb 2026
@Description:		All the library functions prototype closures for Plugins.
]]

--[[
@Module:			+fh+stringx_v3
@Author:			Mike Tate
@Version:			3.0
@LastUpdated:	19 Sep 2020
@Description:	Extended string functions to supplement LUA string library.
@V3.0:				Function Prototype Closure version with Lua 5.1 & 5.3 comaptibility; Added inert(strTxt) function;
@V2.5:				Support FH V6 Encoding = UTF-8;
@V2.4:				Tolerant of integer & nil parameters just link match & gsub;
@V1.0:				Initial version.
]]

local function stringx_v3()

	local fh = {}									-- Local environment table

	-- Supply current file encoding format --
	function fh.encoding()
		if fhGetAppVersion() > 5 then return fhGetStringEncoding() end
		return "ANSI"
	end -- function encoding

	-- Split a string using "," or chosen separator --
	function fh.split(strTxt,strSep)
		local tblFields = {}
		local strPattern = string.format("([^%s]+)", strSep or ",")
		strTxt = tostring(strTxt or "")
		strTxt:gsub(strPattern, function(strField) tblFields[#tblFields+1] = strField end)
		return tblFields
	end -- function split

	-- Split a string into numbers using " " or "," or "x" separators	-- Any non-number remains as a string
	function fh.splitnumbers(strTxt)
		local tblNum = {}
		strTxt = tostring(strTxt or "")
		strTxt:gsub("([^ ,x]+)", function(strNum) tblNum[#tblNum+1] = tonumber(strNum) or strNum end)
		return tblNum
	end -- function splitnumbers

	local strMagic = "([%^%$%(%)%%%.%[%]%*%+%-%?])"							-- UTF-8 replacement for "(%W)"

	-- Hide magic pattern symbols	^ $ ( ) % . [ ] * + - ?
	function fh.plain(strTxt)
		-- Prefix every magic pattern character with a % escape character,
		-- where %% is the % escape, and %1 is the original character capture.
		strTxt = tostring(strTxt or ""):gsub(strMagic,"%%%1")
		return strTxt
	end -- function plain

	-- matches is plain text version of string.match()
	function fh.matches(strTxt,strFind,intInit)
		strFind = tostring(strFind or ""):gsub(strMagic,"%%%1")			-- Hide magic pattern symbols
		return tostring(strTxt or ""):match(strFind,tonumber(intInit))
	end -- function matches

	-- replace is plain text version of string.gsub()
	function fh.replace(strTxt,strOld,strNew,intNum)
		strOld = tostring(strOld or ""):gsub(strMagic,"%%%1")				-- Hide magic pattern symbols
		return tostring(strTxt or ""):gsub(strOld,function() return strNew end,tonumber(intNum))	-- Hide % capture symbols
	end -- function replace

	-- Hide % escape/capture symbols in replacement so they are inert
	function fh.inert(strTxt)
		strTxt = tostring(strTxt or ""):gsub("%%","%%%%")					-- Hide all % symbols
		return strTxt
	end -- function inert

	-- convert is pattern without captures version of string.gsub()
	function fh.convert(strTxt,strOld,strNew,intNum)
		return tostring(strTxt or ""):gsub(tostring(strOld or ""),function() return strNew end,tonumber(intNum))	-- Hide % capture symbols
	end -- function convert

	local dicUpper = { }
	local dicLower = { }
	local dicCaseX = { }
	-- ASCII unaccented letter translations for Upper, Lower, and Case Insensitive
	for intUpper = string.byte("A"), string.byte("Z") do
		local strUpper = string.char(intUpper)
		local strLower = string.char(intUpper - string.byte("A") + string.byte("a"))
		dicUpper[strLower] = strUpper
		dicLower[strUpper] = strLower
		local strCaseX = "["..strUpper..strLower.."]"
		dicCaseX[strLower] = strCaseX
		dicCaseX[strUpper] = strCaseX
	end

	-- Supply character length of ANSI text --
	function fh.length(strTxt)
		return string.len(strTxt or "")
	end -- function length

	-- Supply character substring of ANSI text --
	function fh.substring(strTxt,i,j)
		return string.sub(strTxt or "",i,j)
	end -- function substring

	-- Translate upper/lower case ANSI letters to pattern that matches both --
	function fh.caseless(strTxt)
		strTxt = tostring(strTxt or ""):gsub("[A-Za-z]",dicCaseX)
		return strTxt
	end -- function caseless

	if fh.encoding() == "UTF-8" then

		-- Supply character length of UTF-8 text --
		function fh.length(strTxt)
			isFlag = fhIsConversionLossFlagSet()
			strTxt = fhConvertUTF8toANSI(strTxt or "")
			fhSetConversionLossFlag(isFlag)
			return string.len(strTxt)
		end -- function length

		local strUTF8 = "([%z\1-\127\194-\244][\128-\191]*)"			-- Cater for Lua 5.1 %z or Lua 5.3 \0
		if fhGetAppVersion() > 6 then
			strUTF8 = "([\0-\127\194-\244][\128-\191]*)"
		end

		-- Supply character substring of UTF-8 text --
		function fh.substring(strTxt,i,j)
			local strSub = ""
			j = j or -1
			if j < 0 then j = j + length(strTxt) + 1 end
			if i < 0 then i = i + length(strTxt) + 1 end
			for strChr in string.gmatch(strTxt or "",strUTF8) do
				if j <= 0 then break end
				j = j - 1
				i = i - 1
				if i <= 0 then strSub = strSub..strChr end
			end
			return strSub
		end -- function substring

		-- Translate lower case to upper case UTF-8 letters --
		function fh.upper(strTxt)
			strTxt = tostring(strTxt or ""):gsub("([a-z\194-\244][\128-\191]*)",dicUpper)
			return strTxt
		end -- function upper

		-- Translate upper case to lower case UTF-8 letters --
		function fh.lower(strTxt)
			strTxt = tostring(strTxt or ""):gsub("([A-Z\194-\244][\128-\191]*)",dicLower)
			return strTxt
		end -- function lower

		-- Translate upper/lower case UTF-8 letters to pattern that matches both --
		function fh.caseless(strTxt)
			strTxt = tostring(strTxt or ""):gsub("([A-Za-z\194-\244][\128-\191]*)",dicCaseX)
			return strTxt
		end -- function caseless

		-- Following tables use ASCII numeric coding to be immune from ANSI/UTF-8 encoding --

		local arrPairs =	-- Upper & Lower case groups of UTF-8 letters with same prefix --
		{--	{ Prefix; Beg ; End ; Inc; Offset Upper > Lower };			-- These include all ANSI letters and many more
			{ "\195"; 0x80; 0x96;  1 ; 32 };								-- 195=0xC3 À U+00C0 to Ö U+00D6 and à U+00E0 to ö U+00F6
			{ "\195"; 0x98; 0x9E;  1 ; 32 };								-- 195=0xC3 Ø U+00D8 to Þ U+00DE and ø U+00F8 to þ U+00FE
			{ "\196"; 0x80; 0xB6;  2 ;  1 };								-- 196=0xC4 A U+0100 to k U+0137 in pairs
			{ "\196"; 0xB9; 0xBD;  2 ;  1 };								-- 196=0xC4 L U+0139 to l U+013E in pairs
			{ "\197"; 0x81; 0x87;  2 ;  1 };								-- 197=0xC5 L U+0141 to n U+0148 in pairs
			{ "\197"; 0x8A; 0xB6;  2 ;  1 };								-- 197=0xC5 ? U+014A to y U+0177 in pairs
			{ "\197"; 0xB9; 0xBD;  2 ;  1 };								-- 197=0xC5 Z U+0179 to ž U+017E in pairs
			{ "\198"; 0x82; 0x84;  2 ;  1 };								-- 198=0xC6 ?  U+0182 to ?  U+0185 in pairs
			-- Add more Unicode groups here as usage increases --
		}
		local dicPairs =	-- Upper v Lower case UTF-8 letters that don't fit groups above --
		{	[string.char(0xC4,0xBF)] = string.char(0xC5,0x80);			-- ? U+013F and ? U+0140
			[string.char(0xC5,0xB8)] = string.char(0xC3,0xBF);			-- Ÿ U+0178 and ÿ U+00FF
		}

		local intBeg1 = string.byte(string.sub("À",1))
		local intBeg2 = string.byte(string.sub("À",2))
		local intEnd1 = string.byte(string.sub("Z",1))
		local intEnd2 = string.byte(string.sub("Z",2))
	--	print(string.format("%#x %#x %#x %#x",intBeg1,intBeg2,intEnd1,intEnd2)) -- Useful to work out numeric coding

		-- Populate the UTF-8 letter translation dictionaries --
		for intGroup, tblGroup in ipairs ( arrPairs ) do				-- UTF-8 accented letter groups
			local strPrefix = tblGroup[1]
			for intUpper = tblGroup[2], tblGroup[3], tblGroup[4] do
				local strUpper = string.char(intUpper)
				local strLower = string.char(intUpper + tblGroup[5])
				local strCaseX = strPrefix.."["..strUpper..strLower.."]"
				strUpper = strPrefix..strUpper
				strLower = strPrefix..strLower
				dicUpper[strLower] = strUpper
				dicLower[strUpper] = strLower
				dicCaseX[strLower] = strCaseX
				dicCaseX[strUpper] = strCaseX
			end
		end
		for strUpper, strLower in pairs ( dicPairs ) do					-- UTF-8 accented letters where upper & lower have different prefix
			dicUpper[strLower] = strUpper
			dicLower[strUpper] = strLower
			local strCaseX = ""
			for intByte = 1, #strUpper do									-- Matches more than just the two letters, but can't do any better
				strCaseX = strCaseX.."["..strUpper:sub(intByte,intByte)..strLower:sub(intByte,intByte).."]"
			end
			dicCaseX[strLower] = strCaseX
			dicCaseX[strUpper] = strCaseX
		end

	end

	-- overload fh functions into string table
	for strIndex, anyValue in pairs(fh) do
		if type(anyValue) == "function" then
			string[strIndex] = anyValue
		end
	end

	return fh

end -- local function stringx_v3

local stringx = stringx_v3()													-- To access FH string extension module

--[[
@Module:			+fh+iterate_v3
@Author:			Mike Tate
@Version:			3.0
@LastUpdated:	25 Aug 2020
@Description:	An iterater functions module to supplement LUA functions.
@V3.0:				Function Prototype Closure version.
@V1.2:				RecordTypes() includes HEAD tag.
@V1.1:				?
@V1.0:				Initial version.
]]

local function iterate_v3()

	local fh = {}																-- Local environment table

	-- Iterator for all records of one chosen type --
	function fh.Records(strType)
		local ptrAll = fhNewItemPtr()										-- Pointer to all records in turn
		local ptrRec = fhNewItemPtr()										-- Pointer to record returned to user
		ptrAll:MoveToFirstRecord(strType)
		return function ()
			ptrRec:MoveTo(ptrAll)
			ptrAll:MoveNext()
			if ptrRec:IsNotNull() then return ptrRec end
		end
	end -- function Records

	-- Iterator for all the record types --
	function fh.RecordTypes()
		local intNext = -1														-- Next record type number
		local intLast = fhGetRecordTypeCount()								-- Last record type number
		return function()
			intNext = intNext + 1
			if intNext == 0 then												-- Includes HEAD tag -- V1.2
				return "HEAD"
			elseif intNext <= intLast then
				return fhGetRecordTypeTag(intNext)							-- Return record type tag
			end
		end
	end -- function RecordTypes

	-- Iterator for all items in all records of chosen types --
	function fh.Items(...)
		local arg = {...}
		local intType = 1														-- Integer record type number
		local tblType = {}														-- Table of record type tags
		local ptrNext = fhNewItemPtr()										-- Pointer to next item in turn
		local ptrItem = fhNewItemPtr()										-- Pointer to item returned to user

		if #arg == 0 then
			for intType = 1, fhGetRecordTypeCount() do					-- No parameters so use all record types
				tblType[intType] = fhGetRecordTypeTag(intType)
			end
		else
			tblType = arg														-- Got parameters so use them instead
		end
	--	print(tblType[intType],intType)
		ptrNext:MoveToFirstRecord(tblType[intType])						-- Get first record of first type

		return function()
			repeat
				while ptrNext:IsNotNull() do									-- Loop through all items
					ptrItem:MoveTo(ptrNext)
					ptrNext:MoveNextSpecial()
					if ptrItem:IsNotNull() then return ptrItem end
				end
				intType = intType + 1											-- Loop through each record type
				if intType <= #tblType then
					ptrNext:MoveToFirstRecord(tblType[intType])
				end
			until intType > #tblType
		end
	end -- function Items

	-- Iterator for all facts of an individual --
	function fh.Facts(ptrIndi)
		local ptrItem = fhNewItemPtr()										-- Pointer to each item at level 1
		local ptrFact = fhNewItemPtr()										-- Pointer to each fact returned to user
		ptrItem:MoveToFirstChildItem(ptrIndi)
		return function ()
			while ptrItem:IsNotNull() do
				ptrFact:MoveTo(ptrItem)
				ptrItem:MoveNext()
				if fhIsFact(ptrFact) then return ptrFact end
			end
		end
	end -- function Facts

	return fh

end -- local function iterate_v3

local iterate = iterate_v3()													-- To access FH iterate items module

--[[
@Module:			+fh+general_v3
@Author:			Mike Tate
@Version:			3.5
@LastUpdated:	12 Dec 2024
@Description:	A general functions module to supplement LUA functions, where filenames use UTF-8 but for a few exceptions.
@V3.5:				Further fix for Crossover/WINE file attributes;
@V3.4:				Further fix for Unix/WINE file attributes;
@V3.3:				Fix problem in fh.MakeFolder(...) when folder path is invalid; Remove Type, ShortName & ShortPath from attributes(...) as unsupported in WINE;
@V3.2:				Added function DetectOldModules(); Updated functions RenameFile(), RenameFolder() & GetFolderContents();
@V3.1:				Functions derived from FH V7 fhFileUtils library using File System Objects, plus additional features;
@V3.0:				Function Prototype Closure version; GetDayNumber() error message reasons;
@V1.5:				Revised SplitFilename(strFilename) for missing extension.
@V1.4:				Revised EstimatedBirthDates() & EstimatedDeathDates() to fix null Dates.
@V1.3:				Add GetDayNumber(), EstimatedBirthDates(), EstimatedDeathDates().
@V1.2:				SplitFilename() updated for directory only paths, and MakeFolder() added.
@V1.1:				pl.path experiment revoked. New DirTree with omit branch option. Avoid using stringx_v2.
@V1.0:				Initial version.
]]

local function general_v3()

	local fh = {}													-- Local environment table

	require("luacom")												-- To create File System Object
	fh.FSO = luacom.CreateObject("Scripting.FileSystemObject")

	-- Report error message --
	local function doError(strMessage,errFunction)
		-- strMessage		~ error message text
		-- errFunction		~ optional error reporting function
		if type(errFunction) == "function" then
			errFunction(strMessage)
		else
			error(strMessage)
		end
	end -- local function doError

	-- Convert filename to ANSI alternative and indicate success --
	function fh.FileNameToANSI(strFileName,strAnsiName)
		-- strFileName		~ full file path
		-- strAnsiFile		~ ANSI file name & type
		-- return values	~ ANSI file path, true if original path was ANSI compatible
		if stringx.encoding() == "ANSI" then return strFileName, true end
		local isFlag = fhIsConversionLossFlagSet()
		fhSetConversionLossFlag(false)
		local strAnsi = fhConvertUTF8toANSI(strFileName)
		local wasAnsi = true
		if fhIsConversionLossFlagSet() then
			strAnsiName = strAnsiName or "ANSI.ANSI"
			strAnsi = fhGetContextInfo("CI_APP_DATA_FOLDER").."\\Plugin Data\\"..strAnsiName
			wasAnsi = false
		end
		fhSetConversionLossFlag(isFlag)
		return strAnsi, wasAnsi
	end -- local function FileNameToANSI

	-- Get parent folder --
	function fh.GetParentFolder(strFileName)
		-- strFileName		~ full file path
		-- return value		~ parent folder path
		local strParent = fh.FSO:GetParentFolderName(strFileName)	--! Faulty in FH v6 with Unicode chars in path
		if fhGetAppVersion() == 6 then
			local _, wasAnsi = fh.FileNameToANSI(strFileName)
			if not wasAnsi then
				strParent = strFileName:match("^(.+)[\\/][^\\/]+[\\/]?$")
			end
		end
		return strParent
	end -- function GetParentFolder

	-- Check if file exists --
	function fh.FlgFileExists(strFileName)
		-- strFileName		~ full file path
		-- return value		~ true if it exists
		return fh.FSO:FileExists(strFileName)
	end -- function FlgFileExists

	-- Check if folder exists --
	function fh.FlgFolderExists(strFolderName)
		-- strFolderName	~ full file path
		-- return value		~ true if it exists
		return fh.FSO:FolderExists(strFolderName)
	end -- function FlgFolderExists

	-- Delete a file if it exists --
	function fh.DeleteFile(strFileName,errFunction)
		-- strFileName		~ full file path
		-- errFunction		~ optional error reporting function
		-- return value		~ true if file does not exist or is deleted else false
		if fh.FSO:FileExists(strFileName) then
			fh.FSO:DeleteFile(strFileName,true)
			if fh.FSO:FileExists(strFileName) then
				doError("File Not Deleted:\n"..strFileName.."\n",errFunction)
				return false
			end
		end
		return true
	end -- function DeleteFile

	-- Delete a folder if it exists including contents --
	function fh.DeleteFolder(strFolderName,errFunction)
		-- strFolderName	~ full folder path
		-- errFunction		~ optional error reporting function
		-- return value		~ true if folder does not exist or is deleted else false
		if fh.FSO:FolderExists(strFolderName) then
			fh.FSO:DeleteFolder(strFolderName,true)
			if fh.FSO:FolderExists(strFolderName) then
				doError("Folder Not Deleted:\n"..strFolderName.."\n",errFunction)
				return false
			end
		end
		return true
	end -- function DeleteFolder

	-- Rename a file if it exists --
	function fh.RenameFile(strFileName,strNewName)
		-- strFileName		~ full file path
		-- strNewName		~ new file name & type
		-- return value		~ true if file exists but new name does not and rename is OK else false
		local strNewFile = fh.GetParentFolder(strFileName).."\\"..strNewName
		if fh.FSO:FileExists(strFileName) and not fh.FSO:FileExists(strNewFile) then
			local fileObject = fh.FSO:GetFile(strFileName)
			fileObject.Name = strNewName
			if fh.FSO:FileExists(strNewFile) then
				return true
			end
		end
		return false
	end -- function RenameFile

	-- Rename a folder if it exists --
	function fh.RenameFolder(strFolderName,strNewName)
		-- strFolderName	~ full folder path
		-- strNewName		~ new folder name
		-- return value		~ true if folder exists but new name does not and rename is OK else false
		local strNewFolder = fh.GetParentFolder(strFolderName).."\\"..strNewName
		if fh.FSO:FolderExists(strFolderName) and not fh.FSO:FolderExists(strNewFolder) then
			local folderObject = fh.FSO:GetFolder(strFolderName)
			folderObject.Name = strNewName
			if fh.FSO:FolderExists(strNewFolder) then
				return true
			end
		end
		return false
	end -- function RenameFolder

	-- Copy a file if it exists and destination is not a folder --
	function fh.CopyFile(strFileName,strDestination)
		-- strFileName		~ full source file path
		-- strDestination	~ full target file path
		-- return value		~ true if file exists and is copied else false
		if fh.MakeFolder(fh.GetParentFolder(strDestination)) and fh.FSO:FileExists(strFileName) and not fh.FSO:FolderExists(strDestination) then
			fh.FSO:CopyFile(strFileName,strDestination)
			if fh.FSO:FileExists(strDestination) then
				return true
			end
		end
		return false
	end -- function CopyFile

	-- Copy a folder if it exists and destination is not a file --
	function fh.CopyFolder(strFolderName,strDestination)
		-- strFolderName	~ full source folder path
		-- strDestination	~ full target folder path
		-- return value		~ true if folder exists and is copied else false
		if fh.MakeFolder(fh.GetParentFolder(strDestination)) and fh.FSO:FolderExists(strFolderName) and not fh.FSO:FileExists(strDestination) then
			fh.FSO:CopyFolder(strFolderName,strDestination)
			if fh.FSO:FolderExists(strDestination) then
				return true
			end
		end
		return false
	end -- function CopyFolder

	-- Move a file if it exists and destination is not a folder --
	function fh.MoveFile(strFileName,strDestination)
		-- strFileName		~ full source file path
		-- strDestination	~ full target file path
		-- return value		~ true if file exists and is moved else false
		if fh.MakeFolder(fh.GetParentFolder(strDestination)) and fh.FSO:FileExists(strFileName) and not fh.FSO:FolderExists(strDestination) then
			if fh.DeleteFile(strDestination) then
				fh.FSO:MoveFile(strFileName,strDestination)
				if fh.FSO:FileExists(strDestination) then
					return true
				end
			end
		end
		return false
	end -- function MoveFile

	-- Move a folder if it exists and destination is not a file --
	function fh.MoveFolder(strFolderName,strDestination)
		-- strFolderName	~ full source folder path
		-- strDestination	~ full target folder path
		-- return value		~ true if folder exists and is moved else false
		if fh.MakeFolder(fh.GetParentFolder(strDestination)) and fh.FSO:FolderExists(strFolderName) and not fh.FSO:FileExists(strDestination) then
			if fh.DeleteFolder(strDestination) then
				fh.FSO:MoveFolder(strFolderName,strDestination)
				if fh.FSO:FolderExists(strDestination) then
					return true
				end
			end
		end
		return false
	end -- function MoveFolder

	local function CreateFolder(strFolderName)				-- V3.3
		fh.FSO:CreateFolder(strFolderName)
	end -- local function CreateFolder(strFolderName)

	-- Make subfolder recursively if does not exist --
	function fh.MakeFolder(strFolderName,errFunction)
		-- strFolderName	~ full source folder path
		-- errFunction		~ optional error reporting function
		-- return value		~ true if folder exists or created else false
		if not fh.FSO:FolderExists(strFolderName) then
			if #strFolderName > 4									-- V3.3
			and not fh.MakeFolder(fh.GetParentFolder(strFolderName),errFunction) then
				return false
			end
			if not pcall(CreateFolder,strFolderName)			-- V3.3
			or not fh.FSO:FolderExists(strFolderName) then
				doError("Cannot Make Folder Path:                 \n"..strFolderName.." \n",errFunction)
				return false
			end
		end
		return true
	end -- function MakeFolder

	-- Check if folder writable --
	function fh.FlgFolderWrite(strFolderName)
		-- strFolderName	~ full source folder path
		-- return value		~ true if folder writable else false
		if fh.FlgFolderExists(strFolderName) then
			if fh.MakeFolder(strFolderName.."\\vwxyz") then
				fh.FSO:DeleteFolder(strFolderName.."\\vwxyz",true)
				return true
			end
		end
		return false
	end -- function FlgFolderWrite

	-- Open File with ANSI path and return Handle --
	function fh.OpenFile(strFileName,strMode)
		-- strFileName		~ full file path
		-- strMode			~ "r", "w", "a" optionally suffixed with "+" &/or "b"
		-- return value		~ file handle
		local fileHandle, strError = io.open(strFileName,strMode)
		if fileHandle == nil then
			error("\n Unable to open file in \""..strMode.."\" mode. \n "..strFileName.." \n "..strError.." \n")
		end
		return fileHandle
	end -- function OpenFile

	-- Save string to file --
	function fh.SaveStringToFile(strContents,strFileName,strFormat)
		-- strContents		~ text string
		-- strFileName		~ full file path
		-- strFormat			~ optional "UTF-8" or "UTF-16LE"
		-- return value		~ true if successful else false
		strFormat = strFormat or "UTF-8"
		if fhGetAppVersion() > 6 then
			return fhSaveTextFile(strFileName,strContents,strFormat)
		end
		local strAnsi, wasAnsi = fh.FileNameToANSI(strFileName)
		local fileHandle = fh.OpenFile(strAnsi,"w")
		fileHandle:write(strContents)
		assert(fileHandle:close())
		if not wasAnsi then
			fh.MoveFile(strAnsi,strFileName)
		end
		return true
	end -- function SaveStringToFile

	-- Load string from file --
	function fh.StrLoadFromFile(strFileName,strFormat)
		-- strFileName		~ full file path
		-- strFormat			~ optional "UTF-8" or "UTF-16LE"
		-- return value		~ file contents
		strFormat = strFormat or "UTF-8"
		if fhGetAppVersion() > 6 then
			return fhLoadTextFile(strFileName,strFormat)
		end
		local strAnsi, wasAnsi = fh.FileNameToANSI(strFileName)
		if not wasAnsi then
			fh.CopyFile(strFileName,strAnsi)
		end
		local fileHandle = fh.OpenFile(strAnsi,"r")
		local strContents = fileHandle:read("*all")
		assert(fileHandle:close())
		return strContents
	end -- function StrLoadFromFile

	-- Returns the Path, Filename, and Extension as 3 values --
	function fh.SplitFilename(strFileName)
		-- strFileName		~ full file path
		-- return values	~ path, name.type, type
		if fh.FSO:FolderExists(strFileName) then
			local strPath = strFileName:gsub("[\\/]$","")
			return strPath.."\\","",""
		end
		strFileName = strFileName.."."
		return strFileName:match("^(.-)([^\\/]-%.([^\\/%.]-))%.?$")
	end -- function SplitFilename

	-- Convert dd/mm/yyyy hh:mm:ss format to integer seconds -- (DateTime format is used in attributes returned by GetFolderContents and DirTree below)
	function fh.IntTime(strDateTime)
		-- strDateTime		~ date time string
		-- return value		~ integer seconds since 01/01/1970 00:00:00 
		local strDay,strMonth,strYear,strHour,strMin,strSec = strDateTime:match("^(%d%d)/(%d%d)/(%d+) (%d%d):(%d%d):(%d%d)")
		if tonumber(strYear) < 1970 then return 0 end
		local isDST = false
		if tonumber(strMonth) > 4 and tonumber(strMonth) < 11 then isDST = true end	-- Approximation is sometimes wrong
		local intTime = os.time( { year=strYear; month=strMonth; day=strDay; hour=strHour; min=strMin; sec=strSec; isdst=isDST; } )
		local tblDat = os.date("*t",intTime)
		if tblDat.isdst then
			intTime = intTime + 3600
			isDST = true
		end
		return intTime
	end -- function IntTime

	-- Return table of attributes for folder --
	local function attribFolder(tblAttr)
		-- tblAttr		~ folder attributes table
		-- return value	~ attributes table like LFS except datetimes
		-- WINE only supports the tblAttr.name & tblAttr.path
		return { mode="directory"; name=tblAttr.name; path=tblAttr.path; }
	end -- local function attribFolder

	-- Return table of attributes for file --
	local function attribFile(tblAttr)
		-- tblAttr		~ file attributes table
		-- return value	~ attributes table like LFS except datetimes
		-- WINE does not support the tblAttr.type, tblAttr.shortname, tblAttr.shortpath & sometimes tblAttr.datecreated
		return { mode="file"; name=tblAttr.name; path=tblAttr.path; size=tblAttr.size; modified=tblAttr.datelastmodified; attributes=tblAttr.attributes; }
	end -- local function attribFile

	-- Return attributes table of all files and folders in a specified folder --
	function fh.GetFolderContents(strFolder,doRecurse)
		-- strFolder		~ full folder path
		-- doRecurse		~ true for recursion
		-- return value	~ attributes table
		local arrList = {}
		if fh.FSO:FolderExists(strFolder) then

			local function getFileList(strFolder)
				local tblList = fh.FSO:GetFolder(strFolder)
				local tblEnum = luacom.GetEnumerator(tblList.SubFolders)
				local tblAttr = tblEnum:Next()
				while tblAttr do
					table.insert(arrList,attribFolder(tblAttr))
					if doRecurse then getFileList(tblAttr.path) end
					tblAttr = tblEnum:Next()
				end
				local tblEnum = luacom.GetEnumerator(tblList.Files)
				local tblAttr = tblEnum:Next()
				while tblAttr do
					table.insert(arrList,attribFile(tblAttr))
					tblAttr = tblEnum:Next()
				end
			end

			getFileList(strFolder)
		end
		return arrList
	end -- function GetFolderContents

	-- Return a Directory Tree entry & attributes on each iteration --
	function fh.DirTree(strDir,...)
		-- strDir			~ full folder path
		-- ...				~ list of folders to omit
		-- return value	~ full path, attributes table
		local arg = {...}
		assert( fh.FSO:FolderExists(strDir), "directory parameter is missing or empty" )

		local function yieldtree(strDir)
			local tblList = fh.FSO:GetFolder(strDir)
			local tblEnum = luacom.GetEnumerator(tblList.SubFolders)
			local tblAttr = tblEnum:Next()
			while tblAttr do	--	for _,tblAttr in luacom.pairs(tblList.SubFolders) do	-- pairs not working in FH v6 so use tblEnum code
				coroutine.yield(tblAttr.path,attributes(tblAttr,"directory"))
				local isOK = true
				for _,strOmit in ipairs (arg) do
					if tblAttr.path:match(strOmit) then 	-- Omit tree branch
						isOK = false
						break
					end
				end
				if isOK then yieldtree(tblAttr.path) end
				tblAttr = tblEnum:Next()
			end
			local tblEnum = luacom.GetEnumerator(tblList.Files)
			local tblAttr = tblEnum:Next()
			while tblAttr do	--	for _,tblAttr in luacom.pairs(tblList.Files) do		-- pairs not working in FH v6 so use tblEnum code
				coroutine.yield(tblAttr.path,attributes(tblAttr,"file"))
				tblAttr = tblEnum:Next()
			end
		end

		return coroutine.wrap(function() yieldtree(strDir) end)
	end -- function DirTree

	-- Detect FH V5/6 old library modules and advise removal --
	function fh.DetectOldModules()
		if fhGetAppVersion() > 6 then
			local strPath = fhGetContextInfo("CI_APP_DATA_FOLDER").."\\Plugins\\"
			local arrFile = { "compat53.lua"; "ltn12.lua"; "luasql\\sqlite3.dll"; "md5.lua"; "pl\\init.lua"; "socket.lua"; "utf8.lua"; "zip.dll"; }
			for _, strFile in ipairs (arrFile) do
				if fh.FSO:FileExists(strPath..strFile) then
					fhMessageBox("\n  Detected some old FH V6 library modules. \n\nPlease remove them by running the plugin: \n\n     'Delete old FH6 Plugin Module Files' \n","MB_OK","MB_ICONEXCLAMATION")
					break
				end
			end
		end
	end -- function DetectOldModules

	if fhGetAppVersion() > 6 then unpack = table.unpack end

	-- Invoke FH Shell Execute API --
	function fh.DoExecute(strExecutable,...)
		-- strExecutable	~ full path of executable
		-- ...					~ parameter list and optional error reporting function
		-- return value		~ true if successful else false
		local arg = {...}
		local errFunction = fhMessageBox
		if type(arg[#arg]) == 'function' then
			errFunction = arg[#arg]
			table.remove(arg)
		end
		local isOK, intErrorCode, strErrorText = fhShellExecute(strExecutable,unpack(arg))
		if not isOK then
			errFunction(tostring(strErrorText).." ("..tostring(intErrorCode)..")")
		end
		return isOK
	end -- function DoExecute

	-- Obtain the Day Number for any Date Point --									-- Fix problems with invalid dates in DayNumber function
	function fh.GetDayNumber(datDate)
		-- datDate		~ date point
		-- return value	~ day number
		if datDate:IsNull() then return 0 end
		local intDay = fhCallBuiltInFunction("DayNumber",datDate) 				-- Only works for Gregorian dates that were not skipped nor BC dates
		if not intDay then
			local strError = "because "													-- Error message reason	-- V3.0
			local calendar = datDate:GetCalendar()
			local oldMonth = datDate:GetMonth()
			local oldDayNo = datDate:GetDay()
			local intMonth = math.min( oldMonth, 12 ) 								-- Limit month to 12, and day to last of each month
			local intDayNo = math.min( oldDayNo, ({0;31;28;31;30;31;30;31;31;30;31;30;31;})[intMonth+1] )
			local intYear  = datDate:GetYear()
			if oldDayNo > intDayNo then strError = strError.."day "..oldDayNo.." too big "   end
			if oldMonth > intMonth then strError = strError.."month "..oldMonth.." too big " end
			if calendar == "Hebrew" and intYear > 3761 then
				intYear = intYear - 3761
				strError = strError.."Hebrew year > 3761 "
			elseif calendar ~= "Gregorian" then
				strError = strError..calendar.." disallowed "
			end
			if     intYear == 1752 and intMonth ==  9 and intDayNo <= 13 then	-- Use 2 Sep 1752 for 3 - 13 Sep 1752 dates skipped
				intDayNo = 2
				strError = strError.."3 - 13 Sep 1752 skipped "
			elseif intYear == 1582 and intMonth == 10 and intDayNo <= 14 then	-- Use 4 Oct 1582 for 5 - 14 Oct 1582 dates skipped
				intDayNo = 4
				strError = strError.."5 - 14 Oct 1582 skipped "
			end	
			local setDate = fhNewDatePt(intYear,intMonth,intDayNo,datDate:GetYearDD())
			intDay = fhCallBuiltInFunction("DayNumber",setDate) 					-- Remove BC and Julian, Hebrew, French calendars
			if not intDay then intDay = 0 end
			local oldDate = fhNewDate()		oldDate:SetSimpleDate(datDate)		-- Report problem to user
			local newDate = fhNewDate()		newDate:SetSimpleDate(setDate)
			local strIsBC = ""
			if datDate:GetBC() then
				strError = strError.." B.C. disallowed "
				intDay = -intDay
				strIsBC = "and Day Number negated"
			end
			fhMessageBox("\n Get Day Number issue for date \n "..oldDate:GetDisplayText().." \n "..strError.." \n So replaced it with date \n "..newDate:GetDisplayText().." \n "..strIsBC,"MB_OK","MB_ICONEXCLAMATION")
		end
		return intDay
	end -- function GetDayNumber

	local dtpYearMin = fhNewDatePt(1000)												-- Minimum year to use when earliest estimate is null
	local dtpYearMax = fhNewDatePt(2000)												-- Maximum year to use when latest estimate is null

	function fh.GetYearToday()																-- Get the Year for Today
		-- return value	~ integer year today
		local intYearToday = fhCallBuiltInFunction("Year",fhCallBuiltInFunction("Today"))
		dtpYearMax = fhNewDatePt(intYearToday)											-- Set maximum year date point
		return intYearToday
	end -- function GetYearToday()

	local function getDeathFacts(ptrIndi)												-- Iterate Death, Burial, Cremation facts
		-- ptrIndi		~ pointer to individual
		-- return value	~ pointer to fact
		local arrFact = { "~.DEAT"; "~.BURI"; "~.CREM"; }
		local intFact = 0
		local ptrFact = fhNewItemPtr()													-- Pointer to each fact returned to user
		return function ()
			while intFact < #arrFact do
				intFact = intFact + 1
				ptrFact = fhGetItemPtr(ptrIndi,arrFact[intFact])
				if ptrFact:IsNotNull() then return ptrFact end
			end
		end
	end -- local function getDeathFacts

	-- Ensure Estimated Date EARLIEST <= LATEST <= Fact Date --					-- Fix errors in EstimatedBirth/DeathDate function
	local function estimatedDates(strFunc,ptrIndi,intGens,getFact,intYrs)
		-- strFunc			~ "EstimatedBirthDate" or "EstimatedDeathDate"
		-- ptrIndi			~ Individual of interest
		-- intGens			~ Number of generations (may be nil)
		-- getFact			~ Iterator function for facts
		-- intYrs 			~ Years to add to After dates
		-- return values	~ EARLIEST, MID, LATEST dates
		intGens = intGens or 2
		local dtpMin = fhCallBuiltInFunction(strFunc,ptrIndi,"EARLIEST",intGens)
		local dtpMax = fhCallBuiltInFunction(strFunc,ptrIndi,"LATEST",intGens)
		local dtpMid = fhNewDatePt()
		if not ( dtpMin:IsNull() and dtpMax:IsNull() ) then 						-- Skip if both null	
			if dtpMax:IsNull() then dtpMax = dtpYearMax elseif dtpMin:IsNull() then dtpMin = dtpYearMin end
			for ptrFact in getFact(ptrIndi) do
				local datFact = fhGetValueAsDate(fhGetItemPtr(ptrFact,"~.DATE"))
				if not datFact:IsNull() then												-- Find 1st Fact Date
					local dtpLast = datFact:GetDatePt1()								-- Last date = DatePt1 for Simple, Range, and Before
					local strType = datFact:GetSubtype()								-- Between = DatePt2 and After = DatePt1 + intYrs
					if   strType == "Between" then dtpLast = datFact:GetDatePt2()
					elseif strType == "After" then dtpLast = fhNewDatePt(dtpLast:GetYear()+intYrs,dtpLast:GetMonth(),dtpLast:GetDay()) end		-- Compare only uses Year, Month, Day so omitted	,dtpLast:GetYearDD(),dtpLast:GetBC(),dtpLast:GetCalendar()
					if dtpMax:Compare(dtpLast) > 0 then dtpMax = dtpLast end
					if dtpMin:Compare(dtpMax)  > 0 then dtpMin = dtpMax  end
					if strType ~= "After" then break end								-- Now EARLIEST <= LATEST <= Last date
				end
			end	
			local intDays = ( fh.GetDayNumber(dtpMax) - fh.GetDayNumber(dtpMin) ) / 2
			local intYear,remYear = math.modf( intDays / 365.2422 )				-- Offset year @ 365.2422 days per year, and remainder fraction
			local intMnth = math.floor( ( remYear * 12 ) + 0.1 )					-- Offset month is remainder fraction of year * 12
			dtpMid = fhCallBuiltInFunction("CalcDate",dtpMin,intYear,intMnth)	-- Need approximate MID year & month
		end
		return { Min=dtpMin; Mid=dtpMid; Max=dtpMax; }								-- Return EARLIEST, MID, LATEST dates
	end -- local function estimatedDates

	-- Make EstimatedBirthDate EARLIEST <= LATEST <= 1st Fact Date --			-- Fix errors in EstimatedBirthDate function
	function fh.EstimatedBirthDates(ptrIndi,intGens)
		-- ptrInd				~ pointer to individual
		-- intGens			~ generations to include
		-- return values	~ EARLIEST, MID, LATEST dates
		return estimatedDates("EstimatedBirthDate",ptrIndi,intGens,iterate.Facts,10)
	end -- function EstimatedBirthDates

	-- Make EstimatedDeathDate EARLIEST <= LATEST <= DEAT/BURI/CREM Date --	-- Fix errors in EstimatedDeathDate function
	function fh.EstimatedDeathDates(ptrIndi,intGens)
		-- ptrInd				~ pointer to individual
		-- intGens			~ generations to include
		-- return values	~ EARLIEST, MID, LATEST dates
		return estimatedDates("EstimatedDeathDate",ptrIndi,intGens,getDeathFacts,100)
	end -- function EstimatedDeathDates

	--[[
	@function:		BuildDataRef
	@description:	Get Full Data Reference for Pointer
	@parameters:		Item Pointer
	@returns:			Data Reference String, Record Id Integer, Record Type Tag String
	@requires:		None
	]]
	function fh.BuildDataRef(ptrRef)

		local strDataRef = ""										-- Data Reference with instance indices e.g. INDI.RESI[2].ADDR
		local intRecId   = 0										-- Record Id for associated Record
		local strRecTag  = ""										-- Record Tag of associated Record type i.e. INDI, FAM, NOTE, SOUR, etc

		-- getDataRef() is called recursively per level of the Data Ref
		-- ptrRef points to the upper Data Ref levels yet to be analysed
		-- strRef compiles the lower Data Ref levels including instances

		local function getDataRef(ptrRef,strRef)
			local ptrTag = ptrRef:Clone()
			local strTag = fhGetTag(ptrTag)						-- Current level Tag
			ptrTag:MoveToParentItem(ptrTag)
			if ptrTag:IsNotNull() then							-- Parent level exists
				local intSib = 1
				local ptrSib = ptrRef:Clone()					-- Pointer to siblings with same Tag
				ptrSib:MovePrev("SAME_TAG")
				while ptrSib:IsNotNull() do						-- Count previous siblings with same Tag
					intSib = intSib + 1
					ptrSib:MovePrev("SAME_TAG")
				end
				if intSib > 1 then 	strTag = strTag.."["..intSib.."]" end
				getDataRef(ptrTag,"."..strTag..strRef)			-- Now analyse the parent level
			else
				strDataRef = strTag..strRef						-- Record level reached, so set return values
				intRecId   = fhGetRecordId(ptrRef)
				strRecTag  = strTag
				if not fhIsValidDataRef(strDataRef) then print("BuildDataRef: "..strDataRef.." is Invalid") end
			end
		end -- local function getDataRef

		if type(ptrRef) == "userdata" then getDataRef(ptrRef,"") end

		return strDataRef, intRecId, strRecTag

	end -- function BuildDataRef

	--[[
	@function:		GetDataRefPtr
	@description:	Get Pointer for Full Data Reference
	@parameters:		Data Reference String, Record Id Integer, Record Type Tag String (optional)
	@returns:			Item Pointer which IsNull() if any parameters are invalid
	@requires:		None
	]]
	function fh.GetDataRefPtr(strDataRef,intRecId,strRecTag)
		strDataRef = strDataRef or ""
		if not strRecTag then
			strRecTag = strDataRef:gsub("^(%u+).*$","%1")	-- Extract Record Tag from Data Ref
		end
		local ptrRef = fhNewItemPtr()
		ptrRef:MoveToRecordById(strRecTag,intRecId or 0)	-- Lookup the Record by Id
		ptrRef:MoveTo(ptrRef,strDataRef)						-- Move to the Data Ref
		return ptrRef
	end -- function GetDataRefPtr

	function fh.TblDataRef(ptrRef)
		local tblRef = {}
		tblRef.DataRef, tblRef.RecId, tblRef.RecTag = BuildDataRef(ptrRef)
		return tblRef
	end -- function TblDataRef

	function fh.PtrDataRef(tblRef)
		local tblRef = tblRef or {}								-- Ensure table and its fields exist
		return GetDataRefPtr(tblRef.DataRef or "",tblRef.RecId or 0,tblRef.RecTag or "")
	end -- function PtrDataRef

	return fh

end -- local function general_v3

local general = general_v3()										-- To access FH general tools module

--[[
@Module:			+fh+tablex_v3
@Author:			Mike Tate
@Version:			3.1
@LastUpdated:	08 Jan 2022
@Description:	A Table Load Save Module.
@V3.1:				Cater for full UTF-8 filenames.
@V3.0:				Function Prototype Closure version.
@V1.2:				Added local definitions of _ to ensure nil gets returned on error.
@V1.1:				?
@V1.0:				Initial version 0.94 is Lua 5.1 compatible.
]]

local function tablex_v3()

	local fh = {}									-- Local environment table

	------------------------------------------------------ Start Table Load Save
	-- require "_tableloadsave"
	--[[
	Save Table to File/Stringtable
	Load Table from File/Stringtable
	v 0.94

	Lua 5.1 compatible

	Userdata and indices of these are not saved
	Functions are saved via string.dump, so make sure it has no upvalues
	References are saved
	----------------------------------------------------
	table.save( table [, filename] )

	Saves a table so it can be called via the table.load function again
	table must a object of type 'table'
	filename is optional, and may be a string representing a filename or true/1

	table.save( table )
		on success: returns a string representing the table (stringtable)
		(uses a string as buffer, ideal for smaller tables)
	table.save( table, true or 1 )
		on success: returns a string representing the table (stringtable)
		(uses io.tmpfile() as buffer, ideal for bigger tables)
	table.save( table, "filename" )
		on success: returns 1
		(saves the table to file "filename")
	on failure: returns as second argument an error msg
	----------------------------------------------------
	table.load( filename or stringtable )

	Loads a table that has been saved via the table.save function

	on success: returns a previously saved table
	on failure: returns as second argument an error msg
	----------------------------------------------------

	chillcode, http://lua-users.org/wiki/SaveTableToFile
	Licensed under the same terms as Lua itself.
	]]--

	-- declare local variables
	--// exportstring( string )
	--// returns a "Lua" portable version of the string
	local function exportstring( s )
		s = string.format( "%q",s )
		-- to replace
		s = string.gsub( s,"\\\n","\\n" )
		s = string.gsub( s,"\r","\\r" )
		s = string.gsub( s,string.char(26),"\"..string.char(26)..\"" )
		return s
	end
--// The Save Function
function fh.save( tbl,filename )
	local charS,charE = "   ","\n"
	local file,err,_,stransi,wasansi 					-- V1.2 -- V3.1 -- Added _,stransi,wasansi	--!
	-- create a pseudo file that writes to a string and return the string
	if not filename then
		file =  { write = function( self,newstr ) self.str = self.str..newstr end, str = "" }
		charS,charE = "",""
	-- write table to tmpfile
	elseif filename == true or filename == 1 then
		charS,charE,file = "","",io.tmpfile()
	-- write table to file
	-- use io.open here rather than io.output, since in windows when clicking on a file opened with io.output will create an error
	else
		stransi,wasansi = general.FileNameToANSI(filename)	-- V3.1 -- Cater for non-ANSI filename	--!
		file,err = io.open( stransi, "w" )
		if err then return _,err end
	end
	-- initiate variables for save procedure
	local tables,lookup = { tbl },{ [tbl] = 1 }
	file:write( "return {"..charE )
	for idx,t in ipairs( tables ) do
		if filename and filename ~= true and filename ~= 1 then
			file:write( "-- Table: {"..idx.."}"..charE )
		end
		file:write( "{"..charE )
		local thandled = {}
		for i,v in ipairs( t ) do
			thandled[i] = true
			-- escape functions and userdata
			if type( v ) ~= "userdata" then
				-- only handle value
				if type( v ) == "table" then
					if not lookup[v] then
						table.insert( tables, v )
						lookup[v] = #tables
					end
					file:write( charS.."{"..lookup[v].."},"..charE )
				elseif type( v ) == "function" then
					file:write( charS.."loadstring("..exportstring(string.dump( v )).."),"..charE )
				else
					local value =  ( type( v ) == "string" and exportstring( v ) ) or tostring( v )
					file:write(  charS..value..","..charE )
				end
			end
		end
		for i,v in pairs( t ) do
			-- escape functions and userdata
			if (not thandled[i]) and type( v ) ~= "userdata" then
				-- handle index
				if type( i ) == "table" then
					if not lookup[i] then
						table.insert( tables,i )
						lookup[i] = #tables
					end
					file:write( charS.."[{"..lookup[i].."}]=" )
				else
					local index = ( type( i ) == "string" and "["..exportstring( i ).."]" ) or string.format( "[%d]",i )
					file:write( charS..index.."=" )
				end
				-- handle value
				if type( v ) == "table" then
					if not lookup[v] then
						table.insert( tables,v )
						lookup[v] = #tables
					end
					file:write( "{"..lookup[v].."},"..charE )
				elseif type( v ) == "function" then
					file:write( "loadstring("..exportstring(string.dump( v )).."),"..charE )
				else
					local value =  ( type( v ) == "string" and exportstring( v ) ) or tostring( v )
					file:write( value..","..charE )
				end
			end
		end
		file:write( "},"..charE )
	end
	file:write( "}" )
	-- Return Values
	-- return stringtable from string
	if not filename then
		-- set marker for stringtable
		return file.str.."--|"
	-- return stringttable from file
	elseif filename == true or filename == 1 then
		file:seek ( "set" )
		-- no need to close file, it gets closed and removed automatically
		-- set marker for stringtable
		return file:read( "*a" ).."--|"
	-- close file and return 1
	else
		file:close()
		if not ( wasansi ) then		-- V3.1 -- Cater for non-ANSI filename	--!
			general.MoveFile(stransi,filename)
		end
		return 1
	end
end

--// The Load Function
function fh.load( sfile )
	local tables,err,_ 					-- V1.2 -- Added _
	-- catch marker for stringtable
	if string.sub( sfile,-3,-1 ) == "--|" then
		tables,err = loadstring( sfile )
	else
		local stransi,wasansi = general.FileNameToANSI(sfile)	-- V3.1 -- Cater for non-ANSI filename	--!
		if not ( wasansi ) then
			general.CopyFile(sfile,stransi)
		end
		tables,err = loadfile( stransi )
		if not ( wasansi ) then
			general.DeleteFile(stransi)								-- V3.1 -- Cater for non-ANSI filename	--!
		end
	end
	if err then return _,err
	end
	tables = tables()
	for idx = 1,#tables do
		local tolinkv,tolinki = {},{}
		for i,v in pairs( tables[idx] ) do
			if type( v ) == "table" and tables[v[1]] then
				table.insert( tolinkv,{ i,tables[v[1]] } )
			end
			if type( i ) == "table" and tables[i[1]] then
				table.insert( tolinki,{ i,tables[i[1]] } )
			end
		end
		-- link values, first due to possible changes of indices
		for _,v in ipairs( tolinkv ) do
			tables[idx][v[1]] = v[2]
		end
		-- link indices
		for _,v in ipairs( tolinki ) do
			tables[idx][v[2]],tables[idx][v[1]] =  tables[idx][v[1]],nil
		end
	end
	return tables[1]
end

------------------------------------------------------ End Table Load Save

	-- overload fh functions into table
	for strIndex, anyValue in pairs(fh) do
		if type(anyValue) == "function" then
			table[strIndex] = anyValue
		end
	end

	return fh

end -- local function tablex_v3

local tablex  = tablex_v3 ()													-- To access FH table extension module

--[[
@Module:			+fh+encoder_v3
@Author:			Mike Tate
@Version:			3.6
@LastUpdated:	27 Aug 2024
@Description:	Text encoder module for HTML XHTML XML URI UTF8 UTF16 ISO CP1252/ANSI character codings.
@V3.6:				In fh.FileLines(...) cater for empty file;
@V3.5:				Function Prototype Closure version with Lua 5.1 & 5.3 comaptibility.
@V3.4:				Ensure expressions involving gsub return just text parameter.
@V3.3:				Adds UNICODE U+10000 to U+10FFFF UTF-16 Supplementary Planes.
@V3.2:				Update for ANSI & Unicode to ASCII for sorting, Soundex, etc.
@V3.1:				Update for Unicode UTF-16 & UTF-8 and fhConvertANSItoUTF8 & fhConvertUTF8toANSI, name change UTF to UTF8 & CP to ANSI.
@V2.0:				StrUTF8_Encode() replaced by StrUTF_CP1252() for entire UTF-8 range, plus new StrCP1252_ISO().
@V1.0:				Initial version.
]]

local function encoder_v3()

	local fh = {}													-- Local environment table

	local fhVersion = fhGetAppVersion()

	local br_Tag = "<br />"										-- Markup language break tag default
	local br_Lua = "<br ?/?>"										-- Lua pattern for break tag recognition

	local tblCodePage = {}											-- Code Page to XML/XHTML/HTML/URI/UTF8 encodings: http://en.wikipedia.org/wiki/Windows-1252 & 1250 & etc

	-- Control characters "\000" to "\031" for URI & Markup "[%c]" encodings are disallowed except for "\t" to "\r"

	tblCodePage["\000"] = ""	-- NUL
	tblCodePage["\001"] = ""	-- SOH
	tblCodePage["\002"] = ""	-- STX
	tblCodePage["\003"] = ""	-- ETX
	tblCodePage["\004"] = ""	-- EOT
	tblCodePage["\005"] = ""	-- ENQ
	tblCodePage["\006"] = ""	-- ACK
	tblCodePage["\a"] = ""		-- BEL
	tblCodePage["\b"] = ""		-- BS
	tblCodePage["\t"] = "+"	-- HT	space  in Markup	see setURIEncodings() and setMarkupEncodings() below
	tblCodePage["\n"] = "%0A"	-- LF	br_Tag in Markup
	tblCodePage["\v"] = "%0A"	-- VT	br_Tag in Markup
	tblCodePage["\f"] = "%0A"	-- FF	br_Tag in Markup
	tblCodePage["\r"] = "%0D"	-- CR	br_Tag in Markup
	tblCodePage["\014"] = ""	-- SO
	tblCodePage["\015"] = ""	-- SI
	tblCodePage["\016"] = ""	-- DLE
	tblCodePage["\017"] = ""	-- DC1
	tblCodePage["\018"] = ""	-- DC2
	tblCodePage["\019"] = ""	-- DC3
	tblCodePage["\020"] = ""	-- DC4
	tblCodePage["\021"] = ""	-- NAK
	tblCodePage["\022"] = ""	-- SYN
	tblCodePage["\023"] = ""	-- ETB
	tblCodePage["\024"] = ""	-- CAN
	tblCodePage["\025"] = ""	-- EM
	tblCodePage["\026"] = ""	-- SUB
	tblCodePage["\027"] = ""	-- ESC
	tblCodePage["\028"] = ""	-- FS
	tblCodePage["\029"] = ""	-- GS
	tblCodePage["\030"] = ""	-- RS
	tblCodePage["\031"] = ""	-- US

	-- ASCII characters "\032" to "\127" for URI "[%s%p]" encodings: http://en.wikipedia.org/wiki/URL and http://en.wikipedia.org/wiki/Percent-encoding

	tblCodePage[" "] = "+"		-- or "%20"	Space
	tblCodePage["!"] = "%21"	-- Reserved character
	tblCodePage['"'] = "%22"								--	"&quot;" in Markup	see setURIEncodings() and setMarkupEncodings() below
	tblCodePage["#"] = "%23"	-- Reserved character
	tblCodePage["$"] = "%24"	-- Reserved character
	tblCodePage["%"] = "%25"	-- Must be encoded
	tblCodePage["&"] = "%26"	-- Reserved character	-- "&amp;"  in Markup	see setURIEncodings() and setMarkupEncodings() below
	tblCodePage["'"] = "%27"	-- Reserved character	-- "&apos;" in Markup	see setURIEncodings() and setMarkupEncodings() below
	tblCodePage["("] = "%28"	-- Reserved character
	tblCodePage[")"] = "%29"	-- Reserved character
	tblCodePage["*"] = "%2A"	-- Reserved character
	tblCodePage["+"] = "%2B"	-- Reserved character
	tblCodePage[","] = "%2C"	-- Reserved character
--	tblCodePage["-"] = "%2D"	-- Unreserved character not encoded
--	tblCodePage["."] = "%2E"	-- Unreserved character not encoded
	tblCodePage["/"] = "%2F"	-- Reserved character
--	Digits 0 to 9				-- Unreserved characters not encoded
	tblCodePage[":"] = "%3A"	-- Reserved character
	tblCodePage[";"] = "%3B"	-- Reserved character
	tblCodePage["<"] = "%3C"								-- "&lt;"  in Markup	see setURIEncodings() and setMarkupEncodings() below
	tblCodePage["="] = "%3D"	-- Reserved character
	tblCodePage[">"] = "%3E"								-- "&gt;"  in Markup	see setURIEncodings() and setMarkupEncodings() below
	tblCodePage["?"] = "%3F"	-- Reserved character
	tblCodePage["@"] = "%40"	-- Reserved character
--	Letters A to Z				-- Unreserved characters not encoded
	tblCodePage["["] = "%5B"	-- Reserved character
	tblCodePage["\\"]= "%5C"
	tblCodePage["]"] = "%5D"	-- Reserved character
	tblCodePage["^"] = "%5E"
--	tblCodePage["_"] = "%5F"	-- Unreserved character not encoded
	tblCodePage["`"] = "%60"
--	Letters a to z				-- Unreserved characters not encoded
	tblCodePage["{"] = "%7B"
	tblCodePage["|"] = "%7C"
	tblCodePage["}"] = "%7D"
--	tblCodePage["~"] = "%7E"	-- Unreserved character not encoded
	tblCodePage["\127"] = ""	-- DEL

	-- Code Page 1252 Unicode characters "\128" to "\255" for UTF-8 scheme "[€-ÿ]" encodings: http://en.wikipedia.org/wiki/UTF-8

	tblCodePage["€"] = string.char(0xE2,0x82,0xAC)	-- "&euro;"
	tblCodePage["\129"] = ""  -- Undefined
	tblCodePage["‚"] = string.char(0xE2,0x80,0x9A)
	tblCodePage["ƒ"] = string.char(0xC6,0x92)
	tblCodePage["„"] = string.char(0xE2,0x80,0x9E)
	tblCodePage["…"] = string.char(0xE2,0x80,0xA6)
	tblCodePage["†"] = string.char(0xE2,0x80,0xA0)
	tblCodePage["‡"] = string.char(0xE2,0x80,0xA1)
	tblCodePage["ˆ"] = string.char(0xCB,0x86)
	tblCodePage["‰"] = string.char(0xE2,0x80,0xB0)
	tblCodePage["Š"] = string.char(0xC5,0xA0)
	tblCodePage["‹"] = string.char(0xE2,0x80,0xB9)
	tblCodePage["Œ"] = string.char(0xC5,0x92)
	tblCodePage["\141"] = ""  -- Undefined
	tblCodePage["Ž"] = string.char(0xC5,0xBD)
	tblCodePage["\143"] = ""  -- Undefined
	tblCodePage["\144"] = ""  -- Undefined
	tblCodePage["‘"] = string.char(0xE2,0x80,0x98)
	tblCodePage["’"] = string.char(0xE2,0x80,0x99)
	tblCodePage["“"] = string.char(0xE2,0x80,0x9C)
	tblCodePage["”"] = string.char(0xE2,0x80,0x9D)
	tblCodePage["•"] = string.char(0xE2,0x80,0xA2)
	tblCodePage["–"] = string.char(0xE2,0x80,0x93)
	tblCodePage["—"] = string.char(0xE2,0x80,0x94)
	tblCodePage["\152"] = string.char(0xCB,0x9C)	-- Small Tilde
	tblCodePage["™"] = string.char(0xE2,0x84,0xA2)
	tblCodePage["š"] = string.char(0xC5,0xA1)
	tblCodePage["›"] = string.char(0xE2,0x80,0xBA)
	tblCodePage["œ"] = string.char(0xC5,0x93)
	tblCodePage["\157"] = ""  -- Undefined
	tblCodePage["ž"] = string.char(0xC5,0xBE)
	tblCodePage["Ÿ"] = string.char(0xC5,0xB8)
	tblCodePage["\160"] = string.char(0xC2,0xA0)	-- "&nbsp;"	No Break Space
	tblCodePage["¡"] = string.char(0xC2,0xA1)		-- "&iexcl;"
	tblCodePage["¢"] = string.char(0xC2,0xA2)		-- "&cent;"
	tblCodePage["£"] = string.char(0xC2,0xA3)		-- "&pound;"
	tblCodePage["¤"] = string.char(0xC2,0xA4)		-- "&curren;"
	tblCodePage["¥"] = string.char(0xC2,0xA5)		-- "&yen;"
	tblCodePage["¦"] = string.char(0xC2,0xA6)
	tblCodePage["§"] = string.char(0xC2,0xA7)
	tblCodePage["¨"] = string.char(0xC2,0xA8)
	tblCodePage["©"] = string.char(0xC2,0xA9)
	tblCodePage["ª"] = string.char(0xC2,0xAA)
	tblCodePage["«"] = string.char(0xC2,0xAB)
	tblCodePage["¬"] = string.char(0xC2,0xAC)
	tblCodePage["­"] = string.char(0xC2,0xAD)		-- "&shy;"	Soft Hyphen
	tblCodePage["®"] = string.char(0xC2,0xAE)
	tblCodePage["¯"] = string.char(0xC2,0xAF)
	tblCodePage["°"] = string.char(0xC2,0xB0)
	tblCodePage["±"] = string.char(0xC2,0xB1)
	tblCodePage["²"] = string.char(0xC2,0xB2)
	tblCodePage["³"] = string.char(0xC2,0xB3)
	tblCodePage["´"] = string.char(0xC2,0xB4)
	tblCodePage["µ"] = string.char(0xC2,0xB5)
	tblCodePage["¶"] = string.char(0xC2,0xB6)
	tblCodePage["·"] = string.char(0xC2,0xB7)
	tblCodePage["¸"] = string.char(0xC2,0xB8)
	tblCodePage["¹"] = string.char(0xC2,0xB9)
	tblCodePage["º"] = string.char(0xC2,0xBA)
	tblCodePage["»"] = string.char(0xC2,0xBB)
	tblCodePage["¼"] = string.char(0xC2,0xBC)
	tblCodePage["½"] = string.char(0xC2,0xBD)
	tblCodePage["¾"] = string.char(0xC2,0xBE)
	tblCodePage["¿"] = string.char(0xC2,0xBF)
	tblCodePage["À"] = string.char(0xC3,0x80)
	tblCodePage["Á"] = string.char(0xC3,0x81)
	tblCodePage["Â"] = string.char(0xC3,0x82)
	tblCodePage["Ã"] = string.char(0xC3,0x83)
	tblCodePage["Ä"] = string.char(0xC3,0x84)
	tblCodePage["Å"] = string.char(0xC3,0x85)
	tblCodePage["Æ"] = string.char(0xC3,0x86)
	tblCodePage["Ç"] = string.char(0xC3,0x87)
	tblCodePage["È"] = string.char(0xC3,0x88)
	tblCodePage["É"] = string.char(0xC3,0x89)
	tblCodePage["Ê"] = string.char(0xC3,0x8A)
	tblCodePage["Ë"] = string.char(0xC3,0x8B)
	tblCodePage["Ì"] = string.char(0xC3,0x8C)
	tblCodePage["Í"] = string.char(0xC3,0x8D)
	tblCodePage["Î"] = string.char(0xC3,0x8E)
	tblCodePage["Ï"] = string.char(0xC3,0x8F)
	tblCodePage["Ð"] = string.char(0xC3,0x90)
	tblCodePage["Ñ"] = string.char(0xC3,0x91)
	tblCodePage["Ò"] = string.char(0xC3,0x92)
	tblCodePage["Ó"] = string.char(0xC3,0x93)
	tblCodePage["Ô"] = string.char(0xC3,0x94)
	tblCodePage["Õ"] = string.char(0xC3,0x95)
	tblCodePage["Ö"] = string.char(0xC3,0x96)
	tblCodePage["×"] = string.char(0xC3,0x97)
	tblCodePage["Ø"] = string.char(0xC3,0x98)
	tblCodePage["Ù"] = string.char(0xC3,0x99)
	tblCodePage["Ú"] = string.char(0xC3,0x9A)
	tblCodePage["Û"] = string.char(0xC3,0x9B)
	tblCodePage["Ü"] = string.char(0xC3,0x9C)
	tblCodePage["Ý"] = string.char(0xC3,0x9D)
	tblCodePage["Þ"] = string.char(0xC3,0x9E)
	tblCodePage["ß"] = string.char(0xC3,0x9F)
	tblCodePage["à"] = string.char(0xC3,0xA0)
	tblCodePage["á"] = string.char(0xC3,0xA1)
	tblCodePage["â"] = string.char(0xC3,0xA2)
	tblCodePage["ã"] = string.char(0xC3,0xA3)
	tblCodePage["ä"] = string.char(0xC3,0xA4)
	tblCodePage["å"] = string.char(0xC3,0xA5)
	tblCodePage["æ"] = string.char(0xC3,0xA6)
	tblCodePage["ç"] = string.char(0xC3,0xA7)
	tblCodePage["è"] = string.char(0xC3,0xA8)
	tblCodePage["é"] = string.char(0xC3,0xA9)
	tblCodePage["ê"] = string.char(0xC3,0xAA)
	tblCodePage["ë"] = string.char(0xC3,0xAB)
	tblCodePage["ì"] = string.char(0xC3,0xAC)
	tblCodePage["í"] = string.char(0xC3,0xAD)
	tblCodePage["î"] = string.char(0xC3,0xAE)
	tblCodePage["ï"] = string.char(0xC3,0xAF)
	tblCodePage["ð"] = string.char(0xC3,0xB0)
	tblCodePage["ñ"] = string.char(0xC3,0xB1)
	tblCodePage["ò"] = string.char(0xC3,0xB2)
	tblCodePage["ó"] = string.char(0xC3,0xB3)
	tblCodePage["ô"] = string.char(0xC3,0xB4)
	tblCodePage["õ"] = string.char(0xC3,0xB5)
	tblCodePage["ö"] = string.char(0xC3,0xB6)
	tblCodePage["÷"] = string.char(0xC3,0xB7)
	tblCodePage["ø"] = string.char(0xC3,0xB8)
	tblCodePage["ù"] = string.char(0xC3,0xB9)
	tblCodePage["ú"] = string.char(0xC3,0xBA)
	tblCodePage["û"] = string.char(0xC3,0xBB)
	tblCodePage["ü"] = string.char(0xC3,0xBC)
	tblCodePage["ý"] = string.char(0xC3,0xBD)
	tblCodePage["þ"] = string.char(0xC3,0xBE)
	tblCodePage["ÿ"] = string.char(0xC3,0xBF)

	-- Set XML/XHTML/HTML "[%c\"&'<>]" Markup encodings: http://en.wikipedia.org/wiki/XML and http://en.wikipedia.org/wiki/HTML
	local function setMarkupEncodings()
		tblCodePage["\t"] = " "		-- HT	"\t" to "\r" are treated as white space in Markup Languages by default
		tblCodePage["\n"] = br_Tag	-- LF	
		tblCodePage["\v"] = br_Tag	-- VT	line break tag	"<br>" or "<br >" or "<br/>" or "<br />" is better
		tblCodePage["\f"] = br_Tag	-- FF
		tblCodePage["\r"] = br_Tag	-- CR
		tblCodePage['"']  = "&quot;"
		tblCodePage["&"]  = "&amp;"
		tblCodePage["'"]  = "&apos;"
		tblCodePage["<"]  = "&lt;"
		tblCodePage[">"]  = "&gt;"
	end -- local function setMarkupEncodings

	-- Set URI/URL/URN "[%s%p]" encodings: http://en.wikipedia.org/wiki/URL and http://en.wikipedia.org/wiki/Percent-encoding
	local function setURIEncodings()
		tblCodePage["\t"] = "+"		-- HT	space
		tblCodePage["\n"] = "%0A" 	-- LF	newline
		tblCodePage["\v"] = "%0A" 	-- VT	newline
		tblCodePage["\f"] = "%0A" 	-- FF	newline
		tblCodePage["\r"] = "%0D" 	-- CR	return
		tblCodePage['"']  = "%22"
		tblCodePage["&"]  = "%26"
		tblCodePage["'"]  = "%27"
		tblCodePage["<"]  = "%3C"
		tblCodePage[">"]  = "%3E"
	end -- local function setURIEncodings

	-- Encode characters according to gsub pattern & lookup table --
	local function strEncode(strText,strPattern,tblPattern)
		return ( (strText or ""):gsub(strPattern,tblPattern) )		-- V3.4
	end -- local function strEncode

	-- Encode CP1252/ANSI characters into UTF-8 codes --
	function fh.StrANSI_UTF8(strText)
		if fhVersion > 5 then
			strText = fhConvertANSItoUTF8(strText)
		else
			strText = strEncode(strText,"[\127-ÿ]",tblCodePage)
		end
		return strText
	end -- function StrANSI_UTF8

	function fh.StrCP_UTF(strText)			-- Legacy
		return fh.StrANSI_UTF8(strText)
	end -- function StrCP1252_UTF8

	function fh.StrCP1252_UTF(strText)		-- Legacy
		return fh.StrANSI_UTF8(strText)
	end -- function StrCP1252_UTF

	-- Encode CP1252/ANSI or UTF-8 characters into UTF-8 --
	function fh.StrEncode_UTF8(strText)
		if stringx.encoding() == "ANSI" then
			return fh.StrANSI_UTF8(strText)
		else
			return strText
		end
	end -- function StrEncode_UTF8

	-- Encode CP1252/ANSI characters into XML/XHTML/HTML/UTF8 codes --
	local strANSI_XML = "[%z\001-\031\"&'<>\127-ÿ]"
	if fhVersion > 6 then											-- Cater for Lua 5.1 %z or Lua 5.3 \0
			strANSI_XML = "[\000-\031\"&'<>\127-ÿ]"
	end

	function fh.StrANSI_XML(strText)
		setMarkupEncodings()
		strText = (strText or ""):gsub(br_Lua,"\n")			-- Convert <br> & <br > & <br/> & <br /> to \n that becomes br_Tag
		strText = strEncode(strText,strANSI_XML,tblCodePage)
		return strText
	end -- function StrANSI_XML

	function StrCP_XML(strText)			-- Legacy
		return fh.StrANSI_XML(strText)
	end -- function StrCP_XML

	function StrCP1252_XML(strText)		-- Legacy
		return fh.StrANSI_XML(strText)
	end -- function StrCP1252_XML

	-- Encode UTF-8 ASCII characters into XML/XHTML/HTML codes --
	local strUTF8_XML = "[%z\001-\031\"&'<>\127]"
	if fhVersion > 6 then											-- Cater for Lua 5.1 %z or Lua 5.3 \0
			strUTF8_XML = "[\000-\031\"&'<>\127]"
	end

	function fh.StrUTF8_XML(strText)
		setMarkupEncodings()
		strText = (strText or ""):gsub(br_Lua,"\n")			-- Convert <br> & <br > & <br/> & <br /> to \n that becomes br_Tag
		strText = strEncode(strText,strUTF8_XML,tblCodePage)
		return strText
	end -- function StrUTF8_XML

	-- Encode CP1252/ANSI or UTF-8 ASCII characters into XML/XHTML/HTML codes --
	function fh.StrEncode_XML(strText)
		if stringx.encoding() == "ANSI" then
			return fh.StrANSI_XML(strText)
		else
			return fh.StrUTF8_XML(strText)
		end
	end -- function StrEncode_XML

	-- Encode Item Text characters into XML/HTML/UTF-8 codes --
	function fh.StrGetItem_XML(ptrItem,strTags)
		return fh.StrEncode_XML(fhGetItemText(ptrItem,strTags))
	end -- function StrGetItem_XML

	-- Encode CP1252/ANSI characters into URI codes --
	function fh.StrANSI_URI(strText)
		setURIEncodings()
		strText = (strText or ""):gsub(br_Lua,"\n")			-- Convert <br> & <br > & <br/> & <br /> to \n that becomes %0A
		strText = strEncode(strText,"[^0-9A-Za-z]",tblCodePage)
		return strText
	end -- function StrANSI_URI

	function fh.StrCP_URI(strText)
		return fh.StrANSI_URI(strText)
	end -- function StrCP_URI

	function fh.StrCP1252_URI(strText)
		return fh.StrANSI_URI(strText)
	end -- function StrCP1252_URI

	-- Encode UTF-8 ASCII characters into URI codes --
	local strUTF8_URI = "[%z\001-\127]"
	if fhVersion > 6 then											-- Cater for Lua 5.1 %z or Lua 5.3 \0
			strUTF8_URI = "[\000-\127]"
	end

	function fh.StrUTF8_URI(strText)
		setURIEncodings()
		strText = (strText or ""):gsub(br_Lua,"\n")			-- Convert <br> & <br > & <br/> & <br /> to \n that becomes br_Tag
		strText = strEncode(strText,strUTF8_URI,tblCodePage)
		return strText
	end -- function StrUTF8_URI

	-- Encode CP1252/ANSI or UTF-8 ASCII characters into URI codes --
	function fh.StrEncode_URI(strText)
		if stringx.encoding() == "ANSI" then
			return fh.StrANSI_URI(strText)
		else
			return fh.StrUTF8_URI(strText)
		end
	end -- function StrEncode_URI

	function fh.StrUTF8_Encode(strText) -- Legacy from V1.0
		return fh.StrUTF8_ANSI(strText)
	end -- function StrUTF8_Encode

	-- Encode UTF-8 bytes into single CP1252/ANSI character V2.0 upvalues --
	local strByteRange = "["..string.char(0xC0).."-"..string.char(0xFF).."]"
	local tblBytePoint = {0xC0;0xE0;0xF0;0xF8;0xFC;}		-- Byte codes for 2-byte, 3-byte, 4-byte, 5-byte, 6-byte UTF-8
	local tblUTF8 = {}
	for strByte = string.byte("€"), string.byte("ÿ") do
		local strChar = string.char(strByte)					-- Use CodePage to UTF-8 table to populate UTF-8 to CodePage table
		local strCode = tblCodePage[strChar]
		tblUTF8[strCode] = strChar
	end

	-- Encode UTF-8 bytes into single CP1252/ANSI character --
	function fh.StrUTF8_ANSI(strText)
		strText = strText or ""
		if fhVersion > 5 then return fhConvertUTF8toANSI(strText) end
		if strText:match(strByteRange) then						-- If text contains characters that need translating then
			local intChar = 0										-- Input character index
			local strChar = ""										-- Current character
			local strCode = ""										-- UTF-8 multi-byte code
			local tblLine = {}										-- Translated output line
			repeat
				intChar = intChar + 1								-- Step through each character in text
				strChar = strText:sub(intChar,intChar)
				if strChar:match(strByteRange) then				-- Convert UTF-8 bytes into CP character
					strCode = strChar								-- First UTF-8 byte code, whose top bits say how many bytes to append
					for intByte, strByte in ipairs(tblBytePoint) do
						if string.byte(strChar) >= strByte then
							intChar = intChar + 1					-- Append next UTF-8 byte code character
							strCode = strCode..strText:sub(intChar,intChar)
						else
							break
						end 
					end
					strChar = tblUTF8[strCode] or "¿"			-- Translate UTF-8 code into CP character
				end
				table.insert(tblLine,strChar)					-- Accumulate output char by char
			until intChar >= #strText
			strText = table.concat(tblLine)
		end
		return strText
	end -- function StrUTF8_ANSI

	function fh.StrUTF_CP(strText)			-- Legacy
		return fh.StrUTF8_ANSI(strText)
	end -- function StrUTF_CP

	function fh.StrUTF_CP1252(strText)		-- Legacy
		return fh.StrUTF8_ANSI(strText)
	end -- function StrUTF_CP1252

	-- Encode CP1252/ANSI or UTF-8 characters into ANSI --
	function fh.StrEncode_ANSI(strText)
		if stringx.encoding() == "ANSI" then
			return strText or ""
		else
			return fh.StrUTF8_ANSI(strText)
		end
	end -- function StrEncode_ANSI

	-- Set ISO-8859-1 "[\127-Ÿ]" encodings: http://en.wikipedia.org/wiki/ISO/IEC_8859-1
	local tblISO8859 = { }
	tblISO8859["\127"]=""  -- DEL
	tblISO8859["€"] = "EUR"	
	tblISO8859["\129"]=""  -- Undefined
	tblISO8859["‚"] = "¸"
	tblISO8859["ƒ"] = "f"
	tblISO8859["„"] = "¸¸"
	tblISO8859["…"] = "..."
	tblISO8859["†"] = "+"
	tblISO8859["‡"] = "±"
	tblISO8859["ˆ"] = "^"
	tblISO8859["‰"] = "%"
	tblISO8859["Š"] = "S"
	tblISO8859["‹"] = "<"
	tblISO8859["Œ"] = "OE"
	tblISO8859["\141"]=""  -- Undefined
	tblISO8859["Ž"] = "Z"
	tblISO8859["\143"]=""  -- Undefined
	tblISO8859["\144"]=""  -- Undefined
	tblISO8859["‘"] = "'"
	tblISO8859["’"] = "'"
	tblISO8859["“"] = '"'
	tblISO8859["”"] = '"'
	tblISO8859["•"] = "º"
	tblISO8859["–"] = "-"
	tblISO8859["—"] = "-"
	tblISO8859["\152"]="~" -- Small Tilde
	tblISO8859["™"] = "TM"
	tblISO8859["š"] = "s"
	tblISO8859["›"] = ">"
	tblISO8859["œ"] = "oe"
	tblISO8859["\157"]=""  -- Undefined
	tblISO8859["ž"] = "z"
	tblISO8859["Ÿ"] = "Y"

	-- Encode CP1252/ANSI characters into ISO-8859-1 codes --
	function fh.StrANSI_ISO(strText)
		return strEncode(strText,"[\127-Ÿ]",tblISO8859)
	end -- function StrANSI_ISO

	function fh.StrCP_ISO(strText)			-- Legacy
		return fh.StrANSI_ISO(strText)
	end -- function StrCP_ISO

	function fh.StrCP1252_ISO(strText)		-- Legacy
		return fh.StrANSI_ISO(strText)
	end -- function StrCP1252_ISO

	function fh.StrUTF8_ISO(strText)
		return fh.StrANSI_ISO(fh.StrUTF8_ANSI(strText))
	end -- function StrUTF8_ISO

	-- Encode CP1252/ANSI or UTF-8 ASCII characters into ISO-8859-1 codes --
	function fh.StrEncode_ISO(strText)
		if stringx.encoding() == "ANSI" then
			return fh.StrANSI_ISO(strText)
		else
			return fh.StrUTF8_ISO(strText)
		end
	end -- function StrEncode_ISO

	-- Convert UTF-8 bytes to a UTF-16 word or pair --
	local tblByte = {}
	local tblLead = { 0x80; 0xC0; 0xE0; 0xF0; 0xF8; 0xFC; }

	function fh.StrUtf8toUtf16(strChar)

		-- Convert any UTF-8 multibytes to UTF-16 --
		local function strUtf8()
			if #tblByte > 0 then
				local intUtf16 = 0
				for intIndex, intByte in ipairs (tblByte) do	-- Convert UTF-8 bytes to UNICODE U+0080 to U+10FFFF
					if intIndex == 1 then
						intUtf16 = intByte - tblLead[#tblByte]
					else
						intUtf16 = intUtf16 * 0x40 + intByte - 0x80
					end
				end
				if intUtf16 > 0xFFFF then							-- U+10000 to U+10FFFF Supplementary Planes -- V2.6
					tblByte = {}
					intUtf16 = intUtf16 - 0x10000
					local intLow10 = 0xDC00 + ( intUtf16 % 0x400 )				-- Low 16-bit Surrogate
					local intTop10 = 0xD800 + math.floor( intUtf16 / 0x400 )	-- High 16-bit Surrogate
					local intChar1 = intTop10 % 0x100
					local intChar2 = math.floor( intTop10 / 0x100 )
					local intChar3 = intLow10 % 0x100
					local intChar4 = math.floor( intLow10 / 0x100 )
					return string.char(intChar1,intChar2,intChar3,intChar4)	-- Surrogate 16-bit Pair
				end
				if intUtf16 < 0xD800								-- U+0080 to U+FFFF (except U+D800 to U+DFFF) -- V2.6
				or intUtf16 > 0xDFFF then							-- Basic Multilingual Plane
					tblByte = {}
					local intChar1 = intUtf16 % 0x100
					local intChar2 = math.floor( intUtf16 / 0x100 )
					return string.char(intChar1,intChar2)		-- BPL 16-bit
				end
				local strUtf8 = ""									-- U+D800 to U+DFFF Reserved Code Points -- V2.6
				for intIndex, intByte in ipairs (tblByte) do
					strUtf8 = strUtf8..string.format("%.2X ",intByte)
				end
				local strUtf16 = string.format("%.4X ",intUtf16)
				fhMessageBox("\n UTF-16 Reserved Code Point U+D800 to U+DFFF \n UTF-16 = "..strUtf16.."  UTF-8 = "..strUtf8.."\n Character will be replaced by a question mark. \n","MB_OK","MB_ICONEXCLAMATION")
				tblByte = {}
				return "?\0"
			end
			return ""
		end -- local function strUtf8

		local intUtf8 = string.byte(strChar)
		if intUtf8 < 0x80 then										-- U+0000 to U+007F (ASCII)
			return strUtf8()..strChar.."\0"						-- Previous UTF-8 multibytes + current ASCII char
		end
		if intUtf8 >= 0xC0 then									-- Next UTF-8 multibyte start
			local strUtf16 = strUtf8()
			table.insert(tblByte,intUtf8)
			return strUtf16											-- Previous UTF-8 multibytes
		end
		table.insert(tblByte,intUtf8)
		return ""
	end -- function StrUtf8toUtf16

	-- Encode UTF-8 bytes into UTF-16 words --
	function fh.StrUTF8_UTF16(strText)
		tblByte = {}													-- (0xFF) flushes last UTF-8 character
		return ( ((strText or "")..string.char(0xFF)):gsub("(.)",fh.StrUtf8toUtf16) )	-- V3.4
	end -- function StrUTF8_UTF16

	-- Encode CP1252/ANSI or UTF-8 characters into UTF-16 words --
	function fh.StrEncode_UTF16(strText)
		if stringx.encoding() == "ANSI" then
			strText = fh.StrANSI_UTF8(strText)
		end
		return fh.StrUTF8_UTF16(strText)
	end -- function StrEncode_UTF16

	local intTop10 = 0

	-- Convert a UTF-16 word or pair to UTF-8 bytes --
	function fh.StrUtf16toUtf8(strChar1,strChar2)
		local intUtf16 = string.byte(strChar2) * 0x100 + string.byte(strChar1)
		if intUtf16 < 0x80 then									-- U+0000 to U+007F (ASCII) 
			return string.char(intUtf16)
		end
		if intUtf16 < 0x800 then									-- U+0080 to U+07FF
			local intByte1 = intUtf16 % 0x40
			intUtf16 = math.floor( intUtf16 / 0x40 )
			local intByte2 = intUtf16
			return string.char( intByte2 + 0xC0, intByte1 + 0x80 )
		end
		if intUtf16 < 0xD800										-- U+0800 to U+FFFF
		or intUtf16 > 0xDFFF then
			local intByte1 = intUtf16 % 0x40
			intUtf16 = math.floor( intUtf16 / 0x40 )
			local intByte2 = intUtf16 % 0x40
			intUtf16 = math.floor( intUtf16 / 0x40 )
			local intByte3 = intUtf16
			return string.char( intByte3 + 0xE0, intByte2 + 0x80, intByte1 + 0x80 )
		end
		if intUtf16 < 0xDC00 then									-- U+10000 to U+10FFFF High 16-bit Surrogate Supplementary Planes -- V2.6
			intTop10 = ( intUtf16 - 0xD800 ) * 0x400 + 0x10000
			return ""
		end
		intUtf16 = intUtf16 - 0xDC00 + intTop10				-- U+10000 to U+10FFFF Low 16-bit Surrogate Supplementary Planes -- V2.6
		local intByte1 = intUtf16 % 0x40
		intUtf16 = math.floor( intUtf16 / 0x40 )
		local intByte2 = intUtf16 % 0x40
		intUtf16 = math.floor( intUtf16 / 0x40 )
		local intByte3 = intUtf16 % 0x40
		intUtf16 = math.floor( intUtf16 / 0x40 )
		local intByte4 = intUtf16
		return string.char( intByte4 + 0xF0, intByte3 + 0x80, intByte2 + 0x80, intByte1 + 0x80 )
	end -- function StrUtf16toUtf8

	-- Encode UTF-16 words into UTF-8 bytes --
	function fh.StrUTF16_UTF8(strText)
		return ( (strText or ""):gsub("(.)(.)",fh.StrUtf16toUtf8) )	-- V3.4
	end -- function StrUTF16_UTF8

	-- Encode UTF-16 words into ANSI characters --
	function fh.StrUTF16_ANSI(strText)
		return fh.StrUTF8_ANSI(fh.StrUTF16_UTF8(strText))
	end -- function StrUTF16_ANSI

	-- Read UTF-16/UTF-8/ANSI file converted to chosen encoding via line iterator --
	local strUtf16 = "^.%z"
	if fhVersion > 6 then											-- Cater for Lua 5.1 %z or Lua 5.3 \0
			strUtf16 = "^.\0"
	end

	function fh.FileLines(strFileName,strEncoding)			-- Derived from http://lua-users.org/wiki/EnhancedFileLines
		local bomUtf16= "^"..string.char(0xFF,0xFE)			-- "ÿþ"
		local bomUtf8 = "^"..string.char(0xEF,0xBB,0xBF)	-- ""
		local fncConv = tostring									-- Function to convert input to current encoding
		local intHead = 1											-- Index to start of current text line
		local intLump = 1024
		local fHandle = general.OpenFile(strFileName,"rb")
		local strText = fHandle:read(1024) or ""				-- Read first lump from file and cater for empty file
		local intBOM = 0
		strEncoding = strEncoding or string.encoding()
		if strText:match(bomUtf16)
		or strText:match(strUtf16) then
			strText,intBOM = strText:gsub(bomUtf16,"") 		-- Strip UTF-16 BOM
			if strEncoding == "ANSI" then						-- Define UTF-16 conversion to current encoding
				fncConv = fh.StrUTF16_ANSI
			else
				fncConv = fh.StrUTF16_UTF8
			end
		elseif strText:match(bomUtf8) then
			strText,intBOM = strText:gsub(bomUtf8,"")			-- Strip UTF-8 BOM
			if strEncoding == "ANSI" then 						-- Define UTF-8 conversion to current encoding
				fncConv = fh.StrUTF8_ANSI
			end
		else
			if strEncoding == "UTF-8" and #strText > 0 then	-- Define ANSI conversion to current encoding and cater for empty file
				fncConv = fh.StrANSI_UTF8
			end
		end
		strText = fncConv(strText)								-- Convert first lump of text

		return function()											-- Iterator function
			local intTail,strTail									-- Index to end of current text line, and terminating characters
			while true do
				intTail, strTail = strText:match("()([\r\n].)",intHead)
				if intTail or not fHandle then
					if intHead > 1 then intLump = 0 end
					break												-- End of line or end of file
				elseif fHandle then
					local strLump = fHandle:read(1024)			-- Read next lump from file
					if strLump then									-- Strip old text and add converted lump
						strText = strText:sub(intHead)..fncConv(strLump)
						intHead = 1
						intLump = 1024
					else
						assert(fHandle:close())					-- End of file
						fHandle = nil
					end
				end
			end
			if not intTail then
				intTail = #strText									-- Last fragment of file
			elseif strTail == "\r\n" then
				intTail = intTail + 1								-- Adjust tail for both \r & \n
			end
			local strLine = strText:sub(intHead,intTail)		-- Extract line from text
			intHead = intTail + 1
			if #strLine > 0 then									-- Return pruned line, tail chars, lump bytes read
				local strBody, strTail = strLine:match("^(.-)([\r\n]+)$")
				return strBody, strTail, intLump
			end    
		end
	end -- function FileLines

	-- Set "[€-ÿ]" ASCII encodings same as Unidecode below
	local tblASCII = { }
	tblASCII["€"] = "=E"
	tblASCII["\129"]="" -- Undefined
	tblASCII["‚"] = ","
	tblASCII["ƒ"] = "f"
	tblASCII["„"] = ",,"
	tblASCII["…"] = "..."
	tblASCII["†"] = "|+"
	tblASCII["‡"] = "|++"
	tblASCII["ˆ"] = "^"
	tblASCII["‰"] = "%0"
	tblASCII["Š"] = "S"
	tblASCII["‹"] = "<"
	tblASCII["Œ"] = "OE"
	tblASCII["\141"]="" -- Undefined
	tblASCII["Ž"] = "Z"
	tblASCII["\143"]="" -- Undefined
	tblASCII["\144"]="" -- Undefined
	tblASCII["‘"] = "'"
	tblASCII["’"] = "'"
	tblASCII["“"] = "\""
	tblASCII["”"] = "\""
	tblASCII["•"] = "*"
	tblASCII["–"] = "-"
	tblASCII["—"] = "--"
	tblASCII["\152"]="~" -- Small Tilde
	tblASCII["™"] = "TM"
	tblASCII["š"] = "s"
	tblASCII["›"] = ">"
	tblASCII["œ"] = "oe"
	tblASCII["\157"]="" -- Undefined
	tblASCII["ž"] = "z"
	tblASCII["Ÿ"] = "Y"
	tblASCII["\160"]=" " -- "&nbsp;" No Break Space
	tblASCII["¡"] = "!"  -- "&iexcl;"
	tblASCII["¢"] = "=c" -- "&cent;"
	tblASCII["£"] = "=L" -- "&pound;"
	tblASCII["¤"] = "=$" -- "&curren;"
	tblASCII["¥"] = "=Y" -- "&yen;"
	tblASCII["¦"] = "|"
	tblASCII["§"] = "=SS"
	tblASCII["¨"] = "\""
	tblASCII["©"] = "(C)"
	tblASCII["ª"] = "a"
	tblASCII["«"] = "<<"
	tblASCII["¬"] = "-"
	tblASCII["­"] = "-"  -- "&shy;" Soft Hyphen
	tblASCII["®"] = "(R)"
	tblASCII["¯"] = "-"
	tblASCII["°"] = "=o"
	tblASCII["±"] = "+-"
	tblASCII["²"] = "2"
	tblASCII["³"] = "3"
	tblASCII["´"] = "'"
	tblASCII["µ"] = "=u"
	tblASCII["¶"] = "=p"
	tblASCII["·"] = "*"
	tblASCII["¸"] = ","
	tblASCII["¹"] = "1"
	tblASCII["º"] = "o"
	tblASCII["»"] = ">>"
	tblASCII["¼"] = "1/4"
	tblASCII["½"] = "1/2"
	tblASCII["¾"] = "3/4"
	tblASCII["¿"] = "?"
	tblASCII["À"] = "A"
	tblASCII["Á"] = "A"
	tblASCII["Â"] = "A"
	tblASCII["Ã"] = "A"
	tblASCII["Ä"] = "A"
	tblASCII["Å"] = "A"
	tblASCII["Æ"] = "AE"
	tblASCII["Ç"] = "C"
	tblASCII["È"] = "E"
	tblASCII["É"] = "E"
	tblASCII["Ê"] = "E"
	tblASCII["Ë"] = "E"
	tblASCII["Ì"] = "I"
	tblASCII["Í"] = "I"
	tblASCII["Î"] = "I"
	tblASCII["Ï"] = "I"
	tblASCII["Ð"] = "D"
	tblASCII["Ñ"] = "N"
	tblASCII["Ò"] = "O"
	tblASCII["Ó"] = "O"
	tblASCII["Ô"] = "O"
	tblASCII["Õ"] = "O"
	tblASCII["Ö"] = "O"
	tblASCII["×"] = "*"
	tblASCII["Ø"] = "O"
	tblASCII["Ù"] = "U"
	tblASCII["Ú"] = "U"
	tblASCII["Û"] = "U"
	tblASCII["Ü"] = "U"
	tblASCII["Ý"] = "Y"
	tblASCII["Þ"] = "TH"
	tblASCII["ß"] = "ss"
	tblASCII["à"] = "a"
	tblASCII["á"] = "a"
	tblASCII["â"] = "a"
	tblASCII["ã"] = "a"
	tblASCII["ä"] = "a"
	tblASCII["å"] = "a"
	tblASCII["æ"] = "ae"
	tblASCII["ç"] = "c"
	tblASCII["è"] = "e"
	tblASCII["é"] = "e"
	tblASCII["ê"] = "e"
	tblASCII["ë"] = "e"
	tblASCII["ì"] = "i"
	tblASCII["í"] = "i"
	tblASCII["î"] = "i"
	tblASCII["ï"] = "i"
	tblASCII["ð"] = "d"
	tblASCII["ñ"] = "n"
	tblASCII["ò"] = "o"
	tblASCII["ó"] = "o"
	tblASCII["ô"] = "o"
	tblASCII["õ"] = "o"
	tblASCII["ö"] = "o"
	tblASCII["÷"] = "/"
	tblASCII["ø"] = "o"
	tblASCII["ù"] = "u"
	tblASCII["ú"] = "u"
	tblASCII["û"] = "u"
	tblASCII["ü"] = "u"
	tblASCII["ý"] = "y"
	tblASCII["þ"] = "th"
	tblASCII["ÿ"] = "y"

	-- Encode CP1252/ANSI characters into ASCII codes [\000-\127] --
	function fh.StrANSI_ASCII(strText)
		return strEncode(strText,"[€-ÿ]",tblASCII)
	end -- function StrANSI_ASCII

	--[=[
		Unidecode converts each codepoint into a few ASCII characters.
		Lookup table indexed by codepoint [0x0000]-[0xFFFF] gives an ASCII string.
		i.e. strASCII = Unidecode[intByte2][intByte1] or "=?" allowing for partially populated table.
		See http://search.cpan.org/dist/Text-Unidecode/ and follow Browse to:
		See http://cpansearch.perl.org/src/SBURKE/Text-Unidecode-1.22/lib/Text/Unidecode/
		where each x??.pm gives 256 ASCII conversions.
		Start with the first few European accented characters, and add the others later.
	--]=]

	local Unidecode = { }

	function fh.StrUnidecode(strChar1,strChar2)				-- Decode UTF-16 byte pair into ASCII characters
		return Unidecode[string.byte(strChar2)][string.byte(strChar1)] or "=?"
	end  -- function StrUnidecode

	-- Encode UTF-8 characters into ASCII codes [\000-\126] --
	function fh.StrUTF8_ASCII(strText)
		strText = fh.StrUTF8_UTF16(strText)						-- Convert to UTF-16 Unicode and then to ASCII
		return ( strText:gsub("(.)(.)",fh.StrUnidecode) )
	end -- function StrUTF8_ASCII

	-- Encode CP1252/ANSI or UTF-8 into ASCII codes [\000-\126] --
	function fh.StrEncode_ASCII(strText)
		if stringx.encoding() == "ANSI" then
			return fh.StrANSI_ASCII(strText)
		else
			return fh.StrUTF8_ASCII(strText)
		end
	end -- function StrEncode_ASCII

	-- Set markup language break tag --
	function fh.SetBreakTag(br_New)
		if not (br_New or ""):match(br_Lua) then				-- Ensure new break tag is "<br>" or "<br >" or "<br/>" or "<br />"
			br_New = "<br />"
		end
		br_Tag = br_New
	end -- function SetBreakTag

	for intByte = 0x00, 0xFF do Unidecode[intByte] = { } end

	Unidecode[0x00] =
	{[0]="\00";"\01";"\02";"\03";"\04";"\05";"\06";"\a";"\b";"\t";"\n";"\v";"\f";"\r";"\14";"\15";"\16";"\17";"\18";"\19";"\20";"\21";"\22";"\23";"\24";"\25";"\26";"\27";"\28";"\29";"\30";"\31";
		" ";"!";'"';"#";"$";"%";"&";"'";"(";")";"*";"+";",";"-";".";"/";"0";"1";"2";"3";"4";"5";"6";"7";"8";"9";":";";";"<";"=";">";"?";							-- 0x20 to 0x3F
		"@";"A";"B";"C";"D";"E";"F";"G";"H";"I";"J";"K";"L";"M";"N";"O";"P";"Q";"R";"S";"T";"U";"V";"W";"X";"Y";"Z";"[";"\\";"]";"^";"_";							-- 0x40 to 0x5F
		"`";"a";"b";"c";"d";"e";"f";"g";"h";"i";"j";"k";"l";"m";"n";"o";"p";"q";"r";"s";"t";"u";"v";"w";"x";"y";"z";"{";"|";"}";"~";"\127";							-- 0x60 to 0x7F
		""; ""; ""; ""; ""; ""; ""; ""; ""; ""; ""; ""; ""; ""; ""; ""; ""; ""; ""; ""; ""; ""; ""; ""; ""; ""; ""; ""; ""; ""; ""; "";								-- 0x80 to 0x9F
		" ";"!";"=c";"=L";"=$";"=Y";"|";"=SS";'"';"(C)";"a";"<<";"-";"-";"(R)";"-";"=o";"+-";"2";"3";"'";"=u";"=P";"*";",";"1";"o";">>";"1/4";"1/2";"3/4";"?";	-- 0xA0 to 0xBF
		"A";"A";"A";"A";"A";"A";"AE";"C";"E";"E";"E";"E";"I";"I";"I";"I";"D";"N";"O";"O";"O";"O";"O";"*";"O";"U";"U";"U";"U";"Y";"TH";"ss"; 						-- 0xC0 to 0xDF
		"a";"a";"a";"a";"a";"a";"ae";"c";"e";"e";"e";"e";"i";"i";"i";"i";"d";"n";"o";"o";"o";"o";"o";"/";"o";"u";"u";"u";"u";"y";"th";"y";							-- 0xE0 to 0xFF
	}
	Unidecode[0x01] =
	{[0]="A";"a";"A";"a";"A";"a";"C";"c";"C";"c";"C";"c";"C";"c";"D";"d";"D";"d";"E";"e";"E";"e";"E";"e";"E";"e";"E";"e";"G";"g";"G";"g";							-- 0x00 to 0x1F
		"G";"g";"G";"g";"H";"h";"H";"h";"I";"i";"I";"i";"I";"i";"I";"i";"I";"i";"IJ";"ij";"J";"j";"K";"k";"k";"L";"l";"L";"l";"L";"l";"L";							-- 0x20 to 0x3F
		"l";"L";"l";"N";"n";"N";"n";"N";"n";"'n";"ng";"NG";"O";"o";"O";"o";"O";"o";"OE";"oe";"R";"r";"R";"r";"R";"r";"S";"s";"S";"s";"S";"s";						-- 0x40 to 0x5F
		"S";"s";"T";"t";"T";"t";"T";"t";"U";"u";"U";"u";"U";"u";"U";"u";"U";"u";"U";"u";"W";"w";"Y";"y";"Y";"Z";"z";"Z";"z";"Z";"z";"s";							-- 0x60 to 0x7F
		"b";"B";"B";"b";"6";"6";"O";"C";"c";"D";"D";"D";"d";"d";"3";"@";"E";"F";"f";"G";"G";"hv";"I";"I";"K";"k";"l";"l";"W";"N";"n";"O";							-- 0x80 to 0x9F
		"O";"o";"OI";"oi";"P";"p";"YR";"2";"2";"SH";"sh";"t";"T";"t";"T";"U";"u";"Y";"V";"Y";"y";"Z";"z";"ZH";"ZH";"zh";"zh";"2";"5";"5";"ts";"w";				-- 0xA0 to 0xBF
		"|";"||";"|=";"!";"DZ";"Dz";"dz";"LJ";"Lj";"lj";"NJ";"Nj";"nj";"A";"a";"I";"i";"O";"o";"U";"u";"U";"u";"U";"u";"U";"u";"U";"u";"@";"A";"a";				-- 0xC0 to 0xDF
		"A";"a";"AE";"ae";"G";"g";"G";"g";"K";"k";"O";"o";"O";"o";"ZH";"zh";"j";"DZ";"Dz";"dz";"G";"g";"HV";"W";"N";"n";"A";"a";"AE";"ae";"O";"o";				-- 0xE0 to 0xFF
	}
	Unidecode[0x02] =
	{[0]="A";"a";"A";"a";"E";"e";"E";"e";"I";"i";"I";"i";"O";"o";"O";"o";"R";"r";"R";"r";"U";"u";"U";"u";"S";"s";"T";"t";"Y";"y";"H";"h";							-- 0x00 to 0x1F
		"N";"d";"OU";"ou";"Z";"z";"A";"a";"E";"e";"O";"o";"O";"o";"O";"o";"O";"o";"Y";"y";"l";"n";"t";"j";"db";"qp";"A";"C";"c";"L";"T";"s";						-- 0x20 to 0x3F
		"z";"[?]";"[?]";"B";"U";"^";"E";"e";"J";"j";"q";"q";"R";"r";"Y";"y";"a";"a";"a";"b";"o";"c";"d";"d";"e";"@";"@";"e";"e";"e";"e";"j";						-- 0x40 to 0x5F
		"g";"g";"g";"g";"u";"Y";"h";"h";"i";"i";"I";"l";"l";"l";"lZ";"W";"W";"m";"n";"n";"n";"o";"OE";"O";"F";"r";"r";"r";"r";"r";"r";"r";							-- 0x60 to 0x7F
		"R";"R";"s";"S";"j";"S";"S";"t";"t";"u";"U";"v";"^";"w";"y";"Y";"z";"z";"Z";"Z";"?";"?";"?";"C";"@";"B";"E";"G";"H";"j";"k";"L";							-- 0x80 to 0x9F
		"q";"?";"?";"dz";"dZ";"dz";"ts";"tS";"tC";"fN";"ls";"lz";"WW";"]]";"h";"h";"h";"h";"j";"r";"r";"r";"r";"w";"y";"'";'"';"`";"'";"`";"`";"'";				-- 0xA0 to 0xBF
		"?";"?";"<";">";"^";"V";"^";"V";"'";"-";"/";"\\";",";"_";"\\";"/";":";".";"`";"'";"^";"V";"+";"-";"V";".";"@";",";"~";'"';"R";"X";							-- 0xC0 to 0xDF
		"G";"l";"s";"x";"?";"";"";"";"";"";"";"";"V";"=";'"';"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";	-- 0xE0 to 0xFF
	}
	Unidecode[0x03] =
	{
	}
	Unidecode[0x04] =
	{
	}
	Unidecode[0x20] =
	{[0]=" ";" ";" ";" ";" ";" ";" ";" ";" ";" ";" ";" ";"";"";"";"";"-";"-";"-";"-";"--";"--";"||";"_";"'";"'";",";"'";'"';'"';",,";'"';																-- 0x00 to 0x1F
		"|+";"|++";"*";"*>";".";"..";"...";".";"\n";"\n\n";"";"";"";"";"";" ";"%0";"%00";"'";"''";"'''";"`";"``";"```";"^";"<";">";"*";"!!";"!?";"-";"_";											-- 0x20 to 0x3F
		"-";"^";"***";"--";"/";"-[";"]-";"[?]";"?!";"!?";"7";"PP";"(]";"[)";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";	-- 0x40 to 0x5F
		"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"";"";"";"";"";"";"0";"";"";"";"4";"5";"6";"7";"8";"9";"+";"-";"=";"(";")";"n";													-- 0x60 to 0x7F
		"0";"1";"2";"3";"4";"5";"6";"7";"8";"9";"+";"-";"=";"(";")";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";						-- 0x80 to 0x9F
		"ECU";"CL";"Cr";"FF";"L";"mil";"N";"Pts";"Rs";"W";"NS";"D";"=E";"K";"T";"Dr";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";		-- 0xA0 to 0xBF
		"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";"";											-- 0xC0 to 0xDF
		"";"";"";"";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";"[?]";		-- 0xE0 to 0xFF
	}
	Unidecode[0x21] =
	{[34]="TM";
	}

	return fh

end -- local function encoder_v3

local encoder = encoder_v3()										-- To access FH encoder chars module

--[[
@Module:			+fh+progbar_v3
@Author:			Mike Tate
@Version:			3.1
@LastUpdated:		23 Jan 2026
@Description:		Progress Bar library module.
@V3.1:				Use NATIVEPARENT amd CENTERPARENT.
@V3.0:				Function Prototype Closure version.
@V1.0:				Initial version.
]]

local function progbar_v3()

	local fh = {}														-- Local environment table

	require "iuplua"													-- To access GUI window builder

	iup.SetGlobal("CUSTOMQUITMESSAGE","YES")					-- Needed for IUP 3.28

	local tblBars = {}												-- Table for optional external attributes
	local strBack = "255 255 255"									-- Background colour default is white
	local strBody = "0 0 0"											-- Body text colour default is black
	local strFont = nil												-- Font dialogue default is current font
	local strStop = "255 0 0"										-- Stop button colour default is red
	local intPosX = iup.CENTERPARENT								-- Show window default position is central	-- V3.1
	local intPosY = iup.CENTERPARENT
	local intMax, intVal, intPercent, intStart, intDelta, intScale, strClock, isBarStop
	local lblText, barGauge, lblDelta, btnStop, dlgGauge

	local function doFocus()										-- Bring the Progress Bar window into Focus
		dlgGauge.BringFront="YES"									-- If used too often, inhibits other windows scroll bars, etc
	end -- local function doFocus

	local function doUpdate()										-- Update the Progress Gauge and the Delta % with clock
		barGauge.Value = intVal
		lblDelta.Title = string.format("%4d %%      %s ",math.floor(intPercent),strClock)
	end -- local function doUpdate

	local function doReset()										-- Reset all dialogue variables and Update display
		intVal		= 0													-- Current value of Progress Bar
		intPercent= 0.01												-- Percentage of progress
		intStart	= os.time()										-- Start time of progress
		intDelta	= 0													-- Delta time of progress
		intScale	= math.ceil( intMax / 1000 )					-- Scale of percentage per second of progress (initial guess is corrected in Step function)
		strClock	= "00 : 00 : 00"									-- Clock delta time display
		isBarStop	= false											-- Stop button pressed signal
		doUpdate()
		doFocus()
	end -- local function doReset

	function fh.Start(strTitle,intMaximum)						-- Create & start Progress Bar window
		if not dlgGauge then
			strTitle	= strTitle or ""								-- Dialogue and button title
			intMax		= intMaximum or 100							-- Maximun range of Progress Bar, default is 100
			local strSize = tostring( math.max( 100, string.len(" Stop "..strTitle) * 8 ) ).."x30"			-- Adjust Stop button size to Title
			lblText	= iup.label	{ Title=" "; Expand="YES"; Alignment="ACENTER"; Tip="Progress Message"; }
			barGauge	= iup.progressbar { RasterSize="400x30"; Value=0; Max=intMax; Tip="Progress Bar"; }
			lblDelta	= iup.label	{ Title=" "; Expand="YES"; Alignment="ACENTER"; Tip="Percentage and Elapsed Time"; }
			btnStop	= iup.button	{ Title=" Stop "..strTitle; RasterSize=strSize; FgColor=strStop; Tip="Stop Progress Button"; action=function() isBarStop = true end; }	-- Signal Stop button pressed	return iup.CLOSE -- Often caused main GUI to close !!!
			dlgGauge	= iup.dialog	{ Title=strTitle.." Progress "; Font=strFont; FgColor=strBody; Background=strBack; DialogFrame="YES";	-- Remove Windows minimize/maximize menu
								iup.vbox{ Alignment="ACENTER"; Gap="10"; Margin="10x10";
									lblText;
									barGauge;
									lblDelta;
									btnStop;
								};
								move_cb	= function(self,x,y) tblBars.X = x tblBars.Y = y end;
								close_cb	= btnStop.action;		-- Windows Close button = Stop button
							}
			if type(tblBars.GUI) == "table"
			and type(tblBars.GUI.ShowDialogue) == "function" then
				dlgGauge.move_cb = nil								-- Use GUI library to show & move window
				tblBars.GUI.ShowDialogue("Bars",dlgGauge,btnStop,"showxy")
			else
				if fhGetAppVersion() > 6 then 					-- Window centres on FH parent	-- V3.1
					iup.SetAttribute(dlgGauge,"NATIVEPARENT",fhGetContextInfo("CI_PARENT_HWND"))
				end
				dlgGauge:showxy(intPosX,intPosY)					-- Show the Progress Bar window
			end
			doReset()													-- Reset the Progress Bar display
		end
	end -- function Start

	function fh.Message(strText)									-- Show the Progress Bar message
		if dlgGauge then lblText.Title = strText end
	end -- function Message

	function fh.Step(intStep)										-- Step the Progress Bar forward
		if dlgGauge then
			intVal = intVal + ( intStep or 1 )					-- Default step is 1
			local intNew = math.ceil( intVal / intMax * 100 * intScale ) / intScale
			if intPercent ~= intNew then							-- Update progress once per percent or per second, whichever is smaller
				intPercent = math.max( 0.1, intNew )				-- Ensure percentage is greater than zero
				if intVal > intMax then intVal = intMax intPercent = 100 end		-- Ensure values do not exceed maximum
				intNew = os.difftime(os.time(),intStart)
				if intDelta < intNew then							-- Update clock of elapsed time
					intDelta = intNew
					intScale = math.ceil( intDelta / intPercent )	-- Scale of seconds per percentage step
					local intHour = math.floor( intDelta / 3600 )
					local intMins = math.floor( intDelta / 60 - intHour * 60 )
					local intSecs = intDelta - intMins * 60 - intHour * 3600
					strClock = string.format("%02d : %02d : %02d",intHour,intMins,intSecs)
				end
				doUpdate()											-- Update the Progress Bar display
			end
			iup.LoopStep()
		end
	end -- function Step

	function fh.Focus()												-- Bring the Progress Bar window to front
		if dlgGauge then doFocus() end
	end -- function Focus

	function fh.Reset()												-- Reset the Progress Bar display
		if dlgGauge then doReset() end
	end -- function Reset

	function fh.Stop()												-- Check if Stop button pressed
		iup.LoopStep()
		return isBarStop
	end -- function Stop

	function fh.Close()												-- Close the Progress Bar window
		isBarStop = false
		if dlgGauge then dlgGauge:destroy() dlgGauge = nil end
	end -- function Close

	function fh.Setup(tblSetup)									-- Setup optional table of external attributes
		if tblSetup then
			tblBars = tblSetup
			strBack = tblBars.Back or strBack						-- Background colour
			strBody = tblBars.Body or strBody						-- Body text colour
			strFont = tblBars.Font or strFont						-- Font dialogue
			strStop = tblBars.Stop or strStop						-- Stop button colour
			intPosX = tblBars.X or intPosX						-- Window position
			intPosY = tblBars.Y or intPosY
		end
	end -- function Setup

	return fh

end -- local function progbar_v3

local progbar = progbar_v3()										-- To access FH progress bars module

--[[
@Module:			+fh+iup_gui_v3
@Author:			Mike Tate
@Version:			4.6
@LastUpdated:		18 Feb 2026
@Description:		Graphical User Interface Library Module
@V4.6:				Improve handling of centred window RasterSize in fh.ShowDialogue(...);
@V4.5:				Adjust CheckVersionInStore() for dedicated button use;
@V4.4:				Introduce use of NATIVEPARENT and CENTERPARENT to centre on parent window by default; Ensure not off screen; Monitors with -ve X;
@V4.3:				Added memo options to CheckVersionInStore;
@V4.2:				Skip if standalone GEDCOM in fh.SaveSettings() and getDataFiles();
@V4.1:				CheckVersionInStore() save & retrieve latest version in file; Remove old wiki Help features;
@V4.0:				Cater for full UTF-8 filenames;
@V3.9:				ShowDialogue() popup closure fhSleep() added; CheckVersionInStore() at monthly intervals;
@V3.8:				Function Prototype Closure version.
@V3.7:				AssignAttributes(tblControls) now allows any string attribute to invoke a function.
@V3.6:				anyMemoDialogue() sets TopMost attribute.
@V3.5:				Replace IsNormalWindow(iupDialog) with SetWindowCoord(tblName) and update CheckWindowPosition(tblName) to prevent negative values freezing main dialog.
@V3.4:				Use general.MakeFolder() to ensure key folders exist, add Get/PutRegKey(), check Registry IE Shell Version in HelpDialogue(), better error handling in LoadSettings().
@V3.3:				LoadFolder() and SaveFolder() use global folder as default for local folder to improve synch across PC.
@V3.2:				Load & Save settings now use a single clipboard so Local PC settings are preserved across synchronised PC.
@V3.1:				IUP 3.11.2 iup.GetGlobal("VERSION") to top, HelpDialogue conditional ExpandChildren="YES/NO", RefreshDialogue uses NaturalSize, SetUtf8Mode(), Load/SaveFolder(), etc
@V3.0:				ShowDialogue "dialog" mode for Memo, new DestroyDialogue, NewHelpDialogue tblAttr for Font, AssignAttributes intSkip, CustomDialogue iup.CENTERPARENT+, IUP Workaround, BalloonToggle, Initialise test Plugin file exists.
@V2.0:				Support for Plugin Data scope, new FontDialogue, RefreshDialogue, AssignAttributes, httpRequest handler, keep "dialog" mode.
@V1.0:				Initial version.
]]

local function iup_gui_v3()

	local fh = {}																			-- Local environment table

	require "iuplua"																		-- To access GUI window builder
	require "iupluacontrols"															-- To access GUI window controls
	require "lfs"																			-- To access LUA filing system
	require "iupluaole"																	-- To access OLE subsystem
	require "luacom"																		-- To access COM subsystem

	iup.SetGlobal("CUSTOMQUITMESSAGE","YES")										-- Needed for IUP 3.28

	local iupVersion = iup.GetGlobal("VERSION")										-- Obtain IUP module version

	-- "iuplua" Omitted Constants Workaround --

	iup.TOP    = iup.LEFT
	iup.BOTTOM = iup.RIGHT
	iup.RED    = iup.RGB(1,0,0)
	iup.GREEN  = iup.RGB(0,1,0)
	iup.BLUE   = iup.RGB(0,0,1)
	iup.BLACK  = iup.RGB(0,0,0)
	iup.WHITE  = iup.RGB(1,1,1)
	iup.YELLOW = iup.RGB(1,1,0)

	-- Shared Interface Attributes & Functions --

	fh.Version	= " "																	-- Plugin Version
	fh.History	= fh.Version															-- Version History

	fh.Red			= "255 0 0"															-- Color attributes (must exclude leading zeros & spaces to allow value comparisons)
	fh.Maroon		= "128 0 0"
	fh.Amber		= "250 160 0"
	fh.Orange		= "255 165 0"
	fh.Yellow		= "255 255 0"
	fh.Olive		= "128 128 0"
	fh.Lime		= "0 255 0"
	fh.Green		= "0 128 0"
	fh.Cyan		= "0 255 255"
	fh.Teal		= "0 128 128"
	fh.Blue		= "0 0 255"
	fh.Navy		= "0 0 128"
	fh.Magenta	= "255 0 255"
	fh.Purple		= "128 0 128"
	fh.Black		= "0 0 0"
	fh.Gray		= "128 128 128"
	fh.Silver		= "192 192 192"
	fh.Smoke		= "240 240 240"
	fh.White		= "255 255 255"
	fh.Risk		= fh.Red																-- Risk colour for hazardous controls such as Close/Delete buttons
	fh.Warn		= fh.Magenta															-- Warn colour for caution controls and warnings
	fh.Safe		= fh.Green															-- Safe colour for active controls such as most buttons
	fh.Info		= fh.Black															-- Info colour for text controls such as labels/tabs
	fh.Head		= fh.Black															-- Head colour for headings
	fh.Body		= fh.Black															-- Body colour for body text
	fh.Back		= fh.White															-- Background colour for all windows

	fh.Gap			= "8"																	-- Layout attributes Gap was "10"
	fh.Border		= "8x8"																-- 	was BigMargin="10x10"
	fh.Margin		= "1x1"																--	was MinMargin
	fh.Balloon	= "NO"																	-- Tooltip balloon mode

	fh.FontSet	=	0																	-- Legacy GUI font set assigned by FontAssignment but used globally
	fh.FontHead	= ""
	fh.FontBody	= ""

	local GUI = { }																		-- Sub-table for GUI Dialogue attributes to allow any "Name"

	--[[
	GUI.Name			table of dialogue attributes, where Name is Font, Main, Memo, Bars, etc
	GUI.Name.CoordX	x co-ordinate	( Loaded & Saved by default )
	GUI.Name.CoordY	y co-ordinate	( Loaded & Saved by default )
	GUI.Name.Dialog	dialogue handle
	GUI.Name.Focus	focus button handle
	GUI.Name.Frame	dialogframe mode, "normal" = dialogframe="NO" else "YES", "showxy" = showxy(), "popup" or "keep" = popup(), default is "normal & showxy"
	GUI.Name.Height	height
	GUI.Name.Raster	rastersize		( Loaded & Saved by default )
	GUI.Name.Width	width
	GUI.Name.Back		ProgressBar background colour
	GUI.Name.Body		ProgressBar body text colour
	GUI.Name.Font		ProgressBar font style
	GUI.Name.Stop		ProgressBar Stop button colour
	GUI.Name.GUI		Module table usable by other modules e.g. progbar.Setup
	--]]

	-- tblScrn[1] = origin x, tblScrn[2] = origin y, tblScrn[3] = width, tblScrn[4] = height
	local tblScrn = stringx.splitnumbers(iup.GetGlobal("VIRTUALSCREEN"))		-- Used by CustomDialogue() and CheckWindowPosition() and ShowDialogue() below
	local intMinX = tblScrn[1]
	local intMinY = tblScrn[2]															-- V4.4
	local intMaxW = tblScrn[3]
	local intMaxH = tblScrn[4]

	function fh.BalloonToggle()														-- Toggle tooltips Balloon mode
		local tblToggle = { YES="NO"; NO="YES"; }
		fh.Balloon = tblToggle[fh.Balloon]
		fh.SaveSettings()
	end -- function BalloonToggle

	iup.SetGlobal("UTF8MODE","NO")
	iup.SetGlobal("UTF8MODE_FILE","NO") 												-- V4.0
	function fh.SetUtf8Mode()															-- Set IUP into UTF-8 mode
		if iupVersion == "3.5" or stringx.encoding() == "ANSI" then return false end
		iup.SetGlobal("UTF8MODE","YES")
		iup.SetGlobal("UTF8MODE_FILE","YES")											-- V4.0
		return true
	end -- function SetUtf8Mode

	local function tblOfNames(...)													-- Get table of dialogue Names including "Font","Main" by default	-- V4.4
		local arg = {...}
		local tblNames = {"Font";"Main";}
		for intName, strName in ipairs(arg) do
			if type(strName) == "string"
			and strName ~= "Font"
			and strName ~= "Main" then
				table.insert(tblNames,strName)
			end
		end
		return tblNames
	end -- local function tblOfNames

	local function tblNameFor(strName)												-- Get table of parameters for chosen dialogue Name
		strName = tostring(strName)
		if not GUI[strName] then														-- Need new table with default minimum & raster size, and X & Y co-ordinates
			GUI[strName] = { }
			local tblName = GUI[strName]
			tblName.Raster = "x"
			tblName.CoordX = iup.CENTERPARENT
			tblName.CoordY = iup.CENTERPARENT
		end
		return GUI[strName]
	end -- local function tblNameFor

	local function intDimension(intMin,intVal,intMax)								-- Return a number bounded by intMin and intMax
		if not intVal then return 0 end												-- Except if no value then return 0
		intVal = tonumber(intVal) or (intMin+intMax)/2
		return math.max(intMin,math.min(intVal,intMax))
	end -- local function intDimension

	function fh.CustomDialogue(strName,strRas,intX,intY)							-- GUI custom window raster size, and X & Y co-ordinates
		-- strRas  nil = old size, "x" or "0x0" = min size, "999x999" = new size
		-- intX/Y  nil = central, "99" = co-ordinate position
		local tblName = tblNameFor(strName)
		local tblSize = {}
		local intWide = 0
		local intHigh = 0
		strRas = strRas or tblName.Raster
		if strRas then																	-- Ensure raster size is between minimum and screen size
			tblSize = stringx.splitnumbers(strRas)
			intWide = intDimension(intWide,tblSize[1],intMaxW)
			intHigh = intDimension(intHigh,tblSize[2],intMaxH)
			strRas = tostring(intWide.."x"..intHigh)
		end
		if intX and intX < iup.CENTERPARENT then
			intX = intDimension(intMinX,intX,intMaxW-intWide)						-- Ensure X co-ordinate positions window on screen	-- V4.4
		end
		if intY and intY < iup.CENTERPARENT then
			intY = intDimension(intMinY,intY,intMaxH-intHigh)						-- Ensure Y co-ordinate positions window on screen	-- V4.4
		end
		tblName.Raster  = strRas or "x"
		tblName.CoordX  = tonumber(intX) or iup.CENTERPARENT						-- V4.4
		tblName.CoordY  = tonumber(intY) or iup.CENTERPARENT						-- V4.4
	end -- function CustomDialogue

	function fh.DefaultDialogue(...)													-- GUI default window minimum & raster size, and X & Y co-ordinates
		for intName, strName in ipairs(tblOfNames(...)) do
			fh.CustomDialogue(strName)
		end
	end -- function DefaultDialogue

	function fh.DialogueAttributes(strName)											-- Provide named Dialogue Attributes
		local tblName = tblNameFor(strName)											-- tblName.Dialog = dialog handle, so any other attributes could be retrieved
		local tblSize = stringx.splitnumbers(tblName.Raster or "x")				-- Split Raster Size into width=tblSize[1] and height=tblSize[2]
		tblName.Width = tblSize[1]
		tblName.Height= tblSize[2]
		tblName.Back = fh.Back															-- Following only needed for NewProgressBar
		tblName.Body = fh.Body
		tblName.Font = fh.FontBody
		tblName.Stop = fh.Risk
		tblName.GUI  = fh																-- Module table
		return tblName
	end -- function DialogueAttributes

	local strDefaultScope = "Project"													-- Default scope for Load/Save data is per Project/User/Machine as set by PluginDataScope()

	local tblClipProj = { }
	local tblClipUser = { }																-- Clipboards of sticky data for each Plugin Data scope -- V3.2
	local tblClipMach = { }

	local function doLoadData(strParam,strDefault,strScope)						-- Load sticky data for Plugin Data scope
		strScope = tostring(strScope or strDefaultScope):lower()
		local tblClipData = tblClipProj
		if     strScope:match("user") then tblClipData = tblClipUser
		elseif strScope:match("mach") then tblClipData = tblClipMach
		end
		return tblClipData[strParam] or strDefault
	end -- local function doLoadData

	function fh.LoadGlobal(strParam,strDefault,strScope)							-- Load Global Parameter for all PC
		return doLoadData(strParam,strDefault,strScope)
	end -- function LoadGlobal

	function fh.LoadLocal(strParam,strDefault,strScope) 							-- Load Local Parameter for this PC
		return doLoadData(fh.ComputerName.."-"..strParam,strDefault,strScope)
	end -- function LoadLocal

	local function doLoadFolder(strFolder)											-- Use relative paths to let Paths change -- V3.3
		strFolder = strFolder:gsub("^FhDataPath",function() return fh.FhDataPath end)	-- Full path to .fh_data folder
		strFolder = strFolder:gsub("^PublicPath",function() return fh.PublicPath end)	-- Full path to Public folder
		strFolder = strFolder:gsub("^FhProjPath",function() return fh.FhProjPath end)	-- Full path to project folder
		return strFolder
	end -- local function doLoadFolder

	function fh.LoadFolder(strParam,strDefault,strScope)							-- Load Folder Parameter for this PC -- V3.3
		local strFolder = doLoadFolder(fh.LoadLocal(strParam,"",strScope))
		if not general.FlgFolderExists(strFolder) then								-- If no local folder try global folder
			strFolder = doLoadFolder(fh.LoadGlobal(strParam,strDefault,strScope))
		end
		return strFolder
	end -- function LoadFolder

	function fh.LoadDialogue(...)														-- Load Dialogue Parameters for "Font","Main" by default
		for intName, strName in ipairs(tblOfNames(...)) do
			local tblName = tblNameFor(strName)
			tblName.Raster = tostring(fh.LoadLocal(strName.."R",tblName.Raster))
			tblName.CoordX = tonumber(fh.LoadLocal(strName.."X",tblName.CoordX))
			tblName.CoordY = tonumber(fh.LoadLocal(strName.."Y",tblName.CoordY))
			fh.CheckWindowPosition(tblName)
		end
	end -- function LoadDialogue

	function fh.LoadSettings(...)														-- Load Sticky Settings from File
		for strFileName, tblClipData in pairs ({ ProjectFile=tblClipProj; PerUserFile=tblClipUser; MachineFile=tblClipMach; }) do
			strFileName = fh[strFileName]
			if general.FlgFileExists(strFileName) then								-- Load Settings File in table lines with key & val fields
				local tblField = {}
				local strClip = general.StrLoadFromFile(strFileName)				-- V4.0
				for strLine in strClip:gmatch("[^\r\n]+") do						-- V4.0
					if #tblField == 0
					and strLine:match("^return {")									-- Unless entire Sticky Data table was saved
					and type(table.load) == "function" then
						local tblClip, strErr = table.load(strFileName)			-- Load Settings File table
						if strErr then error(strErr.."\n\nMay need to Delete the following Plugin Data .dat file:\n\n"..strFileName.."\n\nError detected.") end
						for i,j in pairs (tblClip) do
							tblClipData[i] = tblClip[i]
						end
						break
					end
					tblField = stringx.split(strLine,"=")
					if tblField[1] then tblClipData[tblField[1]] = tblField[2] end
				end
			else
				for i,j in pairs (tblClipData) do
					tblClipData[i] = nil												-- Restore defaults and clear any junk		-- V4.0
				end
			end
		end
		fh.Safe    = tostring(fh.LoadGlobal("SafeColor",fh.Safe))
		fh.Warn    = tostring(fh.LoadGlobal("WarnColor",fh.Warn))
		fh.Risk    = tostring(fh.LoadGlobal("RiskColor",fh.Risk))
		fh.Head    = tostring(fh.LoadGlobal("HeadColor",fh.Head))
		fh.Body    = tostring(fh.LoadGlobal("BodyColor",fh.Body))
		fh.FontHead= tostring(fh.LoadGlobal("FontHead" ,fh.FontHead))
		fh.FontBody= tostring(fh.LoadGlobal("FontBody" ,fh.FontBody))
		fh.History = tostring(fh.LoadGlobal("History"  ,fh.History))
		fh.Balloon = tostring(fh.LoadGlobal("Balloon"  ,fh.Balloon, "Machine"))
		fh.LoadDialogue(...)
	end -- function LoadSettings

	local function doSaveData(strParam,anyValue,strScope)							-- Save sticky data for Plugin Data scope
		strScope = tostring(strScope or strDefaultScope):lower()
		local tblClipData = tblClipProj
		if     strScope:match("user") then tblClipData = tblClipUser
		elseif strScope:match("mach") then tblClipData = tblClipMach
		end
		tblClipData[strParam] = anyValue
	end -- local function doSaveData

	function fh.SaveGlobal(strParam,anyValue,strScope)							-- Save Global Parameter for all PC
		doSaveData(strParam,anyValue,strScope)
	end -- function SaveGlobal

	function fh.SaveLocal(strParam,anyValue,strScope)								-- Save Local Parameter for this PC
		doSaveData(fh.ComputerName.."-"..strParam,anyValue,strScope)
	end -- function SaveLocal

	function fh.SaveFolder(strParam,strFolder,strScope)							-- Save Folder Parameter for this PC
		strFolder = stringx.replace(strFolder,fh.FhDataPath,"FhDataPath")		-- Full path to .fh_data folder
		strFolder = stringx.replace(strFolder,fh.PublicPath,"PublicPath")		-- Full path to Public folder
		strFolder = stringx.replace(strFolder,fh.FhProjPath,"FhProjPath")		-- Full path to project folder
		fh.SaveGlobal(strParam,strFolder,strScope)									-- V3.3
		fh.SaveLocal(strParam,strFolder,strScope)									-- Uses relative paths to let Paths change
	end -- function SaveFolder

	function fh.SaveDialogue(...)														-- Save Dialogue Parameters for "Font","Main" by default
		for intName, strName in ipairs(tblOfNames(...)) do
			local tblName = tblNameFor(strName)
			fh.SaveLocal(strName.."R",tblName.Raster)
			fh.SaveLocal(strName.."X",tblName.CoordX)
			fh.SaveLocal(strName.."Y",tblName.CoordY)
		end
	end -- function SaveDialogue

	function fh.SaveSettings(...)														-- Save Sticky Settings to File
		fh.SaveDialogue(...)
		fh.SaveGlobal("SafeColor",fh.Safe)
		fh.SaveGlobal("WarnColor",fh.Warn)
		fh.SaveGlobal("RiskColor",fh.Risk)
		fh.SaveGlobal("HeadColor",fh.Head)
		fh.SaveGlobal("BodyColor",fh.Body)
		fh.SaveGlobal("FontHead" ,fh.FontHead)
		fh.SaveGlobal("FontBody" ,fh.FontBody)
		fh.SaveGlobal("History"  ,fh.History)
		fh.SaveGlobal("Balloon"  ,fh.Balloon, "Machine")
		for strFileName, tblClipData in pairs ({ ProjectFile=tblClipProj; PerUserFile=tblClipUser; MachineFile=tblClipMach; }) do
			for i,j in pairs (tblClipData) do											-- Check if table has any entries
				strFileName = fh[strFileName]
				if #strFileName > 0 then												-- Skip for standalone GEDCOM -- V4.2
					if type(table.save) == "function" then							-- Save entire Settings File table per Project/User/Machine
						table.save(tblClipData,strFileName)
					else
						local tblClip = {}
						for strKey,strVal in pairs(tblClipData) do					-- Else save Settings File lines with key & val fields		-- V4.0
							table.insert(tblClip,strKey.."="..strVal.."\n")		-- V4.0
						end
						local strClip = table.concat(tblClip,"\n")					-- V4.0
						if not general.SaveStringToFile(strClip,strFileName) then
							error("\nSettings file not saved successfully.\n\nMay need to Delete the following Plugin Data .dat file:\n\n"..strFileName.."\n\nError detected.")
						end	
					end
				end
				break
			end
		end
	end -- function SaveSettings

	function fh.CheckWindowPosition(tblName)										-- Ensure dialogue window coordinates are on Screen
		local tblSize = stringx.splitnumbers(tblName.Raster or "600x400","x")	-- Get window dimensions from the previous use of plugin -- V4.4
		local intWinW = tblSize[1]
		local intWinH = tblSize[2]
		if tonumber(tblName.CoordX) == nil
		or tonumber(tblName.CoordY) == nil
		or tonumber(tblName.CoordX) < intMinX										-- V4.4 -- V3.5
		or tonumber(tblName.CoordY) < intMinY										-- V4.4 -- V3.5
		or tonumber(tblName.CoordX) + intWinW > intMaxW							-- V4.4
		or tonumber(tblName.CoordY) + intWinH > intMaxH then						-- V4.4
			tblName.CoordX = iup.CENTERPARENT											-- V4.4
			tblName.CoordY = iup.CENTERPARENT											-- V4.4
		end
	end -- function CheckWindowPosition

	function fh.SetWindowCoord(tblName)												-- Set the Window coordinates if not Maximised or Minimised -- V3.5
		-- tblPosn[1] = origin x, tblPosn[2] = origin y, tblPosn[3] = width, tblPosn[4] = height
		local tblPosn = stringx.splitnumbers(tblName.Dialog.ScreenPosition)
		local intPosX = tblPosn[1] or -1
		local intPosY = tblPosn[2] or -1
		if intPosX < 0 and intPosY < 0 then											-- If origin is negative (-8, -8 = Maximised, -3200, -3200 = Minimised)
			return false																	-- then is Maximised or Minimised
		end
		tblName.CoordX = intPosX														-- Otherwise set the Window coordinates
		tblName.CoordY = intPosY
		return true
	end -- function SetWindowCoord

	function fh.ShowDialogue(strName,iupDialog,btnFocus,strFrame)				-- Set standard frame attributes and display dialogue window
		local tblName = tblNameFor(strName)
		iupDialog = iupDialog or tblName.Dialog										-- Retrieve previous parameters if needed
		btnFocus  = btnFocus  or tblName.Focus
		strFrame  = strFrame  or tblName.Frame
		strFrame  = strFrame  or "show norm"											-- Default frame mode is dialog:showxy(X,Y) with DialogFrame="NO" ("normal" to vary size, otherwise fixed size)
		strFrame  = strFrame:lower()													-- Other modes are "show", "popup" & "keep" with DialogFrame="YES", or with "normal" for DialogFrame="NO" ("show" for active windows, "popup"/"keep" for modal windows)
		if strFrame:gsub("%s-%a-map%a*[%s%p]*","") == "" then						-- May be prefixed with "map" mode to just map dialogue initially, also may be suffixed with "dialog" to inhibit iup.MainLoop() to allow progress messages
			strFrame = "map show norm"													-- If only "map" mode then default to "map show norm"
		end
		if type(iupDialog) == "userdata" then
			tblName.Dialog = iupDialog
			tblName.Focus  = btnFocus													-- Preserve parameters
			tblName.Frame  = strFrame
			iupDialog.Background = fh.Back											-- Background colour
			iupDialog.Shrink = "YES"													-- Sometimes needed to shrink controls to raster size
			if type(btnFocus) == "userdata" then										-- Set button as focus for Esc and Enter keys
				iupDialog.StartFocus   = iupDialog.StartFocus   or btnFocus
				iupDialog.DefaultEsc   = iupDialog.DefaultEsc   or btnFocus
				iupDialog.DefaultEnter = iupDialog.DefaultEnter or btnFocus
			end
			if tblName.CoordX == iup.CENTERPARENT
			or tblName.CoordY == iup.CENTERPARENT then								-- When centred on parent refresh this window size	-- V4.4 -- V4.6
				fh.RefreshDialogue(strName)
			else
				iupDialog.MinSize = "x"													-- Minimum size (default "x" becomes nil)
				iupDialog.RasterSize = tblName.Raster or "x"						-- Raster size  (default "x" becomes nil)
			end
			iupDialog.MaxSize = intMaxW.."x"..intMaxH								-- Maximum size is virtual screen size
			if strFrame:match("norm") then											-- DialogFrame mode is "NO" by default for variable size window
				if strFrame:match("pop") or strFrame:match("keep") then
					iupDialog.MinBox = "NO"												-- For "popup" and "keep" hide Minimize and Maximize icons
					iupDialog.MaxBox = "NO"
				else 
					strFrame = strFrame.." show"										-- If not "popup" nor "keep" then use "showxy" mode
				end
			else
				iupDialog.DialogFrame = "YES"											-- Define DialogFrame mode for fixed size window
			end
			iupDialog.close_cb  = iupDialog.close_cb  or function() return iup.CLOSE end	-- Define default window X close, move, and resize actions
			iupDialog.move_cb   = iupDialog.move_cb   or function(self) if iup.MainLoopLevel() > 0 then fh.SetWindowCoord(tblName) end end			-- V3.5 -- V4.4
			iupDialog.resize_cb = iupDialog.resize_cb or function(self) if iup.MainLoopLevel() > 0 then tblName.Raster=self.RasterSize end end	-- V3.5 -- V4.4 -- V4.6
			fh.RefreshDialogue(strName)												-- Refresh to set Natural Size as Minimum Size
			if strName == "Main" then
				if fhGetAppVersion() > 6 then 										-- Main window centres on FH parent	-- V4.4
					iup.SetAttribute(iupDialog,"NATIVEPARENT",fhGetContextInfo("CI_PARENT_HWND"))
				end
			else
				local tblMain = tblNameFor("Main")									-- Others popup centrally in Main 	-- V4.4
				local iupMain = tblMain.Dialog
				if iupMain then															-- Centre based on size of windows	-- V1.4
					local arrName = stringx.splitnumbers(tblName.Raster or "0x0")
					local arrMain = stringx.splitnumbers(tblMain.Raster or arrName[1].."x"..arrName[2])
					tblName.CoordX = iupMain.X + math.floor( (arrMain[1] - arrName[1]) / 2 )
					tblName.CoordY = iupMain.Y + math.floor( (arrMain[2] - arrName[2]) / 2 )
				elseif fhGetAppVersion() > 6 then 									-- This window centres on FH parent	-- V4.4
					iup.SetAttribute(iupDialog,"NATIVEPARENT",fhGetContextInfo("CI_PARENT_HWND"))
				end
			end
			if strFrame:match("map") then												-- Only dialogue mapping is required
				iupDialog:map()
				tblName.Frame = strFrame:gsub("%s-%a-map%a*[%s%p]*","")			-- Remove "map" from frame mode ready for subsequent call
				return
			end
			if iup.MainLoopLevel() == 0												-- Called from outside Main GUI, so must use showxy() and not popup()
			or strFrame:match("dialog")
			or strFrame:match("sho") then												-- Use showxy() to dispay dialogue window for "showxy" or "dialog" mode
				iupDialog:showxy(tblName.CoordX,tblName.CoordY)
				if not strFrame:match("dialog")										-- Inhibit MainLoop if "dialog" mode	-- V4.1
				and iup.MainLoopLevel() == 0 then iup.MainLoop() end
			else
				iupDialog:popup(tblName.CoordX,tblName.CoordY)						-- Use popup() to display dialogue window for "popup" or "keep" modes
				fhSleep(200,150)															-- Sometimes needed to prevent MainLoop() closure!	-- V3.9
			end
			if not strFrame:match("dialog") and strFrame:match("pop") then
				tblName.Dialog = nil													-- When popup closed, clear key parameters, but not for "keep" mode
				tblName.Raster = nil
				tblName.CoordX = nil -- iup.CENTERPARENT
				tblName.CoordY = nil -- iup.CENTERPARENT
			elseif tblName.CoordX ~= iup.CENTERPARENT								-- V4.4
			   and tblName.CoordY ~= iup.CENTERPARENT then
				fh.SetWindowCoord(tblName)												-- Set Window coordinate pixel values -- V3.5
			end
		end
	end -- function ShowDialogue

	function fh.DestroyDialogue(strName)												-- Destroy existing dialogue
		local tblName = tblNameFor(strName)
		if tblName then
			local iupDialog = tblName.Dialog
			if type(iupDialog) == "userdata" then
				iupDialog:destroy()
				tblName.Dialog = nil													-- Prevent future misuse of handle -- 22 Jul 2014
			end
		end
	end -- function DestroyDialogue

	local function strDialogueArgs(strArgA,strArgB,comp)							-- Compare two argument pairs and return matching pair
		local tblArgA = stringx.splitnumbers(strArgA)
		local tblArgB = stringx.splitnumbers(strArgB)
		local strArgX = tostring(comp(tblArgA[1] or 100,tblArgB[1] or 100))
		local strArgY = tostring(comp(tblArgA[2] or 100,tblArgB[2] or 100))
		return strArgX.."x"..strArgY
	end -- local function strDialogueArgs

	function fh.RefreshDialogue(strName)												-- Refresh dialogue window size after Font change, etc
		local tblName = tblNameFor(strName)
		local iupDialog = tblName.Dialog												-- Retrieve the dialogue handle
		if type(iupDialog) == "userdata" then
			iupDialog.Size = iup.NULL
			iupDialog.MinSize = iup.NULL												-- V3.1
			iup.Refresh(iupDialog)														-- Refresh window to Natural Size and set as Minimum Size
			if not iupDialog.RasterSize then
				iupDialog:map()
				iup.Refresh(iupDialog)
			end
			local strSize = iupDialog.NaturalSize or iupDialog.RasterSize				-- IUP 3.5 NaturalSize = nil, IUP 3.11 needs NaturalSize					-- V3.1
			iupDialog.MinSize = strDialogueArgs(iupDialog.MaxSize,strSize,math.min)	-- Set Minimum Size to smaller of Maximm Size or Natural/Raster Size	-- V3.1
			iupDialog.RasterSize = strDialogueArgs(tblName.Raster,strSize,math.max)	-- Set Current Size to larger of Current Size or Natural/Raster Size	-- V3.1
			iup.Refresh(iupDialog)
			tblName.Raster = iupDialog.RasterSize
			if iupDialog.Visible == "YES" then										-- Ensure visible dialogue origin is on screen
				tblName.CoordX = math.max(tblName.CoordX,10)
				tblName.CoordY = math.max(tblName.CoordY,10)						-- Set both coordinates to larger of current value or 10 pixels
				if iupDialog.Modal then													--	V3.8
					if iupDialog.Modal == "NO" then
						iupDialog.ZOrder = "BOTTOM"									-- Ensure dialogue is subservient to any popup
						iupDialog:showxy(tblName.CoordX,tblName.CoordY)			-- Use showxy() to reposition main window
					else
						iupDialog:popup(tblName.CoordX,tblName.CoordY)				-- Use popup() to reposition modal window
					end
				end
			else
				iupDialog.BringFront="YES"
			end
		end
	end -- function RefreshDialogue

	function fh.AssignAttributes(tblControls)										-- Assign the attributes of all controls supplied
		local anyFunction = nil
		for iupName, tblAttr in pairs ( tblControls or {} ) do
			if type(iupName) == "userdata" and type(tblAttr) == "table" then	-- Loop through each iup control
				local intSkip = 0														-- Skip counter for attributes same for all controls
				for intAttr, anyName in ipairs ( tblControls[1] or {} ) do		-- Loop through each iup attribute
					local strName = nil
					local strAttr = nil
					local strType = type(anyName)
					if strType == "string" then										-- Attribute is different for each control in tblControls
						strName = anyName
						strAttr = tblAttr[intAttr-intSkip]
					elseif strType == "table" then									-- Attribute is same for all controls as per tblControls[1] 
						intSkip = intSkip + 1
						strName = anyName[1]
						strAttr = anyName[2]
					elseif strType == "function" then
						intSkip = intSkip + 1
						anyFunction = anyName
						break
					end
					if type(strName) == "string" and ( type(strAttr) == "string" or type(strAttr) == "function" ) then
						local anyRawGet = rawget(fh,strAttr)							-- Use rawget() to stop require("pl.strict") complaining
						if type(anyRawGet) == "string" then
							strAttr = anyRawGet											-- Use internal module attribute such as Head or FontBody
						elseif type(iupName[strName]) == "string"
						   and type(strAttr) == "function" then						-- Allow string attributes to invoke a function	-- V3.7
							strAttr = strAttr()
						end
						iupName[strName] = strAttr										-- Assign attribute to control
					end
				end
			end
		end
		if anyFunction then anyFunction() end										-- Perform any control assignment function
	end -- function AssignAttributes

	-- Font Dialogue Attributes and Functions --

	fh.FontBody = iup.GetGlobal("DEFAULTFONT")										-- Set default font for Body and Head text
	fh.FontHead = fh.FontBody:gsub(", B?o?l?d?",", Bold ")

	function fh.FontDialogue(tblAttr,strName)										-- GUI Font Face & Style Dialogue
		tblAttr = tblAttr or {}
		strName = strName or "Main"
		local isFontChosen = false
		local btnFontHead = iup.button { Title="Choose Headings Font and default Colour"; }
		local btnFontBody = iup.button { Title="Choose Body text Font and default Colour"; }
		local btnCol_Safe = iup.button { Title=" Safe Colour "; }
		local btnCol_Warn = iup.button { Title=" Warning Colour "; }
		local btnCol_Risk = iup.button { Title=" Risky Colour "; }
		local btnDefault  = iup.button { Title=" Default Fonts "; }
		local btnMinimum  = iup.button { Title=" Minimum Size "; }
		local btnDestroy  = iup.button { Title=" Close Dialogue "; }
		local frmSetFonts = iup.frame  { Title=" Set Window Fonts & Colours ";
									iup.vbox { Alignment="ACENTER"; Margin=fh.Margin; Homogeneous="YES";
										btnFontHead;
										btnFontBody;
										iup.hbox { btnCol_Safe; btnCol_Warn; btnCol_Risk; Homogeneous="YES"; };
										iup.hbox { btnDefault ; btnMinimum ; btnDestroy ; Homogeneous="YES"; };
									} -- iup.vbox end
								} -- iup.frame end

		-- Create dialogue and turn off resize, maximize, minimize, and menubox except Close button
		local dialogFont  = iup.dialog { Title=" Set Window Fonts & Colours "; Gap=fh.Gap; Margin=fh.Border; frmSetFonts; }

		local tblButtons = { }

		local function setDialogues()													-- Refresh the Main dialogue	-- V4.4
			fh.AssignAttributes(tblAttr)												-- Assign parent dialogue attributes
			fh.RefreshDialogue(strName)												-- Refresh parent window size & position and bring infront of other window
			fh.RefreshDialogue("Font")													-- Refresh Font window size & position and bring infront of parent window
		end -- local function setDialogues

		local function getFont(strColor)												-- Set font button function
			local strTitle = " Choose font style & default colour for "..strColor:gsub("Head","Heading").." text "
			local strValue = "Font"..strColor											-- The font codes below are not recognised by iupFontDlg and result in empty font face!
			local strFont  = rawget(fh,strValue):gsub(" Black,",","):gsub(" Light, Bold",","):gsub(" Extra Bold,",","):gsub(" Semibold,",",")
			local iupFontDlg = iup.fontdlg { Title=strTitle; Color=rawget(fh,strColor); Value=strFont; }
			iupFontDlg:popup()															-- Popup predefined font dialogue
			if iupFontDlg.Status == "1" then
				if iupFontDlg.Value:match("^,") then									-- Font face missing so revert to original font
					iupFontDlg.Value = rawget(fh,strValue)
				end
				fh[strColor] = iupFontDlg.Color										-- Set Head or Body color attribute
				fh[strValue] = iupFontDlg.Value										-- Set FontHead or FontBody font style
				fh.AssignAttributes(tblButtons)										-- Assign the button & frame attributes
				setDialogues()
				isFontChosen = true
			end
		end -- local function getFont

		local function getColor(strColor)												-- Set colour button function
			local strTitle = " Choose colour for "..strColor:gsub("Warn","Warning"):gsub("Risk","Risky").." button & message text "
			local iupColorDlg = iup.colordlg { Title=strTitle; Value=rawget(fh,strColor); ShowColorTable="YES"; }
			iupColorDlg.DialogFrame="YES"
			iupColorDlg:popup()															-- Popup predefined color dialogue fixed size window
			if iupColorDlg.Status == "1" then
				fh[strColor] = iupColorDlg.Value										-- Set Safe or Warn or Risk color attribute
				fh.AssignAttributes(tblButtons)										-- Assign the button & frame attributes
				setDialogues()
				isFontChosen = true
			end
		end -- local function getColor

		local function setDefault()													-- Action for Default Fonts button
			fh.Safe = fh.Green
			fh.Warn = fh.Magenta
			fh.Risk = fh.Red																-- Set default colours
			fh.Body = fh.Black
			fh.Head = fh.Black
			fh.FontBody = iup.GetGlobal("DEFAULTFONT")								-- Set default fonts for Body and Head text
			fh.FontHead = fh.FontBody:gsub(", B?o?l?d?",", Bold")
			fh.AssignAttributes(tblButtons)											-- Assign the button & frame attributes
			setDialogues()
			isFontChosen = true
		end -- local function setDefault

		local function setMinimum()													-- Action for Minimum Size button
			local tblName = tblNameFor(strName)
			local iupDialog = tblName.Dialog											-- Retrieve the parent dialogue handle
			if type(iupDialog) == "userdata" then
				tblName.Raster = "10x10"												-- Refresh parent window to Minimum Size & adjust position
				fh.RefreshDialogue(strName)
			end
			local tblFont = tblNameFor("Font")
			tblFont.Raster = "10x10"													-- Refresh Font window to Minimum Size & adjust position
			fh.RefreshDialogue("Font")
		end -- local function setMinimum

		tblButtons  =  {  {  "Font" ; "FgColor" ; "Tip" ; "action" ; {"TipBalloon";"Balloon";} ; {"Expand";"YES";} ; };
			[btnFontHead] = { "FontHead"; "Head"; "Choose the Heading text Font Face, Style, Size, Effects, and default Colour"; function() getFont("Head") end; };
			[btnFontBody] = { "FontBody"; "Body"; "Choose the Body text Font Face, Style, Size, Effects, and default Colour"   ; function() getFont("Body") end; };
			[btnCol_Safe] = { "FontBody"; "Safe"; "Choose the colour for Safe operations"   ; function() getColor("Safe") end; };
			[btnCol_Warn] = { "FontBody"; "Warn"; "Choose the colour for Warning operations"; function() getColor("Warn") end; };
			[btnCol_Risk] = { "FontBody"; "Risk"; "Choose the colour for Risky operations"  ; function() getColor("Risk") end; };
			[btnDefault ] = { "FontBody"; "Safe"; "Restore default Fonts and Colours"; function() setDefault() end; };
			[btnMinimum ] = { "FontBody"; "Safe"; "Reduce window to its minimum size"; function() setMinimum() end; };
			[btnDestroy ] = { "FontBody"; "Risk"; "Close this dialogue "; function() return iup.CLOSE end; };
			[frmSetFonts] = { "FontHead"; "Head"; };
			} 

		fh.AssignAttributes(tblButtons)												-- Assign the button & frame attributes
		fh.ShowDialogue("Font",dialogFont,btnDestroy,"keep normal")				-- Popup the Set Window Fonts dialogue: "keep normal"  : vary size & posn, and remember size & posn
	--	fh.ShowDialogue("Font",dialogFont,btnDestroy,"popup normal")			-- Popup the Set Window Fonts dialogue: "popup normal" : vary size & posn, but redisplayed centred
	--	fh.ShowDialogue("Font",dialogFont,btnDestroy,"keep")						-- Popup the Set Window Fonts dialogue: "keep" : fixed size, vary posn, and only remember posn
	--	fh.ShowDialogue("Font",dialogFont,btnDestroy,"popup")						-- Popup the Set Window Fonts dialogue: "popup": fixed size, vary posn, but redisplayed centred
		dialogFont:destroy()
		return isFontChosen

	end -- function FontDialogue

	local function anyMemoControl(anyName,fgColor)									-- Compose any control Title and FgColor
		local strName = tostring(anyName)												-- anyName may be a string, and fgColor is default FgColor
		local tipText = nil
		if type(anyName) == "table" then												-- anyName may be a table = { Title string ; FgColor string ; ToolTip string (optional); }
			strName = anyName[1]
			fgColor = anyName[2]:match("%d* %d* %d*") or fgColor
			tipText = anyName[3]
		end
		return strName, fgColor, tipText
	end -- local function anyMemoControl

	local function anyMemoDialogue(strHead,anyHead,strMemo,anyMemo,...)		-- Display framed memo dialogue with buttons
		local arg = {...}																-- Fix for Lua 5.2+
		local intButt = 0																-- Returned value if "X Close" button is used
		local tblButt = { [0]="X Close"; }											-- Button names lookup table
		local strHead, fgcHead, tipHead = anyMemoControl(anyHead or "",strHead)
		local strMemo, fgcMemo, tipMemo = anyMemoControl(anyMemo or "",strMemo)
		-- Create the GUI labels and buttons
		local lblMemo = iup.label { Title=strMemo; FgColor=fgcMemo; Tip=tipMemo; TipBalloon=fh.Balloon; Alignment="ACENTER"; Padding=fh.Margin; Expand="YES"; WordWrap="YES"; }
		local lblLine = iup.label { Separator="HORIZONTAL"; }
		local iupHbox = iup.hbox  { Homogeneous="YES"; }
		local btnButt = iup.button { }
		local strTop  = "YES"															-- Make dialogue TopMost -- V3.6
		local strMode = "popup"
		if arg[1] == "Keep Dialogue" then												-- Keep dialogue open for a progress message
			strMode = "keep dialogue"
			lblLine = iup.label { }
			if not arg[2] then strTop = "NO" end										-- User chooses TopMost -- V3.6
		else
			if #arg == 0 then arg[1] = "OK" end 										-- If no buttons listed then default to an "OK" button
			for intArg, anyButt in ipairs(arg) do
				local strButt, fgcButt, tipButt = anyMemoControl(anyButt,fh.Safe)
				tblButt[intArg] = strButt
				btnButt = iup.button { Title=strButt; FgColor=fgcButt; Tip=tipButt; TipBalloon=fh.Balloon; Expand="NO"; MinSize="80"; Padding=fh.Margin; action=function() intButt=intArg return iup.CLOSE end; }
				iup.Append( iupHbox, btnButt )
			end
		end
		-- Create dialogue and turn off resize, maximize, minimize, and menubox except Close button
		local iupMemo = iup.dialog { Title=fh.Plugin..fh.Version..strHead; TopMost=strTop;	-- TopMost added -- V3.6
								iup.vbox { Alignment="ACENTER"; Gap=fh.Gap; Margin=fh.Margin;
									iup.frame { Title=strHead; FgColor=fgcHead; Font=fh.FontHead;
										iup.vbox { Alignment="ACENTER"; Font=fh.FontBody; lblMemo; lblLine; iupHbox; };
									};
								};
							}
		fh.ShowDialogue("Memo",iupMemo,btnButt,strMode)							-- Show popup Memo dialogue window with righthand button in focus (if any)
		if strMode == "keep dialogue" then return lblMemo, iupMemo end			-- Return label & dialogue controls so message can be changed and dialogue destroyed
		iupMemo:destroy()
		return intButt, tblButt[intButt]												-- Return button number & title that was pressed
	end -- local function anyMemoDialogue

	function fh.MemoDialogue(anyMemo,...)											-- Multi-Button GUI like iup.Alarm and fhMessageBox, with "Memo" in frame
		return anyMemoDialogue(fh.Head,"Memo",fh.Body,anyMemo,...)
	end -- function MemoDialogue

	function fh.WarnDialogue(anyHead,anyMemo,...)									-- Multi-Button GUI like iup.Alarm and fhMessageBox, with heading in frame
		return anyMemoDialogue(fh.Warn,anyHead,fh.Warn,anyMemo,...)
	end -- function WarnDialogue

	function fh.GetRegKey(strKey)														-- Read Windows Registry Key Value
		local luaShell = luacom.CreateObject("WScript.Shell")
		local anyValue = nil
		if pcall( function() anyValue = luaShell:RegRead(strKey) end ) then
			return anyValue																-- Return Key Value if found
		end
		return nil
	end -- function GetRegKey

	function fh.PutRegKey(strKey,anyValue,strType)									-- Write Windows Registry Key Value
		local luaShell = luacom.CreateObject("WScript.Shell")
		local strAns = nil
		if pcall( function() strAns = luaShell:RegWrite(strKey,anyValue,strType) end ) then
			return true
		end
		return nil
	end -- function PutRegKey

	local function httpRequest(strRequest)											-- Luacom http request protected by pcall() below
		local http = luacom.CreateObject("winhttp.winhttprequest.5.1")
		http:Open("GET",strRequest,false)
		http:Send()
		return http.Responsebody
	end -- local function httpRequest

	function fh.VersionInStore(strPlugin)											-- Obtain the Version in Plugin Store by Name only	-- V4.5
		local strVersion = "0"
		if strPlugin then
			local strFile = fh.MachinePath.."\\VersionInStore "..strPlugin..".dat"
			general.DeleteFile(strFile)
			local lblMemo, iupMemo = fh.MemoDialogue("Checking for updated version in the Family Historian 'Plugin Store'.","Keep Dialogue")
			local strRequest ="http://www.family-historian.co.uk/lnk/checkpluginversion.php?name="..strPlugin
			local isOK, strReturn = pcall(httpRequest,strRequest)
			iupMemo:destroy()
			if not isOK then																-- Problem with Internet access
				fhMessageBox(strReturn.."\n The Internet appears to be inaccessible. ","MB_OK","MB_ICONEXCLAMATION")
			elseif strReturn ~= nil then
				strVersion = strReturn:match("([%d%.]*),%d*")						-- Version digits & dots then comma and Id digits 
			end
		end
		return strVersion or "0"
	end -- function VersionInStore

	local function intVersion(strVersion)											-- Convert version string to comparable integer
		local intVersion = 0
		local arrNumbers = {}
		strVersion:gsub("(%d+)", function(strDigits) table.insert(arrNumbers,strDigits) end)	-- V4.1
		for i=1,5 do
			intVersion = intVersion * 100 + tonumber(arrNumbers[i] or 0)
		end
		return intVersion
	end -- local function intVersion

	function fh.CheckVersionInStore()													-- Check if later Version available in Plugin Store	-- V4.5
		local strNewVer = fh.VersionInStore(fh.Plugin:gsub(" %- .*",""))
		local strOldVer = fh.Version
		if intVersion(strNewVer) > intVersion(strOldVer:match("%D*([%d%.]*)")) then
			fh.MemoDialogue("Later Version "..strNewVer.." of this Plugin is available from the 'Plugin Store'.")
		else
			fh.MemoDialogue("No later Version of this Plugin is available from the 'Plugin Store'.")
		end
	end -- function CheckVersionInStore

	function fh.PluginDataScope(strScope)											-- Set default Plugin Data scope to per-Project, or per-User, or per-Machine
		strScope = tostring(strScope):lower()
		if strScope:match("mach") then												-- Per-Machine
			strDefaultScope = "Machine"
		elseif strScope:match("user") then											-- Per-User
			strDefaultScope = "User"
		end																					-- Per-Project is default
	end -- function PluginDataScope

	local function getPluginDataFileName(strScope)									-- Get plugin data filename for chosen scope
		local isOK, strDataFile = pcall(fhGetPluginDataFileName,strScope)
		if not isOK then strDataFile = fhGetPluginDataFileName() end			-- Before V5.0.8 parameter is disallowed and default = CURRENT_PROJECT
		return strDataFile
	end -- local function getPluginDataFileName

	local function getDataFiles(strScope)											-- Compose the Plugin Data file & path & root names
		local strPluginName = fh.Plugin
		local strPluginPlain = stringx.plain(strPluginName)
		local strDataRoot = ""															-- Plugin data file root name -- V4.2
		local strDataPath = ""															-- Plugin data folder path name
		local strDataFile = getPluginDataFileName(strScope)						-- Allow plugins with variant filenames to use same plugin data files
		strDataFile = strDataFile:gsub("\\"..strPluginPlain:gsub(" ","_"):lower(),"\\"..strPluginName)
		strDataFile = strDataFile:gsub("\\"..strPluginPlain..".+%.[D,d][A,a][T,t]$","\\"..strPluginName..".dat")
		if #strDataFile > 0 then														-- Standalone GEDCOM path is ""
			strDataPath = strDataFile:gsub("\\"..strPluginPlain.."%.[D,d][A,a][T,t]$","")
			strDataRoot = strDataPath.."\\"..strPluginName
			general.MakeFolder(strDataPath)											-- V3.4
		end
		return strDataFile, strDataPath, strDataRoot
	end -- local function getDataFiles

	function fh.Initialise(strVersion,strPlugin)									-- Initialise the GUI module with optional Version & Plugin name

		local strAppData = fhGetContextInfo("CI_APP_DATA_FOLDER")
		fh.Plugin = fhGetContextInfo("CI_PLUGIN_NAME")								-- Plugin Name from file
		fh.Version = strVersion or " "												-- Plugin Version
		if fh.Version == " " then
			local strTitle = "\n@Title is missing"
			local strAuthor = "\n@Author is missing"
			local strVersion = "\n@Version is missing"
			local strPlugin = strAppData.."\\Plugins\\"..fh.Plugin..".fh_lua"
			if general.FlgFileExists(strPlugin) then
				for strLine in io.lines(strPlugin) do								-- Read each line from the Plugin file
					strPlugin = strLine:match("^@Title:[\t-\r ]*(.*)")
					if strPlugin then
						strPlugin = strPlugin:gsub("&&","&")
	--?					if fh.Plugin:match("^"..strPlugin:gsub("(%W)","%%%1")) then
						if fh.Plugin:match("^"..stringx.plain(strPlugin)) then
							fh.Plugin = strPlugin										-- Prefer Title to Filename if it matches
							strTitle = nil
						else
							strTitle = "\n@Title differs from Filename"				-- Report abnormality
						end
					end
					if strLine:match("^@Author:%s*(.*)") then						-- Check @Author exists
						strAuthor = nil
					end 
					fh.Version = strLine:gsub("^@Version:%D*([%d%.]*)%D*"," %1 ")
					if fh.Version ~= strLine then										-- Obtain the @Version from Plugin file
						strVersion = nil
						break
					end
				end
				if strTitle or strAuthor or strVersion then							-- Report any header abnormalities
					fhMessageBox("\nScript Header: "..fh.Plugin..(strTitle or "")..(strAuthor or "")..(strVersion or ""),"MB_OK","MB_ICONEXCLAMATION")
				end
			else
				fhMessageBox("\nPlugin has not been saved!","MB_OK","MB_ICONEXCLAMATION")
			end
		end
		fh.History = fh.Version															-- Version History
		fh.Plugin  = strPlugin or fh.Plugin											-- Plugin Name from argument or default from file

		fh.DefaultDialogue()															-- Default "Font","Main" dialogues

		fh.MachineFile,fh.MachinePath,fh.MachineRoot = getDataFiles("LOCAL_MACHINE")		-- Plugin data names per machine
		fh.PerUserFile,fh.PerUserPath,fh.PerUserRoot = getDataFiles("CURRENT_USER")		-- Plugin data names per user
		fh.ProjectFile,fh.ProjectPath,fh.ProjectRoot = getDataFiles("CURRENT_PROJECT")	-- Plugin data names per project

		fh.FhDataPath = fhGetContextInfo("CI_PROJECT_DATA_FOLDER")							-- Paths used by Load/SaveFolder for relative folders		-- V4.0
		fh.PublicPath = fhGetContextInfo("CI_PROJECT_PUBLIC_FOLDER")						-- Public data folder path name		-- V4.0
		if fh.FhDataPath == "" then
			fh.FhDataPath = fh.ProjectPath:gsub("\\Plugin Data$","")
		end
		if fh.PublicPath == "" then
			fh.PublicPath = fh.ProjectPath
			fh.FhProjPath = fh.PublicPath:gsub("^(.+)\\.-\\Plugin Data$","%1")
		else
			general.MakeFolder(fh.PublicPath)											-- V3.4
			fh.FhProjPath = fh.PublicPath:gsub("^(.+)\\.-\\Public$","%1")
		end
		fh.CalicoPie = strAppData:gsub("\\Calico Pie\\.*","\\Calico Pie")		-- Program Data Calico Pie path name

		fh.ComputerName = os.getenv("COMPUTERNAME")									-- Local PC Computer Name

	end -- function Initialise

	fh.Initialise()																		-- Initialise module with default values

	return fh

end -- local function iup_gui_v3

local iup_gui = iup_gui_v3()															-- To access FH IUP GUI build module

local StrLocation																	-- Declare global as upvalue because it is set to nil 
local IntChecked																		-- Declare global as upvalue because it may be set nil
local StrSync																			-- Declare global as upvalue because it is not declared
local DicFactSet																		-- Declare global as upvalue because it may be set nil

-- Translate text to current output language --		-- V5.1
function StrTranslate(strText)
	-- strText	~ Text to translate
	return g_trans_misc["~HTML:"..strText] or g_trans_misc[strText] or strText
end -- function StrTranslate

-- Global Data Constants Definition --
function PresetGlobalData()

	g_trans_misc = { Output_Language = ""; }			-- Create global Language Pack translation table -- V5.1
	if fhGetAppVersion() > 6 then
		local strPart = "Family Historian"				-- V6.1
		if fhGetAppVersion() > 7 then
			strPart = "Family Historian 8"				-- V6.1
		end
		local strLang = iup_gui.GetRegKey("HKCU\\SOFTWARE\\Calico Pie\\"..strPart.."\\2.0\\Preferences\\Output Language Id") or ""	-- V6.1
		if #strLang > 2 then
			local strPack = fhGetContextInfo("CI_APP_DATA_FOLDER").."\\Lang\\"..strLang.."\\lang_trans_misc.fh_lua"
			dofile(strPack)
			g_trans_misc.Output_Language = " for Output Language "..strLang
		else
			g_trans_misc.Output_Language = " for English"
		end
	end

	-- GUI Global Constants

	iup_gui.Gap = "2"
	iup_gui.Margin = "2x2"
	iup_gui.Balloon = "NO" 									-- Needed for PlayOnLinux/Mac	-- V4.7
	iup_gui.SetUtf8Mode()

	IntTabGeoLoc		=	1									-- Main tab (& Preferences tab) position values	-- 1 Aug 2013
	IntTabCreate		=	2
	IntTabPrefer		=	3
	StrLocation		= nil									-- Location entry is unselected	-- V4.9
	StrReverse		= "OFF" 								-- Reverse Display Order is OFF	-- V4.9
	StrClickHere		= "Click here to choose an Entry"
	StrRegBiasTitle	= "Region Bias Code"
	StrGeoCodeTitle	= "Smart Geocoding"
	StrFromTagTitle	= "Locations From"
	StrStorageTitle	= "Database In"
	StrLoggingTitle	= "Geocoder Logging"
	StrColoursTitle	= "Marker Colour"					-- V6.0
	StrMarkersTitle	= "Marker Animation"
	StrPrivacyTitle	= "Privacy Filter"
	StrFactSetTitle	= "Fact Filter"						-- V3.7
	StrMapZoomTitle	= "Map Zoom"
	IntGetZoom		= 12
	IfGotZoom 		= false								-- Used by ReadMapPane()	-- V4.3 
	IntFromPLAC		=	1									-- Locations From values for IntFromTag
	IntFromADDR		=	2
	IntFromBOTH		=	3									-- Locations From values for BOTH variants:
	IntFromP			=	3									--  ~ Place only
	IntFromA			=	4									-- Address ~ only
	IntFromAP			=	5									-- Address ~ Place
	IntFromA_P_		=	6									-- Address ~ [Place]
	IntFrom_A_P		=	7									-- [Address] ~ Place
	IntFrom_A__P_		=	8									-- [Address] ~ [Place]
	IntFromINDI		=	4									-- Special From value for Tree GUI table
	TblFromName		= { "Place Fields only"; "Address Fields only"; " ~ Place only"; "Address ~ only"; "Address ~ Place"; "Address ~ [Place]"; "[Address] ~ Place"; "[Address] ~ [Place]"; }
	TblFromTag		= { "PLAC"; "ADDR"; "BOTH"; }
	StrFromTag		= TblFromTag[IntFromPLAC]
	TblFromType		= { PLAC="Facts Place"; ADDR="Facts Address"; BOTH="Facts Address ~ Place"; }
	TblStoreTAG		= { ""; "REPO"; "SOUR"; "SOUR"; "_PLAC"; }
	IntStoreFILE		=	1									-- Geocode Database values for IntStorage
	IntStoreREPO		=	2
	IntStoreSOUR		=	3
	IntStoreBOTH		=	4
	IntStorePLAC		=	5
	IntStorage		= IntStoreFILE						-- FH6
	IntArchive		= IntStoreFILE						-- FH6
	IntMarkNothing	=	1									-- Marker Style values for IntMarkers
	IntMarkSynchro	=	2
	IntMarkCascade	=	3
	IntLivingFlag		=	1									-- Privacy Flag values for IntPrivacy
	IntPrivateFlag	=	2
	IntBothFlags		=	3
	IntSwitchedOff	=	4
	IsFromNew			= false								-- Signal new Locations From plot data

	-- Plot & Map Status Translation Tables

	StrPlotNoData		= "No Data"							-- Plot status text strings related to TblPlotStates
	StrPlotDefined	= "%-?[0-9]+[%.,]-[0-9]-Defined"	-- Lat/Longitude numerical pattern as below but Defined/non-Tentative 
	StrPlotTentative	= "%-?[0-9]+[%.,]-[0-9]-"			-- Lat/Longitude numerical pattern ( optional "-"  1+ digits  optional "." or ","  0+ digits )	-- Decimal symbols . or ,
	StrPlotNoPlot		= "No Plot"
	StrPlotNotFound	= "Not Found"							-- FH Status Not Found 					-- V4.9
	StrPlotBlocked	= "Blocked"							-- FH Status Block (No Auto-Geocode)	-- V4.9
	StrPlotInvalid	= "Invalid Request"
	StrPlotDenied		= "Request Denied"
	StrPlotQuotaX		= "Quota Exceeded"
	StrPlotUnknown	= "Unknown Error"					-- V3.8
	StrPlotAborted	= "Plot Aborted"						-- Force plot loops to abort when a major error detected	-- V4.2
	IntPlotNoData		=	1									-- Plot status table index values related to TblPlotStates & TblStatusColor
	IntPlotDefined	=	2
	IntPlotTentative	=	3
	IntPlotNotFound	=	4									-- Not Found						-- V4.9
	IntPlotBlocked	=	5									-- Block (No Auto-Geocode)	-- V4.9
	IntPlotInvalid	=	6
	IntPlotDenied		=	7
	IntPlotQuota		=	8
	IntPlotUnknown	=	9									-- V3.8 "Red"
	IntPlotSumTotal	= 10									-- V3.8 "Black"
	StrMapNoMap		= "No Map"							-- Map status text strings related to TblMapStates
	StrMapFilename	= "map[0-9]+%.html"					-- Map Folder filename pattern ( "map"  1+ digits  ".html" )	-- %d V3.5
	StrMapOmitted		= "Omitted"
	StrMapFiltered	= "Filtered"
	IntMapNoMap		=	1									-- Map status table index values related to TblMapStates & TblStatusColor
	IntMapFilename	=	2	
	IntMapOmitted		=	4
	IntMapFiltered	=	6									-- V4.9
	IntMapSumTotal	= 10									-- V3.8 "Black"
	IntHighlight		= 10									-- V3.8
	TblPlotStates 	= { "^$"		; "^"..StrPlotDefined.."$"; "^"..StrPlotTentative.."$"	; "^"..StrPlotNotFound.."$"	; "^"..StrPlotBlocked.."$"; "^"..StrPlotInvalid.."$"; "^"..StrPlotDenied.."$"; "^"..StrPlotQuotaX.."$"; "^"..StrPlotUnknown.."$"; }	-- V3.8	-- V4.9
	TblMapStates		= { "^$"		; "^"..StrMapFilename.."$"; "^$"							; "^"..StrMapOmitted.."$"		; "^$" 						; "^"..StrMapFiltered.."$"; }
	TblStatusColour	= { "Gray"	; "Green"					; "Purple"						; "Amber"							; "Amber"						; "Red"						; "Red"						; "Red"						; "Red"						; "Black"; }		-- V4.9
	StrMasterWeb 		= StrMasterWeb:gsub("Map of Locations for",StrTranslate("Map of Locations for"))	-- V5.1

	-- Filename Global Constants

	StrFileNameHTM = iup_gui.MachineRoot..".htm"						-- Map Pane HTML file
	StrFileNameACT = iup_gui.MachineRoot..".act"						-- Map Pane Action file
	StrFileNameLOC = iup_gui.ProjectRoot..".loc"						-- Locations Geocode database file
	StrFileNameCSV = iup_gui.ProjectRoot.." {GeoPlot}.csv" 			-- Locations plotting CSV log file

	-- FH Web Page HTML code strings
	StrEOL = "\r\n"															-- End Of Line is \r\n in FH V7 but \n in FH V5/6	-- V5.1
	if fhGetAppVersion() < 7 then StrEOL = "\n" end
	StrDivContent = "<div class=\"fhcontent fhpage%u%a+\">"..StrEOL	-- <div class="fhcontent fhpageFam"> or fhpageInd or fhpageDesc or fhpageIndNarr or fhpageRec	-- V5.1
	StrDivSeeAlso = "<div class=\"FhSeeAlso\">"..StrEOL				-- <div class="FhSeeAlso">\r\n	-- V5.1
	StrOldSeeAlso = "[<!%- ]-<p>[^\r\n]-</p>[ %->]-"					-- <!-- <p>See also</p> -->		-- V5.1 -- Cater for 'Improve Website or CD DVD HTML' plugin Tree Diagrams tab 'See also' section
	StrNewSeeAlso = "<p>"..StrTranslate("See also").."</p>"			-- <p>See also</p> translated		-- V5.1 -- May have <!-- comment --> tags and may be translated to a foreign language
	StrBegSeeAlso = StrEOL.."<ul>"..StrEOL								-- \r\n<ul>\r\n						-- V5.1
	StrEndSeeAlso = "</ul>"..StrEOL.."</div>"..StrEOL					-- </ul>\r\n</div>\r\n				-- V5.1
	StrH1Heading  = "<h1 class=\"FhHdg1\">"								-- <h1 class="FhHdg1">
	StrMapHeaders = ""														-- Map file script between "</title>" and "</head>"  to replace {fhheaders}
	StrMapMenubar = ""														-- Map file script between "<body>" and "<div class" to replace {fhmenubar}
	DicGeoPlot = { "Disabled"; "Google"; "Geoapify"; Disabled=0; Google=1; Geoapify=2; }				-- V5.3 -- V6.0
	IntGeoPlot = DicGeoPlot.Disabled										-- Geocoder for plotting			-- V5.3
	StrAPI_Key = "" 															-- Google API Key such as "BIzaSyDfg3XAWDnSmAcAasUKvnScySIPnMtVUjp"	-- V4.2
	StrGeo_Key = ""															-- Geoapify API Key such as "c9c19e912a1349ddb87c18882a4495ef9" 		-- V5.3
	DicMapMode = { "GoogleLegacy"; "LeafletMaps"; "GoogleAdvanced"; Unknown=0; GoogleLegacy=1; LeafletMaps=2; GoogleAdvanced=3; }			-- V5.3 -- V6.0
	IntWebMaps = DicMapMode.Unknown										-- Web Page Maps from DicMapMode	-- V6.0 -- V5.3
	IntMapMode = DicMapMode.LeafletMaps									-- Plugin Map Pane is LeafletMaps	-- V6.0 -- V5.3 -- V5.2 -- V4.3
	DicColours = { "Red"; "Blue"; "Green"; Red=1; Blue=2; Green=3; }										-- V6.0
	IntLegends = 3															-- Plugin Map Pane Legends 3=Medium as below		-- V5.3
	ArrMapPane = {
		"width:140%;height:140%;zoom: 70%;";								-- 1 = Tiny
		"width: 98%;height: 98%;zoom:100%;";								-- 2 = Small
		"width: 70%;height: 70%;zoom:140%;";								-- 3 = Medium
		"width: 50%;height: 50%;zoom:196%;";								-- 4 = Large
		"width: 40%;height: 40%;zoom:245%;";								-- 5 = Huge
	}
	StrMapPath = "|"															-- Map paths with their map files in new format	-- V4.3
	StrBaseMap = ""															-- Base Layer Map chosen in Map Pane in Leaflet Mode	-- V4.3
	StrTypeMap = "road"														-- Type Id of Map chosen in Map Pane in Bing Mode	-- V5.3
	TblPlotLog = {}
	local arrLead = { "^GeocodeResponse%."; "^ResourceSets%.ResourceSet%."; "^root%."; }
	for intGeoPlot=1, #DicGeoPlot do										-- Initialise locations plotting CSV log file data	-- V5.3
		TblPlotLog[intGeoPlot] = {}
		TblPlotLog[intGeoPlot].Head = {}
		TblPlotLog[intGeoPlot].Lead = arrLead[intGeoPlot]				-- Leading label prefix to be removed
	end
end -- function PresetGlobalData

-- Reset Sticky Settings to Default Values --
function ResetDefaultSettings()
	iup_gui.CustomDialogue("Main","0x0")				-- Custom "Main" dialogue minimum size & centralisation
	iup_gui.CustomDialogue("Font","0x0")				-- Custom "Font" dialogue minimum size & centralisation
	iup_gui.CustomDialogue("List","600x400")		-- Custom "List" dialogue default size and centralisation
	iup_gui.DefaultDialogue("Bars","Memo","List")	-- GUI window RasterSize and X & Y co-ordinates for "Main","Font","Bars","Memo","List" dialogues
	IntTabPosn = 0										-- Initial tab is undefined	-- 1 Aug 2013	was IntTabGeoLoc	Initial tab is Geocode Location Plots			
	IsSplashOn = "API"									-- Is new user API Help page shown			-- V5.3 -- V4.9
	StrWarnAns = "Yes"									-- Quota Exceeded warning window shown
	StrGeoCode = "ON"									-- Smart Geocoding option is ON
	IntRegBias = 235										-- Default Geocoder Region Bias TLD Code is UK
	IntLocZoom = 12										-- Map Pane Zoom for Geocode Location Plots
	IntFromTag = IntFromPLAC							-- Locations From Tag is "PLAC"
	StrFromTag = TblFromTag[IntFromTag]				-- V3.5
	StrLogging = "OFF"									-- Logging plot XML fields to CSV is OFF	-- V5.3
	StrFolder  = iup_gui.PublicPath					-- Output folder path for Web page Maps	-- V5.1 ANSI in FH V5 else UTF-8
	IsFromADDR = ""										-- Locations From ADDR for Address ~ Place sub-options -- Only for Adjust V3.1 below
	IsFromPLAC = ""										-- Locations From PLAC for Address ~ Place sub-options -- Only for Adjust V3.1 below
	IntMapZoom = 12										-- Map Pane Zoom for Create Web Page Maps
	IntMarkers = IntMarkCascade						-- Markers Style is Cascade Drop
	IntColours = DicColours.Red						-- Map Marker Pin Colours 1=Red, 2=Blue, 3=Green			-- V6.0
	StrColours = DicColours[IntColours]
	IntPrivacy = IntSwitchedOff						-- Privacy Filter is Switched Off
	StrFactSet = "OFF"									-- Facts Filter option is OFF -- V3.7
	StrBaseMap = ""										-- Base Layer Map chosen in Map Pane in Leaflet Mode	-- V4.3
end -- function ResetDefaultSettings

-- Load Sticky Settings from File --
function LoadSettings()
	iup_gui.LoadSettings("Bars","Memo","List")
	IntTabPosn  = tonumber(iup_gui.LoadGlobal("TabPosn",IntTabPosn))
	StrLocation = tostring(iup_gui.LoadGlobal("Entry" ,StrLocation))
	StrWarnAns  = tostring(iup_gui.LoadGlobal("WarnAns",StrWarnAns))
	IsSplashOn  = tostring(iup_gui.LoadGlobal("Splash" ,IsSplashOn,"Per Machine"))	-- V4.9
	IntGeoPlot  = tonumber(iup_gui.LoadGlobal("GeoPlot",IntGeoPlot))					-- V5.3
	StrAPI_Key  = tostring(iup_gui.LoadGlobal("API_Key",StrAPI_Key,"Per Machine"))	-- V4.2
	StrGeo_Key  = tostring(iup_gui.LoadGlobal("Geo_Key",StrGeo_Key,"Per Machine"))	-- V5.3
	if IntGeoPlot > DicGeoPlot.Geoapify
	or IntGeoPlot == DicGeoPlot.Geoapify and #StrGeo_Key < 9
	or IntGeoPlot == DicGeoPlot.Google and #StrAPI_Key < 9 then		-- V6.0
		IntGeoPlot = DicGeoPlot.Disabled
	end
	StrGeoCode  = tostring(iup_gui.LoadGlobal("GeoCode",StrGeoCode))
	StrReverse  = tostring(iup_gui.LoadGlobal("Reverse",StrReverse))					-- V4.9
	IntRegBias  = tonumber(iup_gui.LoadGlobal("RegBias",IntRegBias))
	IntStorage  = tonumber(iup_gui.LoadGlobal("Storage",IntStorage))
	IntArchive  = tonumber(iup_gui.LoadGlobal("Archive",IntArchive)) -- FH6
	IntLocZoom  = tonumber(iup_gui.LoadGlobal("LocZoom",IntLocZoom))
	IntFromTag  = tonumber(iup_gui.LoadGlobal("FromTag",IntFromTag))
	StrLogging  = tostring(iup_gui.LoadGlobal("Logging",StrLogging))					-- V5.3
	StrFolder   = tostring(iup_gui.LoadFolder("Folder" ,StrFolder ))
	IntWebMaps  = tonumber(iup_gui.LoadGlobal("WebMaps",IntWebMaps))					-- V5.3
	if IntWebMaps == DicMapMode.Unknown then							-- First use		-- V5.3 -- V6.0
		IntWebMaps = DicMapMode.LeafletMaps								-- Maps use Leaflet Mode
		IntMapMode = DicMapMode.LeafletMaps
		if StrLocation ~= "nil" then
			fhMessageBox("\n Please see the 'Set Preference Options' tab new options :- \n\n The 'Location Plot Options' tab offers Google and Geoapify geocoding. \n\n The 'Web Page Map Options' tab offers Google and Leaflet map displays. \n")
		end
	end
	if IntWebMaps ~= DicMapMode.LeafletMaps and #StrAPI_Key < 9 then
		IntWebMaps = DicMapMode.LeafletMaps								-- Maps need Leaflet Mode -- V6.0
	end
	IntMarkers  = tonumber(iup_gui.LoadGlobal("Markers",IntMarkers))
	IntColours  = tonumber(iup_gui.LoadGlobal("Colours",IntColours))					-- V6.0
	StrColours  = DicColours[IntColours]														-- V6.0
	IntLegends  = tonumber(iup_gui.LoadGlobal("Legends",IntLegends))					-- V5.3
	StrMapPath  = tostring(iup_gui.LoadGlobal("MapPath",StrMapPath))					-- V4.3
	IsFromADDR  = tostring(iup_gui.LoadGlobal("Is~ADDR",IsFromADDR)) -- Only for Adjust V3.1 below
	IsFromPLAC  = tostring(iup_gui.LoadGlobal("Is~PLAC",IsFromPLAC)) -- Only for Adjust V3.1 below
	IntMapZoom  = tonumber(iup_gui.LoadGlobal("MapZoom",IntMapZoom))
	IntPrivacy  = tonumber(iup_gui.LoadGlobal("Privacy",IntPrivacy))
	StrFactSet  = tostring(iup_gui.LoadGlobal("FactSet",StrFactSet)) -- V3.7
	StrBaseMap  = tostring(iup_gui.LoadGlobal("BaseMap",StrBaseMap)) -- V4.3
	IntChecked  = tonumber(iup_gui.LoadGlobal("Checked",IntChecked)) -- Only for Adjust V3.0 below
	if IntFromTag == IntFromBOTH and IntChecked then			-- Adjust V3.0 Address ~ Place to V3.2 Address ~ [Place] variant
		IntFromTag = IntFromA_P_
	end
	local tblFromTag = { "--"; "-+"; "+-"; "++"; }
	if tblFromTag[IntFromTag - 2] == IsFromADDR..IsFromPLAC then IntFromTag = IntFromTag + 2 end		-- Adjust V3.1 to V3.2 Address ~ Place variants
	local intFromTag = math.min(IntFromTag,IntFromBOTH)		-- V3.5
	local strFromTag = TblFromTag[intFromTag]					-- V3.5
	StrFromTag = strFromTag											-- V3.5
	SetIsFromFlags()
	SaveSettings()							-- Save sticky data settings
end -- function LoadSettings

-- Save Sticky Settings to File --
function SaveSettings()
	iup_gui.SaveGlobal("MapPane",nil,"Machine")	-- Remove old settings	-- V5.3
	iup_gui.SaveGlobal("IsGoogle",nil,"Machine")
	iup_gui.SaveGlobal("BingAPI",nil,"Machine")
	iup_gui.SaveGlobal("BingKey",nil,"Machine")					-- V6.0
	iup_gui.SaveGlobal("AzurKey",nil,"Machine")					-- V6.0
	iup_gui.SaveGlobal("TypeMap",nil,"Machine")					-- V6.0
	iup_gui.SaveGlobal("MapMode",nil,"Machine")					-- V6.0
	iup_gui.SaveGlobal("PlotNew",nil,"Machine")
	iup_gui.SaveGlobal("TabPosn",IntTabPosn)
	iup_gui.SaveGlobal("Entry" ,StrLocation)
	iup_gui.SaveGlobal("WarnAns",StrWarnAns)
	iup_gui.SaveGlobal("Splash" ,IsSplashOn,"Per Machine")	-- V4.9
	iup_gui.SaveGlobal("GeoPlot",IntGeoPlot)					-- V5.3
	iup_gui.SaveGlobal("API_Key",StrAPI_Key,"Per Machine")	-- V4.2
	iup_gui.SaveGlobal("Geo_Key",StrGeo_Key,"Per Machine")	-- V5.3
	iup_gui.SaveGlobal("GeoCode",StrGeoCode)
	iup_gui.SaveGlobal("Reverse",StrReverse)					-- V4.9
	iup_gui.SaveGlobal("MapPath",StrMapPath)					-- V4.3
	iup_gui.SaveGlobal("RegBias",IntRegBias)
	iup_gui.SaveGlobal("Storage",IntStorage)
	iup_gui.SaveGlobal("Archive",IntArchive)					-- FH6
	iup_gui.SaveGlobal("LocZoom",IntLocZoom)
	iup_gui.SaveGlobal("FromTag",IntFromTag)
	iup_gui.SaveGlobal("Logging",StrLogging)					-- V5.3
	iup_gui.SaveFolder("Folder" ,StrFolder )
	iup_gui.SaveGlobal("WebMaps",IntWebMaps)					-- V5.3
	iup_gui.SaveGlobal("Markers",IntMarkers)
	iup_gui.SaveGlobal("Colours",IntColours)					-- V6.0
	iup_gui.SaveGlobal("Legends",IntLegends)					-- V5.3
	iup_gui.SaveGlobal("MapZoom",IntMapZoom)
	iup_gui.SaveGlobal("Privacy",IntPrivacy)
	iup_gui.SaveGlobal("FactSet",StrFactSet)					-- V3.7
	iup_gui.SaveGlobal("BaseMap",StrBaseMap)					-- V4.3
	iup_gui.SaveSettings("Bars","Memo","List")					-- Includes "Main","Font","Bars","Memo","List" dialogues and "FontSet" & "History"
end -- function SaveSettings

-- Check if Google Maps API Key is Missing --					-- V5.3
function IsGoogleKeyMissing()
	return ( #StrAPI_Key ~= 39 )
end -- function IsGoogleKeyMissing

-- Check if Google Maps used and no API Key --					-- V6.0
function IsGoogleMapAndNoKey(intGeoPlot,intWebMaps)
	return ( IsGoogleKeyMissing() and ( intGeoPlot == DicGeoPlot.Google or intWebMaps ~= DicMapMode.LeafletMaps ) )
end -- function IsGoogleMapAndNoKey

-- Check if Geoapify API Key is Missing --						-- V5.3
function IsGeo_KeyMissing()
	return ( #StrGeo_Key ~= 32 )
end -- function IsGeo_KeyMissing

-- Check if Geoapify used and no API Key --						-- V6.0
function IsGeoapifyAndNoKey(intGeoPlot)
	return ( IsGeo_KeyMissing() and intGeoPlot == DicGeoPlot.Geoapify )
end -- function IsGeoapifyAndNoKey

-- Show Status Colour & Message Text --
function ShowStatusMessage(strColour,strMessage)
	if IsGoogleMapAndNoKey(IntGeoPlot,IntWebMaps)				-- V6.0
	or IsGeoapifyAndNoKey(IntGeoPlot) then						-- V6.0
		strMessage = "Geocoder API Key Required see Help && Advice F.A.Q."
		strColour = iup_gui.Risk
	end
	LblMessage.FgColor = strColour
	LblMessage.Title = strMessage
	iup.Flush()
end -- function ShowStatusMessage

function ShowDelErr(strError)
	ShowStatusMessage(iup_gui.Risk,strError)
end -- ShowDelErr

-- Set IsFromADDR & IsFromPLAC Flags --
function SetIsFromFlags()
	IsFromADDR = true						-- Locations From ADDR for Address ~ Place sub-options
	IsFromPLAC = true						-- Locations From PLAC for Address ~ Place sub-options
	if IntFromTag < IntFromBOTH then
		return false
	end
	if IntFromTag == IntFromAP or IntFromTag == IntFromA_P_ then IsFromADDR = nil end
	if IntFromTag == IntFromAP or IntFromTag == IntFrom_A_P then IsFromPLAC = nil end
	return true
end -- function SetIsFromFlags

-- Have the Latitude & Longitude been Geocoded? --
function IsNotGeocoded(tblLocData)
	if tblLocData then
		if tblLocData.lat == "" or tblLocData.lng == "" then
			return true
		end
	end
	return false
end -- function IsNotGeocoded

-- Have the Latitude & Longitude got numbers? -- 
function IsPlotted(tblLocData)
	if tblLocData then
		if tonumber(tblLocData.lat) and tonumber(tblLocData.lng) then
			return true
		end
	end
	return false
end -- function IsPlotted

-- Encode Map Marker style into a table --
function TblMarkerStyle()
	local strAnimate = "DROP"
	local strTimeout = "300"
	if IntMarkers == IntMarkNothing then strAnimate = "null" end
	if IntMarkers == IntMarkCascade then strTimeout = "2000" end
	return { Timeout=strTimeout; Animate=strAnimate; }
end -- function TblMarkerStyle

-- Ensure old map files use new format -- 						-- V4.3
function FixOldMapFiles(strFolder)
	local strAns
	if not StrMapPath:matches(strFolder) then
		local arrFolder = general.GetFolderContents(strFolder)
		for _, dicFile in ipairs (arrFolder) do					-- V5.1
			local strFile = dicFile.path
			if strFile:match("^map%d+%.html$") then				-- Found map file
				local strText, intRepl = general.StrLoadFromFile(strFile):replace("\n    <link type='text/css' rel='stylesheet' href='http://code.google.com/apis/maps/documentation/javascript/examples/default.css'>","")	-- V4.4
				if intRepl > 0 then 								-- Old format Google css link found & removed, so perform other updates?
					strAns = strAns or fhMessageBox("\n Update other map files in this folder to new format? \n","MB_YESNO","MB_ICONQUESTION")
					if strAns == "No" then return end
					strText = strText:replace(" = new google.maps.LatLng("," = NewLatLong(")
					if not strText:matches("      LocName[0] = ") then
						strText = strText:gsub("(      Caption%[(%d+)] = '(.-)';\n)","%1      LocName[%2] = '%3';\n")
					end
					general.SaveStringToFile(strText,strFile)	-- Save updated map file
				end
			end
		end
		StrMapPath = StrMapPath..strFolder.."|"					-- Add folder to list of map paths using new format
	end
end -- function FixOldMapFiles
					
-- Create the mapfacts.js & mapfacts.css & mapfacts.png files --	-- V6.0
function MakeSharedFiles(strFolder,intMapMode)
	local tblMarkerStyle = TblMarkerStyle()
	local strMasterJS = StrMasterJS
	local strAPI_Key = "Missing"
	local strMapMode = DicMapMode[intMapMode or IntWebMaps]						-- V5.3
	local strColours = StrColours:lower()											-- V6.0 -- Marker pin colours
	if strMapMode:match("Google") then strAPI_Key = StrAPI_Key end				-- V4.3
	strMasterJS = strMasterJS:replace("{mapmode}",StrMasterMap[strMapMode],1)	-- V4.3 -- V5.1 -- V5.3
	strMasterJS = strMasterJS:replace("{api_key}",strAPI_Key,1)		-- V4.2	-- V4.3 -- V5.1
	strMasterJS = strMasterJS:replace("{map_id#}","DEMO_MAP_ID",1)				-- V6.0 -- For Google Maps Advanced Marker Elements Map_Id
	strMasterJS = strMasterJS:replace("{colours}",strColours)					-- V6.0 -- Marker pin colours
	strMasterJS = strMasterJS:replace("{animate}",tblMarkerStyle.Animate,1)	-- V5.1
	strMasterJS = strMasterJS:replace("{timeout}",tblMarkerStyle.Timeout,1)	-- V5.1
	strMasterJS = strMasterJS:replace("{setzoom}",IntMapZoom,1)					-- V5.1
	strMasterJS = strMasterJS:replace("{basemap}",StrBaseMap,1)		-- V4.3	-- V5.1 Leaflet Base Layer
	strMasterJS = strMasterJS:replace("Latitude" ,StrTranslate("Latitude"),1)	-- V5.1
	strMasterJS = strMasterJS:replace("Longitude",StrTranslate("Longitude"),1)
	general.SaveStringToFile(strMasterJS,strFolder.."\\mapfacts.js")
	local strMasterCSS = StrMasterCSS
	general.SaveStringToFile(strMasterCSS,strFolder.."\\mapfacts.css")
	if not strMapMode:match("Advanced") then										-- V6.0 -- Create mapfacts.png file from leaflet markers URL
		local strMasterURL = "http://raw.githubusercontent.com/pointhi/leaflet-color-markers/master/img/marker-icon-"..strColours..".png"	-- See https://github.com/pointhi/leaflet-color-markers
		local strMasterPNG = strFolder.."\\mapfacts.png"
		local strAnsi, wasAnsi = general.FileNameToANSI(strMasterPNG)
		local fileHandle = general.OpenFile(strAnsi,"wb")
		luacom.config.last_error = ""
		WinHttp:Open("GET",strMasterURL,false)
		WinHttp:Send()
		if WinHttp.Status == 200 then
			fileHandle:write(WinHttp.ResponseBody)
			assert(fileHandle:close())
			if not wasAnsi then
				general.MoveFile(strAnsi,strMasterPNG)
			end
		else
			fhMessageBox("\nError ~ Failed to Create Map Marker Icon mapfacts.png from:\n\n"..strMasterURL.."\n\nReason: "..luacom.config.last_error.."\n\nStatus: "..(WinHttp.Status or "nil").."\n\n","MB_OK","MB_ICONEXCLAMATION")
		end
	end
end -- function MakeSharedFiles

-- Open the Map Pane HTML Application --
function OpenMapPane()
	general.DeleteFile(StrFileNameHTM,ShowDelErr)
	general.DeleteFile(StrFileNameACT,ShowDelErr)
	MakeSharedFiles(iup_gui.MachinePath,IntMapMode)		-- Use chosen Plugin Map Pane mode	-- V5.3
	if not general.FlgFileExists(StrFileNameHTM) then		-- Create Map Pane HTML file
		local tblMarkerStyle = TblMarkerStyle()
		local strScript = StrMasterHTM
		strScript = strScript:replace("{setzoom}",IntLocZoom,1)					-- V5.1
		strScript = strScript:replace("{animate}",tblMarkerStyle.Animate,1)	-- V5.1
		strScript = strScript:replace("{basemap}",StrBaseMap,1)		-- V4.3	-- V5.1 Leaflet Base Layer
		strScript = strScript:replace("{mappane}",ArrMapPane[IntLegends],1)	-- V5.3
		strScript = strScript:replace("{plugin}",iup_gui.Plugin)		-- Three replacements
		general.SaveStringToFile(strScript,StrFileNameHTM)
	end
	OLEcontrol.com:Navigate(StrFileNameHTM)
	ShowStatusMessage(iup_gui.Safe,"Map Pane Files Opened")
--[=[
	OLEcontrol.com:GoHome()		-- GoSearch() GoHome() work -- GoBack() GoForward() Refresh() do not
	OLEcontrol.com:GoSearch()
--]=]
end -- function OpenMapPane

-- Run Map Action and wait for Synchronisation --
function RunMapAction(strA,str1,str2,str3,str4,str5)			-- V4.2
	if general.FlgFileExists(StrFileNameACT) then return end	-- Ignore new request before old one has completed	-- V6.0 Removed API Key checks as only Leaflet maps
	dialogMain.Active = "NO"
	local intCheck = 60												-- V6.0 was 60
	local strStatus = ""
	local strSync = ""
	if StrSync == "+" then StrSync = "*" else StrSync = "+" end		-- Alternate synchronisation characters
	general.SaveStringToFile("<ACT>\n"..StrSync.."\n"..tostring(strA).."\n"..tostring(str1).."\n"..tostring(str2).."\n"..tostring(str3).."\n"..tostring(str4).."\n"..tostring(str5).."\n</ACT>\n",StrFileNameACT)	-- V4.2
	repeat
		fhSleep(50,40)												-- Check for Map Pane synchronisation, or excessive checks	-- V6.0 was (50,40)
		intCheck = intCheck - 1
		strStatus = tostring( OLEcontrol.com.Document:getElementById("status").innerText )
		if strStatus then											-- Retrieve synch character status from HTML Document -- V6.1
			strSync = strStatus:sub(1,1)
		end
	until strSync == StrSync or intCheck < 0
	if intCheck < 0 then
		ShowStatusMessage(iup_gui.Risk,"Map Action Sync Failed")	-- V4.3
		if WinHttp then WinHttpCopy = WinHttp end
		WinHttp = nil													-- Inhibit GeocodeLocation() API function on fault
	else
		if not WinHttp then WinHttp = WinHttpCopy end
	end
--?	ShowStatusMessage(iup_gui.Blue,strStatus.innerText.."  intCheck="..tostring(intCheck)) -- Diagnostic
	general.DeleteFile(StrFileNameACT,ShowDelErr)				-- Remove old Map Pane Action file
	dialogMain.Active = "YES"
end -- function RunMapAction

-- Hide Characters & < > in Map pane script --					-- V4.2
local arrHideChars = { ["&"]="\01as"; ["<"]="\01lt"; [">"]="\01gt"; }
function HideChars(strCaption)
	return ( strCaption:gsub("[&<>]",arrHideChars) )
end -- function HideChars

-- Mark Lat/Longitude, LocName & Caption in Map pane --		-- V4.2
function MarkMapPane(strLatitude,strLongitude,strLocName,strCaption,strMode)
	RunMapAction("MARK",tonumber(strLatitude),tonumber(strLongitude),strLocName,HideChars(strCaption or strLocName),strMode)
end -- function MarkMapPane

-- Move Lat/Longitude of existing Marker in Map pane --		-- V4.3
function MoveMapPane(strLatitude,strLongitude)
	RunMapAction("MOVE",tonumber(strLatitude),tonumber(strLongitude))
end -- function MoveMapPane

-- Nullify all existing Markers from Map pane --
function NullMapPane()
	RunMapAction("NULL")
end -- function NullMapPane

-- Plot Lat/Longitude, LocName, Caption & InfoWin data for Map pane unless offline --	-- V4.2 with LocName
function PlotMapPane(strLatitude,strLongitude,strLocName,strCaption,strInfoWin)
	if WinHttp then RunMapAction("PLOT",tonumber(strLatitude),tonumber(strLongitude),strLocName,HideChars(strCaption or strLocName),strInfoWin) end
end -- function PlotMapPane

-- Drop multiple markers in chosen style in Map pane --
function DropMapPane(setBound)
	local tblMarkerStyle = TblMarkerStyle()
	RunMapAction("DROP",tblMarkerStyle.Animate,tblMarkerStyle.Timeout,setBound)	-- V4.3 setBound
end -- function DropMapPane

-- Zoom to selected level in Map pane --
function ZoomMapPane(intZoomLevel)
	RunMapAction("ZOOM",tostring(tonumber(intZoomLevel)))
end -- function ZoomMapPane

-- Read marker Location & map Zoom level from Map pane --
StrMapStatus = ""
function ReadMapPane()										-- V4.3
	local	strStatus = OLEcontrol.com.Document:getElementById("status").innerText
	if strStatus then
		if StrMapStatus ~= strStatus then
			StrMapStatus = strStatus
			local strZoom = strStatus:match("Zoom=(%d+)")
			if strZoom and #strZoom > 0 then -- and IntTabPosn == IntTabPrefer then
				local intGetZoom = tonumber(strZoom)		-- Get map zoom level after change	-- V4.3
				IntGetZoom = intGetZoom or IntGetZoom	-- Ensure it is a number
				IfGotZoom = true
			end
			local strLat, strLng, strAct = strStatus:match("Lat=(.+) Lng=(.+) Act=(%w+)")
			if strLat and strLng and strAct and IntTabPosn == IntTabGeoLoc then
				IntMapLat = tonumber(strLat)				-- Get marker location for action	-- V4.3
				IntMapLng = tonumber(strLng)
				StrMapAct = strAct							-- Mid, End, New
			end
			local strBase = strStatus:match("Base=([%w ]+)")
			if strBase then
				StrBaseMap = strBase						-- Get Leaflet Base Layer Map after change	-- V4.3
			end
			local strType = strStatus:match("Type=([%w ]+)")
			if strType then
				local dicType = { r="road"; a="aerial"; be2="birdseye"; os="ordnanceSurvey"; }
				StrTypeMap = dicType[strType] or "road" -- Get Bing Maps Type Id after change	-- V5.3
			end
		end
	end
end -- function ReadMapPane

-- Close the Map Pane HTML Application --
function CloseMapPane()
	general.DeleteFile(StrFileNameACT,ShowDelErr)
	general.DeleteFile(StrFileNameHTM,ShowDelErr)
	general.DeleteFile(iup_gui.MachinePath.."\\mapfacts.js",ShowDelErr)
	general.DeleteFile(iup_gui.MachinePath.."\\mapfacts.css",ShowDelErr)
	general.DeleteFile(iup_gui.MachinePath.."\\mapfacts.png",ShowDelErr)	-- V6.0
	ShowStatusMessage(iup_gui.Safe,"Map Pane Files Closed")
	fhSleep(100)
end -- function CloseMapPane

-- Get Text from XML Data using Node name --
function StrGetXmlText(strData,strNode)
	return strData:match("<"..strNode..">(.-)</"..strNode..">") or ""
end -- function StrGetXmlText

-- Determine state Index of a Plot/Map Status value --
function IntStatusIndex(strStatus,tblStates)
	local intIndex = IntPlotNoData
	if strStatus then
		for intState, strState in ipairs(tblStates) do
			if string.find(strStatus,strState) then
				intIndex = intState
				break
			end
		end
	end
	return intIndex
end -- function IntStatusIndex

-- Refresh the Latitude or Longitude value --
function RefreshValue(txtControl,txtValue,strPlotMode)
	txtValue = txtValue or ""
	strPlotMode = strPlotMode or ""
	local intIndex = IntStatusIndex(txtValue..strPlotMode,TblPlotStates)
	if intIndex == IntPlotDefined
	or intIndex == IntPlotTentative then					-- Round value to 7 decimal places
		txtValue = string.format("%4.7f",txtValue)
	end
	txtControl.Value	= txtValue
	txtControl.FgColor	= iup_gui[TblStatusColour[intIndex]]
end -- function RefreshValue

WinHttp = luacom.CreateObject("winhttp.winhttprequest.5.1")
luacom.config.abort_on_error = false						-- V5.4
luacom.config.abort_on_API_error = false				-- V5.4

-- Update Latitude & Longitude Geocode data --
function setLatitudeLongitude(strLat,strLng,strLocName,strPlotMode) -- V4.3
	local tblLocData = TblLocationData[strLocName]
	if tblLocData then										-- V6.0
		if ( type(strLat) == "number" and type(strLng) == "number" )
		or not tblLocData.plc
		or not ( tonumber(tblLocData.lat) and tonumber(tblLocData.lng) ) then
			tblLocData.lat = strLat							-- Update if new numeric Lat/Long, not in Place, or not old numeric Lat/Long -- V3.9
			tblLocData.lng = strLng							-- i.e. Do not update unless new data is better than old data
			SaveLocationsData(strLocName)					-- e.g. 'Blue Springs, Hancock County, Ohio, USA' plots in FH Medina but not in Google Maps
			if strLocName == StrLocation then
				RefreshValue(TxtLatitude, tblLocData.lat,strPlotMode)
				RefreshValue(TxtLongitude,tblLocData.lng,strPlotMode)
			end
		end
	end
end -- function setLatitudeLongitude

-- Geocode a Location Address into Latitude & Longitude --
function StrGeocodeLocation(strLocName)

	local strLog = strLocName
	local strTxt = ""
	local strAns = ""
	local tblAns = { OK="Ok"; ["1"]="Ok"; ["0"]=StrPlotNotFound; ZERO_RESULTS=StrPlotNotFound; INVALID_REQUEST=StrPlotInvalid; REQUEST_DENIED=StrPlotDenied; OVER_QUERY_LIMIT=StrPlotQuotaX; UNKNOWN_ERROR=StrPlotUnknown;		-- V3.8 -- V4.9
					 unknown=StrPlotNotFound; amenity="Ok"; building="Ok"; street="Ok"; suburb="Ok"; district="Ok"; postcode="Ok"; city="Ok"; county="Ok"; state="Ok"; country="Ok"; }		-- V5.3 Geoapify

	if TblLocationData[strLocName] and WinHttp then
		local tblLoc = {}
		local strLoc = strLocName
		local strSub = TblLocationData[strLocName].sub or ""
		if strSub ~= "" then strLoc = strSub end			-- Replace Location with Standardized value if it exists
		strLoc = strLoc or ""									-- V5.0
		strLoc = strLoc:gsub('%[%[.-%]%]','')				-- Exclude double bracketed text from Geocoder as a user aid
		strLoc = strLoc:gsub('".-"','')						-- Exclude double quoted text from Geocoder as a user aid
		strLoc = strLoc:gsub('%-',' ')						-- Replace each hyphen with space to fix Stoke-upon-Trent -- V3.9
		strLoc = strLoc:gsub(", -, -",", "):gsub(", -, -",", "):gsub("^[, ]+",""):gsub("[, ]+$","")	-- Purge blank comma parts	-- V5.0
		strLog = strLoc
		tblLoc = strLoc:split(",~"..string.char(7))			-- Split Location into fields separated by comma, tilde, or blob bell char
		local strReg = TblRegBias[IntRegBias]:sub(1,2)		-- Extract the Region Bias 2-letter code used in URL below
		local strNat = TblNation[strReg] or "Other"			-- Nation recognition to resolve smart code conflicts -- V5.0
		tblLoc[0] = tblLoc[1]									-- Cater for reverse Place order where Country is 1st -- V5.0
		local intLoc = 0
		repeat 													-- Tidy and convert tblLoc entry 0, then #tblLoc down to 1	-- V5.0
			strLoc = tblLoc[intLoc]								-- i.e. 1st part, then last part back to 1st part
			strLoc = strLoc:gsub("^ *(.-) *$","%1")			-- Strip leading and trailing spaces -- %s V3.5
			if StrGeoCode == "ON" then							-- If the Smart Geocoding option enabled, then translate any codes, etc
				local strGeo = TblLocCode.Default[strLoc:upper()] or strLoc -- V5.0
				strNat = TblNation[strGeo:lower()] or strNat	-- Nation recognition to resolve Smart Code conflicts -- V5.0
				strGeo = TblLocCode[strNat][strLoc:upper()] or strGeo
				for _,tblGeo in ipairs(TblGeoCode) do		-- Translations to improve Geocoder plotting
					if string.match(strGeo.." ",tblGeo[1]) then
						strGeo = tblGeo[2]
						break
					end
				end
				strLoc = strGeo:gsub("^UK$","United Kingdom")	-- V5.3
			end -- if StrGeoCode == "ON"
			tblLoc[intLoc] = strLoc
			if intLoc == 0 then intLoc = #tblLoc else intLoc = intLoc - 1 end
		until intLoc < 1
		strLoc = table.concat(tblLoc,", ")					-- Rebuild location from tblLoc excluding tblLoc[0] -- V5.0
		strLoc = encoder.StrEncode_URI(strLoc)
		local strURL = ""
		local strStat = ""										-- Response XML keywords	-- V5.3
		local strLati = ""
		local strLong = ""
		local strConf = ""
		local strType = ""
		if IntGeoPlot == DicGeoPlot.Google then				-- Google Maps geocoder URL & XML keywords 	-- V5.3
			strURL = "https://maps.googleapis.com/maps/api/geocode/xml?address="..strLoc.."&region="..strReg.."&key="..StrAPI_Key
			strStat = "status"
			strLati = "lat"
			strLong = "lng"
			strConf = "partial_match"							-- "true" = Confidence partial match
			strType = ""
		elseif IntGeoPlot == DicGeoPlot.Geoapify then		-- Geoapify geocoder URL & XML keywords 	-- V5.3
			-- Convert pairs of UTF8 bytes to their %Hex codes
			strLoc = strLoc:gsub("([\194-\244])([\128-\191])",function(strOne,strTwo) return "%"..string.format("%02x",strOne:byte()).."%"..string.format("%02x",strTwo:byte()) end)
			strURL = "https://api.geoapify.com/v1/geocode/search?text="..strLoc.."&limit=1&bias=countrycode:"..strReg.."&format=xml&apiKey="..StrGeo_Key
			strStat = "result_type"
			strLati = "lat"
			strLong = "lon"
			strConf = "confidence"
			strType = "result_type"								-- better than "category"
		else
			return StrPlotUnknown								-- Should never get here
		end
		local strLat = ""
		local strLng = ""
		local strGeo = ""
		local strTyp = ""
		local intSleep = 125									-- Interval between Geocoder calls
		local intCheck = 2										-- Max repeats for not "Ok" status
		repeat
			luacom.config.last_error = ""						-- V5.4
			WinHttp:Open("GET",strURL,0)						-- 0 was false
			WinHttp:Send()
			if WinHttp.Status == 200 then						-- V5.4
				strTxt = WinHttp.ResponseText					-- Get status values as in tblAns above
			else													-- V5.4
				local strAns = fhMessageBox("\nError ~ Geocoder Failed to Complete WinHttp Request:\n\n"..strURL.."\n\nReason: "..luacom.config.last_error.."\n\nStatus: "..(WinHttp.Status or "nil").."\n\nIf using a VPN then try pausing it and Geocode again.\n","MB_OKCANCEL","MB_ICONEXCLAMATION")
				if strAns == "Cancel" then return StrPlotAborted end
				strTxt = ""
			end
			strAns = StrGetXmlText(strTxt,strStat)			-- V5.3
			strAns = tblAns[strAns] or StrPlotUnknown		-- V3.8
			strLat = StrGetXmlText(strTxt,strLati)			-- V5.3
			strLng = StrGetXmlText(strTxt,strLong)			-- V5.3
			strGeo = StrGetXmlText(strTxt,strConf)			-- V5.3
			strTyp = StrGetXmlText(strTxt,strType)			-- V5.3
			if not (strLat and strLng) then
				strAns = StrPlotNotFound						-- V5.3
			end
			if strAns == "Ok" then
				intCheck = 0										-- V3.8
			else													-- Try again after one second -- V3.8
				setLatitudeLongitude(strAns,strAns,strLocName)
				strGeo = "Geocoder API "..strAns.." see Help && Advice F.A.Q."
				ShowStatusMessage(iup_gui.Warn,strGeo)		-- V4.2
				if strAns == StrPlotDenied
				or strAns == StrPlotQuotaX
				or strAns == StrPlotUnknown then				-- V4.2
					strAns = StrPlotAborted
				elseif strAns == StrPlotNotFound then		-- Remove leading location part for next try -- V5.0
					strURL = strURL:gsub("address=.-%%2C%+","address=",1)
				end
				fhSleep(1000,100)
			end
			intCheck = intCheck - 1
		until intCheck < 0										-- V3.8
		if strAns == StrPlotAborted							-- V4.2
		or strAns == StrPlotNotFound then						-- V5.3
			setLatitudeLongitude("","",strLocName) 			-- Do NOT record abort errors in the Latitude & Longitude fields
			ShowStatusMessage(iup_gui.Risk,strGeo)
		else
			setLatitudeLongitude( tonumber(strLat) or strAns, tonumber(strLng) or strAns, strLocName )
			if strGeo == "true" then strGeo = "partial match" elseif strGeo == "" then strGeo = "good match" end	-- V5.3
			if tonumber(strGeo) then strGeo = tostring( math.floor( tonumber(strGeo) * 100 ) ).."%" end	-- V5.3
			strGeo = DicGeoPlot[IntGeoPlot+1].." "..strTyp.."  Confidence: "..strGeo
			ShowStatusMessage(iup_gui.Safe,"OK  "..strGeo)	-- V5.3
			TblLocationData[strLocName].geo = strGeo		-- V5.3+
		end
		fhSleep(intSleep,90)
	end
	if StrLogging == "ON" then									-- If logging enabled then convert XML to Plot Log tables for CSV log file	-- V5.3
		local tblPlotLog = TblPlotLog[IntGeoPlot]
		local strLeader  = tblPlotLog.Lead
		local intCol = 0
		local strPre = ""
		local dicRow = {}
		strTxt = strTxt:gsub("^.-<","<")
		strTxt = "<Original_Location>"..strLog.."</Original_Location>\n"..strTxt
		for strLab, strVal in strTxt:gmatch("<(/?[%w_]+)>[\r\n ]*([^<]*)") do	-- Match opening <label>value or closing </label> ignoring white space
			if #strVal == 0 then
				if strLab:match("^/") then						-- Found closing </label> so try to remove from end of prefix
					strPre = strPre:gsub(strLab:match("^/(.*)").."%.$","")
				else												-- Found opening <label> without value so add to end of prefix
					strPre = strPre..strLab.."."
					strPre = strPre:gsub(strLeader,"")		-- Remove superfluous leading label prefix
				end
			elseif #strVal < 99 then							-- Omit very long values such as Copyright text
				strLab = strPre..strLab
				while dicRow[strLab] do							-- Compose unique CSV column header label
					strLab = strLab.." "
				end
				strVal = strVal:gsub(",",";")					-- Prevent commas upsetting CSV
				dicRow[strLab] = strVal							-- Save value against header label
				if tblPlotLog.Head[strLab] then
					intCol = tblPlotLog.Head[strLab]			-- Header found before so get its column
				else
					intCol = intCol + 1
					tblPlotLog.Head[strLab] = intCol			-- Otherwise assign next column to header and shift rest along one column
					table.insert(tblPlotLog.Head,intCol,strLab)
					for intCol = intCol + 1, #tblPlotLog.Head do
						tblPlotLog.Head[tblPlotLog.Head[intCol]] = intCol
					end
				end
			end
		end
		table.insert(tblPlotLog,dicRow)						-- Save row of CSV labelled values
	end
	return strAns
end -- function StrGeocodeLocation

-- Save Log File in CSV format --								-- V5.3
function SavePlotLogFileCSV()
	for intGeoPlot=1, #DicGeoPlot do							-- For each geocoder check if any rows saved
		local tblPlotLog = TblPlotLog[intGeoPlot]
		if #tblPlotLog > 0 then
			local strCSV = ""
			for _,strLab in ipairs (tblPlotLog.Head) do		-- Compose the CSV header labels
				strCSV = strCSV..strLab..","
			end
			strCSV = strCSV.."\n"
			for _,dicRow in ipairs (tblPlotLog) do			-- Compose the CSV row values
				for _,strLab in ipairs (tblPlotLog.Head) do
					strCSV = strCSV..(dicRow[strLab] or "")..","
				end
				strCSV = strCSV.."\n"
			end
			local strFileName = StrFileNameCSV:gsub("{GeoPlot}",DicGeoPlot[intGeoPlot+1])
			general.SaveStringToFile(strCSV,strFileName)	-- Save the CSV file to Plugin Data folder
		end
	end
end -- function SavePlotLogFileCSV

-- Create Locations Geocode Database --
function CreateLocationsData()
	if TblLocationCode == nil then TblLocationCode = {} end
	for _, strFromTag in ipairs(TblFromTag) do -- "PLAC" and "ADDR" and "BOTH"
		if TblLocationCode[strFromTag] == nil then TblLocationCode[strFromTag] = {} end
		if string.encoding() == "UTF-8" and TblLocationCode.Encoding ~= "UTF-8" then
			local tblNew = {}
			for strLoc, tblLoc in pairs (TblLocationCode[strFromTag]) do
				local strNew = strLoc
				if strFromTag == "BOTH" then strNew = strNew:gsub("…"," ~ ") end	-- FH6
				strNew = fhConvertANSItoUTF8(strNew)
				tblNew[strNew] = tblLoc
			end
			TblLocationCode[strFromTag] = tblNew
		end
		for strLoc, tblLoc in pairs (TblLocationCode[strFromTag]) do
			tblLoc.mod = tostring(tblLoc.mod):gsub("Manual","Defined")				-- V4.9
			tblLoc.lat = tostring(tblLoc.lat):gsub(StrPlotNoPlot,StrPlotNotFound)	-- V4.9
			tblLoc.lng = tostring(tblLoc.lng):gsub(StrPlotNoPlot,StrPlotNotFound)	-- V4.9
			tblLoc.plc = nil					-- V3.5 needed to remove old Place Record names
		end
	end
	TblLocationCode.Encoding = string.encoding()
end -- function CreateLocationsData

-- Load Locations Geocode Database File --
function LoadLocationsFile()
	TblLocationCode = table.load(StrFileNameLOC)
	CreateLocationsData()
	table.save(TblLocationCode,StrFileNameLOC)
	TblLocationData = TblLocationCode[StrFromTag]
end -- function LoadLocationsFile

-- Save Locations Geocode Database File --
function SaveLocationsFile()
	table.save(TblLocationCode,StrFileNameLOC)
end -- function SaveLocationsFile

-- Load Place Record details into Locations database -- FH6
function LoadPlaceRecord(tblLocData,ptrRecord)
	tblLocData.sub = fhGetItemText(ptrRecord,"~.STAN")
	tblLocData.lat = fhGetItemText(ptrRecord,"~.LATLONG:LAT_NUMERIC")
	tblLocData.lng = fhGetItemText(ptrRecord,"~.LATLONG:LONG_NUMERIC")
	tblLocData.mod = ""
	tblLocData.geo = " "												-- Geocoder returned status	-- V5.3+
	local strStat = fhGetItemText(ptrRecord,"~.STAT")				-- Status "" or "Tentative" with Lat/Long, or "Block(No Auto-Geocode)" or "Not Found" or "" without Lat/Long
	if strStat == "No Auto-Geocode" then								-- "mod" = "Defined" or "" with Lat/Long , or "lat"/"lng" = "Blocked" or "Not Found" or ""	-- V4.9
		strStat = StrPlotBlocked										-- "Blocked"	-- V4.9
	end
	if strStat == "" then
		if tblLocData.lat..tblLocData.lng ~= "" then				-- With Lat/Long	-- V4.9
			tblLocData.mod = "Defined"
		end
	elseif strStat ~= "Tentative" then								-- "Blocked" or "Not Found"	-- V4.9
		tblLocData.lat = strStat
		tblLocData.lng = strStat
	end
end -- function LoadPlaceRecord

-- Load Locations Database from File or Gedcom --
function LoadLocationsData()
	ShowStatusMessage(iup_gui.Warn,"Loading Location data, please wait...")
	if IntStorage == IntStoreFILE
	or IntStorage == IntStorePLAC then
		LoadLocationsFile()
		if IntStorage == IntStoreFILE then return end
	end
	CreateLocationsData()
	local strStoreTag = TblStoreTAG[IntStorage] 		-- "REPO" or "SOUR" or "_PLAC"
	local ptrRecord = fhNewItemPtr()
	ptrRecord:MoveToFirstRecord(strStoreTag)
	while ptrRecord:IsNotNull() do
		for _, strFromTag in ipairs(TblFromTag) do		-- "PLAC" and "ADDR" and "BOTH"
			TblLocationData = TblLocationCode[strFromTag]
			local strType = TblFromType[strFromTag]
			if strFromTag == "BOTH" and string.encoding() == "UTF-8" then
				strType = "Facts Address[ \226][~\128][ \166]Place" -- string.char(0xE2,0x80,0xA6) = "…"
			end
			local strHead = "Map Life "..strType.." Location"
			local ptrType = fhGetItemPtr(ptrRecord,"~._TYPE")		-- Source Type
			local ptrRepo = fhGetItemPtr(ptrRecord,"~.REPO")		-- Source to Repository link
			local ptrNote = fhGetItemPtr(ptrRecord,"~.NOTE2")		-- Source or Repository local Note
			local ptrTarget = fhNewItemPtr()
			local strLocName = fhGetDisplayText(ptrRecord)
			if IntStorage == IntStoreBOTH and fhGetValueAsText(ptrType):match(strType) then
				ptrRepo = fhGetValueAsLink(ptrRepo)
				if ptrRepo:IsNotNull() then
					ptrNote = fhGetItemPtr(ptrRepo,"~.NOTE2")		-- Linked Repository local Note
				end
			end
			local isHeadFound = false
			if ptrNote:IsNotNull() then
				if fhCallBuiltInFunction("GetParagraph", ptrNote, 1):match(strHead) then isHeadFound = true end
--?				if fhGetLabelledText( ptrNote, strType) == "Location" then isHeadFound = true end	-- alternative
			end
			if IntStorage == IntStoreREPO and isHeadFound then ptrTarget = ptrRecord end
			if IntStorage == IntStoreSOUR and isHeadFound and fhGetValueAsText(ptrType):match(strType) then ptrTarget = ptrRecord end
			if IntStorage == IntStoreBOTH and isHeadFound and fhGetValueAsText(ptrType):match(strType) then ptrTarget = ptrRepo end
			if IntStorage == IntStorePLAC and strFromTag == "PLAC" then ptrTarget = ptrRecord end	-- FH6 Place Record
			if ptrTarget:IsNotNull() then
				if strFromTag == "BOTH" and string.encoding() == "UTF-8" then
					strLocName = strLocName:gsub("…"," ~ ")			--FH6
					local strType = TblFromType[strFromTag]
					fhSetValueAsText(fhGetItemPtr(ptrRecord,"~.TITL"),strLocName)
					fhSetValueAsText(ptrType,strType)
					if IntStorage == IntStoreBOTH then
						fhSetValueAsText(fhGetItemPtr(ptrRepo,"~.NAME"),strLocName)
					end
					fhSetValueAsText(ptrNote,(fhGetValueAsText(ptrNote):gsub("…"," ~ ",1)))
				end
				TblLocationData[strLocName] = {}
				if IntStorage == IntStorePLAC then																-- FH6 Place Record
					TblLocationData[strLocName].plc = fhGetRecordId(ptrRecord)
					LoadPlaceRecord(TblLocationData[strLocName],ptrTarget)
				else
					TblLocationData[strLocName].rec = fhGetRecordId(ptrRecord)
					TblLocationData[strLocName].sub = (fhGetLabelledText( ptrNote, "Standard =")..fhGetLabelledText( ptrNote, "Substitute =")):gsub("^[ \t]+","")	-- V4.9
					TblLocationData[strLocName].lat = fhGetLabelledText( ptrNote, "Latitude ="  ):gsub("^[ \t]+",""):gsub(StrPlotNoPlot,StrPlotNotFound)	-- V4.9
					TblLocationData[strLocName].lng = fhGetLabelledText( ptrNote, "Longitude =" ):gsub("^[ \t]+",""):gsub(StrPlotNoPlot,StrPlotNotFound)	-- V4.9
					TblLocationData[strLocName].mod = fhGetLabelledText( ptrNote, "Plot Mode =" ):gsub("^[ \t]+","")
					TblLocationData[strLocName].geo = fhGetLabelledText( ptrNote, "Geocoding =" ):gsub("^[ \t]+","")	-- V5.3+
				end
			end
			TblLocationCode[strFromTag] = TblLocationData
		end
--?		SaveLocationsFile() -- diagnostic
		ptrRecord:MoveNext("SAME_TAG")
	end -- while not ptrRecord:IsNull()
	TblLocationData = TblLocationCode[StrFromTag]
end -- function LoadLocationsData

-- Create new Location Data Entry --
function MakeNewLocationData(strLocName)
	TblLocationData[strLocName] = { sub = ""; lat = ""; lng = ""; mod = ""; geo = ""; } 				-- V5.3+
end -- function MakeNewLocationData

-- Create new Repository Record --
function MakeNewRepositoryRecord(strLocName)
	local ptrRecord = fhCreateItem("REPO")
	fhSetValueAsText(fhCreateItem("NAME",ptrRecord),strLocName)
	return ptrRecord, fhGetRecordId(ptrRecord)
end -- function MakeNewRepositoryRecord

-- Create new Source Record --
function MakeNewSourceRecord(strLocName)
	local ptrRecord = fhCreateItem("SOUR")
	fhSetValueAsText(fhCreateItem("TITL",ptrRecord),strLocName)
	fhSetValueAsText(fhCreateItem("_TYPE",ptrRecord),TblFromType[StrFromTag])
	return ptrRecord, fhGetRecordId(ptrRecord)
end -- function MakeNewSourceRecord

-- Save Locations Database entry to File or Gedcom --
function SaveLocationsData(strLocName)
	if IntStorage == IntStoreFILE then
		TblLocationData[strLocName].rec = nil
		return
	end
	local strType, strHead, strData, ptrNote, ptrRepo		-- V3.5
	local ptrRecord = fhNewItemPtr()
	local intRecord = TblLocationData[strLocName].rec or 0
	if IntStorage == IntStorePLAC then						-- FH6 -- V3.5
		intRecord = TblLocationData[strLocName].plc or 0
	else
		strType = TblFromType[StrFromTag]
		strHead = "Map Life "..strType.." Location\n"
		strData = "Standard = \t"..(TblLocationData[strLocName].sub or "")..
					"\nLatitude = \t"..(TblLocationData[strLocName].lat or "")..
					"\nLongitude = \t"..(TblLocationData[strLocName].lng or "")..
					"\nPlot Mode = \t"..(TblLocationData[strLocName].mod or "").."\n"
	end
	if intRecord > 0 then
		-- Repository or Source or Place record exists
		local strStoreTag = TblStoreTAG[IntStorage] 		-- "REPO" or "SOUR" or "_PLAC"
		ptrRecord:MoveToRecordById(strStoreTag,intRecord)
		if IntStorage ~= IntStorePLAC then
			ptrNote = fhGetItemPtr(ptrRecord,"~.NOTE2")		-- Source or Repository local Note
			ptrRepo = fhGetItemPtr(ptrRecord,"~.REPO")		-- Source to Repository link
--?			if IntStorage == IntStoreBOTH and fhGetValueAsText(fhGetItemPtr(ptrRecord,"~._TYPE")) == strType then -- also check Source Type ?
			if IntStorage == IntStoreBOTH then
				ptrRepo = fhGetValueAsLink(ptrRepo)
				if ptrRepo:IsNotNull() then
					ptrNote = fhGetItemPtr(ptrRepo,"~.NOTE2")	-- Linked Repository local Note
				end
			end
		end
	else
		-- Process all Place records else create new Repository or Source Record
		if IntStorage == IntStorePLAC then					-- FH6
			if StrFromTag ~= "PLAC" then return end			-- FH6
			local ptrRec = fhNewItemPtr()
			ptrRec:MoveToFirstRecord("_PLAC")
			while ptrRec:IsNotNull() do
				local strLoc = fhGetItemText(ptrRec,"~.TEXT")
				if not TblLocationData[strLoc] then			-- Cater for Place record without any Place field
					MakeNewLocationData(strLoc)
				end
				TblLocationData[strLoc].plc = fhGetRecordId(ptrRec)
				if strLoc == strLocName then ptrRecord = ptrRec:Clone() end
				if IsNotGeocoded(TblLocationData[strLoc]) then
					LoadPlaceRecord(TblLocationData[strLoc],ptrRec)
				end
				ptrRec:MoveNext()
			end
		else
			if IntStorage == IntStoreREPO then
				ptrRecord, intRecord = MakeNewRepositoryRecord(strLocName)
			else
				ptrRecord, intRecord = MakeNewSourceRecord(strLocName)
				if IntStorage == IntStoreBOTH then			-- link Source to a new Repository
					ptrRepo = MakeNewRepositoryRecord(strLocName)
					fhSetValueAsLink(fhCreateItem("REPO",ptrRecord),ptrRepo)
					ptrRecord = ptrRepo
				end
			end
			ptrNote = fhCreateItem("NOTE2",ptrRecord)		-- Source or Repository local Note
			TblLocationData[strLocName].rec = intRecord
		end
	end
	if IntStorage == IntStorePLAC then						-- FH6 Place Record
		local isOK
		local strSub = tostring(TblLocationData[strLocName].sub)
		local strLat = tostring(TblLocationData[strLocName].lat)
		local strLng = tostring(TblLocationData[strLocName].lng)
		local strMod = tostring(TblLocationData[strLocName].mod)
		local ptrStat = fhGetItemPtr(ptrRecord,"~.STAT")
		local strStat = fhGetValueAsText(ptrStat)
		local strMode = strStat
		local ptrStan = fhGetItemPtr(ptrRecord,"~.STAN")
		local strStan = fhGetValueAsText(ptrStan)
		if strSub == "" and strStan ~= "" then
			if ptrStan:IsNotNull() then
				isOK = fhDeleteItem(ptrStan)
			end
		elseif strSub ~= strStan then
			if ptrStan:IsNull() then
				ptrStan = fhCreateItem("STAN",ptrRecord)
			end
			isOK = fhSetValueAsText(ptrStan,strSub)
		end
		if strLat == StrPlotBlocked and strLng == StrPlotBlocked then
			strMode = "No Auto-Geocode"						-- Block(No Auto-Geocode) -- V4.9
			strLat = ""
			strLng = ""
			TblLocationData[strLocName].mod = ""				-- Ensure 'No Plot' status is updated -- V3.9
		elseif strLat == StrPlotNotFound and strLng == StrPlotNotFound then
			strMode = "Not Found"								-- Not Found -- V4.9
			strLat = ""
			strLng = ""
			TblLocationData[strLocName].mod = ""				-- Ensure 'No Plot' status is updated -- V3.9
		elseif strLat == "" and strLng == "" then
			strMode = ""											-- No Data	-- V4.9
			TblLocationData[strLocName].mod = ""				-- Ensure 'No Plot' status is updated -- V3.9
		end
		local ptrLatLong = fhGetItemPtr(ptrRecord,"~.LATLONG")
		local strLatLong = fhGetItemText(ptrRecord,"~.LATLONG:LAT_NUMERIC")..", "..fhGetItemText(ptrRecord,"~.LATLONG:LONG_NUMERIC")
		local strLatLng = strLat:gsub("0+$","")..", "..strLng:gsub("0+$","")	-- Remove trailing zeros	-- V4.9
		if strLatLng == ", " and strLatLong ~= ", " then	-- Change to no Lat/Long plot -- V4.9
			if ptrLatLong:IsNotNull() then
				isOK = fhDeleteItem(ptrLatLong)
			end
--			if strMode ~= "No Auto-Geocode" then
--	#			strMode = "Not Found"
--			end
		elseif strLatLng ~= strLatLong then
			if tonumber(strLat) and tonumber(strLng) then	-- V4.9
				if ptrLatLong:IsNull() then
					ptrLatLong = fhCreateItem("LATLONG",ptrRecord)
				end
				isOK = fhSetValueAsText(ptrLatLong,strLatLng)
				if strMod == "Defined" then strMode = ""
				elseif strMode ~= "Tentative" then
					strMode = ""
					TblLocationData[strLocName].mod = "Defined"
				end
			end
		end
		if strMode == "" and strStat ~= "" then				-- Status: "" or "Tentative" with Lat/Long, or "No Auto-Geocode" or "Not Found" without Lat/Long
			isOK = fhDeleteItem(ptrStat)
		elseif strMode ~= strStat then
			if ptrStat:IsNull() then
				ptrStat = fhCreateItem("STAT",ptrRecord)
			end
			isOK = fhSetValueAsText(ptrStat,strMode)
		end
	else
		fhSetValueAsText(ptrNote,strHead..strData)			-- Save Location Data in local Note
	end
--?	ShowStatusMessage(iup_gui.Blue,"Locations Database Entry Saved") -- diagnostic
end -- function SaveLocationsData

-- Delete Locations Database Record in Gedcom --
function DeleteLocationRecord(intRecord)
	local strType = TblFromType[StrFromTag]
	local ptrRecord = fhNewItemPtr()
	ptrRecord:MoveToRecordById(TblStoreTAG[IntStorage],intRecord or 0)
	if ptrRecord:IsNotNull() then
--?		if IntStorage == IntStoreBOTH and fhGetValueAsText(ptrType) == strType then -- also check Source Type ?
		if IntStorage == IntStoreBOTH then
			local ptrRepo = fhGetItemPtr(ptrRecord,"~.REPO")
			ptrRepo = fhGetValueAsLink(ptrRepo)
			if ptrRepo:IsNotNull() then
				fhDeleteItem(ptrRepo)	-- Delete Repository record linked to Source
			end
		end
		fhDeleteItem(ptrRecord)		-- Delete primary Source or Repository record
--?		ShowStatusMessage(iup_gui.Blue,"Locations Database Entry Deleted") -- diagnostic
	end
end -- function DeleteLocationRecord

-- Convert Locations Database entries from/to File or Records --
function ConvertLocationsData(intOldStorage,intNewStorage)
	local ptrSourRecord = fhNewItemPtr()
	local ptrRepoRecord = fhNewItemPtr()
	local intOldRecordId = 0
	local intNewRecordId = 0

	-- Move local Note from one record to another
	local function doMoveNotes(ptrGetRecord,ptrSetRecord)
		local ptrGetNote = fhGetItemPtr(ptrGetRecord,"~.NOTE2")
		local ptrSetNote = fhGetItemPtr(ptrSetRecord,"~.NOTE2")
		if ptrSetNote:IsNull() then
			ptrSetNote = fhCreateItem("NOTE2",ptrSetRecord)
		end
		fhSetValueAsText(ptrSetNote,fhGetValueAsText(ptrGetNote))
		if intNewStorage == IntStoreBOTH then
			fhDeleteItem(ptrGetNote)	-- Delete original local Note
		else
			fhDeleteItem(ptrGetRecord)	-- Delete original Record
		end
	end -- local function doMoveNotes

	if intOldStorage == IntStorePLAC then						-- V3.5
		intOldStorage = IntArchive									-- If converting from Place Records, reinstate archive storage media
		if intOldStorage == intNewStorage then return end
		IntStorage = intNewStorage									-- V4.7
		LoadLocationsData()
	end
	if intNewStorage == IntStorePLAC then						-- V3.5
		local strLocName, tblLocData = next(TblLocationCode.PLAC)
		if tblLocData then tblLocData.plc = 0 end				-- V3.6
	end
	for _, strFromTag in ipairs(TblFromTag) do					-- "PLAC" and "ADDR" and "BOTH"
		StrFromTag = strFromTag
		TblLocationData = TblLocationCode[StrFromTag]
		for strLocName, tblLocData in pairs(TblLocationData) do
			intOldRecordId = tblLocData.rec

			if     intOldStorage == IntStoreFILE then			-- Convert from Data File to any Gedcom Records
				IntStorage = intNewStorage
				SaveLocationsData(strLocName)

			elseif intNewStorage == IntStoreFILE then			-- Convert from Gedcom Records to Data File
				IntStorage = intOldStorage
				if intOldRecordId then								-- FH6
					DeleteLocationRecord(intOldRecordId)
				end
				tblLocData.rec = nil

			elseif intNewStorage == IntStorePLAC then			-- Convert from Gedcom Records to Place Records -- FH6
				if strFromTag == "PLAC" then
					IntStorage = intNewStorage
					SaveLocationsData(strLocName)
				end
--[=[
			elseif intNewStorage == IntStoreREPO
				 or intNewStorage == IntStoreSOUR
				 or intNewStorage == IntStoreBOTH then			-- Convert to Gedcom Records	-- V4.9 ????
				IntStorage = intNewStorage
				SaveLocationsData(strLocName)
--]=]
			elseif intOldStorage == IntStoreREPO then			-- Convert Repository to Source / Source & Repository
				ptrSourRecord, intNewRecordId = MakeNewSourceRecord(strLocName)
				tblLocData.rec = intNewRecordId
				if intOldRecordId then								-- FH6
					ptrRepoRecord:MoveToRecordById("REPO",intOldRecordId)
				else
					ptrRepoRecord, intOldRecordId = MakeNewRepositoryRecord(strLocName)
				end
				if intNewStorage == IntStoreSOUR then
					doMoveNotes(ptrRepoRecord,ptrSourRecord)
				elseif intNewStorage == IntStoreBOTH then
					fhSetValueAsLink(fhCreateItem("REPO",ptrSourRecord),ptrRepoRecord)
				end

			elseif intOldStorage == IntStoreSOUR then			-- Convert Source to Repository / Source & Repository
				ptrRepoRecord, intNewRecordId = MakeNewRepositoryRecord(strLocName)
				if intOldRecordId then								-- FH6
					ptrSourRecord:MoveToRecordById("SOUR",intOldRecordId)
				else
					ptrSourRecord, intOldRecordId = MakeNewSourceRecord(strLocName)
					tblLocData.rec = intOldRecordId
				end
				doMoveNotes(ptrSourRecord,ptrRepoRecord)
				if intNewStorage == IntStoreREPO then
					tblLocData.rec = intNewRecordId
				elseif intNewStorage == IntStoreBOTH then
					fhSetValueAsLink(fhCreateItem("REPO",ptrSourRecord),ptrRepoRecord)
				end

			elseif intOldStorage == IntStoreBOTH then			-- Convert Source & Repository to Repository / Source
				if intOldRecordId then								-- FH6
					ptrSourRecord:MoveToRecordById("SOUR",intOldRecordId)
					ptrRepoRecord = fhGetValueAsLink(fhGetItemPtr(ptrSourRecord,"~.REPO"))
				else
					ptrSourRecord, intOldRecordId = MakeNewSourceRecord(strLocName)
					tblLocData.rec = intOldRecordId
					ptrRepoRecord, intOldRecordId = MakeNewRepositoryRecord(strLocName)
					fhSetValueAsLink(fhCreateItem("REPO",ptrSourRecord),ptrRepoRecord)
				end
				if intNewStorage == IntStoreREPO then
					tblLocData.rec = fhGetRecordId(ptrRepoRecord)
					fhDeleteItem(ptrSourRecord)
				elseif intNewStorage == IntStoreSOUR then
					doMoveNotes(ptrRepoRecord,ptrSourRecord)
				end

			end
			iup.Flush()
		end
		TblLocationCode[StrFromTag] = TblLocationData
--?		SaveLocationsFile() -- diagnostic
		fhUpdateDisplay()
	end
	if intNewStorage == IntStoreFILE then SaveLocationsFile() end
--?	if intOldStorage == IntStoreFILE then general.DeleteFile(StrFileNameLOC,ShowDelErr) end	-- Delete or keep Locations file ?
	StrFromTag = TblFromTag[math.min(IntFromTag,IntFromBOTH)]
	TblLocationData = TblLocationCode[StrFromTag]
	fhUpdateDisplay()
	ShowStatusMessage(iup_gui.Safe,"Locations Database Converted")
end -- function ConvertLocationsData

local strControls = "[%z\01-\06\08-\32\127]+"
if fhGetAppVersion() > 6 then												-- Cater for Lua 5.1 %z or Lua 5.3 \0	-- V4.7
		strControls = "[\00-\06\08-\32\127]+"
end

-- Replace control chars with space but CRLF with blob in Location name --
function StrControls2SpaceNew(strLocName)
	-- Replace \n with blob is essential for TblLocationData dictionary keys & Records
	-- Replace all control chars with a space works better in GUI, Records, etc
	if IntFromTag ~= IntFromPLAC then										-- Except Place Fields need spaces to match Place Records and cannot include \n \r \t etc -- V3.5
		strLocName = (strLocName or ""):gsub("\n",string.char(7))	-- Replace newline chars with bell char blobs
		strLocName = strLocName:gsub(strControls," ")					-- Replace any other multiple control & layout chars with one space -- %c%s V3.5 -- V4.7
	end
	return strLocName
end -- function StrControls2SpaceNew

-- Determine if special data tag of Husband/Wife for Family Record Id, or Residence or LDS Ordinations for Location Place/Address --
function IsDataTag(ptrFact)
	local strGetTag = fhGetTag(ptrFact)
	for _, strDataTag in ipairs( { "HUSB"; "WIFE"; "RESI"; "BAPL"; "CONL"; "ENDL"; "SLGC"; "SLGS"; } ) do 
		if strGetTag == strDataTag then return true end
	end
	return false
end -- function IsDataTag

-- Tidy multi commas & white space to single comma & space, remove any leading & trailing comma & space		-- V4.2
function StrTidyCommas(strText)
	strText = strText:gsub("[\t-\r ]*,[\t-\r ]*",",")		-- Remove white space before & after each comma	-- V4.2
	strText = strText:gsub(",+",", ")							-- Reduce multiple commas to one comma & space		-- V4.2
	strText = strText:gsub("^[, ]*(.-)[, ]*$","%1")		-- Remove any leading or trailing comma & space	-- V4.2
	return strText
end -- function StrTidyCommas

-- Get Display Text & Remove Multiple/Leading/Trailing "," Place/Address Column Separators
-- NEW in V3.3 but prevents commas & spaces matching with FH6 _PLAC Records so needs adjusting
-- Used only by StrMakeLocation() and doAddFactData() in StrCreateWebPageMap() i.e. next two functions
function StrGetDisplayText(ptrFact,strTag,strMode)
	strTag  = strTag  or ""
	strMode = strMode or "STD"
	local strText = fhGetDisplayText(ptrFact,strTag,strMode)
	if strMode == "STD" or strTag:match("ADDR") then		-- V3.3 FH6
		strText = StrTidyCommas(strText)						-- V4.2
		if strMode == "STD" then
			strText = strText:gsub(" from, "," from ")		-- Remove comma after "from"	-- V4.2
			strText = strText:gsub(" from$","")				-- Remove redundant "from"		-- V4.2
			strText = strText:gsub(" to, "," to ")			-- Remove comma after "to"		-- V4.2
			strText = strText:gsub(" to$","")					-- Remove redundant "to"		-- V4.2
			strText = strText:gsub(" in, "," in ")			-- Remove comma after "in"
			strText = strText:gsub(" in$","")					-- Remove redundant "in"
		end
	end
	return strText
end -- function StrGetDisplayText

-- Make Location Name and Plot/Map Data from Fact ADDR & PLAC data --
-- Used only by doBuildLocationsList() and doAddFactData() in StrCreateWebPageMap()
function StrMakeLocation(ptrFact)
	local strFromTag = TblFromTag[math.min(IntFromTag,IntFromBOTH)]			-- "PLAC" or "ADDR" or "BOTH"
	local strLocName = ""
	local strLocData = ""
	local strADDR	= ""
	local strPLAC	= ""
	local str_Name	= ""
	local str_Data	= ""
	local str_PLAC	= fhGetDisplayText(ptrFact,"~._PLAC","MIN")				--_ To/From _PLAC for Emmigration & Immigration 19 June 2013
	if IntFromTag < IntFromBOTH then												-- "PLAC" or "ADDR" only
		strLocName = StrGetDisplayText(ptrFact,"~."..strFromTag,"MIN") 		-- "~.PLAC" or "~.ADDR" tag	-- Changed in V3.3
		strADDR = strLocName
		if strFromTag == "PLAC" then str_Name = str_PLAC end					--_
	else
		strADDR = StrGetDisplayText(ptrFact,"~.ADDR","MIN")					-- "BOTH" Address ~ Place variant	-- Changed in V3.3
		strPLAC = StrGetDisplayText(ptrFact,"~.PLAC","MIN")					-- Changed in V3.3
		strLocData = strADDR.." ~ "..strPLAC										-- Return full Address ~ Place variant for Plots
		str_Data = " ~ "..str_PLAC
		if IntFromTag == IntFromP then strADDR = "" end						-- " ~ Place only" variant
		if IntFromTag == IntFromA then strPLAC = "" end						-- "Address ~ only" variant
		if IntFromTag == IntFromA then str_PLAC = "" end						--_ "Address ~ only" variant 19 June 2013
		if ( IsFromADDR or strADDR ~= "" ) and ( IsFromPLAC or strPLAC ~= "" ) then
			strLocName = strADDR.." ~ "..strPLAC
			strLocName = strLocName:gsub("^ ~ $","")
		end
		if ( IsFromADDR ) and ( IsFromPLAC or str_PLAC ~= "" ) then
			str_Name	= " ~ "..str_PLAC											--_ 19 June 2013 looks OK
			str_Name = str_Name:gsub("^ ~ $","")									--_ 19 June 2013
		end
	end
	if IntTabPosn == IntTabCreate then
		if strFromTag ~= "PLAC" then
			strLocData = "<br />"..strADDR										-- Return the Address to append for Maps	-- V4.2 <br />
		else
			strLocData = ""
		end
	end
	return strLocName, strLocData, str_Name, str_Data							--_ 19 June 2013 looks OK
end -- function StrMakeLocation

local strControlChar = "[%z\001-\031\127]+"
if fhGetAppVersion() > 6 then														-- Cater for Lua 5.1 %z or Lua 5.3 \0	-- V4.7
		strControlChar = "[\000-\031\127]+"
end

-- Create Web Page Map for one Individual --
function StrCreateWebPageMap(ptrIndi)

	local strMapFolder	= StrFolder.."\\"
	local intFactIndex	=	0		-- Relative ordinal position of Fact 
	local tblFactsData	= {}		-- Date, Fact Data, Location & Index per Fact
	local tblLocations	= {}		-- Count of Facts per Location
	local tblMapMarker	= {}		-- Map Marker number per Location
	local tblFamily		= {}		-- Record Id of Spouse Family Records
	local tblSpouse		= {}		-- Spouse Name of Spouse Family Records
	local strJavaLists	= ""		-- Map Marker Java Lists for Locations
	local isGeocoderOk	= true		-- Is Google Geocoder API OK		-- V4.2

	-- Check if Dictionary of Facts is empty												-- V5.1
	local function isDicFactSetEmpty()
		return next(DicFactSet or {}) == nil
	end -- local function isDicFactSetEmpty

	-- Find Fact Labels in HTML Web Pages for Family Group Sheet or Individual Summary Report	-- V3.7 -- V5.1 -- Rewritten
	local function doFindFactLabels()
		if StrFactSet == "ON" and IsFamilyTreeWebsite() and isDicFactSetEmpty() then
			local strFind = '<li class="fhcol1 fhseclabel"><p>(.-):</p></li>'			-- FH V7 tabbed report fact label pattern
			if fhGetAppVersion() < 7 then strFind = '<tr><td>([^<]-[^:.])</td>' end	-- FH V5/6 tabbed report fact label pattern
			DicFactSet = {}																		-- Dictionary of fact labels found in web pages
			local arrFile = {}																	-- Array of fam/ind HTML files in folder
			local intFact = 0																	-- Count of fact labels found
			local tblList = general.FSO:GetFolder(StrFolder)
			local tblEnum = luacom.GetEnumerator(tblList.Files)
			local tblAttr = tblEnum:Next()													-- Search files in folder
			while tblAttr do
				local strFile = tblAttr.path
				if strFile:match("\\[fi][an][md][0-9]+%.html$") then						-- fam/ind HTML file found
					if #arrFile % 500 == 0 then
						ShowStatusMessage(iup_gui.Warn,"Facts Filter Search, please wait...")
					end
					table.insert(arrFile,strFile)												-- Save fam/ind HTML files
				elseif strFile:match("\\[j-z][^\\]+$") then break end						-- Abort when subsequent file found
				tblAttr = tblEnum:Next()
			end
			if #arrFile > 2000 then
				progbar.Start("Facts Filter Search",#arrFile)								-- Start progress bar
			end
			for intFile, strFile in ipairs (arrFile) do										-- Search fam/ind HTML files found
				local strText = general.StrLoadFromFile(strFile)
				local strType = strText:match('<div class="fhcontent fhpage(%u%a+)">',500)
				if strType == "Fam" or strType == "Ind" then
					for strFact in strText:gmatch(strFind) do 								-- Find fact labels in Family Group Sheet or Individual Summary Report
						if not DicFactSet[strFact] then
							intFact = intFact + 1												-- Count and save new fact label
							DicFactSet[strFact] = true
						end
					end
				else
					break																	 		-- Narrative, Outline, Record Detail reports are not supported
				end
				if intFile % 100 == 0 then
					progbar.Message("Searching "..strFile:match("[fi][an][md]").." html pages found "..intFact.." Fact labels.")
					progbar.Step(100)
					if progbar.Stop() then break end
				end
			end
			progbar.Close()
		end
	end -- local function doFindFactLabels

	-- Add Fact Data to Table
	local function doAddFactData(ptrFact,strFactData)										-- Revised for EMIG/IMMIgration facts	-- V4.2
		if isDicFactSetEmpty()
		or DicFactSet[StrTranslate(fhCallBuiltInFunction("FactLabel",ptrFact))] then	-- V5.1 -- V3.7
			strFactData = strFactData..StrGetDisplayText(ptrFact)							-- Add description of Fact including Place	-- V3.3 -- V4.2 -- V5.1
			local isLocation = false
			local strLocName,strAddress,str2ndPlace,strPlcName = StrMakeLocation(ptrFact)-- Get Location name, optional Address to append, and from/to Place, etc	-- V4.2
			if strLocName ~= "" then															-- Ignore empty Location
				strLocName = StrControls2SpaceNew(strLocName)								-- Uses newline bell blob
				tblLocations[strLocName] = (tblLocations[strLocName] or 0) + 1			-- Count how many Facts at this Location
				strAddress = strAddress:gsub("^<br />$","")									-- V4.2 <br />
				isLocation = true
			end
			if str2ndPlace ~= "" then 															-- Ignore empty 2nd Place
				if strPlcName == "" then strPlcName = str2ndPlace end
				strPlcName = StrControls2SpaceNew(strPlcName)								-- Uses newline bell blob
				tblLocations[strPlcName] = (tblLocations[strPlcName] or 0) + 1			-- Count how many Facts at this Location
				str2ndPlace = StrTidyCommas(str2ndPlace:gsub("^ ~ ",""))
				strFactData, str2ndPlace = strFactData:match("(.+)( %l+ "..str2ndPlace:plain()..")$")
				isLocation = true
			end
			if isLocation then																	-- Either Location or 2nd Place
				local datDate = fhNewDate()
				local ptrDate = fhGetItemPtr(ptrFact,"~.DATE")								-- Obtain the Date in order to sort Facts -- V4.2
				if ptrDate:IsNotNull() then
					datDate = fhGetValueAsDate(ptrDate)
				else
					datDate:SetSimpleDate(fhCallBuiltInFunction("Today"))
				end
				intFactIndex = intFactIndex + 1												-- Save the Fact details
				table.insert( tblFactsData, { Date=datDate:Clone(); FactData=strFactData; LocName=strLocName; Address=strAddress; Place=str2ndPlace; PlcName=strPlcName; Index=intFactIndex; } )
			end
		end
	end -- local function doAddFactData

	-- Determine if Privacy Flags allow output
	local function flagsOK(ptrIndi)
		if ( IntPrivacy == IntLivingFlag  or IntPrivacy == IntBothFlags ) and fhGetDisplayText(ptrIndi,"~._FLGS.__LIVING","MIN")  == "Y" then return false end -- or fhGetItemText(ptrIndi,"~._FLGS.__LIVING")
		if ( IntPrivacy == IntPrivateFlag or IntPrivacy == IntBothFlags ) and fhGetDisplayText(ptrIndi,"~._FLGS.__PRIVATE","MIN") == "Y" then return false end -- or fhGetItemText(ptrIndi,"~._FLGS.__PRIVATE")
		return true
	end -- local function flagsOK

	-- Process one Individual Record and associated Family Records
	local function doProcessIndividual(ptrIndi)
		-- Process Individual Facts
		local ptrFact = fhNewItemPtr()
		ptrFact:MoveToFirstChildItem(ptrIndi)
		while ptrFact:IsNotNull() do
			if fhIsFact(ptrFact) or IsDataTag(ptrFact) then
				doAddFactData(ptrFact,"")
			end
			ptrFact:MoveNext("ANY")
		end
		-- Loop all Family Facts
		local ptrFamily	= fhNewItemPtr()
		local ptrSpouse	= fhNewItemPtr()
		local ptrChild	= fhNewItemPtr()
		local intFamily	= 1
		local intSpouse	= 1
		local intChild	= 1

		-- Determine if found a new Spouse Family Record Id
		local function isNewSpouse()
			local strRecordId = fhGetRecordId(ptrFamily)
			for _, strFamilyId in ipairs(tblFamily) do
				if strFamilyId == strRecordId then return false end
			end
			return true
		end -- local function isNewSpouse

		ptrFamily:MoveTo(ptrIndi,"~.FAMS["..intFamily.."]>")
		while ptrFamily:IsNotNull() do
			-- Process Family Facts
			ptrFact:MoveToFirstChildItem(ptrFamily)
			while ptrFact:IsNotNull() do
				if fhIsFact(ptrFact) or IsDataTag(ptrFact) then
					ptrSpouse:MoveTo(ptrIndi,"~.~SPOU["..intFamily.."]>")
					if flagsOK(ptrSpouse) then
						local strSpouse = fhGetDisplayText(ptrIndi,"~.~SPOU["..intFamily.."]>NAME","MIN")
						doAddFactData(ptrFact,"with "..strSpouse..":<br />")	-- V4.2 <br />
						if isNewSpouse() then
							tblFamily[intSpouse] = fhGetRecordId(ptrFamily)
							tblSpouse[intSpouse] = strSpouse
							intSpouse = intSpouse + 1
						end
					end
				end
				ptrFact:MoveNext("ANY")
			end
			-- Process Child Births
			intChild = 1
			ptrChild:MoveTo(ptrFamily,"~.CHIL["..intChild.."]>")
			while ptrChild:IsNotNull() do
				if flagsOK(ptrChild) then 
					ptrFact:MoveTo(ptrChild,"~.BIRT")
					if fhIsFact(ptrFact) then
						doAddFactData(ptrFact,"Child: "..fhGetDisplayText(ptrChild,"~.NAME","MIN")..":<br />")	-- V4.2 <br />
					end
				end
				intChild = intChild + 1
				ptrChild:MoveTo(ptrFamily,"~.CHIL["..intChild.."]>")
			end
			intFamily = intFamily + 1
			ptrFamily:MoveTo(ptrIndi,"~.FAMS["..intFamily.."]>")
		end
	end -- local function doProcessIndividual

	-- Build JavaScript Map Marker Location Lists for HTML
	local function intJavaScriptLists()
		local strLatitude = ""
		local strLongitude = ""
		local intLocTotal = 0
		local arrJavaLists = {}										-- V4.2
		local intMapMarker = 0
		tblMapMarker = {}
		for strLocName, intLocCount in pairs(tblLocations) do
			if 	not TblLocationData[strLocName] then				-- v3.6
				MakeNewLocationData(strLocName)					-- v3.6
			end
			if IsNotGeocoded(TblLocationData[strLocName]) and IntGeoPlot ~= DicGeoPlot.Disabled then	-- V5.3
				local strAns = StrGeocodeLocation(strLocName)	-- V4.2
				SaveLocationsData(strLocName)						-- v3.6
				IsFromNew = true										-- v3.6
				iup.Flush()
				if strAns == StrPlotAborted then					-- V4.2
					isGeocoderOk = false
					return -1
				end
			end
		end
		for strLocName, intLocCount in pairs(tblLocations) do
			tblMapMarker[strLocName] = ""
			if TblLocationData[strLocName] then
				if IsPlotted(TblLocationData[strLocName]) then
					strLatitude  = TblLocationData[strLocName].lat
					strLongitude = TblLocationData[strLocName].lng
					tblMapMarker[strLocName] = tostring(intMapMarker)	-- Track the Map Marker number for each mapped Location
					strLocName = strLocName:gsub(strControlChar," ")	-- Make control characters spaces	-- V4.7
					strLocName = StrTidyCommas(strLocName)				-- StrTidyCommas	-- V4.2
					strLocName = strLocName:gsub("\\","\\\\")			-- Escape any \ characters	-- V4.2
					local strCaption = strLocName:gsub("'","\\'")		-- Escape any ' characters	-- V4.2 
					local strFactsText = StrTranslate("%d recorded Facts at")								-- V5.1
					if intLocCount == 1 then strFactsText = StrTranslate("%d recorded Fact at") end
					strFactsText = strFactsText:replace("%d",tostring(intLocCount),1)					-- V5.1
					table.insert(arrJavaLists,"      LatLong["..intMapMarker.."] = NewLatLong("..strLatitude..", "..strLongitude..");")
					table.insert(arrJavaLists,"      LocName["..intMapMarker.."] = '"..encoder.StrEncode_XML(strLocName).."';")	-- V3.3 -- StrEncode_XML V3.5	-- V4.2
					table.insert(arrJavaLists,"      Caption["..intMapMarker.."] = '"..strCaption.."';")		-- V4.2
--?					table.insert(arrJavaLists,"      InfoWin["..intMapMarker.."] = '"..intLocCount..strFactsText.." at';")
					table.insert(arrJavaLists,"      InfoWin["..intMapMarker.."] = '"..strFactsText.."';")
					intMapMarker = intMapMarker + 1
					intLocTotal = intLocTotal + intLocCount
				end
			end
		end
		strJavaLists = table.concat(arrJavaLists,"\n")			-- V4.2
		return intLocTotal
	end -- local function intJavaScriptLists

	-- Comparison for table sort that returns true when 1st is less than 2nd
	local function isLessThan(tbl1st,tbl2nd)
		local intResult = tbl1st.Date:Compare(tbl2nd.Date)
		if intResult == 0 then 										-- Dates equal so compare Index to retain original Fact order
			return tbl1st.Index < tbl2nd.Index					-- True if 1st Index < 2nd Index
		else
			return intResult == -1 									-- True if 1st Date < 2nd Date
		end
	end -- local function isLessThan

	-- Compose HTML Anchor Tag for Facts
	local function getAnchor(strName,strFact)					-- V4.2
		local strMark = tblMapMarker[strName] or ""				-- Marker from intJavaScriptLists() above
		if #strMark > 0 and #strFact > 0 then
			return "<a href='#' onclick='ZoomToMarker("..strMark..")'>"..encoder.StrEncode_XML(strFact).."</a>" -- StrEncode_XML V3.5
		end
		return encoder.StrEncode_XML(strFact)					-- Include any Fact with Location but without Marker	-- V4.3
	end -- local function getAnchor

	-- Build Facts List linked to Map Markers for HTML
	local function strFactsList(tblFactLink)					-- V4.2 revised
		local arrFactList = {}
		for _, strFactLink in ipairs(tblFactLink) do
			table.insert(arrFactList,"        <li>"..strFactLink.."</li>")
		end
		table.sort(tblFactsData,isLessThan) 						-- Sort Facts Data table by Date and Index
		for _, tblEntry in ipairs(tblFactsData) do
			local strFactData = tblEntry.FactData or ""
			local strLocName  = tblEntry.LocName  or ""
			local strAddress  = tblEntry.Address  or ""
			local str2ndPlace = tblEntry.Place    or ""
			local strPlcName  = tblEntry.PlcName  or ""
			if #str2ndPlace == 0 then
				if #strLocName > 0 then								-- Simple fact with one Place/Address	-- V4.2
					strFactData = getAnchor(strLocName,strFactData..strAddress)
				end
			else
				if #strLocName == 0 then							-- EMIG/IMMIG with only second Place	-- V4.2
					strFactData = getAnchor(strPlcName,strFactData..str2ndPlace..strAddress)
				else													-- EMIG/IMMIG with both Places/Address -- V4.2
					strFactData = getAnchor(strLocName,strFactData)..getAnchor(strPlcName,str2ndPlace)..getAnchor(strLocName,strAddress)
				end
			end
			if #strFactData > 0 then
				table.insert(arrFactList,"        <li>"..strFactData.."</li>")	-- V4.2
			end
		end
		return table.concat(arrFactList,"\n")					-- V4.2
	end -- local function strFactsList

	-- Insert Link in Featured Web Page
	local function doInsertLink(strWebFile,strMapFile,strIndName)
		strWebFile = strMapFolder..strWebFile
		local strWebPage = general.StrLoadFromFile(strWebFile)
		local strLnkText = ("<li><a href='"..strMapFile.."'>.-"..strIndName.."</a></li>"..StrEOL):gsub("%%","%%%%")	-- V5.1 -- Cater for translated "Map of Locations for"
		if not strWebPage:match(strLnkText) then		-- Insert link to map file, including "See also" section if necessary
			strWebPage = strWebPage:gsub("("..StrDivContent..")("..StrH1Heading..")","%1"..StrDivSeeAlso..StrNewSeeAlso..StrBegSeeAlso..StrEndSeeAlso.."%2",1)	-- V5.1
			strLnkText = strLnkText:gsub("%.%-",StrTranslate("Map of Locations for").."&nbsp;",1)	-- V5.1 -- Translate "Map of Locations for" and add unique separator &nbsp;
			strWebPage = strWebPage:gsub(StrDivSeeAlso..StrOldSeeAlso..StrBegSeeAlso,StrDivSeeAlso..StrNewSeeAlso..StrBegSeeAlso..strLnkText,1)	-- V5.1 -- Cater for 'Improve Website or CD DVD HTML' plugin Tree Diagrams tab 'See also' section
			general.SaveStringToFile(strWebPage,strWebFile)
		end
	end -- local function doInsertLink

	-- Output HTML Web Page Map
	local function strCreateWebPage(ptrIndi)
		local strRecordId	= fhGetRecordId(ptrIndi)
		local strIndName	= encoder.StrEncode_XML(fhGetDisplayText(ptrIndi))		-- StrANSI_XML() added in V3.3 on 06 June 2013 -- StrEncode_XML V3.5
		local strMapFile	= "map"..strRecordId..".html"
		local arrFactLink	= {}
		local strFactsFor	= "'>"..StrTranslate("Detailed Facts for").." "		-- V5.1
		local strWebPage	= " "
		if IsFamilyTreeWebsite() then
			-- If a Family Tree CD/Website featured Individual web page exists link to/from it
			local strIndFile = "ind"..strRecordId..".html"
			if general.FlgFileExists(strMapFolder..strIndFile) then
				table.insert(arrFactLink,"<a href='"..strIndFile..strFactsFor..strIndName.."</a>")	-- V4.2
				-- Also need to link from Individual page
				doInsertLink(strIndFile,strMapFile,strIndName)
			end
			-- If any Family Tree CD/Website featured Family web pages exist link to/from them
			for intFam, strRecordId in ipairs(tblFamily) do
				local strFamFile = "fam"..strRecordId..".html"
				if general.FlgFileExists(strMapFolder..strFamFile) then
					local strSpouse = encoder.StrEncode_XML(tblSpouse[intFam])		-- StrEncode_XML V3.5
					if strSpouse ~= "" then strSpouse = " and "..strSpouse end		-- One or two parent family? Changed in V3.3 on 06 June 2013
					table.insert(arrFactLink,"<a href='"..strFamFile..strFactsFor..strIndName..strSpouse.."</a>")	-- V4.2
					-- Also need to link from Family web page
					doInsertLink(strFamFile,strMapFile,strIndName)
				end
			end
		end
		strWebPage = StrMasterWeb
		strWebPage = strWebPage:replace("{fhheaders}",StrMapHeaders)
		strWebPage = strWebPage:replace("{fhmenubar}",StrMapMenubar)
		strWebPage = strWebPage:replace("{titlename}",strIndName) -- ,2)
		strWebPage = strWebPage:replace("{javalists}",strJavaLists)
		strWebPage = strWebPage:replace("{factslist}",strFactsList(arrFactLink))
		strMapFile = strMapFolder..strMapFile
		general.SaveStringToFile(strWebPage,strMapFile)
		return strMapFile:replace(strMapFolder,"")
	end -- local function strCreateWebPage()

	-- Main body of StrCreateWebPageMap function starts here --

	local strMapState = ""
	local strIndi = fhGetItemText(ptrIndi,"~.NAME:SURNAME_FIRST") -- or fhGetDisplayText(ptrIndi)
	intFactIndex = 0
	tblFactsData	= {}
	tblLocations	= {}
	tblFamily		= {}
	doFindFactLabels()
	if flagsOK(ptrIndi) then
		doProcessIndividual(ptrIndi)
		if intJavaScriptLists() > 0 then
			strMapState = strCreateWebPage(ptrIndi)
		else
			strMapState = StrMapOmitted
		end
	else
		strMapState = StrMapFiltered
	end
	return strMapState, isGeocoderOk		-- V4.2
end -- function StrCreateWebPageMap

-- Does folder contain FH Family Tree CD/Website files --
function IsFamilyTreeWebsite()
	if  general.FlgFileExists(StrFolder.."\\fhstyle.css")
	and general.FlgFileExists(StrFolder.."\\_nameindex.html") then
		return true
	else
		return false
	end
end -- function IsFamilyTreeWebsite()

-- Set default or FH Family Tree CD/Website headers & menubar --
function SetWebPageMapSections()
	-- Set default headers & menubar HTML sections
	local strHeaders = "    <meta http-equiv='content-type' content='text/html; charset=utf-8'>"
	local strMenubar = " "
	if IsFamilyTreeWebsite() then
		local tblNameIndex = {}
		-- Folder contains FH Family Tree CD/Website so map file needs FH headers & FH menubar
		local strNameIndex = general.StrLoadFromFile(StrFolder.."\\_nameindex.html")
		for strLine in strNameIndex:gmatch("(.-)[\n\r]+") do
			table.insert(tblNameIndex,strLine)		-- Read HTML file up to first <div class=...> in body	-- V5.1
			if strLine:find("<div class=\"fhcontent fhpage_Index\">") then break end
		end
		local strNameIndex = table.concat(tblNameIndex,"\n")
		-- Extract headers script from between "</title>" and "</head>"^$()%.[]*+-?)
		strHeaders = strNameIndex:gsub(".-</title>\n*(.-)\n*</head>.*","%1",1)
		-- Remove "<style type="text/css"> /*<![CDATA[*/ ... /*]]>*/ </style>" added by Improve Website or CD DVD HTML Plugin 
		strHeaders = strHeaders:gsub('\n*<style type="text/css"> /%*<!%[CDATA%[%*/ .- /%*%]%]>%*/ </style>',"",1)
		-- Extract menubar script from between "<body>" and "<div class="
		strMenubar = strNameIndex:gsub(".-<body.->(.-)\n*<div class=.*","%1",1)
	end
	StrMapHeaders = strHeaders
	StrMapMenubar = strMenubar
end -- function SetWebPageMapSections

-- GUI Warning Prompt Dialogue --
function GUI_WarnDialogue(strHead,strText)
	local intAnswer, strAnswer =
		iup_gui.WarnDialogue(
			{ strHead; iup_gui.Warn; },
			{ strText; iup_gui.Info; "Description of operation to accept or reject"; },
			{  "Yes" ; iup_gui.Safe; "Accept the operation"; },
			{  "No"  ; iup_gui.Risk; "Reject the operation"; }
		)
	return strAnswer
end -- function GUI_WarnDialogue

-- GUI Some Plots Prompt Dialogue --
function GUI_PlotDialogue(tblPlotCounts)
	local intButton = 0
	local isConfirm = true
	local iupButtons = iup.vbox { Gap=iup_gui.Gap; Margin=iup_gui.Margin; Homogeneous="YES"; }
	-- Create the GUI buttons
	for intAttr, tblAttr in ipairs ( {
		-- 1~Index			;	2~Title					;	3~Tip
		{ IntPlotDefined		; " Plotted Locations "		; "Geocode the Plotted Locations"	; };
		{ IntPlotTentative	; " Tentative Locations"	; "Geocode the Tentative Locations"	; };
		{ IntPlotNotFound	; " No Plot Locations "		; "Geocode the No Plot Locations"	; };
		{ IntPlotInvalid		; " Invalid Locations "		; "Geocode the Invalid Locations"	; };
		{ IntPlotNoData		; " No Data Locations "		; "Geocode the No Data Locations"	; };
		} ) do
		local intIndex = tblAttr[1]						-- Index to Plot Counts and Status Color tables
		local intPlots = tblPlotCounts[intIndex]
		local strTitle = "Geocode Plot the "..intPlots..tblAttr[2]
		if intPlots == 1 then strTitle = strTitle:gsub("Locations","Location",1) end
		local btnButton = iup.button{ Title=strTitle; Tip=tblAttr[3]; Expand="YES"; FgColor=iup_gui[TblStatusColour[intIndex]]; action=function() intButton=intIndex return iup.CLOSE end; }
		if intPlots == 0 then
			btnButton.Active="NO"
			btnButton.Font=iup_gui.FontBody:gsub(", (.-) (%-?[0-9]+)$",", Strikeout %1 %2")	-- V3.5
		end
		iup.Append(iupButtons,btnButton)
	end
	local tglConfirm = iup.toggle { Expand="YES"; RightButton="YES"; Value="ON"; Title="Confirm Every Location Geocode Plot?"; Tip="Choose whether each Location is plotted selectively or unconditionally"; action=function(self,intState) isConfirm=(intState==1) end; }
	local btnDestroy = iup.button { Visible="NO"; Floating="IGNORE"; action=function() return iup.CLOSE end; } -- Hidden button for default Esc and Enter action
	local strHead = "Geocode Plot Some Locations"
	local strText = "Choose the class of Locations to Geocode Plot"
	-- Create dialogue and turn off resize, maximize, minimize, and menubox except Close button
	local	dialogPlot = iup.dialog { Title=iup_gui.Plugin.."  "..strHead;
								iup.vbox { Alignment="ACENTER"; Gap=iup_gui.Gap; Margin=iup_gui.Margin;
									iup.frame { Font=iup_gui.FontHead; FgColor=iup_gui.Info; Title=strHead;
										iup.vbox { Font=iup_gui.FontBody; FgColor=iup_gui.Info; Gap=iup_gui.Gap; Margin=iup_gui.Margin;
											iup.label { Title=strText..":"; Tip=strText; };
											iupButtons; tglConfirm; btnDestroy;
										};
									};
								};
							close_cb	= function() intButton=0 end;
							}
	iup_gui.ShowDialogue("Memo",dialogPlot,btnDestroy,"popup")
	return intButton, isConfirm
end -- function GUI_PlotDialogue

-- GUI Skip/Plot Location Prompt Dialogue --
function GUI_SkipDialogue(strClassColor)
	local isConfirm = true
	local isGeocode = true
	-- Create the GUI buttons
	local	btnPlotThis = iup.button { Title="Plot this Location"; Tip="Plot selected Location"	; }
	local	btnSkipThis = iup.button { Title="Skip this Location"; Tip="Skip selected Location"	; }
	local	btnPlotRest = iup.button { Title="Plot all the Rest" ; Tip="Plot rest of Locations"	; }
	local	btnSkipRest = iup.button { Title="Skip all the Rest" ; Tip="Skip rest of Locations"	; }

	-- Set other GUI button attributes
	for iupName, tblAttr in pairs ( {
		-- Control    = 1~isConfirm; 2~isGeocode
		[btnPlotThis] = { true     ; true  };
		[btnSkipThis] = { true     ; false };
		[btnPlotRest] = { false    ; true  };
		[btnSkipRest] = { false    ; false };
		} ) do
		iupName.Expand  = "YES"
		iupName.FgColor = iup_gui.Safe
		iupName.action  = function() isConfirm=tblAttr[1] isGeocode=tblAttr[2] return iup.CLOSE end
	end

	local strHead = "Geocode Plot Some Locations"
	local strText = "Choose which Locations to Geocode Plot"
	-- Create dialogue and turn off resize, maximize, minimize, and menubox except Close button
	local	dialogSkip = iup.dialog { Title=iup_gui.Plugin.."  "..strHead;
								iup.vbox { Alignment="ACENTER"; Gap=iup_gui.Gap; Margin=iup_gui.Margin;
									iup.frame { Font=iup_gui.FontHead; FgColor=iup_gui.Info; Title=strHead;
										iup.vbox { Font=iup_gui.FontBody; FgColor=strClassColor; Gap=iup_gui.Gap; Margin=iup_gui.Margin;
											iup.label { Title=strText..":"; Tip=strText; };
											iup.hbox { btnPlotThis; btnSkipThis; Homogeneous="YES"; };
											iup.hbox { btnPlotRest; btnSkipRest; Homogeneous="YES"; };
										};
									};
								};
							close_cb	= function() end;
							}
	iup_gui.ShowDialogue("Memo",dialogSkip,btnPlotThis,"popup")
	return isConfirm, isGeocode
end -- function GUI_SkipDialogue

	local	lblBingKey	= iup.label	{ Title=" Bing Maps Key: "				; Tip="Enter your Bing Maps personal API Key code"; }		-- V5.3
	local	txtBingKey	= iup.text	{ Value=" "								; Tip="Enter your Bing Maps personal API Key code"; }		-- V5.3

-- GUI Main Dialogue --
function GUI_MainDialogue()

	local function strDegree(strText)
		if fhGetAppVersion() > 5 then strText = fhConvertANSItoUTF8(strText) end
		return strText
	end -- local function strDegree

	-- Create the Geocode Location Plots controls with title/value and tooltip, etc
	local	btnPlotAll    = iup.button{ Title="Geocode Plot All Locations" ; Tip="Geocode Plot all the Location entries"; }
	local	btnPlotSome   = iup.button{ Title="Geocode Plot Some Locations"; Tip="Geocode Plot selected Location entries"; }
	local	btnPlotThis   = iup.button{ Title="Geocode Plot This Location" ; Tip="Geocode Plot the displayed Location entry"; }

	local	lblPlotStats  = iup.label	{ Title="Statistics:"					; Tip="Geocoder Plot Locations Database Statistics"; }
	local	lblPlotted    = iup.label	{ Title=" "								; Tip="Number of Locations with fully Plotted Latitude/Longitude"; }
	local	lblTentative  = iup.label	{ Title=" "								; Tip="Number of Locations with Tentative plot Latitude/Longitude"; }
	local	lblNoPlot     = iup.label	{ Title=" "								; Tip="Number of Locations with Blocked status or Not Found by Geocoder"; }
	local	lblInvalid    = iup.label	{ Title=" "								; Tip="Number of Locations with Invalid Request/Request Denied from Geocoder"; }
	local	lblNoData     = iup.label	{ Title=" "								; Tip="Number of Locations with no Latitude/Longitude and not yet Geocoded"; }
	local	lblPlotTotal  = iup.label	{ Title=" "								; Tip="Number of Locations listed in total"; }

	local	lblLocation   = iup.label	{ Title="Location:"						; Tip="Select any Location to manage its Database entry"; }
	local	btnLocation   = iup.button{ Title=" "								; Tip="Select any Location to manage its Database entry"; }
	local	lblStandard   = iup.label	{ Title="Standardized:"					; Tip="Standardized modern or alternative Location for this Database entry"; }
	local	txtStandard   = iup.text	{ Value=" "								; Tip="Standardized modern or alternative Location for this Database entry"; }
	local	lblLatitude   = iup.label	{ Title="Latitude:"						; Tip=strDegree("Latitude for chosen Location Database entry \n from -90° S.Pole to +90° N.Pole")	; }
			TxtLatitude   = iup.text	{ Value=" "								; Tip=strDegree("Latitude for chosen Location Database entry \n from -90° S.Pole to +90° N.Pole")	; }
	local	lblLongitude  = iup.label	{ Title="Longitude:"					; Tip=strDegree("Longitude for chosen Location Database entry \n from -180° West to +180° East"); }
			TxtLongitude  = iup.text	{ Value=" "								; Tip=strDegree("Longitude for chosen Location Database entry \n from -180° West to +180° East"); }

	local	btnLocToSub   = iup.button{ Title=" Copy Location\n to Standardized"; Tip="Copy the Location text field to the Standardized text field of this entry"; Padding="0x5"; }
	local	btnClearAll   = iup.button{ Title=" Clear This Entry"				; Tip="Clear the Standardized, Latitude, and Longitude values of this entry"; }
	local	btnClearSub   = iup.button{ Title=" Clear Standardized"			; Tip="Clear the Standardized value of this entry"; }
	local	btnClearLatLng= iup.button{ Title=" Clear Lat/Longitude"			; Tip="Clear the Latitude & Longitude values of this entry"; }				-- V4.9
	local	btnSetBlocked = iup.button{ Title=" Block Geocoding"				; Tip="Set the Latitude & Longitude values to Blocked(no auto-geocode) for this entry"; }	-- V4.9

	local	btnNextPlot   = iup.button{ Title="Next Plotted"					; Tip="Show the succeeding (Alt=preceding) \n Fully Plotted/Nudged Lat/Lng Location entry"; }
	local	btnNextDefined= iup.button{ Title="Next Tentative"				; Tip="Show the succeeding (Alt=preceding) \n Tentative plotted Lat/Lng Location entry"; }
	local	btnNextSubs   = iup.button{ Title="Next Standardized"				; Tip="Show the succeeding (Alt=preceding) \n Standardized substitute Location entry"; }
	local	btnNextUnplot = iup.button{ Title="Next Unplotted"				; Tip="Show the succeeding (Alt=preceding) \n Unplotted Location entry (No Plot, Invalid, or No Data)"; }

	local	btnMoveNorth  = iup.button{ Title="N"								; Tip=strDegree("Nudge the Latitude North dependent on Zoom \n (maximum +90° N.Pole)"); }
	local	btnMoveSouth  = iup.button{ Title="S"								; Tip=strDegree("Nudge the Latitude South dependent on Zoom \n (minimum -90° S.Pole)"); }
	local	btnMoveWest   = iup.button{ Title="W"								; Tip=strDegree("Nudge the Longitude West dependent on Zoom \n (minimum -180° West)"); }
	local	btnMoveEast   = iup.button{ Title="E"								; Tip=strDegree("Nudge the Longitude East dependent on Zoom \n (maximum +180° East)"); }

	-- Create the Create Web Page Maps controls with title/value and tooltip, etc
	local	btnMapAll		= iup.button	{ Title="Map All Individuals"			; Tip="Create the Web Page Maps for all the Individuals"; }
	local	btnMapSome	= iup.button	{ Title="Map Some Individuals"		; Tip="Create the Web Page Maps for selected Individuals"; }
	local	btnMapThis	= iup.button	{ Title="Map This Individual"			; Tip="Create a Web Page Map for the displayed Individual"; }

	local	lblMapStats	= iup.label	{ Title="Statistics:"					; Tip="Mapped Individuals Statistics"; }
	local	lblMapped		= iup.label	{ Title=" "								; Tip="Number of Individuals with Web Page Maps"; }
	local	lblOmitted	= iup.label	{ Title=" "								; Tip="Number of Individuals without any Facts with Plotted Locations"; }
	local	lblFiltered	= iup.label	{ Title=" "								; Tip="Number of Individuals excluded by the Privacy Filter"; }
	local	lblNoMap		= iup.label	{ Title=" "								; Tip="Number of Individuals without Web Page Maps"; }
	local	lblMapTotal	= iup.label	{ Title=" "								; Tip="Number of Individuals listed in total"; }

	local	lblIndiName	= iup.label	{ Title="Individual:"					; Tip="Select any Individual [Record Id] to manage the Web Page Map"; }
	local	btnIndiName	= iup.button	{ Title=" "								; Tip="Select any Individual [Record Id] to manage the Web Page Map"; }
	local	lblMapState	= iup.label	{ Title="Map:"							; Tip="Web Page Map filename or Omitted/Filtered status"; }
	local	txtMapState	= iup.text	{ Value=" "	; ReadOnly="YES"			; Tip="Web Page Map filename or Omitted/Filtered status"; }

	local	btnDeleteMap	= iup.button	{ Title="Delete This Map"				; Tip="Delete this Web Page Map file from the current Maps Folder"; }
	local	btnPrevUnmap	= iup.button	{ Title="Prior Unmapped"				; Tip="Show the preceding Individual without a Web Page Map file"; }
	local	btnNextUnmap	= iup.button	{ Title="Next  Unmapped"				; Tip="Show the succeeding Individual without a Web Page Map file"; }
	local	btnPrevMapped= iup.button	{ Title="Prior Mapped"					; Tip="Show the preceding Individual with a Web Page Map file"; }
	local	btnNextMapped= iup.button	{ Title="Next  Mapped"					; Tip="Show the succeeding Individual with a Web Page Map file"; }

	-- Create the Set Preference Options controls with title/value and tooltip, etc
	local	lblAPI_Key	= iup.label	{ Title="Google Maps Key: "			; Tip="Enter your Google Maps personal API Key code"; }	-- V4.2
	local	txtAPI_Key	= iup.text	{ Value=" "								; Tip="Enter your Google Maps personal API Key code"; }	-- V4.2
	local	lblGeo_Key	= iup.label	{ Title=" Geoapify Key: "				; Tip="Enter your Geoapify personal API Key code"; }		-- V5.3
	local	txtGeo_Key	= iup.text	{ Value=" "								; Tip="Enter your Geoapify personal API Key code"; }		-- V5.3
	local	lblGeoPlot	= iup.label	{ Title=" Plot Geocoder: "				; Tip="Choose a Maps Geocoder for plotting locations"; }	-- V5.3
	local	lstGeoPlot	= iup.list	{ DropDown="YES"; Visible_Items="9"	; VisibleColumns="9"; Tip="Choose a Maps Geocoder for plotting locations"; "Disabled"; "Google Maps"; "Geoapify"; }	-- V5.3 -- V6.0
	local	lblRegBias	= iup.label	{ Title=" "..StrRegBiasTitle..": "	; Tip="Choose region biasing top level domain code for Geocoder"; }
	local	lstRegBias	= iup.list	{ DropDown="YES"; Visible_Items="30"	; VisibleColumns="9"; Tip="Choose region biasing top level domain code for Geocoder"; }

	local	tglGeoCode	= iup.toggle	{ Title="  "..StrGeoCodeTitle..":"	; Tip="Recognise Country/State/Chapman Codes \n and other plotting enhancements"; RightButton="YES"; }
	local	tglLogging	= iup.toggle	{ Title="  "..StrLoggingTitle..":"	; Tip="Enable logging of plot data to CSV"; RightButton="YES"; }	-- V5.3
	local	lblLocZoom	= iup.label	{ Title=StrMapZoomTitle..": "			; Tip="Current default Zoom level for a selected Map Marker"; }
	local	txtLocZoom	= iup.text	{ ReadOnly="YES"							; Tip="Current default Zoom level for a selected Map Marker"; }

	local	lblFromTag	= iup.label	{ Title=StrFromTagTitle..": "			; Tip="Choose Place or Address or both Address ~ Place fields for Locations"; }
	local	lstFromTag	= iup.list	{ DropDown="YES"; Visible_Items="9"	; Tip="Choose Place or Address or both Address ~ Place fields for Locations"; }
	for i = IntFromPLAC, IntFrom_A__P_ do lstFromTag[i] = TblFromName[i] end			-- Add the dropdown list values
	local	lblStorage	= iup.label	{ Title=StrStorageTitle..": "			; Tip="Select the storage option for the Locations Database"; }
	local	lstStorage	= iup.list	{ DropDown="YES"; Visible_Items="9"	; Tip="Select the storage option for the Locations Database"; "Plugin Data Folder"; "Repository Records"; "Source Records"; "Source & Repository"; }
	if fhGetAppVersion() > 5 and IntFromTag == IntFromPLAC then lstStorage[lstStorage.Count+1] = "Place Records" end			-- FH6
	local	timStorage	= iup.timer	{ Time=200; Run="NO"; }

	local	strMapFolder	= "Select the Output Folder for Web Page Maps, and if a\nFamily Tree CD/Website folder then Maps will be integrated"
	local	txtMapFolder	= iup.text	{ MultiLine="YES"; VisibleLines="2"	; Tip=strMapFolder; WordWrap="YES"; }		-- was VisibleLines="3"	
	local	btnMapFolder	= iup.button	{ Title="<< Set Maps Folder"			; Tip=strMapFolder; }
	local	btnDeleteAll	= iup.button	{ Title=" Delete All Maps "			; Tip="Delete ALL Web Page Map files from the selected Folder"; }

	local	lblWebMaps	= iup.label	{ Title=" Web Page Maps:"				; Tip="Choose the display mode for the Web Page Maps"; }				-- V5.3
	local	lstWebMaps	= iup.list	{ DropDown="YES"; Visible_Items="9"	; Tip="Choose the display mode for the Web Page Maps"; "Google Maps (Legacy)"; "Leaflet Maps"; "Google Maps (Advanced)"; }	-- V6.0
	local	lblPrivacy	= iup.label	{ Title=StrPrivacyTitle..":"			; Tip="Exclude Individuals from Maps according to Privacy Flags"; }
	local	lstPrivacy	= iup.list	{ DropDown="YES"							; Tip="Exclude Individuals from Maps according to Privacy Flags"; "Living Flags"; "Private Flags"; "Living & Private"; "Switched Off"; }
	local	tglFactSet	= iup.toggle	{ Title=StrFactSetTitle..":"			; Tip="Exclude any Facts not listed in the CD/Website pages"; RightButton="YES"; }	-- V3.7
	local	lblMapZoom	= iup.label	{ Title=StrMapZoomTitle..":"			; Tip="Current default Zoom level for a selected Map Marker"; }
	local	txtMapZoom	= iup.text	{ ReadOnly="YES"							; Tip="Current default Zoom level for a selected Map Marker"; }

	local	lblMarkers	= iup.label	{ Title=" "..StrMarkersTitle..":"		; Tip="Select how Map Markers are initially placed on Maps"; }
	local	lstMarkers	= iup.list	{ DropDown="YES"							; Tip="Select how Map Markers are initially placed on Maps"; "No Animation"; "Synchro Drop"; "Cascade Drop"; }
	local	lblColours	= iup.label	{ Title=" "..StrColoursTitle..":"		; Tip="Choose the Map Markers pin colour on Maps"; }					-- V6.0
	local	lstColours	= iup.list	{ DropDown="YES"							; Tip="Choose the Map Markers pin colour on Maps"; "Red"; "Blue"; "Green"; }								-- V6.0
	local	lblLegends	= iup.label	{ Title="Map Pane Legends:"			; Tip="Choose the legends size for the Plugin Map Pane"; }			-- V5.3
	local	lstLegends	= iup.list	{ DropDown="YES"							; Tip="Choose the legends size for the Plugin Map Pane"; "Tiny"; "Small"; "Medium"; "Large"; "Huge"; }	-- V5.3

	local	btnCopyPLAC	= iup.button	{ Title="Copy Place Database"			; Tip="Copy ALL Standardized, Latitude, and Longitude values \n from the 'Place Fields only' Locations database \n into the ' ~ Place only' Locations database"; }
	local	btnCopyADDR	= iup.button	{ Title="Copy Address Database"		; Tip="Copy ALL Standardized, Latitude, and Longitude values \n from the 'Address Fields only' Locations database \n into the 'Address ~  only' Locations database"; }
	local	btnPurgeBOTH	= iup.button	{ Title="Purge Addr ~ Place Database"; Tip="Purge ALL Standardized, Latitude, and Longitude values \n not in current '"..TblFromName[IntFromTag].."' Locations data !!!"; }

	local	btnErasePLAC	= iup.button	{ Title="Erase Place Database"		; Tip="Erase ALL Standardized, Latitude, and Longitude values \n from the 'Place Fields only' Locations database !!!"; }
	local	btnEraseADDR	= iup.button	{ Title="Erase Address Database"		; Tip="Erase ALL Standardized, Latitude, and Longitude values \n from the 'Address Fields only' Locations database !!!"; }
	local	btnEraseBOTH	= iup.button	{ Title="Erase Addr ~ Place Database"; Tip="Erase ALL Standardized, Latitude, and Longitude values \n from the 'Address ~ Place' Locations database !!!"; }

	local	btnDefault	= iup.button	{ Title="Restore Defaults"				; Tip="Restore default Settings for Window positions/sizes, Tab selection, \n "..StrGeoCodeTitle..", "..StrRegBiasTitle..", "..StrFromTagTitle..", \n "..StrLoggingTitle..", Maps Output Folder, Quota Warning, \n "..StrMapZoomTitle..", "..StrMarkersTitle..", "..StrColoursTitle..", "..StrPrivacyTitle..", and "..StrFactSetTitle; }
	local	btnSetFont	= iup.button	{ Title="Set Window Fonts"				; Tip="Choose user interface window font style"; }
	local	timMapPane	= iup.timer	{ Time=500; Run="NO"; }					-- Renamed & was Time=900 -- V4.3

	-- Create the Main Dialogue Common controls with title/value and tooltip, etc
	local	lblStatus		= iup.label	{ Title="Status:"						; Tip="Plugin status messages"; }
			LblMessage	= iup.label	{ Title=" "								; Tip="Plugin status messages"; }
	local	btnUpdates	= iup.button	{ Title="Check for Updates"			; Tip="Check for a later version in the Plugin Store"; }		-- V6.0
	local	btnGetHelp	= iup.button	{ Title="   Help && Advice"			; Tip="Obtain online Help and Advice from the Plugin Store"; }
	local	btnDestroy	= iup.button	{ Title="Close Plugin"					; Tip="Close the Plugin"; }

	-- Create the WebBrowser based on its ProgID and connect it to LuaCOM
	OLEcontrol = iup.olecontrol { "Shell.Explorer.1"; DesignMode="NO"; }
	OLEcontrol:CreateLuaCOM()

	-- Create the Geocode Location Plots layout
	local	vboxPlotEntry=	iup.vbox { Margin="1x1";
									iup.hbox { Gap=iup_gui.Gap;
										iup.vbox { Margin="0x0";
											iup.hbox { lblLocation; btnLocation; };
											iup.hbox { lblStandard; txtStandard; }; }; btnLocToSub; iup.vbox { Margin="0x0"; btnClearAll; btnClearSub; };	};											
									iup.hbox { Gap=iup_gui.Gap; lblLatitude;	TxtLatitude;	btnMoveNorth; btnMoveSouth;	btnNextPlot;		btnNextSubs;		btnClearLatLng;	};	-- V4.9
									iup.hbox { Gap=iup_gui.Gap; lblLongitude;	TxtLongitude; btnMoveWest; btnMoveEast;	btnNextDefined;	btnNextUnplot;	btnSetBlocked; };	-- V4.9
								}
	local	vboxPlotData	=	iup.vbox { Margin=iup_gui.Margin;
									iup.hbox { Gap=iup_gui.Gap; lblPlotStats; iup.hbox{ lblPlotted; lblTentative; lblNoPlot; lblInvalid; lblNoData; lblPlotTotal; Homogeneous="YES"; Gap="0"; }; Margin="2x2"; };
									vboxPlotEntry;
								}
	local	framePlotData=	iup.frame { Title=" Manage Location Plots ";
									vboxPlotData;
								}
	local	vboxGeoLoc	=	iup.vbox { Gap=iup_gui.Gap;
									iup.hbox { Gap="10"; btnPlotAll; btnPlotSome; btnPlotThis; Homogeneous="YES"; Margin="0x6"; };
									framePlotData;
								}

	-- Create the Create Web Page Maps layout
	local	vboxMapData	=	iup.vbox { Margin=iup_gui.Margin;
									iup.hbox { Gap=iup_gui.Gap; lblMapStats; iup.hbox{ lblMapped; lblOmitted; lblFiltered; lblNoMap; lblMapTotal; Homogeneous="YES"; Gap="0"; }; Margin="2x2"; };
									iup.hbox { Gap=iup_gui.Gap; lblIndiName; btnIndiName; btnPrevMapped; btnPrevUnmap; };
									iup.hbox { Gap=iup_gui.Gap; lblMapState; txtMapState; btnDeleteMap; btnNextMapped; btnNextUnmap; };
								}
	local	frameMapData	=	iup.frame { Title=" Manage Individual Maps ";
									vboxMapData;
								}
	local	vboxCreate	= 	iup.vbox { Gap="10";
									iup.hbox { Gap="10"; btnMapAll; btnMapSome; btnMapThis; Homogeneous="YES"; Margin="0x6"; };
									frameMapData;
								}

	-- Create the Set Preference Options layout
	local	vboxPlotOpt	=	iup.vbox { Margin=iup_gui.Margin;
									iup.hbox { Gap=iup_gui.Gap; Margin="1x2"; lblAPI_Key; txtAPI_Key; lblGeo_Key; txtGeo_Key; Alignment=":ALEFT"; };	-- V5.3 -- V4.2 -- V4.3
									iup.hbox { Gap=iup_gui.Gap; Margin="1x2"; lblGeoPlot; lstGeoPlot; lblRegBias; lstRegBias; };							-- V5.3
									iup.hbox { Gap=iup_gui.Gap; Margin="1x2"; tglGeoCode; iup.label{ Expand="HORIZONTAL"; }; tglLogging; iup.label{ Expand="HORIZONTAL"; }; iup.hbox{ lblLocZoom; txtLocZoom; }; };			-- V5.3
									iup.hbox { Gap=iup_gui.Gap; Margin="1x2"; iup.hbox{ lblFromTag; lstFromTag; }; iup.hbox{ lblStorage; lstStorage; }; };	-- V5.3
								}
	local	vboxMapsOpt	=	iup.vbox { Margin=iup_gui.Margin;
									iup.hbox { Gap=iup_gui.Gap; Margin="1x1"; txtMapFolder; iup.hbox { btnMapFolder; btnDeleteAll; }; };				-- V5.3
									iup.hbox { Gap=iup_gui.Gap; Margin="1x2"; iup.hbox{ lblWebMaps; lstWebMaps; }; iup.hbox{ lblPrivacy; lstPrivacy; }; iup.label{ Expand="HORIZONTAL"; }; tglFactSet; iup.hbox{ lblMapZoom; txtMapZoom; }; };	-- V5.3 -- V6.0
									iup.hbox { Gap=iup_gui.Gap; Margin="1x2"; iup.hbox{ lblMarkers; lstMarkers; }; iup.hbox{ lblColours; lstColours; }; iup.hbox{ lblLegends; lstLegends; }; };	-- V4.3 -- V6.0
								}
	local	vboxDataOpt	=	iup.vbox { Margin=iup_gui.Margin;
									iup.hbox { Gap="12"; Margin="0x15"; btnCopyPLAC;  btnCopyADDR;  btnPurgeBOTH; Homogeneous="YES"; };
									iup.hbox { Gap="12"; Margin="0x8" ; btnErasePLAC; btnEraseADDR; btnEraseBOTH; Homogeneous="YES"; };
								}
	local	tabPrefer		=	iup.tabs { Tip="Select Preference Options for Location Plots, or Web Page Maps, or Database Management";	-- V4.7
									vboxPlotOpt;	TabTitle0="   Location Plot Options   ";
									vboxMapsOpt;	TabTitle1="   Web Page Map Options   ";
									vboxDataOpt;	TabTitle2="   Database Management   ";
								}
	local	vboxPrefer	= 	iup.vbox { Gap=iup_gui.Gap;
									tabPrefer;
									iup.hbox { Gap="100"; btnDefault; btnSetFont; Homogeneous="YES"; Margin="150x10"; };
								}
	local intPreferTab	= IntTabGeoLoc	

	-- Create the Tab controls layout
	local	tabControls	=	iup.tabs { Tip="Select Geocode plots for Places/Addresses, \n Create HTML web page maps for Individuals, \n or Set Preference Options";	-- V4.7
									vboxGeoLoc;	TabTitle0="   Geocode Location Plots   ";
									vboxCreate;	TabTitle1="    Create Web Page Maps    ";
									vboxPrefer;	TabTitle2="   Set Preference Options   ";
								}

	-- Combine all the above controls
	local	allControls	=	iup.vbox { Margin=iup_gui.Margin;
									tabControls;
									iup.hbox { Gap="4"; lblStatus; LblMessage; btnUpdates; btnGetHelp; btnDestroy; Margin="18x2"; };	-- was Gap="20" -- V4.2 -- V6.0
									iup.hbox { OLEcontrol; MinSize="x440" };						-- If "710" is width, "x500" is max height with no grey bar at top
								}

	-- Create the Main dialogue
			dialogMain	=	iup.dialog { Title=iup_gui.Plugin..iup_gui.Version..StrTranslate("Output_Language");	-- V6.0 ????
									allControls;
								}

	if fhGetAppVersion() > 6 then									-- FH V7 IUP 3.28	-- V4.7
		tabPrefer.TabPadding="0x4"
		tabControls.TabPadding="0x4"
	else																-- FH V6 IUP 3.11	-- V4.7
		tabPrefer.Padding="8x8"
		tabControls.Padding="8x8"
	end

	-- GUI Trees for Location (PLAC, ADDR, BOTH) and Individual (INDI) Lists --

	local tblList = {}												-- Table of chosen GUI tree list controls
	local tblTree = {}												-- Table of all the GUI tree list controls
	local strLeaf = nil												-- Selected leaf node title
	local intLeaf = 0												-- Selected leaf node ident currently
	local intNode = 0												-- Selected leaf node ident previously

	local function doColorLeafNode(intIdent,strColor)			-- Set leaf node colour in tree
		if tblList.Tree and intIdent and intIdent > 0 then
			if intIdent == intNode then
				tblList.Color = strColor							-- If highlighted then only updated saved GUI color
			else
				iup.SetAttributeId(tblList.Tree,"COLOR",intIdent,strColor)
			end
		end
	end -- local function doColorLeafNode

	local function doSelectLeafNode(intIdent)					-- Save leaf node ident & color and highlight in tree
		if tblList.Tree then
			if intNode > 0 then -- Restore old leaf color
				iup.SetAttributeId(tblList.Tree,"COLOR",intNode,tblList.Color)
			end
			intLeaf = intIdent	-- Save new leaf details
			intNode = intIdent
			tblList.Tree.Value = intNode
			tblList.Color = iup.GetAttributeId(tblList.Tree,"COLOR",intNode)
			iup.SetAttributeId(tblList.Tree,"COLOR",intIdent,iup_gui[TblStatusColour[IntHighlight]])
		end
	end -- local function doSelectLeafNode

	local function strGetTreeLeaf(iupTree,intIdent)			-- Translate tree node ident into leaf title/userid and color
		if tonumber(intIdent) then
			if iup.GetAttributeId(iupTree,"KIND",intIdent) == "LEAF" then
				local strColor = iup.GetAttributeId(iupTree,"COLOR",intIdent)
				if strColor == iup_gui[TblStatusColour[IntHighlight]] then strColor = tblList.Color end
				local strLeaf = iup.GetAttributeId(iupTree,"USERID",intIdent)	-- "USERID" replaces "TITLE" that may be truncated at 254 chars -- V4.6
				strLeaf = tblList.dicLeaf[strLeaf] or strLeaf						-- Cater for Reverse Display Order -- V4.9
				return strLeaf, strColor
			end
		end
		return nil
	end -- local function strGetTreeLeaf

	local function doBuildTreeNodes(tblData,strNode,strIndi)	-- Build the GUI tree branches & leaves
		local intSize = 1											-- Number of Head label characters
		local strHead = ""											-- Head label of branch node
		local intHead = 0											-- Head index of branch node
		local tblHead = {}											-- Head table of index per branch
		local tblNode = {}											-- Node table of sorted items for GUI tree
		local intNode = 0											-- Node ident of strNode to be found
		local dicLeaf = {}											-- Lookup for long leaf names -- V4.6
		local strColor = iup_gui[TblStatusColour[IntPlotNoData]]
		tblList.Color = strColor
		if tblList.Tree.Count then
			tblList.Tree.DelNode0 = "CHILDREN"					-- Ensure GUI tree only has root node
		end
		tblList.tblLeaf = {}										-- Holds the tree leaf names
		tblList.dicLeaf = {}										-- Holds reverse order translation -- V4.9
		for strLeaf, _ in pairs(tblData) do
			if tblData[strLeaf].use then							-- Leaf chosen for use is true and not nil
				tblData[strLeaf].use = false						-- V4.9 was nil but false means not true
				if not strIndi and StrReverse == "ON" then		-- Use reverse Place parts display order	-- V4.9
					local strBack = fhCallBuiltInFunction("TextPart",strLeaf,1,0,"REV")
					tblList.dicLeaf[strBack] = strLeaf			-- Set translation back to normal order	-- V4.9
					strLeaf = strBack
				end
				table.insert(tblList.tblLeaf,strLeaf)			-- Create and sort Leaf table so tree is pre-sorted - also used by Plot All/Map All loops
				if #strLeaf >= 254 then								-- Workaround for node table limiting leaf names to 254 chars -- V4.6
					dicLeaf[strLeaf:sub(1,254)] = strLeaf		-- Save long leaf name against truncated leaf name -- V4.6
				end
			end
		end
		local strMask = '^[ -/]+'									-- Disregard leading Leaf non-alphanumeric common symbols " , space, etc, for sort	-- V4.3
		table.sort(tblList.tblLeaf,function(strA,strB) return (strA:gsub(strMask,"")) < (strB:gsub(strMask,"")) end )	-- Do not StrEncode_ASCII?	-- V4.3
		if #tblList.tblLeaf > 1000 then intSize = 4 end		-- Increase initial size of Head labels for large lists
		repeat
			intHead = 0
			tblHead = {}
			tblNode = {}
			for intLeaf, strLeaf in ipairs(tblList.tblLeaf) do	-- Increment the Head prefix size for leading ellipsis Leaf nodes
				local intPref = intSize + ( (strLeaf:match("^ ~ ") and 1) or 0 )
				strHead = strLeaf:gsub(strMask,"")				-- Disregard leading Leaf non-alphanumeric common symbols " , space, etc	-- V4.3
				strHead = strHead:substring(1,intPref)			-- Extract Head label prefix chars from Leaf and convert to uppercase ASCII -- 13 Nov 2014
				strHead = strHead:upper()							-- Do not StrEncode_ASCII?	-- V4.3
				if not tblHead[strHead] then
					tblHead[strHead] = intHead						-- Assign Head index to this label
					intHead = intHead + 1
				end
				local intHead = tblHead[strHead]					-- Create new Head label branch node if necessary
				if not tblNode[intHead] then
					tblNode[intHead] = { branchname=strHead; state="COLLAPSED"; color=iup_gui.Black; }	-- keys must be lowercase -- V3.6 Color=iup_gui.Black added
				end
				local strBack = tblList.dicLeaf[strLeaf] or strLeaf 	-- Cater for Reverse Display Order -- V4.9
				local strData = tblData[strBack][tblList.Status] or ""
				local strMode = tblData[strBack].mod or ""		-- V4.9
				strColor = iup_gui[TblStatusColour[IntStatusIndex(strData..strMode,tblList.States)]]
				table.insert(tblNode[intHead],{ leafname=strLeaf; color=strColor; })						-- keys must be lowercase -- Insert the Leaf node into branch node
				intLeaf = intHead + intLeaf + 1
				if strIndi then tblData[strBack].Leaf = intLeaf end	-- V4.9	-- V3.6
				if strNode then
					if strNode == strBack then intNode = intLeaf end	-- V4.9
				end
			end
			intSize = intSize + 2
		until #tblList.tblLeaf <= 1000 or intHead > 25			-- If large list and few branches (e.g. single name database), then repeat with bigger label 
		tblList.Tree.AutoRedraw = "NO"
		tblList.Dialog:map()
		iup.TreeAddNodes(tblList.Tree,tblNode)					-- Populate the mapped tree control with branch & leaf nodes
		for intIdent = 0, tblList.Tree.Count-1 do				-- Workaround for node table limiting leaf names to 254 chars -- V4.6
			if iup.GetAttributeId(tblList.Tree,"KIND",intIdent) == "LEAF" then
				local strLeaf = iup.GetAttributeId(tblList.Tree,"TITLE",intIdent)
				if #strLeaf >= 254 then
					strLeaf = dicLeaf[strLeaf:sub(1,254)]		-- Lookup long leaf name using truncated leaf name -- V4.6
				end
				iup.SetAttributeId(tblList.Tree,"USERID",intIdent,strLeaf)	-- Set full leaf name as UserId -- V4.6
			end
		end
		tblList.Tree.AutoRedraw = "YES"

		if tblList.BtnEx.Title:sub(1,8) == "Contract" then
			local intIdent = tblList.Tree.Value
			tblList.Tree.ExpandAll = "YES"						-- Expand all branches
			tblList.Tree.Value = intIdent							-- Restore selected leaf node
		end

		return intNode												-- Return ident of strNode if it matched a strLeaf
	end -- local function doBuildTreeNodes

	local function doShowTreeDialogue(iupButton)				-- Show chosen GUI tree list just below its button or at stcky position
		local tblPosn = iup_gui.DialogueAttributes("List")
		if  tblPosn.CoordX == iup.CENTER							-- If dialogue is in default central position then...
		and tblPosn.CoordY == iup.CENTER then
			tblPosn = iup_gui.DialogueAttributes("Main")		-- Get X & Y position of the "Main" window
			local intPosX = tblPosn.CoordX
			local intPosY = tblPosn.CoordY
			tblPosn = iupButton.position:splitnumbers()			-- Add Y position of button control
			intPosY = intPosY + tblPosn[2]
			iup_gui.CustomDialogue("List","600x400",intPosX + 30,intPosY + 155)
		end
		iup_gui.ShowDialogue("List",tblList.Dialog,tblList.BtnNo,"keep")
	end -- local function doShowTreeDialogue

	local function doBuildTreeDialogues()						-- Build all four GUI Trees for PLAC, ADDR, BOTH & INDI and set their positions

		local function doGetTreeLeaf(iupTree,intIdent,intStatus) -- Selection call back for tree node
			if intStatus == 1 then
				intLeaf = intIdent
				strLeaf = strGetTreeLeaf(iupTree,intIdent)		-- Set selected leaf node ident & title
			end
		end -- local function doGetTreeLeaf

		local function doReverseOrder(iupTree,intState)		-- Order toggle action to reverse display order -- V4.9
			if intState == 0 then StrReverse = "OFF" end
			if intState == 1 then StrReverse = "ON" end
			SaveSettings()											-- Save sticky data settings
			for strLeaf, _ in pairs(TblLocationData) do			-- Enable use of locations and rebuild tree nodes -- V4.9
				if TblLocationData[strLeaf].use == false then
					TblLocationData[strLeaf].use = true
				end
			end
			local strLeaf = strGetTreeLeaf(iupTree,iupTree.Value)
			intLeaf = doBuildTreeNodes(TblLocationData,strLeaf)
			iupTree.Value = intLeaf									-- Restore selected leaf node -- V4.9
		end -- local function doReverseOrder

		local function doExpandCollapse(iupTree,iupButton)	-- BtnEx action to expand/contract tree list
			local intIdent = iupTree.Value						-- Save current selected leaf node
			if iupButton.Title:sub(1,6) == "Expand" then
				iupTree.ExpandAll = "YES"							-- Expand all branches
				iupTree.Value = intIdent							-- Restore selected leaf node
				intIdent = intIdent - 1
				iupButton.Title = iupButton.Title:gsub("Expand","Contract",1)
			else
				for intIdent = 1, tonumber(iupTree.Count) do	-- Contract all branches
					if iup.GetAttributeId(iupTree,"KIND",intIdent) == "BRANCH" then
						iup.SetAttributeId(iupTree,"STATE",intIdent,"COLLAPSED")
					end
				end
				iupTree.Value = intIdent							-- Restore selected leaf node
				intIdent = intIdent + 1
				iupButton.Title = iupButton.Title:gsub("Contract","Expand",1)
			end
			iupTree.TopItem = intIdent								-- Force selected leaf node well into window
		end -- local function doExpandCollapse

		local function doMakeTreeDialogue(intTree,strStatus,tblStates,strTree)	-- Make one instance of a Tree GUI
			local tbl = {}
			tbl.Tree  = iup.tree   { Tip="Select any "..strTree.." to manage the entry"; selection_cb=function(self,intIdent,intStatus) doGetTreeLeaf(self,intIdent,intStatus) end; }
			tbl.Order = iup.toggle { Title="Reverse Display Order:"        ; Tip="Reverse the Place parts dispay order"   ; RightButton="YES"; Value=StrReverse; action=function(self,intState) doReverseOrder(tbl.Tree,intState) end; }		-- V4.9
			tbl.BtnNo = iup.button { Title="Cancel "..strTree.." selection"; Tip="Cancel the "..strTree.." List selection"; Expand="HORIZONTAL"; action=function() strLeaf = nil return iup.CLOSE end; }
			tbl.BtnEx = iup.button { Title="Expand "..strTree.." List"     ; Tip="Expand / Contract the List selection"   ; Expand="HORIZONTAL"; action=function() doExpandCollapse(tbl.Tree,tbl.BtnEx) end; }
			tbl.BtnOk = iup.button { Title="OK "..strTree.." selection"    ; Tip="OK the "..strTree.." List selection"    ; Expand="HORIZONTAL"; action=function() strLeaf = strGetTreeLeaf(tbl.Tree,intLeaf) return iup.CLOSE end; }
			if strStatus == "lat" then
				tbl.Vbox = iup.vbox { Alignment="ACENTER"; tbl.Tree; tbl.Order; }		-- V4.9
			else
				tbl.Vbox = iup.vbox { Alignment="ACENTER"; tbl.Tree; }					-- V4.9
			end
--?IUP3.5	tbl.Vpane = iup.vbox   { ExpandChildren="YES"; ... }
			tbl.Vpane = iup.vbox   { Gap=iup_gui.Gap; Margin=iup_gui.Margin; tbl.Vbox; iup.hbox { tbl.BtnNo; tbl.BtnEx; tbl.BtnOk; Homogeneous="YES"; }; }		-- V4.9
			tbl.Dialog= iup.dialog { Title=iup_gui.Plugin.." "..strTree.." List"; tbl.Vpane; close_cb=function() strLeaf = nil return iup.CLOSE end; }
			tblTree[intTree] = tbl
			tblTree[intTree].Status = strStatus
			tblTree[intTree].States = tblStates
		end -- local function doMakeTreeDialogue

		doMakeTreeDialogue(IntFromPLAC,"lat",TblPlotStates,"Place")
		doMakeTreeDialogue(IntFromADDR,"lat",TblPlotStates,"Address")
		doMakeTreeDialogue(IntFromBOTH,"lat",TblPlotStates,"Address ~ Place")
		doMakeTreeDialogue(IntFromINDI,"map",TblMapStates ,"Individual")

	end -- local function doBuildTreeDialogues

	-- GUI Common Shared Service Data & Functions --

	local intLocation	=	0										-- Selected entry from Location dropdown list matching StrLocation
	local strLocation	= "?"										-- Saved copies of last mapped Location values
	local strLatitude	= "?"										-- Used only by doMarkMapPanePlot()
	local strLongitude	= "?"
	local strSubValue	= ""										-- Saved Standardized value before editing
	local intSubCount	=	0										-- Count of Standardized Location entries
	local intLocIndex	=	0										-- Saved Location color index before editing
	local intNoData		=	0										-- Count of never Geocoded Location entries
	local intPlotted	=	0											-- Count of Lat/Lng Plotted Location entries
	local intDefined	=	0											-- Count of Lat/Lng Tentative Location entries
	local intNoPlot		=	0										-- Count of No Plot Found Geocode Location entries
	local intInvalid	=	0											-- Count of Invalid Request/Request Denied Geocode Location entries
	local intUnplotted	=	0										-- Count sum of all Location entries with no plot
	local tblPlotCounts= { 0; 0; 0; 0; 0; 0; 0; 0; }

	local function getCityPlots()									-- Get city map plots for current region bias -- V4.7
		local dicCityPlots = {										-- City map plots for doMarkMapLondon/Single() & doDropStyleMarkers() -- V4.7
			au={ Lati=-33.868; Long=151.202; City="Sidney";		};
			ca={ Lati=045.422; Long=-75.701; City="Ottawa";		};
			nz={ Lati=-41.276; Long=174.764; City="Wellington";	};
			uk={ Lati=051.508; Long=-00.131; City="London";		};
			us={ Lati=040.713; Long=-74.011; City="New York";	};
		}
		local strBias = TblRegBias[IntRegBias]:sub(1,2)
		local dicPlot = dicCityPlots[strBias]
		if not dicPlot then dicPlot = dicCityPlots["uk"] end
		return dicPlot.Lati,dicPlot.Long,dicPlot.City
	end -- local function getCityPlots

	local function setLocation(strLoc,intLoc)					-- Set Location name and droplist index	-- V4.3
		StrLocation = strLoc
		intLocation = intLoc
	end -- local function setLocation

	local function deActivateControls(tblControls)				-- Set GUI controls inactive
		for _, control in ipairs(tblControls) do
			control.Active = "NO"
		end
	end -- local function deActivateControls

	local function doMarkMapPanePlot(strMode)					-- Mark the Latitude/Longitude on Map pane
		local strLoc = btnLocation.Title
		if strLoc..TxtLatitude.Value..TxtLongitude.Value ~= strLocation..strLatitude..strLongitude then
			strLocation	= strLoc									-- Only update Map pane if values have changed
			strLatitude	= TxtLatitude.Value
			strLongitude	= TxtLongitude.Value
			if strMode == "Move" then
				MoveMapPane(strLatitude,strLongitude)			-- V4.3 strMode
			else
				strLoc = strLoc:gsub("&&","&")					-- Remove double ampersands "&" from the button title
				local strLocName = encoder.StrEncode_XML(strLoc)		-- Encode control, &, <, >, " and ' characters	-- V4.2
				local strCaption = strLoc:gsub(strControlChar," ")	-- Make control characters spaces	-- V4.2 -- V4.7
				MarkMapPane(strLatitude,strLongitude,strLocName,strCaption)	-- V4.2
			end
		end
	end -- local function doMarkMapPanePlot

	local function doShowWarnings(strButton)					-- Show Geocode Plot button and Geocoder Quota limit warnings until inhibitted by user
		if StrWarnAns == "Yes" then								-- V4.2 New warning
			local strWarnAns = GUI_WarnDialogue("WARNING!  ADVICE!","Geocoding Location entries requires an API Key and has usage limits.\nIf the 'Status:' shows any 'Geocoder API ...' errors in red then please\nreview the 'Help && Advice' on the 'Map Life Facts FAQ' page.\n          Do you want to continue to see this Warning in future ?\n          (Click the X Close top right to cancel "..strButton..")")
			if strWarnAns == "No" then StrWarnAns = "No" end
			if strWarnAns == "X Close" then return false end
		end
		return true
	end -- local function doShowWarnings

	-- Show the whole World without Marker
	local function doShowWholeWorld()								-- V4.3
		MarkMapPane("","","")
	end -- local function doShowWholeWorld

	-- Drop a Marker for Capital City
	local function doMarkMapCity()								-- V4.3
		MarkMapPane(getCityPlots())								-- V4.7 was MarkMapPane(51.500,-0.125,"Central London")
	end -- local function doMarkMapCity

	-- Refresh the Map Pane in chosen Map Mode
	local function setMapMode(intMapMode)						-- V4.3 -- V5.3
		intMapMode = tonumber(intMapMode) or IntMapMode
		IntMapMode = DicMapMode.LeafletMaps						-- 2 = Leaflet Maps
		CloseMapPane()												-- In case earlier instance is running
		OpenMapPane()													-- Uses WebBrowser OLEcontrol.com:Navigate
		SaveSettings()												-- Save sticky data settings
	end -- local function setMapMode

	setMapMode(IntMapMode) 											-- Update the Map Pane in current Map Mode	-- V4.3

	progbar.Setup(iup_gui.DialogueAttributes("Bars"))			-- Initialise progress bar

	-- GUI Geocode Location Plots Tab Functions --

	local function intLatLngIndex(strLoc)						-- Determine Location color status Index based on Lat/Longitude values
		local strMode = TblLocationData[strLoc].mod or ""
		local strLat  = TblLocationData[strLoc].lat or ""
		local strLng  = TblLocationData[strLoc].lng or ""
		local intLatIndex = IntStatusIndex(strLat..strMode,tblList.States)
		local intLngIndex = IntStatusIndex(strLng..strMode,tblList.States)
		if intLatIndex == intLngIndex then
			return intLatIndex
		else
			return IntPlotInvalid
		end
	end -- local function intLatLngIndex

	local function doSavePlotData(strOpt)						-- Optionally save current Location Standardized value and Lat/Long color Index
		if strOpt then
			strSubValue = TblLocationData[StrLocation].sub or ""
			intLocIndex = intLatLngIndex(StrLocation)
		end
	end -- local function doSavePlotData

	local function doShowPlotData(strOpt)						-- Calculate and display Geocoded Location statistics
		if strOpt then												-- If Standardized/Latitude/Longitude edit then delta adjust for current Location only
			if strSubValue ~= "" then intSubCount = intSubCount - 1 end
			local strSubValue = TblLocationData[StrLocation].sub or ""
			if strSubValue ~= "" then intSubCount = intSubCount + 1 end
			tblPlotCounts[intLocIndex] = tblPlotCounts[intLocIndex] - 1
			local intLocIndex = intLatLngIndex(StrLocation)
			tblPlotCounts[intLocIndex] = tblPlotCounts[intLocIndex] + 1
		else															-- Otherwise recalculate statistics for entire set of Locations
			intSubCount = 0
			for intLocIndex = 1, #tblList.States do
				tblPlotCounts[intLocIndex] = 0
			end
			for intLoc, strLoc in ipairs(tblList.tblLeaf) do
				strLoc = tblList.dicLeaf[strLoc] or strLoc		-- Cater for Reverse Display Order -- V4.9
				local strSub = TblLocationData[strLoc].sub or ""
				if strSub ~= "" then intSubCount = intSubCount + 1 end
				local intLocIndex = intLatLngIndex(strLoc)
				tblPlotCounts[intLocIndex] = tblPlotCounts[intLocIndex] + 1
			end
		end
		intNoData 	= tblPlotCounts[IntPlotNoData]
		intPlotted	= tblPlotCounts[IntPlotDefined]
		intTentative	= tblPlotCounts[IntPlotTentative]
		intNoPlot 	= tblPlotCounts[IntPlotNotFound] + tblPlotCounts[IntPlotBlocked]	-- V4.9
		intInvalid	= 0
		for intIndex = IntPlotInvalid, #tblList.States do
			intInvalid = intInvalid + tblPlotCounts[intIndex]
		end
		tblPlotCounts[IntPlotInvalid] = intInvalid
		intUnplotted	= intNoPlot + intInvalid + intNoData 
		lblPlotted.Title	= intPlotted.." Plotted"
		lblTentative.Title	= intTentative.." Tentative"
		lblNoPlot.Title		= intNoPlot.." "..StrPlotNoPlot
		lblInvalid.Title	= intInvalid.." Invalid"
		lblNoData.Title		= intNoData.." "..StrPlotNoData
		lblPlotTotal.Title	= #tblList.tblLeaf.." Total"
	end -- local function doShowPlotData

	local function doRefreshLocation(strLoc,valOpt,strMode)	-- Refresh the Location Entry details (valOpt = nil, true, false, "Sub", "Lat" or "Lng")
		local strSub = ""
		local strLat = ""
		local strLng = ""
		local strMod = ""
		local strGeo = ""											-- V5.3+
		if valOpt == nil then valOpt = true end
		if strLoc then
			local tblLoc = TblLocationData[strLoc]
			if not tblLoc then										-- Fix missing Location Entry due to iup.Tree node 254 char limit -- V4.6
				for strLocn, tblLocn in pairs (TblLocationData) do
					if strLocn:matches(strLoc) then				-- Should never be needed due to fix in strGetTreeLeaf() and doBuildTreeNodes() -- V4.6
						if StrLocation == strLoc then StrLocation = strLocn end
						strLoc = strLocn
						tblLoc = tblLocn
						break
					end
				end
			end
			if tblLoc then
				strSub = tblLoc.sub
				strLat = tblLoc.lat
				strLng = tblLoc.lng
				strMod = tblLoc.mod or ""
				strGeo = tblLoc.geo or ""							-- V5.3+
				ShowStatusMessage(iup_gui.Safe,"OK  "..strGeo)
			else
				MakeNewLocationData(strLoc)						-- Fix missing Location Entry should never happen -- V4.6
			end
		else
			strLoc = StrClickHere
		end
		btnLocation.Title = strLoc:gsub("&","&&")				-- Duplicate ampersands "&" so they appear in button title
		if valOpt ~= "Sub" then
			txtStandard.Value = strSub
		end
		if valOpt == "Lat" then
			TxtLatitude.FgColor = iup_gui[TblStatusColour[IntStatusIndex(TxtLatitude.Value..strMod,TblPlotStates)]]
		else
			RefreshValue(TxtLatitude, strLat, strMod)
		end
		if valOpt == "Lng" then
			TxtLongitude.FgColor = iup_gui[TblStatusColour[IntStatusIndex(TxtLongitude.Value..strMod,TblPlotStates)]]
		else
			RefreshValue(TxtLongitude, strLng, strMod)
		end
		local strColor = TxtLatitude.FgColor
		if strColor ~= TxtLongitude.FgColor then
			strColor = iup_gui[TblStatusColour[IntPlotInvalid]]
		end
		btnLocation.FgColor   = strColor
		txtStandard.FgColor = strColor
		doColorLeafNode(intLocation,strColor)
		if valOpt then
			doSelectLeafNode(intLocation)							-- Optionally select Entry in list and Mark on Map pane
			if StrLocation then
				if valOpt == true then
					strLocation	= "?"
					strLatitude	= "?"
					strLongitude	= "?"
				end
				if IntTabPosn <= IntTabGeoLoc then				-- V4.2
--?					ZoomMapPane(IntLocZoom)							-- V4.3 needed for Leaflet?
					doMarkMapPanePlot(strMode)						-- V4.3 strMode
				end
			else
				if IntTabPosn == IntTabPrefer then 
					doMarkMapCity()									-- V4.3
				else
					doShowWholeWorld()								-- V4.3
				end
			end
		end	
	end -- local function doRefreshLocation

	local function doGeoLocTabShared(strOpt,strMode)			-- Geocode Location Plots tab actions before & after button functions 
		local strGeo = ""											-- V5.3+
		if StrLocation
		and TblLocationData
		and TblLocationData[StrLocation] then 					-- Save current Location entry to database -- V3.6
			TblLocationData[StrLocation].sub = txtStandard.Value
			TblLocationData[StrLocation].lat = TxtLatitude.Value
			TblLocationData[StrLocation].lng = TxtLongitude.Value
			SaveLocationsData(StrLocation)
			doRefreshLocation(StrLocation,strOpt or "Loc",strMode)
			strGeo = TblLocationData[StrLocation].geo or ""	-- V5.3+
		end
		if not strOpt and IntStorage == IntStoreFILE then SaveLocationsFile() end
		return strGeo													-- V5.3+
	end -- local function doGeoLocTabShared

	local function doGeoLocTabPrefix(strOpt,strMode)			-- Geocode Location Plots tab actions before button functions 
		timMapPane.Run = "NO"										-- V4.3
		deActivateControls( { allControls; tabControls; btnClearAll; btnClearSub; btnClearLatLng; btnSetBlocked; vboxPlotEntry; btnLocation; btnMoveNorth; btnMoveSouth; btnMoveEast; btnMoveWest; btnNextPlot; btnNextDefined; btnNextSubs; btnNextUnplot; btnPlotThis; btnPlotSome; btnPlotAll; } )	-- V3.5 tabControls added -- V4.9
		if strOpt ~= "Lat" then TxtLatitude.Active  = "NO" end
		if strOpt ~= "Lng" then TxtLongitude.Active = "NO" end
		local strGeo = doGeoLocTabShared(strOpt,strMode)		-- V5.3+
		ShowStatusMessage(iup_gui.Safe,"OK  "..strGeo)			-- V5.3+
		doSavePlotData(strOpt)
	end -- local function doGeoLocTabPrefix

	local function doGeoLocTabSuffix(strOpt,strMode)			-- Geocode Location Plots tab actions after button functions 
		doGeoLocTabShared(strOpt,strMode)							-- V4.3 strMode
		doShowPlotData(strOpt)										-- Show the Geocoded Location statistics
		allControls.Active = "YES"
		tabControls.Active = "YES"
		if TxtLatitude.FgColor == TxtLongitude.FgColor then
			if intPlotted > 0 then									-- Enable controls for Plotted entries -- V3.5
				btnNextPlot.Active		= "YES"
			end
			if intTentative > 0 then								-- Enable controls for Defined entries
				btnNextDefined.Active	= "YES"
			end
			if intSubCount > 0 then									-- Enable controls for Standardized entries
				btnNextSubs.Active		= "YES"
			end
			if intUnplotted > 0 then								-- Enable controls for Failed/Invalid/NoData entries
				btnNextUnplot.Active	= "YES"
			end
			if TxtLongitude.Value ~= ""
			and TxtLatitude.Value ~= "" then						-- V4.9
				btnClearLatLng.Active	= "YES"
			end			
			if TxtLongitude.Value ~= StrPlotBlocked
			or TxtLatitude.Value  ~= StrPlotBlocked then		-- V4.9
				btnSetBlocked.Active	= "YES"
			end
			if TxtLatitude.FgColor == iup_gui[TblStatusColour[IntPlotTentative]]
			or TxtLatitude.FgColor == iup_gui[TblStatusColour[IntPlotDefined]] then
				btnMoveNorth.Active		= "YES"					-- Enable nudge North & South controls
				btnMoveSouth.Active		= "YES"
			end
			if TxtLongitude.FgColor == iup_gui[TblStatusColour[IntPlotTentative]]
			or TxtLongitude.FgColor == iup_gui[TblStatusColour[IntPlotDefined]] then
				btnMoveEast.Active		= "YES"					-- Enable nudge East & West controls
				btnMoveWest.Active		= "YES"
			end
			btnLocation.Active		= "YES"
		else
			local strColor = iup_gui[TblStatusColour[IntPlotInvalid]]
			ShowStatusMessage(strColor,"Erroneous Latitude/Longitude values")
			btnLocation.FgColor		= strColor
			txtStandard.FgColor	= strColor
			doColorLeafNode(intLocation,strColor)
		end
		if StrLocation then 										-- Enable current Location entry controls
			vboxPlotEntry.Active	= "YES"
			txtStandard.Active		= "YES"
			TxtLatitude.Active		= "YES"
			TxtLongitude.Active		= "YES"
			if txtStandard.Value  ~= ""
			or TxtLongitude.Value ~= ""
			or TxtLatitude.Value  ~= "" then						-- V4.9
				btnClearAll.Active = "YES"
			end
			if txtStandard.Value	~= "" then					-- V4.9
				btnClearSub.Active	= "YES"
			end
			if IntGeoPlot ~= DicGeoPlot.Disabled then			-- Conditional -- V4.3 -- V5.3
				btnPlotThis.Active	= "YES"
			end
		end
		if IntGeoPlot ~= DicGeoPlot.Disabled then				-- Conditional -- V4.3 -- V5.3
			btnPlotAll.Active		= "YES"
			btnPlotSome.Active		= "YES"
		end
		timMapPane.Run = "YES" 										-- V4.3
	end -- local function doGeoLocTabSuffix

	local function doReadMapPane()								-- Read marker Location or map Zoom level from Map pane	-- V4.3
		ReadMapPane()
		SaveSettings()												-- Save sticky data settings	-- V5.3
		if IntTabPosn == IntTabGeoLoc then
			if IntMapLat and IntMapLng then						-- Update dragging Marker Location as Defined
				if StrLocation then									-- V6.0
					setLatitudeLongitude(IntMapLat,IntMapLng,StrLocation,"Defined")
					local strColor = TxtLatitude.FgColor
					btnLocation.FgColor   = strColor
					txtStandard.FgColor = strColor
					TblLocationData[StrLocation].mod = "Defined"
					if StrMapAct == "End" then						-- Update dragged existing Marker
						doGeoLocTabPrefix()
						doGeoLocTabSuffix() 						-- With or without 'DROP' ?
--?						doGeoLocTabPrefix(nil,"Move")
--?						doGeoLocTabSuffix(nil,"Move")
					elseif StrMapAct == "New" then				-- Create new plot Location Marker
						doGeoLocTabPrefix()
						doGeoLocTabSuffix()
					end
				end
				IntMapLat = false
				IntMapLng = false
			end
		elseif IfGotZoom then
			if IntTabPosn == IntTabPrefer then
				if intPreferTab == IntTabGeoLoc then
					IntLocZoom = IntGetZoom							-- Update Location Zoom
					txtLocZoom.Value = IntLocZoom
				end
				if intPreferTab == IntTabCreate then
					IntMapZoom = IntGetZoom							-- Update Web Page Zoom
					txtMapZoom.Value = IntMapZoom
				end
			end
			IfGotZoom = false
		end
	end -- local function doReadMapPane

	local function doBuildLocationsList(isDelete)				-- Build the Locations list from Database and Gedcom
		doSelectLeafNode(0)
		ShowStatusMessage(iup_gui.Warn,"Loading Location list, please wait...")
		local intFromTag = math.min(IntFromTag,IntFromBOTH)
		local strFromTag = TblFromTag[intFromTag]
		StrFromTag = strFromTag
		tblList = tblTree[intFromTag]
		TblLocationData = TblLocationCode[strFromTag]
		if next(TblLocationData) == nil or not tblList.Tree.Count or IsFromNew then
			IsFromNew = false
			local ptrRecord = fhNewItemPtr()						-- Either the Database is empty or GUI tree is empty or new Address ~ Place variant
			local tblLocations = {}
			for _, strRecordType in ipairs({"INDI";"FAM";}) do
				ptrRecord:MoveToFirstRecord(strRecordType)
				while ptrRecord:IsNotNull() do					-- Search all Individual & Family Records for Facts
					local ptrFact = fhNewItemPtr()
					ptrFact:MoveToFirstChildItem(ptrRecord)
					while ptrFact:IsNotNull() do
						if fhIsFact(ptrFact) or IsDataTag(ptrFact) then			-- Search all Facts for "ADDR" &/or "PLAC" Locations
							local strLocName, strLocFull, strLoc_Name, strLoc_Full = StrMakeLocation(ptrFact)		--_ 19 June 2013
							if strLocFull ~= ""											-- Ignore empty "Address ~ Place" variant Location
							and intPreferTab == IntTabGeoLoc then					-- Ignore unless on Location Plot Options tab
								strLocFull = StrControls2SpaceNew(strLocFull)		-- New format with newline = bell blob
								if not tblLocations[strLocFull] then					-- Log every "ADDR ~ PLAC" Location found
									tblLocations[strLocFull] = true
								end
								local strAddr = strLocFull:gsub(" ~ .*"," ~ ")
								if not tblLocations[strAddr] then						-- Log every "ADDR ~ " Location found
									tblLocations[strAddr] = true
								end
								local strPlac = strLocFull:gsub(".- ~ "," ~ ")
								if not tblLocations[strPlac] then						-- Log every " ~ PLAC" Location found
									tblLocations[strPlac] = true
								end
							end

							-- New for _PLAC tag 19 June 2013
							if strLoc_Full ~= ""										-- Ignore empty " ~ _Place" variant Location
							and intPreferTab == IntTabGeoLoc then					-- Ignore unless on Location Plot Options tab
								strLoc_Full = StrControls2SpaceNew(strLoc_Full)	-- New format with newline = bell blob
								local strPlac = " ~ "..strLoc_Full
								if not tblLocations[strPlac] then						-- Log every " ~ _PLAC" Location found
									tblLocations[strPlac] = true
								end
							end

							if strLocName ~= "" then									-- Ignore empty Location
								strLocName = StrControls2SpaceNew(strLocName)		-- New format with newline = bell blob
								if not tblLocations[strLocName] then					-- Log every Location found
									tblLocations[strLocName] = true
								end
								if not TblLocationData[strLocName] then				-- Add new Location to data
									MakeNewLocationData(strLocName)					-- V3.5
									SaveLocationsData(strLocName)
								end
								TblLocationData[strLocName].use = true				-- Location is chosen for use in GUI tree
							end

							-- New for _PLAC tag 19 June 2013
							if strLoc_Name ~= "" then									-- Ignore empty Location
								strLoc_Name = StrControls2SpaceNew(strLoc_Name)	-- New format with newline = bell blob
								if not tblLocations[strLoc_Name] then				-- Log every Location found
									tblLocations[strLoc_Name] = true
								end
								if not TblLocationData[strLoc_Name] then			-- Add new Location to data
									MakeNewLocationData(strLoc_Name)
									SaveLocationsData(strLoc_Name)
								end
								TblLocationData[strLoc_Name].use = true				-- Location is chosen for use in GUI tree
							end

						end
						ptrFact:MoveNext()
					end
					ptrRecord:MoveNext()
				end
			end
			for strLocName, tblLocData in pairs(TblLocationData) do
				if not tblLocations[strLocName] then								-- Delete old Location from data, except for Place Records
					if IntStorage == IntStorePLAC then							-- FH6
						if TblLocationData[strLocName].plc then					-- V3.5 needed to remove old Place Record names
							TblLocationData[strLocName].use = true
						else
							TblLocationData[strLocName] = nil						-- tblLocData = nil does not always work
						end
					else
						if IntFromTag < IntFromBOTH or isDelete then			-- V3.5 only delete Address only and Place only
							if IntStorage ~= IntStoreFILE then DeleteLocationRecord(tblLocData.rec) end
							TblLocationData[strLocName] = nil						-- tblLocData = nil does not always work
						else
							TblLocationData[strLocName].use = nil				-- V3.5 keep Address ~ Place but don't use
						end
					end
				end
			end
			intLocation = doBuildTreeNodes(TblLocationData,StrLocation)
--?			if IntStorage == IntStoreFILE then SaveLocationsFile() end		-- Always maintain a database file as backup ?
		end
		ShowStatusMessage(iup_gui.Safe,"OK")
		if intLocation == 0 then
			intLocation = tonumber(tblList.Tree.Value)
			StrLocation = strGetTreeLeaf(tblList.Tree,intLocation)
		end
		strLeaf = StrLocation 
		doRefreshLocation(StrLocation)
	end -- local function doBuildLocationsList

	local function doGeoLocTabSettings()							-- Initial settings for Geocode Location Plots tab 
		strLocation	= "?"
		strLatitude	= "?"
		strLongitude	= "?"
		ZoomMapPane(IntLocZoom)
		doGeoLocTabPrefix()
		doBuildLocationsList()
		doGeoLocTabSuffix()
	end -- local function doGeoLocTabSettings

	local function doGeocodePlot(intRange,isConfirm,strClass,isColorOK) 	-- Plot Range of Locations that match Class Color
		doGeoLocTabPrefix()
		local isPlottingOK = true
		if intRange > 9 then isPlottingOK = doShowWarnings("Plotting Locations") end
		if isPlottingOK then
			local intLoc = intLocation								-- Remember current selected Location
			local intStart = intLoc
			TxtLatitude.Active	= "YES"							-- Allow Lat/Long to appear in colour
			TxtLongitude.Active	= "YES"
			dialogMain.Active	= "NO"
			local isGeocode = true
			local isBarStop = false
			if not isConfirm then
				progbar.Start("Plotting Locations",intRange)
				progbar.Message("Plotting Locations ...")
			end
			local intMax = tonumber(tblList.Tree.Count)			-- Cannot traverse tblList.tblLeaf instead, while doRefreshLocation needs GUI tree ident
			if intLoc == 0 then intLoc = 1 end
			for intCount = 0, intMax do							-- Loop once more than number of Locations to return to start Location
				if intLoc > intMax then intLoc = 1 end
				if intLoc < 1 then intLoc = intMax end
				local strLoc, strColor = strGetTreeLeaf(tblList.Tree,intLoc)
				if strLoc then
					if isColorOK(strColor,strClass)				-- Is Location entry Color in correct Class and is not Blocked/No Auto-Geocode	-- V4.9
					and TblLocationData[strLoc].lat ~= StrPlotBlocked
					and TblLocationData[strLoc].lng ~= StrPlotBlocked then
						setLocation(strLoc,intLoc)					-- V4.3
						doRefreshLocation(strLoc,isConfirm)		-- Show the Geocoding Location details before plot
						if intCount == 0
						or intLocation ~= intStart then			-- Always include first Location, but skip when start Location reappears
							if isConfirm then
								isConfirm, isGeocode = GUI_SkipDialogue(strClass)
							end
							intRange = intRange - 1
							if isGeocode then
								doSavePlotData("Loc")
								if IntStorage ~= IntStorePLAC then			-- FH6
									TblLocationData[StrLocation].mod = ""	-- Cancel any Defined Mode
								end
								local strAns = StrGeocodeLocation(StrLocation)
								doRefreshLocation(StrLocation,isConfirm)	-- Show the Geocoding Location details after plot
								doShowPlotData("Loc")
								if strAns == StrPlotAborted then break end 	-- V4.2
								if isConfirm then
									fhSleep(1500,100)
								elseif intRange > 1 then
									progbar.Start("Plotting Locations",intRange)
									progbar.Message("Plotting Locations ...")
								end 
							elseif not isConfirm then
								break
							end
							progbar.Step(1)
							collectgarbage()							-- Memory garbage collection to avoid 'Not responding' -- V4.9
						end
					end
					isBarStop = progbar.Stop()
					if isBarStop then break end
				end
				if isBarStop then break end
				intLoc = intLoc + 1
			end
			TxtLatitude.Active	= "NO"
			TxtLongitude.Active	= "NO"
			doRefreshLocation(StrLocation)
			progbar.Close()
			dialogMain.Active = "YES"
			dialogMain.BringFront = "YES"
		end
		doGeoLocTabSuffix()
	end -- local function doGeocodePlot

	function btnPlotAll:action()									-- Action for Geocode Plot All Locations button
		local intMaximum = #tblList.tblLeaf
		local strExclude = " "										-- Exclude no Locations
		if intPlotted > 0 then
			if GUI_WarnDialogue("Geocode Plot All to include Plotted Locations?","Do you wish to include the "..intPlotted.." Plotted Locations in this Geocode Plot All?") ~= "Yes" then
				intMaximum = intMaximum - intPlotted
				strExclude = iup_gui[TblStatusColour[IntPlotDefined]]	-- Exclude only non-Tentative full Plots
			end
		end
		doGeocodePlot( intMaximum, false, strExclude, function(strColor,strExclude) return strColor ~= strExclude end )
	end -- function btnPlotAll:action

	function btnPlotSome:action()									-- Action for Geocode Plot Some Locations button
		local intSome, isConfirm = GUI_PlotDialogue(tblPlotCounts)
		if intSome > 0 then doGeocodePlot( tblPlotCounts[intSome], isConfirm, iup_gui[TblStatusColour[intSome]], function(strColor,strInclude) return strColor == strInclude end ) end
	end -- function btnPlotSome:action

	function btnPlotThis:action()									-- Action for Geocode Plot This Location button
		doGeoLocTabPrefix()
		if StrLocation then
			TblLocationData[StrLocation].mod = ""
			StrGeocodeLocation(StrLocation)
			doRefreshLocation(StrLocation)
		end
		doGeoLocTabSuffix()
	end -- function btnPlotThis:action

	function btnLocation:action()									-- Action for Location button
		doGeoLocTabPrefix()
		strLeaf = nil
		doShowTreeDialogue(btnLocation)
		if strLeaf then
			setLocation(strLeaf,intLeaf)							-- V4.3
			doRefreshLocation(StrLocation)
		end
		doGeoLocTabSuffix()
	end -- function btnLocation:action

	function btnLocToSub:action()									-- Action for Copy Loc to Sub button
		doGeoLocTabPrefix()
		txtStandard.Value = StrLocation							-- Why reset non-tentative lat/long?	Removed	-- V5.4
--!		if TxtLatitude.FgColor  ~= iup_gui[TblStatusColour[IntPlotTentative]] then RefreshValue(TxtLatitude, "") end
--!		if TxtLongitude.FgColor ~= iup_gui[TblStatusColour[IntPlotTentative]] then RefreshValue(TxtLongitude,"") end
--!		TblLocationData[StrLocation].mod = ""
		doGeoLocTabSuffix()
	end -- function btnLocToSub:action

	function txtStandard:action(strChar,strValue)				-- Action when Standardized text is edited
		doGeoLocTabPrefix("Sub")
		TxtLatitude.Value = ""
		TxtLongitude.Value = ""
		TblLocationData[StrLocation].mod = ""
		if strChar == 0 and strValue == txtStandard.Value then
			doGeoLocTabSuffix("Sub")								-- Delete nothing at left or right special case
			strChar = iup.IGNORE
		end
		return strChar
	end -- function txtStandard:action

	function txtStandard:valuechanged_cb()						-- Call back after Standardized text is edited
		doGeoLocTabSuffix("Sub")
	end -- function txtStandard:valuechanged_cb

	local function doLimitValue(txtLatLong)						-- Limit value to 7 decimal places, else pan & zoom fails	-- V6.0
		local intCaret = tonumber(txtLatLong.Caret)
		local fltValue = tonumber(txtLatLong.Value)
		if fltValue and #txtLatLong.Value > 10 then
			if fltValue < 0 then
				fltValue = math.ceil(fltValue*10000000)
			else
				fltValue = math.floor(fltValue*10000000)
			end
			txtLatLong.Value = tostring(fltValue/10000000)
			txtLatLong.Caret = math.min(intCaret,#txtLatLong.Value+1)
		end
	end -- local function doLimitValue

	function TxtLatitude:action(intChar,strValue)				-- Action when Latitude text is edited
		doGeoLocTabPrefix("Lat")
		local fltValue = tonumber(strValue)
		if fltValue then
			if fltValue < -90 or fltValue > 90 then
				doGeoLocTabSuffix("Lat")
				return iup.IGNORE
			end
		end
		TblLocationData[StrLocation].mod = "Defined"
		if intChar == 0 and strValue == TxtLatitude.Value
		or not strValue:match("^%-?%d*%.?%d*$") then			-- Check if optional minus, digits, dot, digits	-- V6.0
			strLatitude = "?"
			doGeoLocTabSuffix("Lat")								-- Delete nothing at left or right special case
			return iup.IGNORE
		end
		return intChar
	end -- function TxtLatitude:action

	function TxtLatitude:valuechanged_cb()						-- Call back after Latitude text is edited
		doLimitValue(TxtLatitude)
		doGeoLocTabSuffix("Lat")
	end -- function TxtLatitude:valuechanged_cb

	function TxtLongitude:action(intChar,strValue)				-- Action when Longitude text is edited
		doGeoLocTabPrefix("Lng")
		local fltValue = tonumber(strValue)
		if fltValue then
			if fltValue < -180 or fltValue > 180 then
				doGeoLocTabSuffix("Lng")
				return iup.IGNORE
			end
		end
		TblLocationData[StrLocation].mod = "Defined"
		if intChar == 0 and strValue == TxtLongitude.Value
		or not strValue:match("^%-?%d*%.?%d*$") then			-- Check if optional minus, digits, dot, digits	-- V6.0
			strLongitude = "?"
			doGeoLocTabSuffix("Lng")								-- Delete nothing at left or right special case
			return iup.IGNORE
		end
		return intChar
	end -- function TxtLongitude:action

	function TxtLongitude:valuechanged_cb()						-- Call back after Longitude text is edited
		doLimitValue(TxtLongitude)
		doGeoLocTabSuffix("Lng")
	end -- function TxtLongitude:valuechanged_cb

	function btnClearAll:action()									-- Action for Clear Entry button
		doGeoLocTabPrefix()
		txtStandard.Value	= ""
		TxtLatitude.Value	= ""
		TxtLongitude.Value	= ""
		TblLocationData[StrLocation].mod = ""
		TblLocationData[StrLocation].geo = ""					-- V5.3+	
		doGeoLocTabSuffix()
	end -- function btnClearAll:action

	function btnClearSub:action()									-- Action for Clear Standardized button
		doGeoLocTabPrefix()
		txtStandard.Value = ""
		doGeoLocTabSuffix()
	end -- function btnClearSub:action

	function btnClearLatLng:action()								-- Action for Clear Lat/Longitude button	-- V4.9
		doGeoLocTabPrefix()
		TxtLatitude.Value	= ""
		TxtLongitude.Value	= ""
		TblLocationData[StrLocation].mod = ""
		TblLocationData[StrLocation].geo = ""					-- V5.3+	
		doGeoLocTabSuffix()
	end -- function btnClearLatLng:action

	function btnSetBlocked:action()								-- Action for Set Blocked button	-- V4.9
		doGeoLocTabPrefix()
		TxtLatitude.Value	= StrPlotBlocked
		TxtLongitude.Value	= StrPlotBlocked
		TblLocationData[StrLocation].mod = ""
		TblLocationData[StrLocation].geo = ""					-- V5.3+	
		doGeoLocTabSuffix()
	end -- function btnSetBlocked:action

	local function doMovePlot(control,intSign,strOpt)			-- Move Latitude/Longitude plot
		doGeoLocTabPrefix(strOpt)
		doReadMapPane()
		TblLocationData[StrLocation].mod = "Defined"
		TblLocationData[StrLocation].geo = ""					-- V5.3+	
		RefreshValue(control,string.format("%4.7f",control.Value + ( 32 / 2 ^ IntGetZoom ) * intSign),"Defined")
		doGeoLocTabSuffix(strOpt,"Move")
	end -- local function doMovePlot

	function btnMoveNorth:action()								-- Action for move N button
		doMovePlot(TxtLatitude,1,"Lat")
		if tonumber(TxtLatitude.Value) > 90 then RefreshValue(TxtLatitude,90,"Defined") end
	end

	function btnMoveSouth:action()								-- Action for move S button
		doMovePlot(TxtLatitude,-1,"Lat")
		if tonumber(TxtLatitude.Value) < -90 then RefreshValue(TxtLatitude,-90,"Defined") end
	end

	function btnMoveEast:action()									-- Action for move E button
		doMovePlot(TxtLongitude,1,"Lng")
		if tonumber(TxtLongitude.Value) > 180 then RefreshValue(TxtLongitude,TxtLongitude.Value-360,"Defined") end
	end

	function btnMoveWest:action()									-- Action for move W button
		doMovePlot(TxtLongitude,-1,"Lng")
		if tonumber(TxtLongitude.Value) < -180 then RefreshValue(TxtLongitude,TxtLongitude.Value+360,"Defined") end
	end

	local function doNextLocation(isCondition)					-- Find Next/Prior Location that matches Condition
		doGeoLocTabPrefix()
		local intCount = 0
		local intMax = tonumber(tblList.Tree.Count)
		local intLoc = intLocation
		local strLoc = ""
		local tblLoc = {}
		local intWay = 1
		strKeyState = iup.GetGlobal("MODKEYSTATE")				-- State of Shft, Ctrl, Alt, sYs keys in format of 4 chars "SCAY" or space if not pressed
		if strKeyState:match("A") then intWay = -1 end			-- Alt key reverses direction
		repeat
			intCount = intCount + 1
			intLoc = intLoc + intWay
			if intLoc > intMax then intLoc = 1 end
			if intLoc < 1 then intLoc = intMax end
			strLoc = strGetTreeLeaf(tblList.Tree,intLoc)
			if strLoc then
				tblLoc = TblLocationData[strLoc]
				if isCondition(tblLoc,intLoc) then
					setLocation(strLoc,intLoc)						-- V4.3
					doRefreshLocation(StrLocation)
					doGeoLocTabSuffix()
					return
				end
			end
		until intCount == intMax
		doGeoLocTabSuffix()
	end -- local function doNextLocation

	function btnNextPlot:action()									-- Action for Next Plotted entry button -- V3.5 only traverses 'Plotted' and not 'Defined' locations
		doNextLocation( function(tblLoc,intLoc) local strLoc, strColor = strGetTreeLeaf(tblList.Tree,intLoc) return strColor == iup_gui[TblStatusColour[IntPlotDefined]] end )	-- V3.5
	end

	function btnNextDefined:action()								-- Action for Next Defined entry button
		doNextLocation( function(tblLoc,intLoc) local strLoc, strColor = strGetTreeLeaf(tblList.Tree,intLoc) return strColor == iup_gui[TblStatusColour[IntPlotTentative]] end )
	end

	function btnNextSubs:action()									-- Action for Next Standardized entry button
		doNextLocation( function(tblLoc,intLoc) return ( tblLoc.sub or "" ) ~= "" end )
	end

	function btnNextUnplot:action()								-- Action for Next Unplotted entry button
		doNextLocation( function(tblLoc,intLoc) return not IsPlotted(tblLoc) end )
	end

	-- GUI Create Web Page Maps Tab Functions --

	local	tblIndiNames = {}
	local	strIndiName	=	nil										-- Selected entry from Individual dropdown list
	local	intIndiName	=	0
	local	intNoMap		=	0										-- Count of Individuals without Web Page Maps
	local	intMapped		=	0										-- Count of Individuals with Web Page Maps
	local	intOmitted	=	0										-- Count of Individuals with no Facts with Locations
	local	intFiltered	=	0										-- Count of Individuals excluded by Privacy Filter
	local	intUnmapped	=	0										-- Count sum of all Individuals with no Web Page Map
	local	tblMapCounts	= { 0; 0; 0; 0; 0; }

	local function doListMapData()								-- Show the Mapped/Filtered/Omitted statistics or Folder status
		intNoMap		=	tblMapCounts[IntMapNoMap]
		intMapped		=	tblMapCounts[IntMapFilename]
		intOmitted	=	tblMapCounts[IntMapOmitted]
		intFiltered	=	tblMapCounts[IntMapFiltered]
		intUnmapped	=	intOmitted + intFiltered + intNoMap
		lblMapped.Title		= intMapped.." Mapped"
		lblOmitted.Title	= intOmitted.." "..StrMapOmitted
		lblFiltered.Title	= intFiltered.." "..StrMapFiltered
		lblNoMap.Title		= intNoMap.." "..StrMapNoMap
		lblMapTotal.Title	= #tblList.tblLeaf.." Total"
	end -- local function doListMapData

	local function doPresentMap(strMapName)						-- Display map of Individual plot Markers
		local strMapFile = StrFolder.."\\"..strMapName
		if general.FlgFileExists(strMapFile) then
			local intBeg
			local intEnd
			local strOne
			local strTwo
			local intLatitude	= nil
			local intLongitude	= nil
			local strLocName	= nil									-- V4.2
			local strCaption	= nil
			local strInfoWin	= nil
			local intPlot		= 0
			local strFile = general.StrLoadFromFile(strMapFile)
			for strLine in strFile:gmatch("(.-)[\n\r]+") do	-- Check map page title refers to correct Individual	-- V5.1
				intBeg,intEnd,strOne = strLine:find("<title>Map of Locations for (.*)</title>$")
				if not strOne then
					intBeg,intEnd,strOne = strLine:find(" <title>.-&nbsp;(.*)</title>$")	-- V5.1 -- Cater for translated "Map of Locations for"
				end
				if strOne then
					if strOne == encoder.StrEncode_XML(fhGetDisplayText(tblIndiNames[strIndiName].ptr)) then break end	-- StrANSI_XML() added in V3.3 on 06 June 2013 -- StrEncode_XML V3.5
					ShowStatusMessage(iup_gui.Risk,strMapName.." for "..strOne:gsub("&","&&"))
					doShowWholeWorld()								-- V4.3
					return
				end
			end
			NullMapPane()
			ShowStatusMessage(iup_gui.Warn,"Mapping Plot Markers, please wait...")
			local doSeek = false
			for strLine in strFile:gmatch("(.-)[\n\r]+") do	-- V5.1
				if not doSeek then									-- Skip header of Map file	-- V5.1
					doSeek = strLine:match("function Initialise")
				else
					intBeg,intEnd,strOne,strTwo = strLine:find("LatLong%[[0-9]-%] = [Nn]ew.-LatLo?ng%((.-),(.-)%);")	-- Match "NewLatLong(" and legacy "new google.maps.LatLng("	-- V4.3
					if strOne then intLatitude  = tonumber(strOne) end
					if strTwo then intLongitude = tonumber(strTwo) end
					intBeg,intEnd,strOne = strLine:find("LocName%[[0-9]-%] = '(.*)';")			-- V4.2
					if strOne then strLocName = strOne end
					intBeg,intEnd,strOne = strLine:find("Caption%[[0-9]-%] = '(.*)';")
					if strOne then strCaption = strOne end
					intBeg,intEnd,strOne = strLine:find("InfoWin%[[0-9]-%] = '(.*)';")
					if strOne then strInfoWin = strOne end
					strLocName = strLocName or strCaption			-- V4.2 while upgrading
					if intLatitude and intLongitude and strLocName and strCaption and strInfoWin then
						strLocName = strLocName:gsub("\\\\","\\")	-- Unescape a backslash character	-- V4.2
						strCaption = strCaption:gsub("\\\\","\\")	-- Unescape a backslash character	-- V4.2
						strCaption = strCaption:gsub("\\'","'")		-- Unescape apostrophe character	-- V4.2
						PlotMapPane(intLatitude,intLongitude,strLocName,strCaption,strInfoWin)		-- V4.2
						intLatitude	= nil
						intLongitude	= nil
						strLocName	= nil							-- V4.2
						strCaption	= nil
						strInfoWin	= nil
						intPlot = intPlot + 1
					end
					if strLine:find("InitialMap") or intPlot > 30 then break end -- Skip tail of Map file
				end
			end
			ShowStatusMessage(iup_gui.Safe,"OK")
			DropMapPane()
		end
	end  -- local function doPresentMap

	local function doRefreshIndiName(strIndiName,intStep)		-- Refresh the current IndiName Entry details and optionally statistics
		local strColor = iup_gui[TblStatusColour[IntMapNoMap]]
		if strIndiName then
			local strMapState = tblIndiNames[strIndiName].map
			local intIndex = IntStatusIndex(strMapState,TblMapStates)
			strColor = iup_gui[TblStatusColour[intIndex]]
			doColorLeafNode(intIndiName,strColor)
			btnIndiName.Title   = strIndiName:gsub("&","&&")	-- Duplicate ampersands "&" so they appear in button title
			btnIndiName.FgColor = strColor
			txtMapState.Value   = strMapState
			txtMapState.FgColor = strColor
			if not intStep then
				intStep = 0											-- Do not update the statistics
				doSelectLeafNode(intIndiName)						-- Select leaf node in GUI tree list
				if IntTabPosn == IntTabCreate then
					if intIndex == IntMapFilename then
						doPresentMap(strMapState)
					else
						doShowWholeWorld()							-- V4.3
					end
				end
			end
			tblMapCounts[intIndex] = tblMapCounts[intIndex] + intStep -- Optionally update Mapped/Filtered/Omitted statistics
		else
			btnIndiName.Title	= StrClickHere
			btnIndiName.FgColor	= strColor
			txtMapState.Value	= ""
			txtMapState.FgColor	= strColor
		end
	end -- local function doRefreshIndiName

	local function doShowFolderStatus()							-- Show status of Map Output Folder
		if general.FlgFolderExists(StrFolder) then
			ShowStatusMessage(iup_gui.Safe,"OK")
		else
			ShowStatusMessage(iup_gui.Risk,"Output Folder does not exist")
		end
	end -- local function doShowFolderStatus

	local function doCreateTabPrefix()							-- Create Web Page Maps tab actions before button functions 
		deActivateControls( { allControls; tabControls; btnPrevMapped; btnNextMapped; btnPrevUnmap; btnNextUnmap; btnDeleteMap; btnMapThis; btnMapAll; btnMapSome; vboxCreate; } )
		doShowFolderStatus()
	end -- local function doCreateTabPrefix

	local function doCreateTabSuffix()							-- Create Web Page Maps tab actions after button functions 

		for i = 1, #tblList.States do								-- Calculate and show the Mapped/Filtered/Omitted statistics
			tblMapCounts[i] = 0
		end
		for _, strIndiName in ipairs(tblList.tblLeaf) do
			local intIndex = IntStatusIndex(tblIndiNames[strIndiName][tblList.Status],tblList.States)
			tblMapCounts[intIndex] = tblMapCounts[intIndex] + 1
		end
		doListMapData()

		allControls.Active = "YES"
		tabControls.Active = "YES"
		if intMapped > 0 then										-- Enable controls for Mapped entries
			btnPrevMapped.Active	= "YES"	
			btnNextMapped.Active	= "YES"	
		end
		if intUnmapped > 0 then 									-- Enable controls for Omitted/Filtered/NoMap entries
			btnPrevUnmap.Active	= "YES"	
			btnNextUnmap.Active	= "YES"	
		end
		if general.FlgFolderExists(StrFolder) then				-- Enable controls for Delete/Map entries
			btnMapSome.Active	= "YES"
			btnMapAll.Active	= "YES"
			if txtMapState.FgColor == iup_gui[TblStatusColour[IntMapFilename]] then
				btnDeleteMap.Active = "YES"
			end
			if intIndiName > 0 then
				btnMapThis.Active	= "YES"
			end
		end
		vboxCreate.Active		= "YES"
		btnIndiName.Active		= "YES"
	end -- local function doCreateTabSuffix

	local function strMakeIndiName(ptrRecord)					-- Make record pointer into Individual list Name
		local strIndiName	= fhGetItemText(ptrRecord,"~.NAME:SURNAME_FIRST")
		local strRecordId	= fhGetRecordId(ptrRecord)
		if strIndiName:match("^[ ,]*$") then
			strIndiName = "[unnamed person]"						-- V3.5
		end
		return strIndiName.." ["..strRecordId.."]"
	end -- local function strMakeIndiName

	local function doBuildIndiNamesList()						-- Build the IndiNames list from Gedcom
		ShowStatusMessage(iup_gui.Warn,"Loading Individuals list, please wait...")
		tblList = tblTree[IntFromINDI]
		if next(tblIndiNames) == nil then
			local ptrRecord = fhNewItemPtr()
			ptrRecord:MoveToFirstRecord("INDI")
			while ptrRecord:IsNotNull() do
				local strIndiName = strMakeIndiName(ptrRecord)
				tblIndiNames[strIndiName] = {}
				tblIndiNames[strIndiName].ptr = ptrRecord:Clone()
				tblIndiNames[strIndiName].map = ""
				tblIndiNames[strIndiName].use = true				-- Individual is chosen for use
				ptrRecord:MoveNext("SAME_TAG")
			end
			local ptrList = fhGetCurrentRecordSel("INDI")
			if #ptrList >= 1 then
				strIndiName = strMakeIndiName(ptrList[1])		-- Preset to currently selected Individual
			end
		end
		if not tblList.Tree.Count then
			ShowStatusMessage(iup_gui.Warn,"Loading Individuals data, please wait...")
			if general.FlgFolderExists(StrFolder) then			-- Update Map State for every Individual
				for strIndiName, tblIndiName in pairs(tblIndiNames) do
					local strRecordId = fhGetRecordId(tblIndiNames[strIndiName].ptr)
					local strMapName = "map"..strRecordId..".html"
					if not general.FlgFileExists(StrFolder.."\\"..strMapName) then strMapName = "" end
					tblIndiNames[strIndiName].map = strMapName
				end
			end
			ShowStatusMessage(iup_gui.Warn,"Loading Individuals tree, please wait...")
			intIndiName = doBuildTreeNodes(tblIndiNames,strIndiName,"INDI")
		end
		doShowFolderStatus()
		doRefreshIndiName(strIndiName)
	end -- local function doBuildIndiNamesList

	local function doCreateTabSettings()							-- Initial settings for Create Web Page Maps tab
		ZoomMapPane(IntMapZoom)
		doCreateTabPrefix()
		doBuildIndiNamesList()
		doCreateTabSuffix()
	end -- local function doCreateTabSettings

	local function doEraseMapFile(strMapFile)					-- Erase web page hyperlinks and delete map file
		if general.FlgFileExists(strMapFile) then
			-- Does folder contains FH Family Tree CD/Website?
			if IsFamilyTreeWebsite() then
				-- If so then remove map file hyperlink(s) from web page file
--?				local strHyperlink = string.plain("<li><a href='"..strMapFile:gsub("^"..StrFolder:plain().."\\","").."'>Map of Locations for ")..".-</a></li>\n"
				local strHyperlink = string.plain("<li><a href='"..strMapFile:gsub("^"..StrFolder:plain().."\\","",1).."'>").."[^\r\n]-</a></li>"..StrEOL	-- V5.1 -- Cater for translated "Map of Locations for"
				-- Find hyperlink to web page file and extract its file name
--?				local strMatch = "<li><a href='([A-Za-z0-9]-%.html)'>Detailed Facts for "
				local strMatch = "<li><a href='([indfam0-9]-%.html)'>.-</a></li>"				-- V5.1 -- Cater for translated "Detailed Facts for"
				for strWebFile in general.StrLoadFromFile(strMapFile):gmatch(strMatch) do		-- V5.1
					strWebFile = StrFolder.."\\"..strWebFile
					local strWebPage = general.StrLoadFromFile(strWebFile)
					strWebPage = strWebPage:gsub(strHyperlink,"",1)	-- Remove hyperlink to map file
					if not strWebPage:match("<li><a href='map%d+%.html'>[^\r\n]-</a></li>"..StrEOL) then					-- V5.1 -- No other map file hyperlinks
						if strWebPage:match(StrDivSeeAlso..StrOldSeeAlso..StrBegSeeAlso.."<!%-- "..StrEOL) then
							strWebPage = strWebPage:gsub(StrOldSeeAlso,"<!-- "..StrNewSeeAlso.." -->",1)						-- V5.1 -- Hide 'See also' if page hyperlinks hidden 
						else
							strWebPage = strWebPage:gsub(StrDivSeeAlso..StrOldSeeAlso..StrBegSeeAlso..StrEndSeeAlso,"",1)	-- V5.1 -- Remove empty "See also" section
						end
					end
					general.SaveStringToFile(strWebPage,strWebFile)
				end	
			end
			general.DeleteFile(strMapFile,ShowDelErr)
		end
	end -- local function doEraseMapFile

	function btnMapAll:action()									-- Action for Map All Individuals button
		doCreateTabPrefix()
		if general.FlgFolderExists(StrFolder) then
			local isMappingOK = true
			local intIndividuals = tonumber(tblList.Tree.Count)
			if intIndividuals > 9 then isMappingOK = doShowWarnings("Web Page Mapping") end
			if isMappingOK then
				local strMapName = ""
				local intIndividual = intIndiName					-- Preserve current Individual selected
				dialogMain.Active = "NO"
				progbar.Start("Web Page Mapping",intIndividuals)
				progbar.Message("Web Page Mapping All Individuals ...")
				MakeSharedFiles(StrFolder)
				SetWebPageMapSections()
				for intIndividual = 1, intIndividuals do
					progbar.Step(1)
					collectgarbage()									-- Memory garbage collection to avoid 'Not responding' -- V4.9
					local isGeocoderOk = true						-- V4.2
					local strIndiName = strGetTreeLeaf(tblList.Tree,intIndividual)
					if strIndiName then
						intIndiName = intIndividual
						doRefreshIndiName(strIndiName,-1)
						if txtMapState.FgColor == iup_gui[TblStatusColour[IntMapFilename]] then
							doEraseMapFile(StrFolder.."\\"..txtMapState.Value)	-- Unlink and Delete old Web Page Map file
						end
						tblIndiNames[strIndiName].map, isGeocoderOk = StrCreateWebPageMap(tblIndiNames[strIndiName].ptr)
						doRefreshIndiName(strIndiName,1)
						doListMapData()
						iup.Flush()
					end
					if progbar.Stop() or not isGeocoderOk then break end
				end
				if #tblList.tblLeaf == 1 and txtMapState.FgColor == iup_gui[TblStatusColour[IntMapFilename]] then
					strMapName = txtMapState.Value
				end
				intIndiName = intIndividual						-- Restore current Individual selected
				doRefreshIndiName(strIndiName)
				StrMapPath = StrMapPath..StrFolder.."|"			-- Add folder to map paths using new format	-- V4.3
				progbar.Close()
				dialogMain.Active = "YES"
				dialogMain.BringFront = "YES"
				general.DoExecute(StrFolder.."\\"..strMapName)
			end
		end
		doCreateTabSuffix()
	end -- function btnMapAll:action

	function btnMapSome:action()									-- Action for Map Some Individuals button
		doCreateTabPrefix()
		if general.FlgFolderExists(StrFolder) then
			local isMappingOK = doShowWarnings("Web Page Mapping")
			if isMappingOK then
				local ptrList = fhPromptUserForRecordSel("INDI")
				dialogMain.BringFront = "YES"
				if #ptrList > 0 then
					local strMapName = ""
					local intIndividual = intIndiName				-- Preserve current Individual selected
					dialogMain.Active = "NO"
					progbar.Start("Web Page Mapping",#ptrList)
					progbar.Message("Web Page Mapping Some Individuals ...")
					MakeSharedFiles(StrFolder)
					SetWebPageMapSections()
					for _, ptrRecord in ipairs(ptrList) do		-- Loop all listed Individual Records
						progbar.Step(1)
						collectgarbage()								-- Memory garbage collection to avoid 'Not responding' -- V4.9
						local isGeocoderOk = true					-- V4.2
						local strIndiName = strMakeIndiName(ptrRecord)
						intIndiName = tblIndiNames[strIndiName].Leaf	-- V3.6
						doRefreshIndiName(strIndiName,-1)
						if txtMapState.FgColor == iup_gui[TblStatusColour[IntMapFilename]] then
							doEraseMapFile(StrFolder.."\\"..txtMapState.Value)	-- Unlink and Delete old Web Page Map file
						end
						tblIndiNames[strIndiName].map, isGeocoderOk = StrCreateWebPageMap(ptrRecord)
						doRefreshIndiName(strIndiName,1)
						doListMapData()
						iup.Flush()
						if progbar.Stop() or not isGeocoderOk then break end	-- V4.2
					end
					if #ptrList == 1 and txtMapState.FgColor == iup_gui[TblStatusColour[IntMapFilename]] then
						strMapName = txtMapState.Value
					end
					intIndiName = intIndividual					-- Restore current Individual selected
					doRefreshIndiName(strIndiName)
					FixOldMapFiles(StrFolder)						-- Ensure old map files use new format -- V4.3
					progbar.Close()
					dialogMain.Active = "YES"
					dialogMain.BringFront = "YES"
					general.DoExecute(StrFolder.."\\"..strMapName)
				end
			end
		end
		doCreateTabSuffix()
	end -- function btnMapSome:action

	function btnMapThis:action()									-- Action for Map This Individual button
		doCreateTabPrefix()
		if general.FlgFolderExists(StrFolder) then
			if intIndiName > 0 then
				MakeSharedFiles(StrFolder)
				SetWebPageMapSections()
				if txtMapState.FgColor == iup_gui[TblStatusColour[IntMapFilename]] then
					doEraseMapFile(StrFolder.."\\"..txtMapState.Value)	-- Unlink and Delete old Web Page Map file
				end
				tblIndiNames[strIndiName].map = StrCreateWebPageMap(tblIndiNames[strIndiName].ptr)
				doRefreshIndiName(strIndiName)
				FixOldMapFiles(StrFolder)							-- Ensure old map files use new format -- V4.3
				dialogMain.BringFront = "YES"
				if txtMapState.FgColor == iup_gui[TblStatusColour[IntMapFilename]] then
					general.DoExecute(StrFolder.."\\"..txtMapState.Value)	-- Display new Web Page Map file
				end
			end
		end
		doCreateTabSuffix()
	end -- function btnMapThis:action

	function btnDeleteMap:action()								-- Action for Delete This Map button
		doCreateTabPrefix()
		doEraseMapFile(StrFolder.."\\"..txtMapState.Value)	-- Unlink and Delete this Web Page Map file
		tblIndiNames[strIndiName].map = ""
		doRefreshIndiName(strIndiName)
		ShowStatusMessage(iup_gui.Safe,"Web Page Map deleted")
		doCreateTabSuffix()
	end -- function btnDeleteMap:action

	function btnIndiName:action()									-- Action for Individual button
		doCreateTabPrefix()
		strLeaf = nil
		doShowTreeDialogue(btnIndiName)
		if strLeaf then
			strIndiName = strLeaf
			intIndiName = intLeaf
			doRefreshIndiName(strIndiName)
		end
		doCreateTabSuffix()
	end -- function btnIndiName:action

	local function doSuccessorIndiName(intWay,isMapped)		-- Find Prior/Next Map/No Map Individual
		doCreateTabPrefix()											-- intWay=-1/+1 & isMapped=1/nil
		local intCount = 0
		local intMax = tonumber(tblList.Tree.Count)
		local intLoc = intIndiName
		local strLoc = ""
		local tblLoc = {}
		repeat
			intCount = intCount + 1
			intLoc = intLoc + intWay
			if intLoc > intMax then intLoc = 1 end
			if intLoc < 1 then intLoc = intMax end
			strLoc = strGetTreeLeaf(tblList.Tree,intLoc)
			if strLoc then
				local strMap = tblIndiNames[strLoc].map
				if string.find(strMap,"^"..StrMapFilename.."$") == isMapped then
					strIndiName = strLoc
					intIndiName = intLoc
					doRefreshIndiName(strIndiName)
					doCreateTabSuffix()
					return
				end
			end
		until intCount == intMax
		doCreateTabSuffix()
	end -- local function doSuccessorIndiName

	function btnPrevMapped:action()								-- Action for Prior Mapped entry button
		doSuccessorIndiName(-1,1)
	end

	function btnNextMapped:action()								-- Action for Next Mapped entry button
		doSuccessorIndiName(1,1)
	end

	function btnPrevUnmap:action()								-- Action for Prior Unmapped entry button
		doSuccessorIndiName(-1,nil)
	end

	function btnNextUnmap:action()								-- Action for Next Unmapped entry button
		doSuccessorIndiName(1,nil)
	end

	-- GUI Set Preference Options Tab Functions --

	local function doPreferTabPrefix()							-- Set Preference Options tab actions before button functions 
		timMapPane.Run	= "NO"										-- Renamed -- V4.3
		deActivateControls( { allControls; tabControls; tabPrefer; vboxPrefer; btnDeleteAll; btnPurgeBOTH; } )
		doShowFolderStatus()
	end -- local function doPreferTabPrefix

	local function doPreferTabSuffix()							-- Set Preference Options tab actions after button functions 
		allControls.Active	= "YES"
		tabControls.Active	= "YES"
		tabPrefer .Active	= "YES"
		vboxPrefer.Active	= "YES"
		lblAPI_Key.Active	= "YES"								-- V5.3 -- V4.3
		txtAPI_Key.Active	= "YES"
		lblGeo_Key.Active	= "YES"								-- V5.3
		txtGeo_Key.Active	= "YES"
		if general.FlgFolderExists(StrFolder) then				-- Enable controls for Delete Map entries
			btnDeleteAll.Active	= "YES"
		end
		if IntFromTag >= IntFromBOTH then							-- Enable Purge Addr ~ Place button and update tooltip
			btnPurgeBOTH.Active	= "YES"
			btnPurgeBOTH.Tip	= btnPurgeBOTH.Tip:gsub(" '.+' "," '"..TblFromName[IntFromTag].."' ")
		end
		timMapPane.Run			= "YES"							-- Renamed -- V4.3
	end -- local function doPreferTabSuffix

	local function doDropStyleMarkers()							-- Drop example Markers in chosen style
		local strInfoWin = "Example marker for"					-- V4.7 -- was "1 Recorded Fact at"
		local numLatiDelta = 0.020									-- V4.7 -- delta offsets from central plot
		local numLongDelta = 0.030
		local numLati,numLong,strCity = getCityPlots()			-- V4.7 -- lookup city data for current Region Bias
		NullMapPane()
		PlotMapPane(numLati,             numLong,             strCity,         nil,strInfoWin)	-- V4.2 ,nil
		PlotMapPane(numLati+numLatiDelta,numLong+numLongDelta,strCity.." (NW)",nil,strInfoWin)	-- Style Markers should not change from default Zoom of 12	-- V4.3
		PlotMapPane(numLati+numLatiDelta,numLong-numLongDelta,strCity.." (NE)",nil,strInfoWin)
		PlotMapPane(numLati-numLatiDelta,numLong-numLongDelta,strCity.." (SE)",nil,strInfoWin)
		PlotMapPane(numLati-numLatiDelta,numLong+numLongDelta,strCity.." (SW)",nil,strInfoWin)
		DropMapPane(0.03)											-- V4.3
	end -- local function doDropStyleMarkers

	local function doPreferTabSettings(intTab)					-- Initial settings for Set Preference Options tab
		doPreferTabPrefix()
		tabPrefer.ValuePos = intTab - 1							-- 1 Aug 2013
		intPreferTab = intTab
		if intPreferTab == IntTabGeoLoc then
			tblList = tblTree[math.min(IntFromTag,IntFromBOTH)]
			ZoomMapPane(IntLocZoom)
			if btnLocation.FgColor == iup_gui[TblStatusColour[IntPlotTentative]] then
				strLocation = " "									-- Ensure location is marked -- V4.2
				doMarkMapPanePlot()
			else
				doMarkMapCity()										-- V4.3
			end
		elseif intPreferTab == IntTabCreate then
			doBuildIndiNamesList()
			doDropStyleMarkers()
		else
			doShowWholeWorld()										-- V4.3
		end
		doPreferTabSuffix()
	end -- local function doPreferTabSettings

	function tabPrefer:tabchangepos_cb(intNew,intOld)			-- Call back when Set Preference Options tab position is changed  
		tabPrefer.Active = "NO"
		SaveSettings()												-- Save sticky data settings
		doPreferTabSettings(intNew + 1)		-- was not + 1	-- 1 Aug 2013
	end -- function tabPrefer:tabchangepos_cb

	function timMapPane:action_cb() 								-- Call back when Map Pane timer expires -- V4.3
		timMapPane.Run = "NO"
		doReadMapPane()
		timMapPane.Run = "YES"
	end -- function timMapPane:action_cb

	function txtAPI_Key:action(strChar,strValue)				-- Action when Google API Key text is edited	-- V4.2
		local strVal = strValue:gsub("\n","")
		if strVal:match("[^%w-._~]") then							-- Only alphanumeric and -._~ unreserved URI characters valid	-- V4.3 -- V4.6
			local strMessageText = "\n Invalid Google Maps API Key character  " .. string.char(strChar) .. " \n"
			iup_gui.WarnDialogue(									-- V4.3
				{ "API Key Invalid" ; iup_gui.Warn; },
				{ strMessageText    ; iup_gui.Info; "API Key must be alphanumeric"; },
				{ " Continue "      ; iup_gui.Safe; "Continue & discard character"; }
				)
			return iup.IGNORE
		end
		if StrAPI_Key ~= strVal then
			StrAPI_Key = strVal
			SaveSettings()											-- Save sticky data settings
		end
		if strVal ~= strValue then									-- Remove newline characters -- V4.3
			txtAPI_Key.value = strVal
			return iup.IGNORE
		end
		return strChar
	end -- function txtAPI_Key:action

	function txtGeo_Key:action(strChar,strValue)				-- Action when Geoapify Key text is edited	-- V5.3
		local strVal = strValue:gsub("\n","")
		if strVal:match("[^%w-._~]") then							-- Only alphanumeric and -._~ unreserved URI characters valid	-- V4.3 -- V4.6
			local strMessageText = "\n Invalid Geoapify API Key character  " .. string.char(strChar) .. " \n"
			iup_gui.WarnDialogue(									-- V4.3
				{ "API Key Invalid" ; iup_gui.Warn; },
				{ strMessageText    ; iup_gui.Info; "API Key must be alphanumeric"; },
				{ " Continue "      ; iup_gui.Safe; "Continue & discard character"; }
				)
			return iup.IGNORE
		end
		if StrGeo_Key ~= strVal then
			StrGeo_Key = strVal
			SaveSettings()											-- Save sticky data settings
		end
		if strVal ~= strValue then									-- Remove newline characters -- V4.3
			txtGeo_Key.value = strVal
			return iup.IGNORE
		end
		return strChar
	end -- function txtGeo_Key:action

	function lstGeoPlot:action(strText,intItem,intState)		-- Action when plotting Geocoder is chosen -- V5.3
		if intState == 1 then
			doPreferTabPrefix()
			intItem = intItem - 1
			if intItem == DicGeoPlot.Google   and IsGoogleKeyMissing()
			or intItem == DicGeoPlot.Geoapify and IsGeo_KeyMissing() then
				lstGeoPlot.Value = IntGeoPlot + 1
			else
				IntGeoPlot = intItem
				SaveSettings()										-- Save sticky data settings
				ShowStatusMessage(iup_gui.Safe,"OK")				-- V6.0
			end
			doPreferTabSuffix()
		end
	end -- function lstGeoPlot:action

	function lstRegBias:action(strText,intItem,intState)		-- Action when Region Bias Code is chosen
		if intState == 1 then
			if intItem ~= IntRegBias then
				doPreferTabPrefix()
				IntRegBias = intItem
				SaveSettings()										-- Save sticky data settings
				doMarkMapCity()
				doPreferTabSuffix()
			end
		end
	end -- function lstRegBias:action

	function tglGeoCode:action(intState)							-- Action for Smart Geocoding toggle
		doPreferTabPrefix()
		if intState == 0 then StrGeoCode = "OFF" end
		if intState == 1 then StrGeoCode = "ON" end
		SaveSettings()												-- Save sticky data settings
		doPreferTabSuffix()
	end -- function tglGeoCode:action

	local function doAdjustStorage()								-- Adjust Database Store when changing to or from 'Place Fields only' mode and IntStorePLAC in use
		local intArchive = IntArchive
		lstStorage.Value = IntArchive
		IntArchive = IntStorage
		ConvertLocationsData(IntStorage,intArchive)
		IntStorage = intArchive
	end -- local function doAdjustStorage

	function lstFromTag:action(strText,intItem,intState)		-- Action when Locations Source is chosen
		if intState == 1 then
			if intItem ~= IntFromTag then
				doPreferTabPrefix()
				if math.min(intItem,IntFromBOTH) ~= math.min(IntFromTag,IntFromBOTH) then
					setLocation(nil,0)								-- Switching GUI so reset chosen Location -- V4.3
				end
				if fhGetAppVersion() > 5 then						-- FH6
					if intItem == IntFromPLAC then
						lstStorage.AppendItem = "Place Records"
					elseif lstStorage[lstStorage.Count] == "Place Records" then
						lstStorage.Value = IntArchive
						lstStorage.RemoveItem = lstStorage.Count
					end
				end
				if IntFromTag == IntFromPLAC						-- FH6
				or intItem == IntFromPLAC then
					if IntStorage ~= IntArchive then
						doAdjustStorage()
					end
				end
				IntFromTag = intItem
				IsFromNew = SetIsFromFlags()
				ZoomMapPane(IntLocZoom)
				doBuildLocationsList()
				SaveSettings()										-- Save sticky data settings
				doPreferTabSuffix()
			end
		end
	end -- function lstFromTag:action

	function lstStorage:action(strText,intItem,intState)		-- Action when Database Store is chosen
		if intState == 1 then
			if intItem ~= IntStorage then
				doPreferTabPrefix()
				local strRecords = ",\nbut other data you've added to the Records MAY NOT BE PRESERVED"
				if IntStorage == IntStorePLAC then strRecords = "" end
				if IntStorage == IntStoreFILE then
					if intItem == IntStorePLAC then				-- FH6
						strRecords = ",\nbut may modify the Place Records in your Gedcom data.\nUse 'Tools > Preferences > Map Window' or 'Map > Options...'\nand please tick 'Block Refresh for Non-Tentative Geocodes'"	-- V5.4
					else
						strRecords = ",\nbut will create Source &&/or Repository Records in your Gedcom data"
					end
				end
				local strCitations = ""
				if ( IntStorage == IntStoreSOUR and intItem == IntStoreBOTH )
				or ( IntStorage == IntStoreBOTH and intItem == IntStoreSOUR ) then
					strCitations = ",\nand maintain any associated Citations of your data Source Records"
				end
				if GUI_WarnDialogue("WARNING!  BEWARE!","Changing the Database Storage option will preserve all Geocode data"..strCitations..strRecords..".\n    Do you wish to continue with this Database Storage change?") == "Yes" then
					ConvertLocationsData(IntStorage,intItem)
					IntStorage = intItem
					if IntStorage ~= IntStorePLAC
					and IntArchive ~= IntStorePLAC then			-- V3.5 IntArchive only differs when IntStorePLAC in use
						IntArchive = IntStorage
					end
					IsFromNew = true
					doBuildLocationsList()
					SaveSettings()									-- Save sticky data settings
				else
					timStorage.Run = "YES"							-- See action call back below
				end
				doPreferTabSuffix()
			end
		end
	end -- function lstStorage:action

	function timStorage:action_cb()								-- Call back when Database Storage change cancelled timer expires
		timStorage.Run = "NO"
		lstStorage.Value = IntStorage								-- Appears to be only way to ignore & reset drop list selection
	end -- function timStorage:action_cb

	local function doUpdateMapStatus()							-- Update Map State for every Individual
		ShowStatusMessage(iup_gui.Warn,"Loading Individuals data, please wait...")
		for intIndiName = 1, tonumber(tblList.Tree.Count) do
			local strIndiName = strGetTreeLeaf(tblList.Tree,intIndiName)
			if strIndiName then
				local strRecordId = fhGetRecordId(tblIndiNames[strIndiName].ptr)
				local strMapName = "map"..strRecordId..".html"
				local strColor = iup_gui[TblStatusColour[IntMapFilename]]
				if not general.FlgFileExists(StrFolder.."\\"..strMapName) then
					strMapName = ""
					strColor = iup_gui[TblStatusColour[IntMapNoMap]]
				end
				tblIndiNames[strIndiName].map = strMapName
				doColorLeafNode(intIndiName,strColor)
			end
		end
		doShowFolderStatus()
		doRefreshIndiName(strIndiName)
		DicFactSet = {}												-- V3.7
	end -- local function doUpdateMapStatus

	function tglLogging:action(intState)							-- Action for Smart Geocoding toggle
		doPreferTabPrefix()
		if intState == 0 then StrLogging = "OFF" end
		if intState == 1 then StrLogging = "ON" end
		SaveSettings()												-- Save sticky data settings
		doPreferTabSuffix()
	end -- function tglLogging:action

	function txtMapFolder:valuechanged_cb()						-- Call back after Output Folder text is edited
		doPreferTabPrefix()
		local strFolder = txtMapFolder.Value
		if StrFolder ~= strFolder then							-- V5.1
			StrFolder = strFolder
			doUpdateMapStatus()
			SaveSettings()											-- Save sticky data settings
		end
		doPreferTabSuffix()
	end -- function txtMapFolder:valuechanged_cb

	function btnMapFolder:action()								-- Action for Select Output Folder button
		doPreferTabPrefix()
		local strFolder = StrFolder
		local strDirectory = strFolder
		if not general.FlgFolderExists(StrFolder) then strDirectory = iup_gui.PublicPath end
		local dialogFolder = iup.filedlg{ dialogtype="DIR", Title="Please select output folder", directory=strDirectory; }
		dialogFolder:popup (iup.CENTER, iup.CENTER)
		if dialogFolder.status == "-1" then
			ShowStatusMessage(iup_gui.Warn,"No folder selected")
		else
			StrFolder = dialogFolder.Value
			if StrFolder ~= strFolder then doUpdateMapStatus() end
			SaveSettings()											-- Save sticky data settings
		end
		txtMapFolder.Value = StrFolder
		doPreferTabSuffix()
	end -- function btnMapFolder:action

	function btnDeleteAll:action()								-- Action for Delete All Maps button
		doPreferTabPrefix()
		if GUI_WarnDialogue("WARNING!  BEWARE!","All the Web Page Maps will be deleted from the Output Folder.\n   Do you wish to continue to Delete these Web Page Maps?") == "Yes" then
			for _, dicFile in ipairs(general.GetFolderContents(StrFolder,false)) do	-- V6.0
				local strFile = dicFile.path
				local strPath, strName, strType = general.SplitFilename(strFile)
				if string.find(strName,"^"..StrMapFilename.."$") then doEraseMapFile(strFile) end				-- Unlink and Delete every Web Page Map file
				if string.find(strName,"^mapfacts%.[a-z]+$") then general.DeleteFile(strFile,ShowDelErr) end	-- Delete mapfacts.css & mapfacts.js & mapfacts.png files
			end
			for strIndiName, tblIndiName in pairs(tblIndiNames) do
				if strIndiName then tblIndiNames[strIndiName].map = "" end
			end
			doRefreshIndiName(strIndiName)
			ShowStatusMessage(iup_gui.Safe,"Web Page Maps deleted")
		end
		doPreferTabSuffix()
	end -- function btnDeleteAll:action

	function lstWebMaps:action(strText,intItem,intState)		-- Action when Web Page Maps option is chosen	-- V5.3
		if intState == 1 then
			doPreferTabPrefix()
			if IsGoogleMapAndNoKey(0,intItem) then				-- V6.0
				intItem = DicMapMode.LeafletMaps
				lstWebMaps.Value = intItem
			end
			IntWebMaps = intItem 									-- 1 = Google Maps (Legacy), 2 = Leaflet Maps, 3 = Google Maps (Advanced)
			SaveSettings()											-- Save sticky data settings
			ShowStatusMessage(iup_gui.Safe,"OK")					-- V6.0
			doPreferTabSuffix()
		end
	end -- function lstWebMaps:action

	function lstColours:action(strText,intItem,intState)		-- Action when Map Pin Colours option is chosen	-- V6.0
		if intState == 1 then
			doPreferTabPrefix()
			IntColours = intItem									-- 1 = Red, 2 = Blue, 3 = Green
			StrColours = DicColours[IntColours]
			SaveSettings()											-- Save sticky data settings
			setMapMode()
			ZoomMapPane(IntMapZoom)									-- Preserve map zoom
			doDropStyleMarkers()
			doPreferTabSuffix()
		end
	end -- function lstMapMode:action

	function lstLegends:action(strText,intItem,intState)		-- Action when Plugin Map Legends option is chosen	-- V5.3
		if intState == 1 then
			doPreferTabPrefix()
			IntLegends = intItem									-- 1 = Tiny, 2 = Small, 3 = Medium, 4 = Large, 5 = Huge
			SaveSettings()											-- Save sticky data settings
			setMapMode()
			ZoomMapPane(IntMapZoom)									-- Preserve map zoom
			doDropStyleMarkers()
			doPreferTabSuffix()
		end
	end -- function lstLegends:action

	function lstMarkers:action(strText,intItem,intState)		-- Action when Markers Style is chosen
		if intState == 1 then
			doPreferTabPrefix()
			IntMarkers = intItem									-- 1 = No Animation, 2 = Synchro Drop, 3 = Cascade Drop
			SaveSettings()											-- Save sticky data settings
			doDropStyleMarkers()
			doPreferTabSuffix()
		end
	end -- function lstMarkers:action

	function lstPrivacy:action(strText,intItem,intState)		-- Action when Privacy Filter is chosen
		if intState == 1 then
			IntPrivacy = intItem
			SaveSettings()											-- Save sticky data settings
		end
	end -- function lstPrivacy:action

	function tglFactSet:action(intState)							-- Action for Facts Filter toggle -- V3.7
		doPreferTabPrefix()
		if intState == 0 then StrFactSet = "OFF" end
		if intState == 1 then StrFactSet = "ON" end
		SaveSettings()												-- Save sticky data settings
		doPreferTabSuffix()
	end -- function tglFactSet:action

	local function strRecords()									-- Return text for delete data in Records message
		if IntStorage == IntStoreFILE then
			return ""
		else
			return "\n and delete any other data that you may have added to the Location Records"
		end
	end -- local function strRecords

	local function doCopyLocations(intFromTag)					-- Copy Place or Address Database to Address ~ Place Database
		doPreferTabPrefix()
		local strSourceName = TblFromName[intFromTag]
		local strTargetName = TblFromName[intFromTag - IntFromPLAC + IntFromBOTH]
		if GUI_WarnDialogue("WARNING!  BEWARE!","Copying the '"..strSourceName.."' Locations Database will overwrite ALL Geocode data"..strRecords().."\n in all the '"..strTargetName.."' entries in the 'Address ~ Place' Locations database.\n\n   Do you wish to continue to Copy the '"..strSourceName.."' Locations Database?\n") == "Yes" then
			local strSaveTag = StrFromTag							-- Save original StrFromTag
			StrFromTag = TblFromTag[IntFromBOTH]
			TblLocationData = TblLocationCode[StrFromTag]		-- Target Address ~ Place database
			local strPrefix = ""
			local strSuffix = " ~ "
			if intFromTag == IntFromPLAC then						-- Set " ~ " prefix/suffix for Location name in Address ~ Place database
				strPrefix = " ~ "
				strSuffix = ""
			end
			for strLocName, tblLocData in pairs(TblLocationCode[TblFromTag[intFromTag]]) do
				strLocName = StrControls2SpaceNew(strLocName)	-- New format with newline = bell blob -- V3.5
				strLocName = strPrefix..strLocName..strSuffix
				if not TblLocationData[strLocName] then			-- Add new target Location to data
					MakeNewLocationData(strLocName)				-- V3.5
				end
				TblLocationData[strLocName].sub = tblLocData.sub or ""
				TblLocationData[strLocName].lat = tblLocData.lat or ""
				TblLocationData[strLocName].lng = tblLocData.lng or ""
				TblLocationData[strLocName].mod = tblLocData.mod or ""
				TblLocationData[strLocName].geo = tblLocData.geo or "" 	-- V5.3+
				SaveLocationsData(strLocName)
			end
			StrFromTag = strSaveTag									-- Restore saved StrFromTag
			TblLocationData = TblLocationCode[StrFromTag]
			ShowStatusMessage(iup_gui.Safe,"'"..strSourceName.."' Database Copied")
			fhUpdateDisplay()
		end
		IsFromNew = true												-- V3.5
		doPreferTabSuffix()
	end -- local function doCopyLocations

	function btnCopyPLAC:action()									-- Action for Copy Place Database button
		doCopyLocations(IntFromPLAC)
	end -- function btnCopyPLAC:action

	function btnCopyADDR:action()									-- Action for Copy Address Database button
		doCopyLocations(IntFromADDR)
	end -- function btnCopyADDR:action

	function btnPurgeBOTH:action()								-- Action for Purge Addr ~ Place Database button
		doPreferTabPrefix()
		if IntFromTag >= IntFromBOTH then
			local strFromName = TblFromName[IntFromTag]
			if GUI_WarnDialogue("WARNING!  BEWARE!","Purging the '"..strFromName.."' Locations Database will ERASE ALL Geocode data"..strRecords().."\n for all those Location entries not currently included in the Location list.\n\n   Do you wish to continue to Purge the '"..strFromName.."' Locations Database?\n") == "Yes" then
				IsFromNew = SetIsFromFlags()
				doBuildLocationsList("Delete")					-- Rebuild list without Locations not included
				ShowStatusMessage(iup_gui.Safe,"'"..strFromName.."' Database Purged")
				fhUpdateDisplay()
			end
		end
		doPreferTabSuffix()
	end -- function btnPurgeBOTH:action

	local function doEraseLocation(intFromTag)					-- 1="PLAC" or 2="ADDR" or 3="BOTH"
		doPreferTabPrefix()
		local strFromTag = TblFromTag[intFromTag]
		local intFromName = intFromTag							-- V4.0
		if intFromName == IntFromBOTH then intFromName = IntFromAP end
		local strFromName = TblFromName[intFromName]			-- V4.0
		if GUI_WarnDialogue("WARNING!  BEWARE!","Erasing the '"..strFromName.."' Locations Database will ERASE ALL Geocode data"..strRecords()..".\n\n   Do you wish to continue to Erase the '"..strFromName.."' Locations Database?\n") == "Yes" then
			if IntStorage == IntStoreFILE
			or IntStorage == IntStorePLAC then					-- V3.5
				TblLocationCode[strFromTag] = {}
				SaveLocationsFile()
			else
				TblLocationData = TblLocationCode[strFromTag]
				for strLocName, tblLocData in pairs(TblLocationData) do
					DeleteLocationRecord(tblLocData.rec)
				end
				TblLocationCode[strFromTag] = {}
			end
			ShowStatusMessage(iup_gui.Safe,"'"..strFromName.."' Database Erased")
			fhUpdateDisplay()
			tblTree[intFromTag].Tree.DelNode0 = "CHILDREN"		-- V3.6
			if math.min(IntFromTag,IntFromBOTH) == math.min(intFromTag,IntFromBOTH) then
				TblLocationData = TblLocationCode[TblFromTag[IntFromTag]]
				setLocation(nil,0)									-- V3.6 -- V4.3
--				IsFromNew = true	--?
			end
		end
		doPreferTabSuffix()
	end -- local function doEraseLocation

	function btnErasePLAC:action()								-- Action for Erase Place Database button
		doEraseLocation(IntFromPLAC)
	end -- function btnErasePLAC:action

	function btnEraseADDR:action()								-- Action for Erase Address Database button
		doEraseLocation(IntFromADDR)
	end -- function btnEraseADDR:action

	function btnEraseBOTH:action()								-- Action for Erase Addr ~ Place Database button
		doEraseLocation(IntFromBOTH)
	end -- function btnEraseBOTH:action

	local function setControls()									-- Reset GUI control values
		txtAPI_Key.Value  = StrAPI_Key							-- V4.2
		lstGeoPlot.Value  = IntGeoPlot + 1						-- V5.3
		txtGeo_Key.Value  = StrGeo_Key							-- V5.3
		lstRegBias.Value  = IntRegBias
		tglGeoCode.Value  = StrGeoCode
		txtLocZoom.Value  = IntLocZoom
		lstFromTag.Value  = IntFromTag
		tglLogging.Value  = StrLogging
		lstStorage.Value  = IntStorage
		txtMapFolder.Value= StrFolder
		lstWebMaps.Value  = IntWebMaps							-- V5.3
		lstColours.Value  = IntColours							-- V6.0
		lstLegends.Value  = IntLegends							-- V5.3
		txtMapZoom.Value  = IntMapZoom
		lstMarkers.Value  = IntMarkers
		lstPrivacy.Value  = IntPrivacy
		tglFactSet.Value  = StrFactSet							-- V3.7
		btnLocation.Size  = txtStandard.Size:gsub("x[0-9]+","")
		btnIndiName.Size  = txtMapState.Size:gsub("x[0-9]+","") + btnDeleteMap.Size:gsub("x[0-9]+","")
		SaveSettings()												-- Save sticky data settings
	end -- local function setControls

	function btnDefault:action()									-- Action for Restore Defaults button
		local strReverse = StrReverse
		timMapPane.Run = "NO"										-- Renamed -- V4.3
		ResetDefaultSettings()
		if lstStorage[lstStorage.Count] ~= "Place Records" then
			lstStorage.AppendItem = "Place Records"				-- FH6
		end
		if IntArchive > IntStorage then							-- FH6
			doAdjustStorage()
		end
		tabControls.ValuePos = math.max(0,IntTabPosn - 1)		-- Adjust tab selection 10 Nov 2014
		setControls()													-- Reset controls & redisplay Main dialogue
		iup_gui.ShowDialogue("Main")
		iup_gui.DefaultDialogue()									-- V6.1
		setMapMode(IntMapMode)										-- V4.3
		doGeoLocTabSettings()
		SaveSettings()												-- Save sticky data settings
	end -- function btnDefault:action

	function btnSetFont:action()									-- Action for User Interface Font button
		doPreferTabPrefix()
		btnSetFont.Active = "NO"
		iup_gui.FontDialogue(tblControls)
		SaveSettings()												-- Save sticky data settings
		doPreferTabSuffix()
		btnSetFont.Active = "YES"
	end -- function btnSetFont:action

	-- GUI Main Dialogue Global Functions --

	local function showTabControls(intPrefer)					-- Show current tab details	-- V4.2
		if IntTabPosn == IntTabGeoLoc then
			doGeoLocTabSettings()
		elseif IntTabPosn == IntTabCreate then
			doCreateTabSettings()
		elseif IntTabPosn == IntTabPrefer then
			doPreferTabSettings(intPrefer)						-- 1 Aug 2013
		end
	end -- local function showTabControls

	local strAPI_Key = StrAPI_Key									-- V4.2

	function tabControls:tabchangepos_cb(intNew,intOld)		-- Call back when Main tab position is changed  
		timMapPane.Run = "NO"										-- Renamed -- V4.3
		tabControls.Active = "NO"
		if strAPI_Key ~= StrAPI_Key								-- Has the API Key been changed	-- V4.2
		and general.FlgFileExists(StrFolder.."\\mapfacts.js") then
			MakeSharedFiles(StrFolder)								-- Update mapfacts.js with new API Key -- V4.2
			strAPI_Key = StrAPI_Key
		end
		IntTabPosn = intNew + 1										-- 1 Aug 2013
		showTabControls(intOld + 1)								-- V4.2
		if IntStorage == IntStoreFILE then SaveLocationsFile() end
		SaveSettings()												-- Save sticky data settings
	end -- function tabControls:tabchangepos_cb

	local function doExecute(strExecutable, strParameter)		-- Invoke FH Shell Execute API	-- V4.7
		local function ReportError(strMessage)
			iup_gui.WarnDialogue(	"Shell Execute Error",
									"ERROR: "..strMessage.." :\n"..strExecutable.."\n"..strParameter.."\n\n",
									"OK"	)
		end -- local function ReportError
		return general.DoExecute(strExecutable, strParameter, ReportError)
	end -- local function doExecute

	local function doGetHelp(intPage)								-- Display the chosen Help page	-- V4.8
		local strHelp = "https://pluginstore.family-historian.co.uk/page/help/map-life-facts"
		local arrHelp = { "-geocode-location-plots-tab"; "-create-web-page-maps-tab"; "-set-preference-options-tab"; "-geocoding-and-mapping"; }
		local strPage = arrHelp[intPage] or ""
		doExecute( strHelp..strPage )
		fhSleep(3000,500)
		dialogMain.BringFront="YES"
	end -- local function doGetHelp

	function btnUpdates:action()									-- Action for Check for Updates button	-- V6.0
		local strFile = iup_gui.MachinePath.."\\VersionInStore Map Life Facts.dat"
		general.DeleteFile(strFile)
		iup_gui.CheckVersionInStore(true)							-- Notify if later Version
	end -- function btnUpdates:action

	function btnGetHelp:action()									-- Action for Help & Advice button according to current tab	-- V4.8
		doGetHelp(IntTabPosn)
	end -- function btnGetHelp:action

	function dialogMain:show_cb(intState)						-- Call back when GUI window shown, etc	-- V4.2
		if intState == iup.SHOW then								-- IUP_SHOW = 0
			allControls.Active = "NO"
			if not TblLocationData then
				fhSleep(100)											-- Update current tab details after Plugin window displays	-- V4.2 -- V4.9
				LoadLocationsData()
			end
			fhSleep(100)
			doBuildLocationsList()
			if IsSplashOn == "API" then							-- API Splash is shown first			-- V5.3
				IsSplashOn = "Yes"
			elseif IsSplashOn == "Yes" then						-- Display the Splash Main Help		-- V4.9
				IsSplashOn = "No"
				IntTabPosn = IntTabGeoLoc							-- Set tab Geocode Location Plots	-- V4.2
				tabControls.ValuePos = IntTabPosn - 1
				doGetHelp(0)											-- Splash Main Help for new user	-- V4.2 -- V4.8
				dialogMain.BringFront = "YES"
			end
			showTabControls(IntTabGeoLoc)
			allControls.Active	= "YES"
		end
	end -- function dialogMain:show_cb

	local function doCloseMain()
		timMapPane.Run = "NO"										-- Renamed -- V4.3
		dialogMain.show_cb = nil									-- Needed to prevent FH hanging in Plugin debug mode	-- V4.2
		fhSleep(100)
		if IntTabPosn == IntTabGeoLoc then doGeoLocTabPrefix() end
		if IntTabPosn == IntTabCreate then doCreateTabPrefix() end
		if IntTabPosn == IntTabPrefer then doPreferTabPrefix() end
		if IntStorage == IntStoreFILE
		or IntArchive == IntStoreFILE then SaveLocationsFile() end	-- V3.5
		SavePlotLogFileCSV()										-- V5.3
		CloseMapPane()
	end -- local function doCloseMain

	function btnDestroy:action()									-- Action for Close Plugin button
		doCloseMain()
		return iup.CLOSE 
	end -- function btnDestroy:action

	function dialogMain:close_cb()								-- Call back when GUI window Close pressed
		if allControls.Active == "NO" then
			allControls.Active	= "YES"							-- Needed in case of faults that leave controls inactive
			timMapPane.Run = "NO"									-- Added -- V4.3
			return iup.IGNORE
		else
			doCloseMain()
			return iup.CLOSE
		end
	end -- function dialogMain:close_cb

	-- GUI Main Dialogue Initialisation --

--!	iup_gui.ShowDialogue("Main",dialogMain,btnDestroy,"map")	-- Map dialogue to ensure all controls display correctly
	iup_gui.ShowDialogue("Main",dialogMain,nil,"map")			-- Map dialogue to ensure all controls display correctly -- Inhibit Close on Return & Esc	-- V6.0

	tabControls.ValuePos = math.max(0,IntTabPosn - 1)			-- Adjust tab selection	-- 1 & 27 Aug 2013

	local strRegBiasTip = lstRegBias.Tip							-- Populate Region Bias list
	lstRegBias.Tip = nil											-- Temporarily erase tooltip to speed up appenditem
	lstRegBias.AutoRedraw = "NO"
	lstRegBias.RemoveItem = nil
	for _, strRegBias in ipairs(TblRegBias) do
		lstRegBias.AppendItem = strRegBias
	end
	lstRegBias.AutoRedraw = "YES"
	lstRegBias.Tip = strRegBiasTip
	lstRegBias.Value = IntRegBias

	doBuildTreeDialogues()											-- Add extra controls

	local strRight = "ARIGHT:ABOTTOM"

	-- Set other GUI control attributes
	tblControls  =  {  {  "Font"; "FgColor"; "Alignment"; "Size"; "Expand"; "Active"; "Tip"; {"TipBalloon";"Balloon"}; {"help_cb";function() doGetHelp(0) end;}; setControls; };

		[vboxGeoLoc]   = { "FontBody"; "Body"; "ARIGHT" ; false; false       ; "YES" };	-- Geocode Location Plots controls
		[btnPlotAll]   = { "FontBody"; "Safe"; false    ; false; "HORIZONTAL"; false };
		[btnPlotSome]  = { "FontBody"; "Safe"; false    ; false; "HORIZONTAL"; false };
		[btnPlotThis]  = { "FontBody"; "Safe"; false    ; false; "HORIZONTAL"; false };
		[framePlotData]= { "FontHead"; "Head"; false    ; false; false       ; "YES" };
		[vboxPlotData] = { "FontBody"; "Body"; };
		[lblPlotStats] = { "FontBody"; "Body"; strRight ; "048"; false       ; "YES" };
		[lblPlotted]   = { "FontBody"; TblStatusColour[IntPlotDefined]
													; "ACENTER"; false; "HORIZONTAL"; "YES" };
		[lblTentative] = { "FontBody"; TblStatusColour[IntPlotTentative]
													; "ACENTER"; false; "HORIZONTAL"; "YES" };
		[lblNoPlot]    = { "FontBody"; TblStatusColour[IntPlotNotFound]
													; "ACENTER"; false; "HORIZONTAL"; "YES" };
		[lblInvalid]   = { "FontBody"; TblStatusColour[IntPlotInvalid]
													; "ACENTER"; false; "HORIZONTAL"; "YES" };
		[lblNoData]    = { "FontBody"; TblStatusColour[IntPlotNoData]
													; "ACENTER"; false; "HORIZONTAL"; "YES" };
		[lblPlotTotal] = { "FontBody"; TblStatusColour[IntPlotSumTotal]
													; "ACENTER"; false; "HORIZONTAL"; "YES" };
		[lblLocation]  = { "FontBody"; "Body"; strRight ; "048"; false       ; "YES" };
		[btnLocation]  = { "FontBody"; "Safe"; "ALEFT"  ; false; "HORIZONTAL"; "YES" };
		[lblStandard]  = { "FontBody"; "Body"; strRight ; "048"; false       ; "YES" };
		[txtStandard]  = { "FontBody"; "Safe"; false    ; false; "HORIZONTAL"; false };
		[lblLatitude]  = { "FontBody"; "Body"; strRight ; "048"; false       ; "YES" };
		[TxtLatitude]  = { "FontBody"; "Safe"; false    ; false; "HORIZONTAL"; false };
		[lblLongitude] = { "FontBody"; "Body"; strRight ; "048"; false       ; "YES" };
		[TxtLongitude] = { "FontBody"; "Safe"; false    ; false; "HORIZONTAL"; false };

		[btnLocToSub]  = { "FontBody"; "Safe"; false    ; "078"; false       ; false };
		[btnClearAll]  = { "FontBody"; "Safe"; "ALEFT"  ; "078"; false       ; false };
		[btnClearSub]  = { "FontBody"; "Safe"; "ALEFT"  ; "078"; false       ; false };
		[btnClearLatLng]={ "FontBody"; "Safe"; "ALEFT"  ; "078"; false       ; false };	-- V4.9
		[btnSetBlocked]= { "FontBody"; TblStatusColour[IntPlotBlocked]
													; "ALEFT"  ; "078"; false       ; false };	-- V4.9

		[btnNextPlot]  = { "FontBody"; "Safe"; false    ; "078"; false       ; false };
		[btnNextDefined]={ "FontBody"; "Safe"; false    ; "078"; false       ; false };
		[btnNextSubs]  = { "FontBody"; "Safe"; false    ; "078"; false       ; false };
		[btnNextUnplot]= { "FontBody"; "Safe"; false    ; "078"; false       ; false };

		[btnMoveNorth] = { "FontBody"; "Safe"; false    ; "012"; false       ; false };
		[btnMoveSouth] = { "FontBody"; "Safe"; false    ; "012"; false       ; false };
		[btnMoveWest]  = { "FontBody"; "Safe"; false    ; "012"; false       ; false };
		[btnMoveEast]  = { "FontBody"; "Safe"; false    ; "012"; false       ; false };

		[vboxCreate]   = { "FontBody"; "Body"; "ARIGHT" ; false; false       ; "YES" };	-- Create Web Page Maps controls
		[btnMapAll]    = { "FontBody"; "Safe"; false    ; false; "HORIZONTAL"; false };
		[btnMapSome]   = { "FontBody"; "Safe"; false    ; false; "HORIZONTAL"; false };
		[btnMapThis]   = { "FontBody"; "Safe"; false    ; false; "HORIZONTAL"; false };
		[frameMapData] = { "FontHead"; "Head"; false    ; false; false       ; "YES" };
		[vboxMapData]  = { "FontBody"; "Body"; };
		[lblMapStats]  = { "FontBody"; "Body"; strRight ; "048"; false       ; "YES" };
		[lblMapped]    = { "FontBody"; TblStatusColour[IntMapFilename]
													; "ACENTER"; false; "HORIZONTAL"; "YES" };
		[lblOmitted]   = { "FontBody"; TblStatusColour[IntMapOmitted]
													; "ACENTER"; false; "HORIZONTAL"; "YES" };
		[lblFiltered]  = { "FontBody"; TblStatusColour[IntMapFiltered]
													; "ACENTER"; false; "HORIZONTAL"; "YES" };
		[lblNoMap]     = { "FontBody"; TblStatusColour[IntMapNoMap]
													; "ACENTER"; false; "HORIZONTAL"; "YES" };
		[lblMapTotal]  = { "FontBody"; TblStatusColour[IntMapSumTotal]
													; "ACENTER"; false; "HORIZONTAL"; "YES" };
		[lblIndiName]  = { "FontBody"; "Body"; strRight ; "048"; false       ; "YES" };
		[btnIndiName]  = { "FontBody"; "Body"; "ALEFT"  ; false; "HORIZONTAL"; "YES" };
		[lblMapState]  = { "FontBody"; "Body"; strRight ; "048"; false       ; "YES" };
		[txtMapState]  = { "FontBody"; "Body"; false    ; false; "HORIZONTAL"; "YES" };

		[btnDeleteMap] = { "FontBody"; "Risk"; false    ; "078"; false       ; false };
		[btnPrevMapped]= { "FontBody"; "Safe"; false    ; "078"; false       ; false };
		[btnNextMapped]= { "FontBody"; "Safe"; false    ; "078"; false       ; false };
		[btnPrevUnmap] = { "FontBody"; "Safe"; false    ; "078"; false       ; false };
		[btnNextUnmap] = { "FontBody"; "Safe"; false    ; "078"; false       ; false };

		[tabPrefer]    = { "FontHead"; "Head"; false    ; false; false       ; "YES" };	-- Set Preference Options controls
		[vboxPlotOpt]  = { "FontBody"; "Body"; };
		[lblAPI_Key]   = { "FontBody"; "Body"; strRight ; "065"; false       ; false };	-- V4.2 -- V4.3
		[txtAPI_Key]   = { "FontBody"; "Safe"; false    ; "160"; "HORIZONTAL"; false };	-- V4.2
		[lblGeo_Key]   = { "FontBody"; "Body"; strRight ; "050"; "HORIZONTAL"; false };	-- V5.3
		[txtGeo_Key]   = { "FontBody"; "Safe"; false    ; "135"; "HORIZONTAL"; false };	-- V5.3

		[lblGeoPlot]   = { "FontBody"; "Body"; strRight ; "065"; false       ; false };	-- V5.3
		[lstGeoPlot]   = { "FontBody"; "Safe"; false    ; "100"; "HORIZONTAL"; false };	-- V5.3
		[lblRegBias]   = { "FontBody"; "Body"; strRight ; "100"; "HORIZONTAL"; false };
		[lstRegBias]   = { "FontBody"; "Safe"; false    ; "130"; "HORIZONTAL"; false };

		[tglGeoCode]   = { "FontBody"; "Safe"; strRight ; "080"; false       ; false };
		[tglLogging]   = { "FontBody"; "Safe"; false    ; "080"; false       ; false };	-- V5.3
		[lblLocZoom]   = { "FontBody"; "Body"; strRight ; "045"; false       ; false };
		[txtLocZoom]   = { "FontBody"; "Body"; "ARIGHT" ; "022"; false       ; false };

		[lblFromTag]   = { "FontBody"; "Body"; strRight ; "065"; false       ; false };
		[lstFromTag]   = { "FontBody"; "Safe"; false    ; "100"; "HORIZONTAL"; false };
		[lblStorage]   = { "FontBody"; "Body"; strRight ; "100"; "HORIZONTAL"; false };
		[lstStorage]   = { "FontBody"; "Safe"; false    ; "130"; "HORIZONTAL"; false };

		[vboxMapsOpt]  = { "FontBody"; "Body"; };
		[txtMapFolder] = { "FontBody"; "Safe"; false    ; "x24"; "HORIZONTAL"; "YES" };	-- V5.3	-- was "x38"
		[btnMapFolder] = { "FontBody"; "Safe"; false    ;"70x23";false       ; false };	-- V5.3
		[btnDeleteAll] = { "FontBody"; "Risk"; false    ;"70x23";false       ; false };	-- V5.3

		[lblWebMaps]   = { "FontBody"; "Body"; strRight ; "051"; "HORIZONTAL"; false };	-- V6.0 V5.3
		[lstWebMaps]   = { "FontBody"; "Safe"; false    ; "051"; "HORIZONTAL"; false };	-- V6.0 V5.3
		[lblPrivacy]   = { "FontBody"; "Body"; strRight ; "050"; "HORIZONTAL"; false };	-- V6.0
		[lstPrivacy]   = { "FontBody"; "Safe"; false    ; "051"; "HORIZONTAL"; false };	-- V6.0
		[tglFactSet]   = { "FontBody"; "Safe"; false    ; "045"; false       ; false };	-- V6.0 V5.3
		[lblMapZoom]   = { "FontBody"; "Body"; strRight ; "045"; false       ; false };	-- V6.0
		[txtMapZoom]   = { "FontBody"; "Body"; "ARIGHT" ; "022"; false       ; false };	-- V6.0

		[lblMarkers]   = { "FontBody"; "Body"; strRight ; "051"; "HORIZONTAL"; false }; -- V6.0 V4.3
		[lstMarkers]   = { "FontBody"; "Safe"; false    ; "051"; "HORIZONTAL"; false }; -- V6.0 V4.3
		[lblColours]   = { "FontBody"; "Body"; strRight ; "050"; "HORIZONTAL"; false };	-- V6.0
		[lstColours]   = { "FontBody"; "Safe"; false    ; "051"; "HORIZONTAL"; false };	-- V6.0
		[lblLegends]   = { "FontBody"; "Body"; strRight ; "060"; "HORIZONTAL"; false };	-- V6.0 V5.3
		[lstLegends]   = { "FontBody"; "Safe"; false    ; "054"; "HORIZONTAL"; false };	-- V6.0 V5.3

		[vboxDataOpt]  = { "FontBody"; "Body"; };
		[btnCopyPLAC]  = { "FontBody"; "Warn"; false    ; false; "HORIZONTAL"; false };
		[btnCopyADDR]  = { "FontBody"; "Warn"; false    ; false; "HORIZONTAL"; false };
		[btnPurgeBOTH] = { "FontBody"; "Risk"; false    ; false; "HORIZONTAL"; false };

		[btnErasePLAC] = { "FontBody"; "Risk"; false    ; false; "HORIZONTAL"; false };
		[btnEraseADDR] = { "FontBody"; "Risk"; false    ; false; "HORIZONTAL"; false };
		[btnEraseBOTH] = { "FontBody"; "Risk"; false    ; false; "HORIZONTAL"; false };

		[vboxPrefer]   = { "FontBody"; "Body"; "ARIGHT" ; };
		[btnDefault]   = { "FontBody"; "Safe"; false    ; false; "HORIZONTAL"; false };
		[btnSetFont]   = { "FontBody"; "Safe"; false    ; false; "HORIZONTAL"; false };

		[tabControls]  = { "FontHead"; "Head"; false    ; false; false       ; "YES" };
		[allControls]  = { "FontBody"; "Body"; "ARIGHT" ; };

		[lblStatus]    = { "FontBody"; "Body"; strRight ; "028"; false       ; "YES" };	-- Main Dialogue Common controls -- was "36" - V4.2
		[LblMessage]   = { "FontBody"; "Safe"; "ALEFT"  ; false; "HORIZONTAL"; "YES" };
		[btnUpdates]   = { "FontBody"; "Safe"; false    ; "078"; false       ; false };	-- V6.0
		[btnGetHelp]   = { "FontBody"; "Safe"; false    ; "078"; false       ; false };
		[btnDestroy]   = { "FontBody"; "Risk"; false    ; "078"; false       ; false };

		[tblTree[IntFromPLAC].Vpane] = { "FontBody"; "Body"; "ACENTER"; };					-- Place/Individual Tree controls
		[tblTree[IntFromPLAC].BtnNo] = { "FontBody"; "Risk"; };
		[tblTree[IntFromPLAC].BtnEx] = { "FontBody"; "Body"; };
		[tblTree[IntFromPLAC].BtnOk] = { "FontBody"; "Safe"; };
		[tblTree[IntFromADDR].Vpane] = { "FontBody"; "Body"; "ACENTER"; };
		[tblTree[IntFromADDR].BtnNo] = { "FontBody"; "Risk"; };
		[tblTree[IntFromADDR].BtnEx] = { "FontBody"; "Body"; };
		[tblTree[IntFromADDR].BtnOk] = { "FontBody"; "Safe"; };
		[tblTree[IntFromBOTH].Vpane] = { "FontBody"; "Body"; "ACENTER"; };
		[tblTree[IntFromBOTH].BtnNo] = { "FontBody"; "Risk"; };
		[tblTree[IntFromBOTH].BtnEx] = { "FontBody"; "Body"; };
		[tblTree[IntFromBOTH].BtnOk] = { "FontBody"; "Safe"; };
		[tblTree[IntFromINDI].Vpane] = { "FontBody"; "Body"; "ACENTER"; };
		[tblTree[IntFromINDI].BtnNo] = { "FontBody"; "Risk"; };
		[tblTree[IntFromINDI].BtnEx] = { "FontBody"; "Body"; };
		[tblTree[IntFromINDI].BtnOk] = { "FontBody"; "Safe"; };
		}

	iup_gui.AssignAttributes(tblControls)							-- Assign control attributes

	if  IsGoogleKeyMissing()											-- Startup splash Geocoder Help and Warning Message for user without API Key -- V4.2 -- V4.3 -- V5.3
	and IsGeo_KeyMissing()
	and IsSplashOn == "API" then
		local strMessageText = "\nEvery user of this Plugin should obtain at least one personal 'API Key'\nto enable the 'Google Maps' or 'Geoapify' services.\n\nSee the 'Help && Advice' for 'Geocoding and Mapping' features and options.\n\nOtherwise, disable 'Google Maps' && 'Geoapify' and use 'Leaflet Maps' only.\n\n"
		repeat
			local intAnswer =
				iup_gui.WarnDialogue(									-- V4.2
					{ "API Key Required"     ; iup_gui.Warn; },
					{ strMessageText         ; iup_gui.Info; "API Key Required"; },
					{ "   Help && Advice   " ; iup_gui.Safe; "Show the Help and Advice page"; },
					{ " Set Google API Key " ; iup_gui.Safe; "Enter your personal 'API Key' for 'Google Maps'"; },			-- V5.3
					{ "Set Geoapify API Key" ; iup_gui.Safe; "Enter your personal 'API Key' for 'Geoapify'"; },				-- V5.3
					{ " Use Leaflet Maps "   ; iup_gui.Safe; "Disable 'Google & Geoapify API' and use 'Leaflet Maps'"; },	-- V5.3 -- V6.0
					{ " Close the Plugin "   ; iup_gui.Risk; "Close the Plugin immediately"; }
				)
			if intAnswer == 0 or intAnswer == 6 then return end	-- Close the Plugin 	-- V4.2
			if intAnswer == 1 then doGetHelp(4) end					-- Show Help & Advice	-- V4.2 -- V4.8
		until intAnswer >= 2 and intAnswer <= 5						-- V5.3
		IntWebMaps = DicMapMode.Leaflet								-- Enable Leaflet Maps	-- V4.3
		setMapMode(IntWebMaps)
		IntGeoPlot = DicGeoPlot.Disabled								-- Geocoder Disabled 	-- V5.3
		lstGeoPlot.Value = IntGeoPlot + 1
		IntTabPosn = IntTabPrefer
		tabControls.ValuePos = IntTabPosn - 1						-- Set tab and Help for Set Preference Options to enter API Key	-- V4.2
		doGetHelp(3)														-- V4.8
	end

	iup_gui.ShowDialogue("Main")										-- Now display resizable dialogue

end -- function GUI_MainDialogue

IntFhVersion = fhGetAppVersion()

function ConvertANSItoUTF8(strText)
	if IntFhVersion > 5 then			-- Fix for UTF8 characters -- V4.1
		return fhConvertANSItoUTF8(strText)
	end
	return strText
end -- function ConvertANSItoUTF8(strText)

TblRegBias = {							-- Geocoder Region Bias Top Level Domain Codes
	"        None";
	"ac Ascension Island";
	"ad Andorra";
	"ae United Arab Emirates";
	"af Afghanistan";
	"ag Antigua & Barbuda";
	"ai Anguilla";
	"al Albania";
	"am Armenia";
	"an Netherlands Antilles";
	"ao Angola";
	"aq Antarctica";
	"ar Argentina";
	"as American Samoa";
	"at Austria";
	"au Australia";
	"aw Aruba";
	ConvertANSItoUTF8("ax Åland");
	"az Azerbaijan";
	"ba Bosnia & Herzegovina";
	"bb Barbados";
	"bd Bangladesh";
	"be Belgium";
	"bf Burkina Faso";
	"bg Bulgaria";
	"bh Bahrain";
	"bi Burundi";
	"bj Benin";
	"bm Bermuda";
	"bn Brunei";
	"bo Bolivia";
	"br Brazil";
	"bs Bahamas";
	"bt Bhutan";
	"bv Bouvet Island";
	"bw Botswana";
	"by Belarus";
	"bz Belize";
	"ca Canada";
	"cc Cocos (Keeling) Islands";
	"cd Democratic Republic of the Congo";
	"cf Central African Republic";
	"cg Republic of the Congo";
	"ch Switzerland";
	ConvertANSItoUTF8("ci Côte d'Ivoire");
	"ck Cook Islands";
	"cl Chile";
	"cm Cameroon";
	"cn People's Republic of China";
	"co Colombia";
	"cr Costa Rica";
	"cs Czechoslovakia";
	"cu Cuba";
	"cv Cape Verde";
	"cx Christmas Island";
	"cy Cyprus";
	"cz Czech Republic";
	"dd East Germany";
	"de Germany";
	"dj Djibouti";
	"dk Denmark";
	"dm Dominica";
	"do Dominican Republic";
	"dz Algeria";
	"ec Ecuador";
	"ee Estonia";
	"eg Egypt";
	"eh Western Sahara";
	"er Eritrea";
	"es Spain";
	"et Ethiopia";
	"eu European Union";
	"fi Finland";
	"fj Fiji";
	"fk Falkland Islands";
	"fm Federated States of Micronesia";
	"fo Faroe Islands";
	"fr France";
	"ga Gabon";
	"gb Great Britain";
	"gd Grenada";
	"ge Georgia";
	"gf French Guiana";
	"gg Guernsey";
	"gh Ghana";
	"gi Gibraltar";
	"gl Greenland";
	"gm The Gambia";
	"gn Guinea";
	"gp Guadeloupe";
	"gq Equatorial Guinea";
	"gr Greece";
	"gs South Georgia & South Sandwich Islands";
	"gt Guatemala";
	"gu Guam";
	"gw Guinea-Bissau";
	"gy Guyana";
	"hk Hong Kong";
	"hm Heard Island & McDonald Islands";
	"hn Honduras";
	"hr Croatia";
	"ht Haiti";
	"hu Hungary";
	"id Indonesia";
	"ie Republic of Ireland & Northern Ireland";
	"il Israel";
	"im Isle of Man";
	"in India";
	"io British Indian Ocean Territory";
	"iq Iraq";
	"ir Iran";
	"is Iceland";
	"it Italy";
	"je Jersey";
	"jm Jamaica";
	"jo Jordan";
	"jp Japan";
	"ke Kenya";
	"kg Kyrgyzstan";
	"kh Cambodia";
	"ki Kiribati";
	"km Comoros";
	"kn Saint Kitts and Nevis";
	"kp Democratic People's Republic of Korea";
	"kr Republic of Korea";
	"kw Kuwait";
	"ky Cayman Islands";
	"kz Kazakhstan";
	"la Laos";
	"lb Lebanon";
	"lc Saint Lucia";
	"li Liechtenstein";
	"lk Sri Lanka";
	"lr Liberia";
	"ls Lesotho";
	"lt Lithuania";
	"lu Luxembourg";
	"lv Latvia";
	"ly Libya";
	"ma Morocco";
	"mc Monaco";
	"md Moldova";
	"me Montenegro";
	"mg Madagascar";
	"mh Marshall Islands";
	"mk Republic of Macedonia";
	"ml Mali";
	"mm Myanmar";
	"mn Mongolia";
	"mo Macau";
	"mp Northern Mariana Islands";
	"mq Martinique";
	"mr Mauritania";
	"ms Montserrat";
	"mt Malta";
	"mu Mauritius";
	"mv Maldives";
	"mw Malawi";
	"mx Mexico";
	"my Malaysia";
	"mz Mozambique";
	"na Namibia";
	"nc New Caledonia";
	"ne Niger";
	"nf Norfolk Island";
	"ng Nigeria";
	"ni Nicaragua";
	"nl Netherlands";
	"no Norway";
	"np Nepal";
	"nr Nauru";
	"nu Niue";
	"nz New Zealand";
	"om Oman";
	"pa Panama";
	"pe Peru";
	"pf French Polynesia";
	"pg Papua New Guinea";
	"ph Philippines";
	"pk Pakistan";
	"pl Poland";
	"pm Saint-Pierre & Miquelon";
	"pn Pitcairn Islands";
	"pr Puerto Rico";
	"ps Palestinian Territories";
	"pt Portugal";
	"pw Palau";
	"py Paraguay";
	"qa Qatar";
	ConvertANSItoUTF8("re Réunion");
	"ro Romania";
	"rs Serbia";
	"ru Russia";
	"rw Rwanda";
	"sa Saudi Arabia";
	"sb Solomon Islands";
	"sc Seychelles";
	"sd Sudan";
	"se Sweden";
	"sg Singapore";
	"sh Saint Helena";
	"si Slovenia";
	"sj Svalbard & Jan Mayen Islands";
	"sk Slovakia";
	"sl Sierra Leone";
	"sm San Marino";
	"sn Senegal";
	"so Somalia";
	"sr Suriname";
	"ss South Sudan";
	ConvertANSItoUTF8("st São Tomé & Príncipe");
	"su Soviet Union";
	"sv El Salvador";
	"sy Syria";
	"sz Swaziland";
	"tc Turks & Caicos Islands";
	"td Chad";
	"tf French Southern & Antarctic Lands";
	"tg Togo";
	"th Thailand";
	"tj Tajikistan";
	"tk Tokelau";
	"tl East Timor";
	"tm Turkmenistan";
	"tn Tunisia";
	"to Tonga";
	"tp East Timor";
	"tr Turkey";
	"tt Trinidad & Tobago";
	"tv Tuvalu";
	"tw Republic of China (Taiwan)";
	"tz Tanzania";
	"ua Ukraine";
	"ug Uganda";
	"uk United Kingdom";
	"us United States of America";
	"uy Uruguay";
	"uz Uzbekistan";
	"va Vatican City";
	"vc Saint Vincent & Grenadines";
	"ve Venezuela";
	"vg British Virgin Islands";
	"vi United States Virgin Islands";
	"vn Vietnam";
	"vu Vanuatu";
	"wf Wallis & Futuna";
	"ws Samoa";
	"ye Yemen";
	"yt Mayotte";
	"yu Yugoslavia";
	"za South Africa";
	"zm Zambia";
	"zw Zimbabwe";
}

TblNation = {								-- Nation recognition to resolve smart code conflicts -- V5.0

	["canada"]			= "America";	-- America	invokes TblLocCode.America below
	["ca"]					= "America";
	["united states"]	= "America";
	["us"]					= "America";

	["australia"]		= "Austral";		-- Australia	invokes TblLocCode.Austral below
	["au"]					= "Austral";

	["united kingdom"]	= "British";	-- British	invokes TblLocCode.British below
	["uk"]					= "British";
	["britain"]			= "British";
	["england"]			= "British";
	["ireland"]			= "British";
	["isle of man"]		= "British";
	["scotland"]			= "British";
	["wales"]				= "British";
	["channel islands"]	= "British";
	["alderney"]			= "British";
	["guernsey"]			= "British";
	["jersey"]			= "British";
	["sark"]				= "British";
}

TblLocCode = {							-- Geocoder Location Code Translations	(For conflicts, see later national resolution tables)

	Default = {
		ABW="Aruba";						-- Country Codes from	http://en.wikipedia.org/wiki/ISO_3166-1_alpha-3	(ISO 3166-1 alpha-3 codes)
		AFG="Afghanistan";
		AGO="Angola";
		AIA="Anguilla";
		ALA=ConvertANSItoUTF8("Åland Islands");
		ALB="Albania";
		AND="Andorra";
		ARE="United Arab Emirates";
		ARG="Argentina";
		ARM="Armenia";					-- Conflict with ARM="Armagh" Chapman Code
		ASC="Ascension Island";			-- Exceptional Reservation
		ASM="American Samoa";
		ATA="Antarctica";
		ATF="French Southern Territories";
		ATG="Antigua and Barbuda";
		AUS="Australia";
		AUT="Austria";
		AZE="Azerbaijan";
		BDI="Burundi";
		BEL="Belgium";
		BEN="Benin";
		BES="Bonaire, Sint Eustatius and Saba";
		BFA="Burkina Faso";
		BGD="Bangladesh";
		BGR="Bulgaria";
		BHR="Bahrain";
		BHS="Bahamas";
		BIH="Bosnia and Herzegovina";
		BLM=ConvertANSItoUTF8("Saint Barthélemy");
		BLR="Belarus";
		BLZ="Belize";
		BMU="Bermuda";
		BOL="Bolivia";					-- Plurinational State of
		BRA="Brazil";
		BRB="Barbados";
		BRN="Brunei Darussalam";
		BTN="Bhutan";
		BVT="Bouvet Island";
		BWA="Botswana";
		CAF="Central African Republic";
		CAN="Canada";
		CCK="Cocos (Keeling) Islands";
		CHE="Switzerland";
		CHL="Chile";
		CHN="China";
		CIV=ConvertANSItoUTF8("Côte d'Ivoire");
		CMR="Cameroon";
		COD="Democratic Republic of the Congo";
		COG="Congo";
		COK="Cook Islands";
		COL="Colombia";
		COM="Comoros";
		CPT="Clipperton Island";		-- Exceptional Reservation
		CPV="Cape Verde";
		CRI="Costa Rica";
		CUB="Cuba";
		CUW=ConvertANSItoUTF8("Curaçao");
		CXR="Christmas Island";
		CYM="Cayman Islands";
		CYP="Cyprus";
		CZE="Czech Republic";
		DEU="Germany";
		DGA="Diego Garcia";				-- Exceptional Reservation
		DJI="Djibouti";
		DMA="Dominica";
		DNK="Denmark";
		DOM="Dominican Republic";
		DZA="Algeria";
		ECU="Ecuador";
		EGY="Egypt";
		ERI="Eritrea";
		ESH="Western Sahara";
		ESP="Spain";
		EST="Estonia";
		ETH="Ethiopia";
		FIN="Finland";
		FJI="Fiji";
		FLK="Falkland Islands (Malvinas)";
		FRA="France";
		FRO="Faroe Islands";
		FSM="Micronesia";				-- Federated States of
		FXX="France";						-- Metropolitan			-- Exceptional Reservation
		GAB="Gabon";
		GBR="United Kingdom";
		GEO="Georgia (country)";
		GGY="Guernsey";
		GHA="Ghana";
		GIB="Gibraltar";
		GIN="Guinea";
		GLP="Guadeloupe";
		GMB="Gambia";
		GNB="Guinea-Bissau";
		GNQ="Equatorial Guinea";
		GRC="Greece";
		GRD="Grenada";
		GRL="Greenland";
		GTM="Guatemala";
		GUF="French Guiana";
		GUM="Guam";
		GUY="Guyana";
		HKG="Hong Kong";
		HMD="Heard Island and McDonald Islands";
		HND="Honduras";
		HRV="Croatia";
		HTI="Haiti";
		HUN="Hungary";					-- Conflict with HUN="Huntingdonshire" Chapman Code
		IDN="Indonesia";
		IMN="Isle of Man";
		IND="India";
		IOT="British Indian Ocean Territory";
		IRL="Ireland";					-- Equals Chapman Country Code
		IRN="Iran";						-- Islamic Republic of
		IRQ="Iraq";
		ISL="Iceland";
		ISR="Israel";
		ITA="Italy";
		JAM="Jamaica";
		JEY="Jersey";
		JOR="Jordan";
		JPN="Japan";
		KAZ="Kazakhstan";
		KEN="Kenya";						-- Conflict with KEN="Kent" Chapman Code
		KGZ="Kyrgyzstan";
		KHM="Cambodia";
		KIR="Kiribati";
		KNA="Saint Kitts and Nevis";
		KOR="South Korea";				-- Republic of Korea
		KWT="Kuwait";
		LAO="Laos";						-- Lao People's Democratic Republic
		LBN="Lebanon";
		LBR="Liberia";
		LBY="Libya";
		LCA="Saint Lucia";
		LIE="Liechtenstein";
		LKA="Sri Lanka";
		LSO="Lesotho";
		LTU="Lithuania";
		LUX="Luxembourg";
		LVA="Latvia";
		MAC="Macao";
		MAF="Saint Martin";				-- (French part)
		MAR="Morocco";
		MCO="Monaco";
		MDA="Moldova";					-- Republic of
		MDG="Madagascar";
		MDV="Maldives";
		MEX="Mexico";
		MHL="Marshall Islands";
		MKD="Macedonia";					-- the former Yugoslav Republic of
		MLI="Mali";
		MLT="Malta";
		MMR="Myanmar";
		MNE="Montenegro";
		MNG="Mongolia";
		MNP="Northern Mariana Islands";
		MOZ="Mozambique";
		MRT="Mauritania";
		MSR="Montserrat";
		MTQ="Martinique";
		MUS="Mauritius";
		MWI="Malawi";
		MYS="Malaysia";
		MYT="Mayotte";
		NAM="Namibia";
		NCL="New Caledonia";
		NER="Niger";
		NFK="Norfolk Island";			-- Conflict with NFK="Norfolk" Chapman Code
		NGA="Nigeria";
		NIC="Nicaragua";
		NIU="Niue";
		NLD="Netherlands";
		NOR="Norway";
		NPL="Nepal";
		NRU="Nauru";
		NZL="New Zealand";
		OMN="Oman";
		PAK="Pakistan";
		PAN="Panama";
		PCN="Pitcairn";
		PER="Peru";						-- Conflict with PER="Perthshire" Chapman Code
		PHL="Philippines";
		PLW="Palau";
		PNG="Papua New Guinea";
		POL="Poland";
		PRI="Puerto Rico";
		PRK="North Korea";				-- Democratic People's Republic of Korea
		PRT="Portugal";
		PRY="Paraguay";
		PSE="Palestine";					-- Palestinian Territory, Occupied
		PYF="French Polynesia";
		QAT="Qatar";
		REU=ConvertANSItoUTF8("Réunion");
		ROU="Romania";
		RUS="Russian Federation";
		RWA="Rwanda";
		SAU="Saudi Arabia";
		SDN="Sudan";
		SEN="Senegal";
		SGP="Singapore";
		SGS="South Georgia and the South Sandwich Islands";
		SHN="Ascension, Saint Helena";-- Saint Helena, Ascension and Tristan da Cunha	see TAA
		SJM="Svalbard and Jan Mayen";
		SLB="Solomon Islands";
		SLE="Sierra Leone";
		SLV="El Salvador";
		SMR="San Marino";
		SOM="Somalia";					-- Conflict with SOM="Somerset" Chapman Code
		SPM="Saint Pierre and Miquelon";
		SRB="Serbia";
		SSD="South Sudan";
		STP="Sao Tome and Principe";
		SUN="USSR Russia";				-- Exceptional Reservation
		SUR="Suriname";
		SVK="Slovakia";
		SVN="Slovenia";
		SWE="Sweden";
		SWZ="Swaziland";
		SXM="Sint Maarten";				-- (Dutch part)
		SYC="Seychelles";
		SYR="Syrian Arab Republic";
		TAA="Tristan da Cunha";			-- Exceptional Reservation		see SHN
		TCA="Turks and Caicos Islands";
		TCD="Chad";
		TGO="Togo";
		THA="Thailand";
		TJK="Tajikistan";
		TKL="Tokelau";
		TKM="Turkmenistan";
		TLS="Timor-Leste";
		TON="Tonga";
		TTO="Trinidad and Tobago";
		TUN="Tunisia";
		TUR="Turkey";
		TUV="Tuvalu";
		TWN="Taiwan";						-- Province of China
		TZA="Tanzania";					-- United Republic of
		UGA="Uganda";
		UKR="Ukraine";
		UMI="United States Minor Outlying Islands";
		URY="Uruguay";
		USA="United States";
		UZB="Uzbekistan";
		VAT="Vatican City";				-- Holy See (Vatican City State)
		VCT="Saint Vincent and the Grenadines";
		VEN="Venezuela";					-- Bolivarian Republic of
		VGB="British Virgin Islands";
		VIR="U.S. Virgin Islands";
		VNM="Viet Nam";
		VUT="Vanuatu";
		WLF="Wallis and Futuna";
		WSM="Samoa";
		YEM="Yemen";
		ZAF="South Africa";
		ZMB="Zambia";
		ZWE="Zimbabwe";

											-- United States Postal Service State Abbreviations from	http://en.wikipedia.org/wiki/List_of_U.S._state_abbreviations
		AL="Alabama";
		AK="Alaska";
		AZ="Arizona";
		AR="Arkansas";
		CA="California";
		CO="Colorado";
		CT="Connecticut";
		DE="Delaware";
		DC="District of Columbia";
		FL="Florida";
		GA="Georgia";
		HI="Hawaii";
		ID="Idaho";
		IL="Illinois";
		IN="Indiana";
		IA="Iowa";
		KS="Kansas";
		KY="Kentucky";
		LA="Louisiana";
		ME="Maine";
		MD="Maryland";
		MA="Massachusetts";
		MI="Michigan";
		MN="Minnesota";
		MS="Mississippi";
		MO="Missouri";
		MT="Montana";
		NE="Nebraska";
		NV="Nevada";
		NH="New Hampshire";
		NJ="New Jersey";
		NM="New Mexico";
		NY="New York State";
		NC="North Carolina";
		ND="North Dakota";
		OH="Ohio";
		OK="Oklahoma";
		OR="Oregon";
		PA="Pennsylvania";
		RI="Rhode Island";
		SC="South Carolina";
		SD="South Dakota";
		TN="Tennessee";
		TX="Texas";
		UT="Utah";
		VT="Vermont";
		VA="Virginia";
		WA="Washington State";			-- Conflict with WA="Western Australia" Australian State Abbreviation
		WV="West Virginia";
		WI="Wisconsin";
		WY="Wyoming";
		AS="American Samoa";								-- Insular Areas:
		GU="Guam";
		MP="Northern Mariana Islands";
		PR="Puerto Rico";
		VI="Virgin Islands";
		UM="United States Minor Outlying Islands";
		FM="Federated States of Micronesia";				-- Freely Associated States:
		MH="Marshall Islands";
		PW="Palau";
--!		AA="Armed Forces - Americas (except Canada)";	-- US Military Mail:
--!		AE="Armed Forces - Europe, Canada, Middle East, Africa";
--!		AP="Armed Forces - Pacific";
		CZ="Panama Canal";				-- "Canal Zone"	-- Obsolete Codes:
		PI="Philippines Islands";
		TT="Pacific Islands";			-- Trust Territory of the
		CM="Commonwealth of the Northern Mariana Islands";

											-- Talk:List of U.S. State Abbreviations	http://en.wikipedia.org/wiki/Talk:List_of_U.S._state_abbreviations
--!		IA="Indiana";						-- Conflict with IA="Iowa" above
		WN="Washington State";

											-- Canadian Province Abbreviations from	http://canadaonline.about.com/od/postalservices/a/abbreviations-provinces-canada.htm?rd=1
		AB="Alberta";
		BC="British Columbia";
		MB="Manitoba";
		NB="New Brunswick, Canada";
		NL="Newfoundland and Labrador";
		NT="Northwest Territories";	-- Conflict with NT="Northern Territory" Australian State
		NS="Nova Scotia";
		NU="Nunavut";
		ON="Ontario";
		PE="Prince Edward Island";
		QC="Quebec";
		SK="Saskatchewan";
		YT="Yukon";

											-- Chapman Codes from	http://en.wikipedia.org/wiki/Chapman_code			(superset of ISO 3166-2:GB & BS 6879)
		CHI="Channel Islands";			-- Country Codes
		ENG="England";
		IOM="Isle of Man";
		IRL="Ireland";					-- Equals Country Code ISO_3166
		SCT="Scotland";
		WLS="Wales";
		ALL="";							-- All countries

		ALD="Alderney";					-- Channel Islands
		GSY="Guernsey";
		JSY="Jersey";
		SRK="Sark";
	};

	America = {							-- American specific codes
		NT="Northwest Territories";	-- Conflict with NT="Northern Territory" Australian State
		WA="Washington State";			-- Conflict with WA="Western Australia" Australian State Abbreviation
	};

	Austral = {							-- Australian State Abbreviations from 	http://en.wikipedia.org/wiki/States_and_territories_of_Australia	(Postal codes)	-- V5.0
		ACT="Australian Capital Territory";
		JBT="Jervis Bay Territory";
		NSW="New South Wales";
		NT ="Northern Territory";		-- Conflict with NT="Northwest Territories" Canadian Province
		QLD="Queensland";
		SA ="South Australia";
		TAS="Tasmania";
		VIC="Victoria, Australia";		-- Otherwise "Victoria, British Columbia" in Canada is chosen
		WA ="Western Australia";		-- Conflict with WA="Washington State" US Postal Service State Abbreviation
	};

	British = {							-- British Chapman Codes & Postal County Abbreviations -- V5.0
		BDF="Bedfordshire, UK";			-- England Ancient Counties
		BRK="Berkshire, UK";
		BKM="Buckinghamshire, UK";
		CAM="Cambridgeshire, UK";
		CHS="Cheshire County, UK";
		CON="Cornwall, UK";
		CUL="Cumbria, UK";				-- "Cumberland" not recognised by Geocoder
		DBY="Derbyshire, UK";
		DEV="Devonshire, UK";
		DOR="Dorset, UK";
		DUR="County Durham, UK";
		ESS="Essex, UK";
		GLS="Gloucestershire, UK";
		HAM="Hampshire, UK";
		HEF="Herefordshire, UK";
		HRT="Hertfordshire, UK";
		HUN="Huntingdonshire, UK";		-- Conflict with HUN="Hungary" Country Code
		IOW="Isle of Wight, UK";
		KEN="Kent, UK";					-- Conflict with KEN="Kenya" Country Code
		LAN="Lancashire, UK";
		LEI="Leicestershire, UK";
		LIN="Lincolnshire, UK";
		LND="London, UK";				-- (City only)
		MDX="London, UK";				-- "Middlesex" not recognised by Geocoder
		NFK="Norfolk, UK";				-- Conflict with NFK="Norfolk Island" Country Code
		NTH="Northamptonshire, UK";
		NBL="Northumberland, UK";
		NTT="Nottinghamshire, UK";
		OXF="Oxfordshire, UK";
		RUT="Rutland, UK";
		SAL="Shropshire, UK";
		SOM="Somerset, UK";				-- Conflict with SOM="Somalia" Country Code
		STS="Staffordshire, UK";
		SFK="Suffolk, UK";
		SRY="Surrey, UK";
		SSX="Sussex, UK";
		WAR="Warwickshire, UK";
		WES="Cumbria, UK";				-- "Westmorland" not recognised by Geocoder
		WIL="Wiltshire, UK";
		WOR="Worcestershire, UK";
		YKS="Yorkshire, England";
		ERY="Yorkshire, England";		-- East Riding
		NRY="Yorkshire, England";		-- North Riding
		WRY="Yorkshire, England";		-- West Riding

		ABD="Aberdeenshire, UK";		-- Scotland Ancient Counties
		ANS="Angus, UK";					-- (formerly Forfarshire)
		ARL="Argyll County, UK";		-- "Argyllshire" not recognised by Geocoder
		AYR="Ayrshire, UK";
		BAN="Moray, UK";					-- "Banffshire" not recognised by Geocoder
		BEW="Berwickshire, UK";
		BUT="Bute, UK";					-- (Buteshire)
		CAI="Caithness, UK";
		CLK="Clackmannanshire, UK";
		DFS="Dumfriesshire, UK";
		DNB="Dunbartonshire, UK";
		ELN="East Lothian, UK";			-- (formerly Haddingtonshire)
		FIF="Fife, UK";
		INV="Inverness,Highland,UK";	-- "Inverness-shire" not recognised by Geocoder with Fort Augustus
		KCD="Aberdeenshire, UK";		-- "Kincardineshire" not recognised by Geocoder
		KRS="Kinrossshire, UK";
		KKD="Kirkcudbright, UK";		-- "Kirkcudbrightshire"
		LKS="Lanarkshire, UK";
		MLN="Midlothian, UK";			-- (formerly Edinburghshire)
		MOR="Moray, UK";					-- (formerly Elginshire)
		NAI="Nairnshire, UK";
		OKI="Orkney Isles, UK";
		PEE="Peebles, UK";
		PER="Perthshire, UK";			-- Conflict with PER="Peru" Country Code
		RFW="Renfrewshire, UK";
		ROC="Ross, Highland, UK";		-- "Ross and Cromarty" not recognised by Geocoder
		ROX="Scottish Borders, UK";	-- "Roxburghshire"
		SEL="Selkirkshire, UK";
		SHI="Shetland Isles, UK";
		STI="Stirlingshire, UK";
		SUT="Highland, UK";				-- "Sutherland" not recognised by Geocoder
		WLN="West Lothian, UK";			-- (formerly Linlithgowshire)
		WIG="Galloway, Scotland,UK";	-- "Wigtownshire" not recognised by Geocoder

		BOR="Borders, Scotland, UK";	-- Scotland 1975-1996 Regions
		CEN="Central Region, Scotland, UK";
		DGY="Dumfries and Galloway, UK";
		FIF="Fife, UK";					-- Duplicates Ancient County
		GMP="Aberdeenshire, UK";		-- "Grampian" not recognised by Geocoder
		HLD="Highland, UK";
		LTN="Lothian, UK";
		OKI="Orkney Isles, UK";			-- Duplicates Ancient County
		SHI="Shetland Isles, UK";		-- Duplicates Ancient County
		STD="Scotland, UK";				-- "Strathclyde" not recognised by Geocoder
		TAY="Perthshire, UK";			-- "Tayside" not recognised by Geocoder
		WIS="Outer Hebrides, UK";		-- "Western Isles" not recognised by Geocoder

		AGY="Anglesey, UK";				-- Wales Ancient Counties
		BRE="Breconshire, UK";			-- "Brecknock" & "Brecknockshire" not recognised by Geocoder
		CAE="Caernarvon, Wales, UK";	-- "Caernarfonshire" not recognised by Geocoder
		CGN="Cardiganshire, UK";
		CMN="Carmarthenshire, UK";
		DEN="Denbighshire, UK";
		FLN="Flintshire, UK";
		GLA="Glamorgan, UK";
		MER="Merionethshire, UK";
		MON="Monmouthshire, UK";
		MGY="Montgomeryshire, UK";
		PEM="Pembrokeshire, UK";
		RAD="Radnorshire, UK";

		CWD="Clwyd, Wales, UK";			-- Wales 1974-1996
		DFD="Dyfed, Wales, UK";
		GNT="Gwent, Wales, UK";
		GWN="Gwynedd, Wales, UK";
		MGM="Mid Glamorgan, Wales, UK";
		POW="Powys, Wales, UK";
		SGM="South Glamorgan, Wales, UK";
		WGM="Glamorgan, Wales, UK";	-- "West Glamorgan" not recognised by Geocoder

		ANT="Antrim County, UK";		-- Ireland
		ARM="Armagh County, UK";		-- Conflict with ARM="Armenia" Country Code
		CAR="Carlow County, Ireland";
		CAV="Cavan County, Ireland";
		CLA="Clare County, Ireland";
		COR="Cork County, Ireland";
		DON="Donegal County, Ireland";
		DOW="Down County, UK";
		DUB="Dublin County, Ireland";
		FER="Fermanagh County, UK";
		GAL="Galway County, Ireland";
		KER="Kerry County, Ireland";
		KID="Kildare County, Ireland";
		KIK="Kilkenny County, Ireland";
		LET="Leitrim County, Ireland";
		LEX="Laois County, Ireland";	-- "Leix" (formerly Queen's)
		LIM="Limerick County, Ireland";
		LDY="Londonderry County, UK";
		LOG="Longford County, Ireland";
		LOU="Louth County, Ireland";
		MAY="Mayo County, Ireland";
		MEA="Meath County, Ireland";
		MOG="Monaghan County, Ireland";
		OFF="Offaly County, Ireland";	-- (formerly King's)
		ROS="Roscommon County, Ireland";
		SLI="Sligo County, Ireland";
		TIP="Tipperary County, Ireland";
		TYR="Tyrone County, UK";
		WAT="Waterford County, Ireland";
		WEM="Westmeath County, Ireland";
		WEX="Wexford County, Ireland";
		WIC="Wicklow County, Ireland";

											-- UK Postal County Abbreviations subset from	http://en.wikipedia.org/wiki/Postal_counties_of_the_United_Kingdom#Postal_counties_in_operation_from_1974_to_1996
		BEDS		="Bedfordshire, UK";
		BERKS		="Berkshire, UK";
		BUCKS		="Buckinghamshire, UK";
		CAMBS		="Cambridgeshire, UK";
		GLOS		="Gloucestershire, UK";
		HANTS		="Hampshire, UK";
		HERTS		="Hertfordshire, UK";
		HUMBS		="Humberside, UK";			-- N Humbs & S Humbs
		LANCS		="Lancashire, UK";
		LEICS		="Leicestershire, UK";
		LINCS		="Lincolnshire, UK";
		MIDDX		="London, UK";
		NORTHANTS	="Northamptonshire, UK";
		NORTHD		="Northumberland, UK";
		NOTTS		="Nottinghamshire, UK";
		OXON		="Oxfordshire, UK";
		SALOP		="Shropshire, UK";
		STAFFS		="Staffordshire, UK";
		WARKS		="Warwickshire, UK";
		WILTS		="Wiltshire, UK";
		WORCS		="Worcestershire, UK";
		YORKS		="Yorkshire, England"; 		-- N Yorks & S Yorks & W Yorks
		GLAM		="Glamorgan, Wales, UK";	-- M Glam & S Glam & W Glam
	};

	Other = { };							-- Cater for any other unknown Nation
}

TblGeoCode = {						-- Geocoder special translations to improve plotting
										-- UK County Substitutions that tend to work better with Google Geocoder
	{ "^Ecclesall Bierlow"		;	"Ecclesall";	};
	{ "^East%s.*Yorkshire"		;	"Yorkshire, England";	};
	{ "^North%s.*Yorkshire"		;	"Yorkshire, England";	};
	{ "^South%s.*Yorkshire"		;	"Yorkshire, England";	};
	{ "^West%s.*Yorkshire"		;	"Yorkshire, England";	};
	{ "^Yorkshire.*East%s.*"	;	"Yorkshire, England";	};
	{ "^Yorkshire.*North%s.*"	;	"Yorkshire, England";	};
	{ "^Yorkshire.*South%s.*"	;	"Yorkshire, England";	};
	{ "^Yorkshire.*West%s.*"	;	"Yorkshire, England";	};
}

-- Global Script Definitions --										-- V5.1 -- Hex encoded HTM, CSS, JS & HTML files

-- HTM script template for managing the Plugin map pane
StrMasterHTM = [=[
<!DOCTYPE html>
<html>
  <head>
    <title>{plugin}</title>
    <meta name='viewport' content='initial-scale=1.0,user-scalable=no'>
    <meta http-equiv='x-ua-compatible' content='IE=edge'>
    <meta http-equiv='content-type' content='text/html; charset=utf-8'>
    <link type='text/css' rel='stylesheet' href='mapfacts.css'>
    <script type='text/javascript' src='mapfacts.js'> </script>
    <script type='text/javascript' language='JavaScript'>
    function Initialise()											// Initialisation function	-- V4.5 invoked by &callback or window.onload in StrMasterJS shared JavaScript
    {
      MapMark = new Array();										// Map Marker overlays
      LatLong = new Array();										// Map LatLng positions
      LocName = new Array();										// InfoWindow location place name	-- V4.2
      Caption = new Array();										// Marker caption title tooltips
      InfoWin = new Array();										// Marker popup InfoWindow facts for Google & Leaflet Maps
      CheckOnline(												 	// Check if PC is online
        function(){ OfflineError('map'); },
        function(){ OnlineAction(); }
      );
    }
    function OnlineAction()
    {
      SynOld = '';													// Synchronisation old char
      SynNew = '';													// Synchronisation new char
      Action = '';													// Action command code e.g. 'MARK', 'MOVE', 'NULL', 'PLOT', 'DROP', 'ZOOM'
      Value1 = '';													// Latitude or Drop animation or Zoom level
      Value2 = '';													// Longitude or Drop delay
      Value3 = '';													// Location Address/Place
      Value4 = '';													// Caption Address/Place	-- V4.2
      Value5 = '';													// Recorded Facts text
      BanZoom = 1;													// Banned map Zoom level to exclude from reports
      SetZoom = 1;													// Current map Zoom level
      SetZoomLevel({setzoom});									// Integer between 0 and 22
      MarkZoom = SetZoom;											// Default zoom level for single 'MARK' Markers
      Animation = null;												// Marker animation mode
      SetAnimation('{animate}');									// 'null'=No Animation, or 'DROP'=Synchro/Cascade
      SetPause = 0;													// Pause for 0ms before dropping Markers
      SetDelay = 0;													// Marker delay of 0ms=Immediate, ~300ms=No Animation/Synchro, or ~2000ms=Cascade
      SetBound = null;												// Bounds offset to fit Markers well within Map -- V4.3
      SetWait = 250;												// Wait between checks for new action file
      Greenwich = NewLatLong(51.5,0.0);							// Greenwich = (51.5,0.0)
      Home = NewLatLong(5.65,0.0);								// Tema, Accra, Ghana is nearest urban area to (0.0,0.0) N.B. Greenwich = (51.5,0.0)
      MakeMapPane('Plugin','{basemap}');						// Make the map pane display for 'Plugin' mode and Leaflet Base Map Layer
      xmlDoc = new ActiveXObject('Microsoft.XMLDOM');			// or ActiveXObject('msxml2.DOMDocument.6.0'); 
      xmlDoc.async=false;											// See http://msdn.microsoft.com/en-us/library/windows/desktop/ms766487%28v=VS.85%29.aspx
      LoadFile('{plugin}.act');
    }
    function LoadFile(filename)
    {
      var status = xmlDoc.load(filename);						// Load the input action file
      if ( status )
      {
        if ( xmlDoc.parseError.errorCode == 0 )
        {
          var line = xmlDoc.xml.split('\n');					// Split action file into lines & remove carriage returns
          SynNew = line[1].replace('\r','');					// Synchronisation char
          Action = line[2].replace('\r','');					// Action command code
          Value1 = line[3].replace('\r','');					// Latitude or Drop delay or Zoom level
          Value2 = line[4].replace('\r','');					// Longitude or Drop animation style
          Value3 = line[5].replace('\r','');					// Location Address/Place or Bounds offset
          Value4 = line[6].replace('\r','');					// Caption Address/Place	-- V4.2
          Value5 = line[7].replace('\r','');					// Recorded Facts text
          if ( SynOld != SynNew )
          {
            SynOld = SynNew;
            DoAction();												// After synchronisation changes, perform the action
          }
          else														// Wait for new action file to appear
          {
            setTimeout(function(){LoadFile(filename);},SetWait);
          }
        }
        else
        {
          var myErr = xmlDoc.parseError;						// Input action file has errors
          SendStatus('You have error ' + myErr.reason);
        }
      }
      else
      {
        setTimeout(function(){LoadFile(filename);},SetWait);// Wait for new action file to appear
      } 
    }
    function DoAction()												// Perform each action command
    {
      switch (Action)
      {
      case 'MARK':													// MARK the Geocoded location
        RemoveMarkers();
        if ( isNaN(parseFloat(Value1)) || isNaN(parseFloat(Value2)) )
        {
          BanZoom = 1; 												// Lat/Long missing so Zoom out and pan to Home to show whole world
          PanTo(Home,1);
        }
        else
        {																// Mark supplied Latitude & Longitude with popup InfoWindow and tooltip Caption
          LatLong[0] = NewLatLong(Value1,Value2);
          LocName[0] = Value3;									// -- V4.2
          Caption[0] = Value4;									// -- V4.2
          InfoWin[0] = 'Recorded Fact(s) at';
          SetDelay = 0;
          MakeMarkers();
          PanTo(LatLong[0],MarkZoom);							// Zoom in and pan to Marker
          DropMarker(0);
          BanZoom = 0;
        }
        SetWait = 250;												// Increase wait between checks for new action file
        break;

      case 'MOVE':													// MOVE existing Marker without animation or zoom
        if ( isNaN(parseFloat(Value1)) || isNaN(parseFloat(Value2)) )
        {
          BanZoom = 1; 												// Lat/Long missing so Zoom out and pan to Home to show whole world
          PanTo(Home,1);
        }
        else
        {
          LatLong[0] = NewLatLong(Value1,Value2);				// Move to supplied Latitude & Longitude
          MoveMarker(0);
        }
        SetWait = 250;												// Increase wait between checks for new action file
        break;

      case 'NULL':													// NULLify all existing Markers
        RemoveMarkers();
        SetWait = 50;												// Reduce wait between checks for new action file
        break;

      case 'PLOT':													// PLOT details for multiple Markers
        var iEntry = LatLong.length;
        LatLong[iEntry] = NewLatLong(Value1,Value2);
        LocName[iEntry] = Value3;									// -- V4.2
        Caption[iEntry] = Value4;
        InfoWin[iEntry] = Value5;									// -- V4.2
        break;

      case 'DROP':													// DROP multiple Markers in chosen style
        SetAnimation(Value1);										// Set Marker Animation mode Value1 is 'null' for No Animation, or 'DROP' for Synchro/Cascade
        SetDelay = parseInt(Value2);								// Value2 is ~300 for No Animation/Synchro, or ~2000 for Cascade
        if ( isNaN(SetDelay) ) { SetDelay = 300; }
        SetBound = parseFloat(Value3);							// Value3 is Bound offset -- V4.3
        BanZoom = 99;
        MakeMarkers();
        PresentMap();
        GetZoomLevel();
        BanZoom = GetZoom;
        SetWait = SetDelay;										// Increase wait between checks for new action file	-- V6.0
        break;

      case 'ZOOM':													// ZOOM to chosen map Zoom level
        SetZoomLevel(Value1);
        NewZoom(SetZoom);											// -- V5.3
        MarkZoom = SetZoom;										// Remember 'MARK' RepresentMap() Zoom level
        BanZoom = 0;
        SetWait = 250;												// Increase wait between checks for new action file
        break;
      }
      SendZoom(SetZoom);											// Send synch char + map zoom level via HTML page
      LoadFile('{plugin}.act');
    }
    function RemoveMarkers()										// Remove all Marker and LatLong entries from map
    {
      for ( var iEntry = 0; iEntry < LatLong.length; iEntry++ )
      {
        RubMarker(iEntry);
      }
      MapMark.length = 0;
      LatLong.length = 0;
    }
    function GetZoomLevel()										// Get & send map Zoom level change
    {
      GetZoom = OldZoom();											// -- V5.3
      if ( GetZoom>0 && BanZoom<30 && GetZoom!=BanZoom )		// Inhibit values after MARK missing Lat/Lng and DROP sample Markers
      {
        SetZoom = GetZoom;				
        SendZoom(SetZoom);											// Send synch char + map zoom level via HTML page
        BanZoom = GetZoom;
      }
    }
    </script>
    <style>
      body { overflow: hidden; }
    </style>
  </head>
  <body scroll='no'>
    <div id='map' style='left: 1%; {mappane}'>					<!–– Google/Leaflet Map Pane -- V5.3 ––>
    </div>
    <div id='status' style='width:100%; height:0%;' hidden>	<!–– Status is hidden underneath map ––>
      <p>~</p>
    </div>
  </body>
</html>
]=]

-- mapfacts.css lines derived from fhstyle.css except for #facts, #map, @keyframes, and firefox scrollbar fix	-- V3.7 -- V3.9 -- V4.3
-- html, body style from https://developers.google.com/maps/documentation/javascript/examples/default.css needed for both Google and Leaflet map modes	-- V4.3
-- These apply for standalone map pages but are overridden by FH web page <head> style
StrMasterCSS = [=[
body {padding-right: 0px; padding-left: 0px; font-size: 80%; padding-bottom: 0px; margin: 0px; color: #000000; padding-top: 0px; font-family: Verdana, Arial, sans-serif; background-color: #FFFFFF; }
html, body {height: 100%; margin: 0; padding: 0; }
a:link {color: #808080; font-weight: bold; text-decoration: none; }
a:visited {color: #808080; font-weight: bold; text-decoration: none; }
a:hover { color: #515151; font-weight: bold; text-decoration: none; }
a:active { color: #515151; font-weight: bold; text-decoration: none; }
.fhcontent { margin-left: 20px; margin-right: 20px; margin-top: 20px; margin-bottom: 20px; }
.FhHdg1 { clear: both; height: auto; font-size: 20pt; font-weight: 700; border-style: solid; border-top-width: 1pt; border-right-width: 0px; border-bottom-width: 1pt; border-left-width: 0px; margin-top: 24pt; margin-bottom: 8pt; font-family: Verdana, Arial, Helvetica, sans-serif; color: #000000; }
#facts	{width: 49%; float: left; padding-right: 1%; }
#map	{width: 49%; height: 80%; position: absolute; right: 1%; border: 1px solid grey; }
.noinfoscrollbar {line-height: 1.35; overflow: hidden; white-space: nowrap; }
/* Leaflet Maps Animation */
@keyframes drop-L { from { top: -400px; opacity: 1; } to { top: 0px; opacity: 1; } }
.DROP-L {animation: drop-L 0.5s 1; }
@keyframes flash-L { from { opacity: 0.1; } to { opacity: 1.0; } }
.FLASH-L {animation: flash-L 0.3s 6 alternate; }
@keyframes bounce-L { from { top: 0px; }  to  { top: -20px; } }
.BOUNCE-L {animation: bounce-L 0.3s cubic-bezier(0.5,0.05,1,0.5) 2 alternate; }
/* Google Maps Animation */
/* set the default transition time */
:root {
    --delay-time: 0.5s;
}
@keyframes drop-G {
    0% {
        transform: translateY(-200px) scaleY(0.9);
        opacity: 0;
    }
    5% {
        opacity: 0.7;
    }
    50% {
        transform: translateY(0px) scaleY(1);
        opacity: 1;
    }
    65% {
        transform: translateY(-17px) scaleY(0.9);
        opacity: 1;
    }
    75% {
        transform: translateY(-22px) scaleY(0.9);
        opacity: 1;
    }
    100% {
        transform: translateY(0px) scaleY(1);
        opacity: 1;
    }
}
.DROP-G {
    animation: drop-G 0.3s linear forwards var(--delay-time);
}
@keyframes bounce-G {
    0% {
        transform: translateY(0px) scaleY(1);
        opacity: 0;
    }
    20% {
        opacity: 0;
    }
    65% {
        transform: translateY(-17px) scaleY(0.9);
        opacity: 1;
    }
    75% {
        transform: translateY(-22px) scaleY(0.9);
        opacity: 1;
    }
    100% {
        transform: translateY(0px) scaleY(1);
        opacity: 1;
    }
}
.BOUNCE-G {
    animation: bounce-G 0.3s linear forwards var(--delay-time);
}


/* Is this needed? */
.scaleStyle:hover {
    -webkit-transition: 0.2s ease-in-out;
    -moz-transition: 0.2s ease-in-out;
    -o-transition: 0.2s ease-in-out;
    transition: 0.2s ease-in-out;

    -webkit-transform: scale(2);
    -moz-transform: scale(2);
    -o-transform: scale(2);
    -ms-transform: scale(2);
    transform: scale(2);
}
]=]

StrMasterJS = [=[
// Map Life Facts shared JavaScript
function MakeArrays()											// Declare map Marker data arrays
{
  MapMark = new Array();										// google.maps.Marker overlays
  LatLong = new Array();										// google.maps.LatLng positions
  LocName = new Array();										// InfoWindow location place name	-- V4.2
  Caption = new Array();										// Marker caption title tooltips
  InfoWin = new Array();										// Google & Leaflet Maps InfoWindow popups
}
function OfflineError(sDivId)									// Offline error message
{
  document.getElementById(sDivId).innerHTML = '<p>Sorry, your PC appears to be offline, so maps cannot be shown ! ! !<\/p>';
}
function CheckOnline(no,yes)									// Check if PC online
{
  var xhr = new XMLHttpRequest();
  xhr.onload = function() { yes(); }
  xhr.onerror = function() { no(); }
  xhr.open("GET","https://www.google.com/");				// Replace obsolete google.co.uk with google.com	-- V6.0
  xhr.send();
}
function InitialMap()											// Initialise Map pane, Zoom level, and Marker Animation & Delay, then display Map
{
  SynNew = '';													// Inhibit SendStatus() in 'Web Page' mode	-- V4.3
  Animation = null;												// Marker animation mode
  SetAnimation('{animate}');									// 'null'=No Animation, or 'DROP'=Synchro/Cascade
  SetBound = null;												// Bounds offset to present Markers well within Map -- V4.3
  SetPause = 900;												// Pause for 900ms before dropping initial Markers
  SetDelay = parseInt({timeout});								// Marker delay of ~300ms=No Animation/Synchro, or ~2000ms=Cascade
  if ( isNaN(SetDelay) ) {SetDelay = 300;}
  SetZoom = 12;													// Current map Zoom level
  SetZoomLevel({setzoom});										// Integer between 0 and 22
  MarkZoom = SetZoom;											// Default zoom level for single 'MARK' Markers
  Home = NewLatLong(5.65,0.0);								// Tema, Accra, Ghana is nearest urban area to (0.0,0.0) N.B. Greenwich = (51.5,0.0)
  MakeMapPane('Web Page');										// Make the map pane display for 'Web Page' mode
  MakeMarkers();
  PresentMap();
  SetPause = 300;												// Pause for 300ms before dropping Markers
}
function MakeMarkers()											// Make the InfoWindow popups, and create empty Markers
{
  for ( var iEntry = 0; iEntry < LatLong.length; iEntry++ )
  {
    var latlng = GetLatLong(iEntry);
    InfoWin[iEntry] = Respace(InfoWin[iEntry]) + '<br />' + Respace(LocName[iEntry]) + '<br />Latitude&nbsp;&nbsp;&nbsp;&nbsp;:&nbsp;' + latlng[0] + '<br />Longitude&nbsp;:&nbsp;' + latlng[1];
    Caption[iEntry] = Caption[iEntry].replace(/\01as/g,'&');	// Convert ampersand
    Caption[iEntry] = Caption[iEntry].replace(/\01lt/g,'<');	// Convert less than
    Caption[iEntry] = Caption[iEntry].replace(/\01gt/g,'>');	// Convert more than
    NewMarker(iEntry);
  }
}
function RepresentMap()											// Represent original zoomed out Map & Marker(s) after Map click
{
  if ( LatLong.length == 0 )
  {
    BanZoom = 1;													// Lat/Long missing so Zoom out and pan to Home
    PanTo(Home,1);
  }
  else
  if ( SetDelay == 0 )
  {
    PanTo(LatLong[0],MarkZoom);								// Display single Marker created by MARK
    DropMarker(0);
    BanZoom = 0;
  }
  else
  {
    BanZoom = 99;												// Display multiple Markers created by DROP
    PresentMap();
    GetZoom = OldZoom();
    BanZoom = GetZoom;
  }
}
function PresentMap()											// Plot map Markers and adjust map Bounds
{
  try
  {
    if ( LatLong.length == 1 )
    {
      PanTo(LatLong[0],MarkZoom);								// Display single Marker created by MARK -- V5.3
      DropMarker(0);
      BanZoom = 0;
    }
    else
    {
      NewBounds();
      for ( var iEntry = 0; iEntry < LatLong.length; iEntry++ )
      {
        if ( LatLong[iEntry] != 0 )							// Set map Markers and Bounds for Latitude & Longitude
        {
          DropMarker(iEntry);
          SetBounds(iEntry);									// Set bounds so all Markers are included
        }
      }
      FitBounds();												// -- V5.3
    }
  }
  catch(error){return 'Fail';}
}
function DropMarker(iEntry)									// After delay, drop a map Marker at Latitude & Longitude with a tooltip Caption, and InfoWindow popup on click
{
  RubMarker(iEntry);											// Clear old Marker and Drop new Marker
  setTimeout(function(){AddMarker(iEntry);},iTimeout(iEntry));
}
function iTimeout(iEntry)										// Calculate time delay for Marker
{
  var iDelay = SetDelay;										// Immediate/No Animation/Synchro delay is fixed
  if ( SetDelay > 500 )
  {
    if ( iEntry < 10 )
    {
      iDelay = SetDelay * iEntry / 10;						// Cascade delay increased for each Marker
    }
    else
    {
      iDelay = SetDelay * Math.random();					// Cascade delay random when many Markers
    }
  }
  return SetPause + iDelay;
}
function SetZoomLevel(iZoom)									// Set the map Zoom level
{
  SetZoom = parseInt(iZoom);
  if ( isNaN( SetZoom )  ) {SetZoom=12;}
  if ( SetZoom < MinZoom ) {SetZoom=MinZoom;}
  if ( SetZoom > MaxZoom ) {SetZoom=MaxZoom;}
}
function ZoomToMarker(iEntry)									// Zoom to Marker or show whole map
{
  if ( LatLong[iEntry] == 0 )
  {
    FitBounds()													// Missing Latitude & Longitude so show whole map	-- V5.3
  }
  else
  {	
    FlyTo(LatLong[iEntry],MarkZoom);							// Zoom and pan to Marker, and bounce it for 3 secs
    SetMarker(iEntry);
  }
}
function fRoundNumber(fNumber,iDecPoints)					// Round floating Number to chosen Decimal Points
{
  var iPower = Math.pow(10,iDecPoints);
  var fAns = Math.round(fNumber * iPower) / iPower;
  return fAns;
}
function SendStatus(message)									// Send synch char + status message hidden below GUI IUP frame
{
  if ( message == undefined ) {message=' ';}
  if ( SynNew.length == 1 ) {									// Only in 'Plugin' mode -- V4.3
    document.getElementById('status').innerHTML = '<p>' + SynNew + message + '<\/p>';
  }
}
function SendZoom(zoom)											// Send synch char + map zoom level via HTML page	-- V4.3
{
  if ( zoom == undefined ) {zoom=12;}
  SendStatus(' Zoom=' + zoom);
}
function SendLatLng(lat,lng,act)								// Send synch char + Lat & Lng & Act via HTML page -- V4.3
{
  SendStatus(' Lat=' + fRoundNumber(lat,7) + ' Lng=' + fRoundNumber(lng,7) + ' Act=' + act);
}
{mapmode}
]=]

-- Google Maps (Legacy) custom JavaScript					-- V4.3
StrGoogleJS = [=[

MinZoom = 1;
MaxZoom = 22;

function MakeMapPane(mode)
{																	// Note overviewMapControl:true fails when Zoom slider moved up rapidly sometimes!
  Pane = new google.maps.Map(document.getElementById('map'),{zoom:SetZoom,center:Home,overviewMapControl:true,scaleControl:true,mapTypeId:google.maps.MapTypeId.TERRAIN});
  Pane.addListener('click',function(){RepresentMap();});	// Show original map on click
  Pane.addListener('zoom_changed',function(){ZoomMarkers();});	// On zoom change resize marker pins -- V6.0
}
function Respace(sText)											// Respace text
{
  return sText
}
function NewBounds()											// New map Bounds
{
  Bounds = new google.maps.LatLngBounds();
  if ( isNaN(parseFloat(SetBound)) ) {SetBound=0.2;}		// -- V4.3
}
function SetBounds(iEntry)										// Set map Bounds
{
  Bounds.extend(LatLong[iEntry]);
  var ne = Bounds.getNorthEast();
  var sw = Bounds.getSouthWest();
  var n = ne.lat() - SetBound;
  var e = ne.lng() - SetBound;								// Enlarge bounds by 'SetBound' in each direction
  var s = sw.lat() + SetBound;
  var w = sw.lng() + SetBound;
  Bounds.extend(NewLatLong(n,e));
  Bounds.extend(NewLatLong(s,w));
}
function FitBounds()											// Fit map Bounds	-- V5.3
{
  Pane.fitBounds(Bounds);
}
function SetAnimation(sMode)									// Set the Marker Animation mode
{
  Animation = null;
  if ( sMode == 'DROP' )										// 'null'=No Animation, or 'DROP'=Synchro/Cascade  
  {
    Animation = google.maps.Animation.DROP;
  }
}
function OldZoom()												// Get current Zoom level
{
  return Pane.getZoom();
}
function NewZoom(iZoom)											// Set the new Zoom level
{
  Pane.setZoom(iZoom);
}
function FlyTo(latlng,iZoom)									// Pan & Zoom to Lat/Long position smoothly
{
  PanTo(latlng,iZoom);
}
function PanTo(latlng,iZoom)									// Pan & Zoom to Lat/Long position directly
{
  setTimeout(function(){Pane.panTo(latlng);setTimeout(function(){Pane.setZoom(iZoom);},100);},150); // Leaflet Maps use 100 & 400
}
function NewMarker(iEntry)										// New map Marker
{
  InfoWin[iEntry] = new google.maps.InfoWindow({content:InfoWin[iEntry],headerContent:'Location Details'});	// V6.0
  MapMark[iEntry] = new google.maps.Marker();
}
function MakeIcon()												// Make marker icon scaled to zoom	-- V6.0
{
  var iZoom = Math.min(Math.max(Pane.getZoom(),5),13);	// Limit size to Zoom levels 5 to 13
  var iSizeWide = 25 / 12 * iZoom;
  var iSizeHigh = 41 / 12 * iZoom;
  var iAnchor = iSizeHigh+iZoom-6;							// Prevent icon Achor point drifting north as map Zooms in
  var icon = {url:'mapfacts.png',scaledSize:new google.maps.Size(iSizeWide,iSizeHigh),anchor:new google.maps.Point(iSizeWide/2,iAnchor)};
  //Defaults {url:'mapfacts.png',scaledSize:new google.maps.Size(25,41),anchor:new google.maps.Point(12,40)};
  return icon
}
function ZoomMarkers()											// Zoom the marker pins icon size		-- V6.0
{
  for ( var iEntry = 0; iEntry < MapMark.length; iEntry++ )
  {
    if ( LatLong[iEntry] != undefined && MapMark[iEntry].setIcon != undefined ) {MapMark[iEntry].setIcon(MakeIcon());}
  }
}
function AddMarker(iEntry)										// Add draggable map Marker with Tooltip caption and Popup info
{
  SendZoom(SetZoom);
  MapMark[iEntry] = new google.maps.Marker({map:Pane,position:LatLong[iEntry],icon:MakeIcon(),draggable:true,title:Caption[iEntry],animation:Animation});
  MapMark[iEntry].addListener('click'  ,function(){InfoWin[iEntry].open(Pane,MapMark[iEntry]);FlyTo(LatLong[iEntry],MarkZoom);});
  MapMark[iEntry].addListener('drag'   ,function(event){SendAct(event,'Mid');});	// Plotting by dragging -- V4.3
  MapMark[iEntry].addListener('dragend',function(event){SendAct(event,'End');});	// Plotting by dragging
}
function SendAct(event,act)									// Send plotting event Lat/Lng
{
  var latlng = event.latLng;
  SendLatLng(latlng.lat(),latlng.lng(),act);
}
function SetMarker(iEntry)										// Agitate map Marker
{
  MapMark[iEntry].setAnimation(google.maps.Animation.BOUNCE);
  setTimeout(function(){MapMark[iEntry].setAnimation(null);},3000);
}
function RubMarker(iEntry)										// Rubout map Marker
{
  if ( MapMark[iEntry] != 0 )									// V5.0 like leaflet.js
  {
    try     { MapMark[iEntry].setMap(null); }				// V5.0 try & finally to avoid any error message
    catch(e){ }
    finally { MapMark[iEntry] = 0 ; }						// V4.6
  }
}
function MoveMarker(iEntry)									// Move map Marker without animation
{
  try{ MapMark[iEntry].setPosition(LatLong[iEntry]); }	// V5.0 try to avoid any error message
  catch(e){ }
  try{ MapMark[iEntry].setMap(Pane); }						// V5.0 try to avoid any error message
  catch(e){ }
}
function NewLatLong(lat,lng)									// New latitude,longitude
{
  return new google.maps.LatLng(lat,lng);
}
function GetLatLong(iEntry)									// Get latitude,longitude
{
  var fLat = LatLong[iEntry].lat();
  var fLng = LatLong[iEntry].lng();
  return [fRoundNumber(fLat,7),fRoundNumber(fLng,7)];
}
async function InitMap()										// See https://developers.google.com/maps/documentation/javascript/load-maps-js-api -- V6.0
{
  Initialise();													// Call shared script
}
document.write(													// Google java script		-- V4.5		  &callback=Initialise' async defer	-- V4.9	Use v=3.45 for IE	v=weekly -- V6.0
"<script type='text/javascript' src='https://maps.googleapis.com/maps/api/js?v=weekly&key={api_key}&loading=async&callback=InitMap'> </script>"
);
]=]

-- Google Maps Advanced Marker custom JavaScript			-- V6.0
StrAdvanceJS = [=[

MinZoom = 1;
MaxZoom = 22;

function MakeMapPane(mode)
{																	// Note overviewMapControl:true fails when Zoom slider moved up rapidly sometimes!
  Pane = new google.maps.Map(document.getElementById('map'),{mapId:'{map_id#}',zoom:SetZoom,center:Home,overviewMapControl:true,scaleControl:true,mapTypeId:google.maps.MapTypeId.TERRAIN});
  Pane.addListener('click',function(){RepresentMap();});	// Show original map on click
  Pane.addListener('zoom_changed',function(){ZoomMarkers();});	// On zoom change resize marker pins -- V6.0
}
function Respace(sText)											// Respace text
{
  return sText
}
function NewBounds()											// New map Bounds
{
  Bounds = new google.maps.LatLngBounds();
  if ( isNaN(parseFloat(SetBound)) ) {SetBound=0.2;}		// -- V4.3
}
function SetBounds(iEntry)										// Set map Bounds
{
  Bounds.extend(LatLong[iEntry]);
  var ne = Bounds.getNorthEast();
  var sw = Bounds.getSouthWest();
  var n = ne.lat() - SetBound;
  var e = ne.lng() - SetBound;								// Enlarge bounds by 'SetBound' in each direction
  var s = sw.lat() + SetBound;
  var w = sw.lng() + SetBound;
  Bounds.extend(NewLatLong(n,e));
  Bounds.extend(NewLatLong(s,w));
}
function FitBounds()											// Fit map Bounds	-- V5.3
{
  Pane.fitBounds(Bounds);
}
function SetAnimation(sMode)									// Set the Marker Animation mode
{
  Animation = null;
  if ( sMode == 'DROP' )										// 'null'=No Animation, or 'DROP'=Synchro/Cascade
  {
    Animation = 'DROP-G';
  }
}
function OldZoom()												// Get current Zoom level
{
  return Pane.getZoom();
}
function NewZoom(iZoom)											// Set the new Zoom level
{
  Pane.setZoom(iZoom);
}
function FlyTo(latlng,iZoom)									// Pan & Zoom to Lat/Long position smoothly
{
  PanTo(latlng,iZoom);
}
function PanTo(latlng,iZoom)									// Pan & Zoom to Lat/Long position directly
{
  setTimeout(function(){Pane.panTo(latlng);setTimeout(function(){Pane.setZoom(iZoom);},100);},150); // Leaflet Maps use 100 & 400
}
function NewMarker(iEntry)										// New map Marker
{
  InfoWin[iEntry] = new google.maps.InfoWindow({content:InfoWin[iEntry],headerContent:'Location Details'});	// V6.0
  MapMark[iEntry] = new google.maps.marker.AdvancedMarkerElement();
}
function IconScale()											// Set marker icon scale for the zoom	-- V6.0
{
  var iZoom = Math.min(Math.max(Pane.getZoom(),5),15);	// Limit scale to Zoom levels 5 to 15
  return iZoom / 12;
}
function ZoomMarkers()											// Zoom the marker pins icon size		-- V6.0
{
  for ( var iEntry = 0; iEntry < MapMark.length; iEntry++ )
  {
    if ( LatLong[iEntry] != undefined && MapMark[iEntry].content != undefined ) {MapMark[iEntry].content.scale = IconScale();}
  }
}
function AddMarker(iEntry)										// Add draggable map Marker with Tooltip caption and Popup info
{
  SendZoom(SetZoom);
  // Set the marker pin colours and scale
  var markerStyle = new google.maps.marker.PinElement({background:'{colours}',borderColor:'Dark{colours}',glyphColor:'Dark{colours}',scale:IconScale()});
  MapMark[iEntry] = new google.maps.marker.AdvancedMarkerElement({map:Pane,position:LatLong[iEntry],gmpDraggable:true,title:Caption[iEntry]});
  MapMark[iEntry].content = markerStyle.element;
  MapMark[iEntry].addListener('click'  ,function(){InfoWin[iEntry].open(Pane,MapMark[iEntry]);FlyTo(LatLong[iEntry],MarkZoom);});
  MapMark[iEntry].addListener('drag'   ,function(event){SendAct(event,'Mid');});	// Plotting by dragging -- V4.3
  MapMark[iEntry].addListener('dragend',function(event){SendAct(event,'End');});	// Plotting by dragging
  if ( LatLong.length == 1 ) {Pane.setZoom(MarkZoom);}
  if ( Animation == 'DROP-G' )
  {
    MapMark[iEntry].content.style.opacity = '0';
    MapMark[iEntry].content.classList.add(Animation);
    MapMark[iEntry].content.addEventListener('animationend', (event) => {
      MapMark[iEntry].content.classList.remove(Animation);
      MapMark[iEntry].content.style.opacity = '1';
    });
  }
}
function SendAct(event,act)									// Send plotting event Lat/Lng
{
  var latlng = event.latLng;
  SendLatLng(latlng.lat(),latlng.lng(),act);
}
function SetMarker(iEntry)										// Agitate map Marker
{
  MapMark[iEntry].content.classList.add('BOUNCE-G');
  MapMark[iEntry].content.addEventListener('animationend', (event) => {
    MapMark[iEntry].content.classList.remove('BOUNCE-G');
    MapMark[iEntry].content.style.opacity = '1';
  });
}
function RubMarker(iEntry)										// Rubout map Marker
{
  if ( MapMark[iEntry] != 0 )									// V5.0 like leaflet.js
  {
    try     { MapMark[iEntry].setMap(null); }				// V5.0 try & finally to avoid any error message
    catch(e){ }
    finally { MapMark[iEntry] = 0 ; }						// V4.6
  }
}
function MoveMarker(iEntry)									// Move map Marker without animation
{
  try{ MapMark[iEntry].setPosition(LatLong[iEntry]); }	// V5.0 try to avoid any error message
  catch(e){ }
  try{ MapMark[iEntry].setMap(Pane); }						// V5.0 try to avoid any error message
  catch(e){ }
}
function NewLatLong(lat,lng)									// New latitude,longitude
{
  return new google.maps.LatLng(lat,lng);
}
function GetLatLong(iEntry)									// Get latitude,longitude
{
  var fLat = LatLong[iEntry].lat();
  var fLng = LatLong[iEntry].lng();
  return [fRoundNumber(fLat,7),fRoundNumber(fLng,7)];
}
async function InitMap()										// See https://developers.google.com/maps/documentation/javascript/load-maps-js-api -- V6.0
{
  await google.maps.importLibrary("maps");					// Import Google Maps Libraries
  await google.maps.importLibrary("marker");
  Initialise();													// Call shared script
}
document.write(													// Google Maps java script	-- V4.5				 &loading=async&callback=InitMap 	-- V6.0
"<script async type='text/javascript' src='https://maps.googleapis.com/maps/api/js?v=weekly&key={api_key}&loading=async&callback=InitMap'></script>"
);
]=]

-- Leaflet Maps map pane {mapmode} custom JavaScript		-- V4.3
StrLeafletJS = [=[

MinZoom = 1;
MaxZoom = 19;

function MakeMapPane(mode)										// Make map pane where mode is 'Plugin' or 'Web Page'
{
  //  See 'Control.Layers' and 'Layer Groups and Layers Control' and 'Basemap Providers' to switch between alternative base layer maps, etc.
 
  var osmapUrl = 'https://tile.openstreetmap.org/{z}/{x}/{y}.png';															// OpenStreetMap alternative
  var osmapAtt = '&copy; <a href="https://www.openstreetmap.org/about/" target="_new">OpenStreetMap</a> contributors';
  var osmapLay = L.tileLayer(osmapUrl,{attribution:osmapAtt,maxZoom:19,minZoom:1});

  var mbsatKey = 'pk.eyJ1IjoidGF0ZXdpc2UiLCJhIjoiY2ptNmNkZndzM2lhczN3cXV2ODZoeGk4MyJ9.dmr-Fwb6Ll2X6ICPl7AVkQ';	// Mapbox Satellite alternative
  var mbsatUrl = 'https://api.mapbox.com/v4/mapbox.satellite/{z}/{x}/{y}.png?access_token=' + mbsatKey;				// id: mapbox.satellite Nov 2020
  var mbsatAtt = 'Map data &copy; <a href="https://www.openstreetmap.org/about/" target="_new">OpenStreetMap</a> contributors, ' +
    '<a href="https://creativecommons.org/licenses/by-sa/2.0/" target="_new">CC-BY-SA</a>, Imagery &copy; <a href="https://www.mapbox.com/ target="_new"">Mapbox</a>';
  var mbsatLay = L.tileLayer(mbsatUrl,{attribution:mbsatAtt,maxZoom:19,minZoom:1});

  var googlUrl = 'https://mt.google.com/vt/lyrs={layers}&x={x}&y={y}&z={z}';												// Google Maps alternatives		-- V6.0
  var googlAtt = '&copy; Google Maps';																							// https://stackoverflow.com/questions/9394190/leaflet-map-api-with-google-satellite-layer
  var gmapsLay = L.tileLayer(googlUrl,{attribution:googlAtt,maxZoom:19,minZoom:1,layers:'m'});						// Google Street Maps Default
  var gterrLay = L.tileLayer(googlUrl,{attribution:googlAtt,maxZoom:19,minZoom:1,layers:'p'});						// Google Streets with Terrain
  var gsatlLay = L.tileLayer(googlUrl,{attribution:googlAtt,maxZoom:19,minZoom:1,layers:'y'});						// Google Satellite with Labels
  // "lyrs" parameter in URL:  Street Maps: m  Terrain & Labels: p  Satellite Only: s  Satellite & Labels: y  Roads Only: h  Hybrid: s,h  Terrain Only: t  Traffic overlay: m@221097413,traffic  Altered Road Map: r

  var layers = {													// Assign names to the base layers
    'Open Street Map' :osmapLay,
    'Mapbox Satellite':mbsatLay,
    'Google Streets'  :gmapsLay,
    'Google Terrain'  :gterrLay,
    'Google Satellite':gsatlLay,
  };
  var base = '{basemap}';										// Chosen base layer map
  if ( layers[base] == undefined ) {base='Open Street Map';}
  Pane = L.map('map',{center:Home,zoom:SetZoom,zoomSnap:0.5,doubleClickZoom:false,layers:[layers[base]]});
  L.control.layers(layers).addTo(Pane); 					// Add base layer maps with default chosen by layers[base] above
  L.control.scale().addTo(Pane);								// Add scale bar
  Pane.on('click',function(){RepresentMap();});			// Show original map on click
  if ( mode == 'Plugin' )
  {
    Pane.on('baselayerchange',function(event){SendStatus(' Base=' + event.name);});					// Log new base layer map when changed
    Pane.on('rightclick',function(event){SendLatLng(event.latlng.lat,event.latlng.lng,'New');});	// Add new Marker on right-click (not implemented)
    Pane.on('dblclick'  ,function(event){SendLatLng(event.latlng.lat,event.latlng.lng,'New');});	// Add new Marker on double-click
    Pane.on('zoomend',function(){GetZoomLevel();ZoomMarkers();});										// On zoom change log zoom level and resize marker pins -- V6.0
  }
  else
  {
    Pane.on('zoomend',function(){ZoomMarkers();})			// On zoom change resize marker pins -- V6.0
  }
}
function Respace(sText)											// Respace text
{
  return sText
}
function NewBounds()											// New map Bounds
{
  Bounds = L.latLngBounds();
  if ( isNaN(parseFloat(SetBound)) ) {SetBound=0.3;}		// -- V4.3
}
function SetBounds(iEntry)										// Extend map Bounds
{
  Bounds.extend(LatLong[iEntry]);
  var n = Bounds.getNorth() - SetBound;
  var e = Bounds.getEast()  - SetBound;						// Enlarge bounds by 'SetBound' in each direction
  var s = Bounds.getSouth() + SetBound;
  var w = Bounds.getWest()  + SetBound;
  Bounds.extend(NewLatLong(n,e));
  Bounds.extend(NewLatLong(s,w));
}
function FitBounds()											// Fit map Bounds -- V5.3
{
  Pane.fitBounds(Bounds);
}
function SetAnimation(sMode)									// Set the Marker Animation mode
{
  Animation = null;
  if ( sMode == 'DROP' )										// 'null'=No Animation, or 'DROP'=Synchro/Cascade  
  {
    Animation = 'DROP-L';
  }
}
function OldZoom()												// Get current Zoom level
{
  return Pane.getZoom();
}
function NewZoom(iZoom)											// Set the new Zoom level
{
  Pane.setZoom(iZoom);
}
function FlyTo(latlng,iZoom)									// Pan & Zoom to Lat/Long position smoothly with timeout to allow large changes
{
  if ( latlng != 0 )
  {
    setTimeout(function(){Pane.flyTo(latlng,iZoom);},200);	// options: ,{animate:true,duration:2.5}
  }
}
function PanTo(latlng,iZoom)									// Pan & Zoom to Lat/Long position directly with timeout to allow large changes
{
  if ( latlng != 0 )
  {
    setTimeout(function(){Pane.panTo(latlng);setTimeout(function(){Pane.setZoom(iZoom);},100);},400); // -- was 200 & 200 -- Google Maps use 100 & 150 -- V4.9
//  if ( Pane.getZoom() != iZoom ) {Pane.setZoom(iZoom);}
  }
}
function NewMarker(iEntry)										// New map Marker
{
  MapMark[iEntry] = L.marker(LatLong[iEntry]);
}
function MakeIcon()												// Make marker icon scaled to zoom	-- V6.0
{
  var iZoom = Math.min(Math.max(Pane.getZoom(),5),13);	// Limit size to Zoom levels 5 to 13
  var iSizeWide = 25 / 12 * iZoom;
  var iSizeHigh = 41 / 12 * iZoom;
  var iAnchor = iSizeHigh+iZoom-6;							// Prevent icon Achor point drifting north as map Zooms in
  var icon = L.icon({iconUrl:'mapfacts.png',iconSize:[iSizeWide,iSizeHigh],iconAnchor:[iSizeWide/2,iAnchor],popupAnchor:[0,-iSizeHigh],tooltipAnchor:[0,0],className:Animation});
  //Defaults L.icon({iconUrl:'mapfacts.png',iconSize:[25,41],iconAnchor:[12,40],popupAnchor:[0,-40],tooltipAnchor:[0,0],className:Animation});
  // Leaflet L.icon({iconUrl:'https://unpkg.com/leaflet@1.9.4/dist/images/marker-icon.png',iconAnchor:[20,50],popupAnchor:[-8,-52],className:Animation});
  return icon
}
function ZoomMarkers()											// Zoom the marker pins icon size		-- V6.0
{
  for ( var iEntry = 0; iEntry < MapMark.length; iEntry++ )
  {
    if ( LatLong[iEntry] != undefined && MapMark[iEntry].setIcon != undefined ) {MapMark[iEntry].setIcon(MakeIcon());}
  }
}
function AddMarker(iEntry)										// Add draggable map Marker with Tooltip caption and Popup info
{
  SendZoom(SetZoom);
  RubMarker(iEntry);
  MapMark[iEntry] = L.marker(LatLong[iEntry],{icon:MakeIcon(),draggable:true,autoPan:true,title:Caption[iEntry]}).bindPopup(InfoWin[iEntry]);	// ,title:Caption[iEntry] or .bindTooltip(Caption[iEntry])
  MapMark[iEntry].addTo(Pane);
  MapMark[iEntry].on('click'  ,function(){FlyTo(LatLong[iEntry],MarkZoom);});
  MapMark[iEntry].on('drag'   ,function(){SendAct(iEntry,'Mid');}); 	// Plotting by dragging -- V4.3
  MapMark[iEntry].on('dragend',function(){SendAct(iEntry,'End');}); 	// Plotting by dragging
  if ( LatLong.length == 1 ) {Pane.setZoom(MarkZoom);}
  if ( Animation == 'DROP-L' ) {SetMarker(iEntry);}
}
function SendAct(iEntry,act)									// Send dragging event Lat/Lng
{
  var latlng = MapMark[iEntry].getLatLng();
  SendLatLng(latlng.lat,latlng.lng,act);
}
function SetMarker(iEntry)										// Agitate map Marker and remove style classes
{
  var move = 'BOUNCE-L';										// 'BOUNCE-L' or 'FLASH-L'
  var error = false;											// Repeat L.DomUtil as _icon may take time to appear
  do {
    error = false;
    setTimeout( function()
    { try { L.DomUtil.addClass(MapMark[iEntry]._icon,move); } catch(err) {error=true;} }, 300);
  } while(error);
  do {
    error = false;
    setTimeout( function()
    { try { L.DomUtil.removeClass(MapMark[iEntry]._icon,move); L.DomUtil.removeClass(MapMark[iEntry]._icon,Animation); } catch(err) {error=true;} }, 2000);
  } while(error);
}
function RubMarker(iEntry)										// Rubout map Marker
{
  if ( MapMark[iEntry] != 0 )
  {
    try{ MapMark[iEntry].remove(); }							// V5.0 try to avoid any error message
    catch(e){ }
  }
}
function MoveMarker(iEntry)									// Move map Marker without animation
{
  try{ MapMark[iEntry].setLatLng(LatLong[iEntry]); }		// V5.0 try to avoid any error message
  catch(e){ }
  try{ MapMark[iEntry].addTo(Pane); }						// V5.0 try to avoid any error message
  catch(e){ }
}
function NewLatLong(lat,lng)									// New latitude,longitude
{
  return L.latLng(lat,lng);
}
function GetLatLong(iEntry)									// Get latitude,longitude
{
  var fLat = LatLong[iEntry].lat;
  var fLng = LatLong[iEntry].lng;
  return [fRoundNumber(fLat,7),fRoundNumber(fLng,7)];
}
function InitMap()												// See https://leafletjs.com/examples/quick-start/	-- V6.0
{
  Initialise();													// Call shared script
}
document.write(													// Leaflet style sheet and java script
"<link type='text/css' rel='stylesheet' href='https://unpkg.com/leaflet@1.9.4/dist/leaflet.css' integrity='sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY=' crossorigin=''/>" +
"<script type='text/javascript' src='https://unpkg.com/leaflet@1.9.4/dist/leaflet.js' integrity='sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo=' crossorigin=''> </script>"
);
window.onload = function(){ InitMap(); };					// V6.0 equivalent to Google Maps &loading=async&callback=InitMap
]=]

-- HTML script template for displying web page maps
-- Make the map page style even more like the FH web page style		-- V3.7
-- Moved {fhheaders} later in <head> to override mapfacts.css
-- Added <div class="fhcontent"> <h1 class="FhHdg1"> to heading
StrMasterWeb = [=[
<!DOCTYPE html>
<html>
  <head>
    <title>Map of Locations for&nbsp;{titlename}</title>
    <meta name='viewport' content='initial-scale=1.0,user-scalable=no'>
    <link type='text/css' rel='stylesheet' href='mapfacts.css'>
{fhheaders}
    <script type='text/javascript' src='mapfacts.js'> </script>
    <script type='text/javascript'>
    function Initialise()
    {
      MakeArrays();
{javalists}
      InitialMap();
    }
    </script>
  </head>
  <body>{fhmenubar}
    <div class="fhcontent">
      <h1 class="FhHdg1">
        <a href='#' onclick='PresentMap()'>Map of Locations for&nbsp;{titlename}</a>
      </h1>
    </div>
    <div id='facts'>
      <ul>
{factslist}
      </ul>
    </div>
    <div id='map'>
    </div>
  </body>
</html>
]=]

-- Provide scripts for {mapmode} in StrMasterJS above chosen by IntMapMode setting
-- Each script fragment is dedicated to one map pane mode		-- V4.3
StrMasterMap = {
	GoogleLegacy		= StrGoogleJS ;									-- Google Maps Legacy Marker map pane mode JavaScript
	LeafletMaps		= StrLeafletJS ;									-- Leaflet Maps map pane mode JavaScript
	GoogleAdvanced	= StrAdvanceJS ;									-- Google Maps Advanced Marker map pane mode JavaScript	-- V6.0
}

-- Main body of Plugin script starts here --

	if fhGetAppVersion() > 7 then										-- V6.1

		fhMessageBox("\nThis plugin is not supported in Family Historian Version 8.\n","MB_OK","MB_ICONSTOP")

	else

		local strIupSystem = iup.GetGlobal("SYSTEM")				-- Get the Windows System name	-- V4.2
		if strIupSystem:match("WinXP")
		or strIupSystem:match("Vista") then

			fhMessageBox("\nCurrent online mapping tools needed by this Plugin\ndo not support the Internet Explorer versions that\nare available with Windows XP and Windows Vista.\n","MB_OK","MB_ICONSTOP")

		else

			local intPause = collectgarbage("setpause",50)			-- Default = 200  Aggressive = 50	-- Sets pause of collector and returns prev value of pause	-- V4.9

			local intStep = collectgarbage("setstepmul",300)		-- Default = 200  Aggressive = 300	-- Sets step mult of collector & returns prev value of step	-- V4.9

			fhInitialise(5,0,9,"save_recommended")

			PresetGlobalData()											-- Preset global data values

			ResetDefaultSettings()										-- Preset default sticky settings

			LoadSettings()												-- Load sticky data settings

			GUI_MainDialogue()

			SaveSettings()												-- Save sticky data settings

			fhUpdateDisplay()

		end

	end

--[=[
@V6.1:		Library 4.1; Centre windows on FH window; Fix FH V8 Language Pack tag; Inhibit FH V8;	TBD: Fix crash; Handle FH V8 _ADDR records; io.open(strMasterPNG,"wb"); "_blank" > "_new"; 
@V6.0:		In StrMasterJS CheckOnline(...) replace obsolete google.co.uk with google.com; Remove Bing Maps; Add Google Maps to Leaflet panes; Add pin colour options and scaled size; Add Check for Updates button;
			Update Leaflet to unpkg.com/leaflet@1.9.4/dist from 1.7.1; Inhibit invalid or too precise Lat/Long values; Inhibit Enter/Return/Esc key from closing plugin; Fix Delete All Maps;
@V5.4:		'Copy Location to Standardized' button never changes Lat/Long or Status; StrGeocodeLocation(...) reports errors as per https://forums.indigorose.com/forum/autoplay-media-studio-8-5/autoplay-media-studio-8-discussion/297522-how-to-catch-an-error-from-a-plugin-luacom;
@V5.3:		Bing Maps fully supported; Google Maps allowed for Web Page Maps; Geoapify geocoder added; Map Pane legend sizing for screen resoluton;
@V5.2:		Deprecate the Google Maps display pane mode that no longer works; Remove the Leaflet Maps NLS Historic UK option;
@V5.1:		Use library V3.3 with FSO; Cater for 'Improve Website or CD DVD HTML' plugin Tree Diagrams tab 'See also' section; Cater for foreign languages;
@V5.0:		In StrGeocodeLocation(...) purge blank comma parts that upset Google Geocoder; Modify TblLocCode and add TblNation for conflicts and UK county abbreviations like Berks that upset place names elsewhere; If plot Not Found then prune leading location part; Use try & finally in Google & Leaflet JavaScript to avoid error mesages;
@V4.9:		Fix LoadLocationsData(...) fhGetLabelledText() tab prefix; Revised splash Help page for new users; Handle "Blocked/No Auto-Geocode" Place record status & Block Geocoding button; Use FH names for Tentative, Standardized, etc; Aggressive memory management; CheckVersionInStore() at monthly intervals; Reverse Display Order option for Place List; Leaflets Maps PanTo() longer delay; Use Google Maps v=3.45 for IE;
@V4.8: 	Fix iup_gui.HelpDialogue(...) oversights;
@V4.7:		Updated library to Functions Prototypes v3.0; iup_gui.Balloon = "NO" for PlayOnLinux/Mac; Fix problem swapping Locations From & Database In when GEDCOM records involved; Sample city plots based on Region Bias; Updated to unpkg.com/leaflet@1.7.1/dist from 1.0.3 & 1.3.4; Removed Mapbox Streets from Leaflet as not now free;
@V4.6:		Fix problem with Google Maps not rubbing out markers correctly, allow all unreserved URI characters (-.~) in API Key, fix iup.TreeAddNodes() limits leaf Title to ~254 characters (Fixed: IUP v3.28) in doBuildTreeNodes() & strGetTreeLeaf() and failsafe in doRefreshLocation(), Library V2.9 update.
@V4.5:		Fix problem with Google Maps API not allowing Map variable with same name as Map class, so altered to Pane; JavaScript added Google Maps '&callback async defer' and Leaflets Maps 'window.onload' to call Initialise().
@V4.4:		Fix problem with detecting old format map files in FixOldMapFiles(...).
@V4.3:		Ensure API Key is alphanumeric, in Place List disregard leading " , or space so grouped by first alphanumeric character, offer Leaflet Maps alternative to Google Maps JavaScript API, allow Markers to be dragged, and Google Maps to be disabled.
@V4.2:		maps.googleapis.com/maps/api/ moved to StrMasterJS, uses https:, gains &key=StrAPI_Key, drops &sensor=false, plus improvements in StrGeocodeLocation(...), StrMasterHTM & StrMasterWeb drop v=StrApiVersion, EMIG/IMMI 2nd Place fix, better handling of &<>'" with StrTidyCommas() for map locations, new splash popups for API Key required, etc.
@V4.1:		Updated library module for safer IUP GUI, and fix UTF8 characters in lists.
@V4.0:		Exclude [[private]] text. Fix doEraseLocation() intFromTag bug.
@V3.9:		Replace hyphen with space in StrGeocodeLocation() as workaround for Stoke-upon-Trent, change StrMasterHTM & StrMasterWeb to <!DOCTYPE html> and to use StrApiVersion, StrMasterCSS infomsg fix for IE8/9, fix 'No Plot' status in SaveLocationsData(), only use better data in StrGeocodeLocation(), unconditional button Geocode Plot This Location.
@V3.8:		Handle UNKNOWN_ERROR geocode status, use maps.googleapis.com/maps/api/js?v=3.18 for IE v9, and remove Decimal point correction.
@V3.7:		DicFactSet/StrFactSet/tglFactSet filter to limit maps to webpage Facts, and make map page CSS format match web page format more closely. Windows Decimal Symbol correction removed.
@V3.6:		Cater for Decimal point symbol = dot or comma, fix Database Management tab problems, and minor Location/Individual list display glitches.
@V3.5:		Reinstate Strikeout in GUI_PlotDialogue(), better order for Help pages including FAQ, fix spaces in Place Record names, use [unnamed person] when no Name, "List" dialogue position now sticky,
			update all Lua % patterns, and StrEncode_URI/XML(), change "Manual" to "Defined"; "Next Plotted" only traverses "Plotted"; inhibit busy tabs, plus storage mode fixes, and remove old Place Record names.
@V3.4:		Corrections to changing Options 'Locations From', 'Database In' and 'Restore Defaults' for FH V6 when 'Place Records' involved.
@V3.3:		Check Version in Store, Progress Bar function prototype, needs V5.0.9 for Emigration/Immigration, add To/From _PLAC to Plugin Locations but not HTML Maps (TESTING looks OK), update FlgFolderExists(),
			remove multiple "; " Place/Address column separators, use new plain string functions, use StrANSI_XML() and StrANSI_URI(), add Version History help, use new GUI library, minor updates,
			detect lat/long [%+%-]?[0-9]-%.[0-9]-, [%+%-]?[0-9]-%.[0-9]- and use to exclusion of all else for Google Geocoder, XP setToolTip(iupItem) function,
			update to support both V5 and V6 IUP 3.11.2 & _PLAC Records, ellipsis … changed to tilde ~ for ANSI/UTF-8 independant encoding, and location data converted to UTF-8 when needed,
			StrGetDisplayText() NEW in V3.3 and not Plugin Store V3.2, prevented matching _PLAC Records so adjusted OK, degree ° symbol adjusted for ANSI/UTF-8 encoding (see FH6 comments).
@V3.2:		Revised new Address…Place sub-options that retain entries, and new Set Preferences Database Management tab.
@V3.1:		Statistics speed-up for many Locations, fixed Delete key bug in Subs/Lat/Long fields, and four new Address…Place sub-options.
@V3.0:		Force all dialogue backgrounds to white, and make editing Substitute, Latitude & Longitude more responsive.
@V2.9:		Optionally detects previous manually adjusted Locations, allows Geocode Plot/Map All/Some to escape at Quota Warning.
@V2.8:		New Manual statistics class of Location, revised Geocode Plot All button, new Geocode Plot Some button, revised Next... buttons. 
@V2.7:		Translate any delimited Country/State/Chapman/County Code into full name before submitting to Geocoder.
@V2.7:		Use read("*all") and table.concat instead of string concatenation, save Plugin sticky settings more often.
@V2.6:		Fixed 'Not enough memory' for big _nameindex.html, set quota warning on all Plot/Map buttons, FlgFile/FolderExists now use "lfs".
@V2.5:		Corrects missing webpage links when no 'See also' section, adds warning about exceeding 2,500 plots per 24 hours.
@V2.4:		Checks map files belong to chosen Individual, adds Region Biasing option, uses bell blob for newline
@V2.3:		Improved Map options & GUI avoids Blue, default Zoom only set on Preferences tab, multi-plot sample Maps on Create Maps tab.
@V2.2:		Redesigned GUI, colour coded tree lists & statistics, zoom set from Google Map, better map Markers, bug fixes & code revisions.
@V2.1:		Loads fast, resizeable main GUI, trees replace dropdown lists, better tooltips, bigger Google Map pane, bug fixes & code revisions.
@V2.0:		New GUI with tabs, progress bars, and embedded HTML Google Map, plus Google Geocoder API, bug fixes & code revisions.
@V1.1:		Improved version with fhSleep(), Interface Font option, and Knowledge Base Help.
@V1.0:		Initial version using HTML Application (HTA) for Google Maps Geocoder.
--]=]

Source: Map-Life-Facts-11.fh_lua