--[[ @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 = "
" -- Markup language break tag default local br_Lua = "
" -- 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" -- """ in Markup see setURIEncodings() and setMarkupEncodings() below tblCodePage["#"] = "%23" -- Reserved character tblCodePage["$"] = "%24" -- Reserved character tblCodePage["%"] = "%25" -- Must be encoded tblCodePage["&"] = "%26" -- Reserved character -- "&" in Markup see setURIEncodings() and setMarkupEncodings() below tblCodePage["'"] = "%27" -- Reserved character -- "'" 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" -- "<" in Markup see setURIEncodings() and setMarkupEncodings() below tblCodePage["="] = "%3D" -- Reserved character tblCodePage[">"] = "%3E" -- ">" 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) -- "€" 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) -- " " No Break Space tblCodePage["¡"] = string.char(0xC2,0xA1) -- "¡" tblCodePage["¢"] = string.char(0xC2,0xA2) -- "¢" tblCodePage["£"] = string.char(0xC2,0xA3) -- "£" tblCodePage["¤"] = string.char(0xC2,0xA4) -- "¤" tblCodePage["¥"] = string.char(0xC2,0xA5) -- "¥" 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) -- "­" 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 "
" or "
" or "
" or "
" is better tblCodePage["\f"] = br_Tag -- FF tblCodePage["\r"] = br_Tag -- CR tblCodePage['"'] = """ tblCodePage["&"] = "&" tblCodePage["'"] = "'" tblCodePage["<"] = "<" tblCodePage[">"] = ">" 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
&
&
&
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
&
&
&
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
&
&
&
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
&
&
&
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"]=" " -- " " No Break Space tblASCII["¡"] = "!" -- "¡" tblASCII["¢"] = "=c" -- "¢" tblASCII["£"] = "=L" -- "£" tblASCII["¤"] = "=$" -- "¤" tblASCII["¥"] = "=Y" -- "¥" tblASCII["¦"] = "|" tblASCII["§"] = "=SS" tblASCII["¨"] = "\"" tblASCII["©"] = "(C)" tblASCII["ª"] = "a" tblASCII["«"] = "<<" tblASCII["¬"] = "-" tblASCII["­"] = "-" -- "­" 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 "
" or "
" or "
" or "
" br_New = "
" 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 = "
"..StrEOL --
or fhpageInd or fhpageDesc or fhpageIndNarr or fhpageRec -- V5.1 StrDivSeeAlso = "
"..StrEOL --
\r\n -- V5.1 StrOldSeeAlso = "[[^\r\n]-

[ %->]-" -- -- V5.1 -- Cater for 'Improve Website or CD DVD HTML' plugin Tree Diagrams tab 'See also' section StrNewSeeAlso = "

"..StrTranslate("See also").."

" --

See also

translated -- V5.1 -- May have tags and may be translated to a foreign language StrBegSeeAlso = StrEOL.."
    "..StrEOL -- \r\n
      \r\n -- V5.1 StrEndSeeAlso = "
    "..StrEOL.."
"..StrEOL -- \r\n
\r\n -- V5.1 StrH1Heading = "

" --

StrMapHeaders = "" -- Map file script between "" and "" to replace {fhheaders} StrMapMenubar = "" -- Map file script between "" and "
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 ","") -- 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("\n"..StrSync.."\n"..tostring(strA).."\n"..tostring(str1).."\n"..tostring(str2).."\n"..tostring(str3).."\n"..tostring(str4).."\n"..tostring(str5).."\n\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..">(.-)") 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.."®ion="..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 = ""..strLog.."\n"..strTxt for strLab, strVal in strTxt:gmatch("<(/?[%w_]+)>[\r\n ]*([^<]*)") do -- Match opening ignoring white space if #strVal == 0 then if strLab:match("^/") then -- Found closing so try to remove from end of prefix strPre = strPre:gsub(strLab:match("^/(.*)").."%.$","") else -- Found opening