Module:Wnt/Templates/Svgedit

From Wikipedia

Documentation for this module may be created at Module:Wnt/Templates/Svgedit/doc

local p={}

function findfirst(histo, value) -- this stupid algorithm should actually even if unsorted...
    local lessthans=1
    for i = 1, #histo do
        if histo[i]<value then lessthans=lessthans+1 end
    end
    return lessthans -- warning - returns array size + 1 if there are no values >= value in the array
end

function extractprofile(profile, levels)
 -- takes a profile like [[User:Wnt/Templates/Svgedit/Sampleprofile1]] and extracts the colors to use, according to how many levels are requested

   profile = string.match(profile,levels .. "==(.+)") or "no match"
   profile = string.match(profile,"(.-)==") or profile
   local x=0
   local debug=""
   local pattern={}
   for w in string.gmatch(profile, "%x%x%x%x%x%x") do
      x=x+1
      pattern[x]=w
      debug = debug .. w
      end
   return pattern -- an array of levels (or, that's the idea...)
end

function readdata(datafile)
    -- reads in messy data and turns it into an array of keys (id's) and numeric values
    local values={} -- array of id's and numerical data values to be returned
    for line in string.gmatch(datafile,"([^,;\n]*)") do -- everything on first line excluding delimiters return-comma-semicolon
       local id=string.match(line,"(%a%w*)") -- id is first thing on the line beginning in a letter
       local strvalue, exponent=string.match(line,"(\-?\+?%d+)E?e?(%d-)") -- captures signed number, exponent in 1.7E4 type notation.
       value = strvalue -- * 10 ^ (exponent or 0) -- this better not be prone to errors
       if id then values[id] = value end
    end
    return values
end
    
    
function createorders(datafile,profile,override) 
    -- reads in your actual data file (the numbers for each id) and tries to create a nice scheme for it
    -- these three all come from frame parameters
    -- if override is set, anything >= to a specific number is assigned to the given level (or higher)
    -- override (an array) should be accepted for all levels, none, or just some...
    -- set last level in override to 99999999999 or something to make it empty - function has to exclude empty level...
   local maxlevel = string.match(profile,"(%d+)%s*==") + 0
      -- maxlevel is the most allowed by the profile itself (largest numerical heading)
      local values = {}
      values = readdata(datafile) -- array of id's and numerical data values
      local count=0
      local histo={} -- histogram of all the values indexed by integer
      for k,v in ipairs(values) do
          count=count+1
          histo[count]=v -- note values should be coerced to numerical before they're passed here
      end
      table.sort(histo)
      local maxvalue=histo[count]
      local threshold={} -- threshold(1 to maxlevel) is the number a value must be equal to or greater to be assigned that color value or greater
      local threspos={} -- POSITION of threshold, which can go to count + 1 if none are greater to or equal to it
      local threscount=0 --  number of thresholds ACTUALLY set in the override array (not counting numbers too high for any data to meet)
      local thresdone={} -- which thresholds (1 to threscount) have been set already (value 1 to maxlevel)
      for k,v in ipairs(override) do
          if (v <= maxvalue) then 
              threshold[k] = v
              threspos[k] = findfirst(histo,v) 
          else maxlevel = math.min(k-1, maxlevel)
          end
          -- don't set thresholds that won't be used; set the hard maxlevel to one less than the key if the key is defined so nothing can satisfy it (e.g. 99999999)
          threscount=threscount+1
          thresdone[threscount]=k -- array (integer index) of _which_ levels already set here
      end
      table.sort(thresdone)
      thresdone[0] = 0 -- place holder; actually negative infinity value for "threshold 0"
      threspos[0] = 1 -- "threshold 0" starts with the first value in the histogram 
      for x = 1,threscount do
         local first=thresdone[x-1]
         local firstpos=threspos[threshold[first]]
         local last=thresdone[x]
         local lastpos=threspos[threshold[last]]
         for fillinlevel=first+1,last-1 do
             threshold[fillinlevel]=findfirst(histo,math.floor(firstpos + (lastpos-firstpos)*(fillinlevel-first)/(last-first)))
         end  
         -- absolute PUNT code here; would prefer to get nice round-numbered levels, decide linear versus log, etc. but if I don't paper this over I'll never test this thing

         -- figure out how to set the thresholds for each segment where they're not defined yet
      end
      -- set the last thresholds after all those defined (or if none exist)
      -- handling of the last segment _could be_ (but isn't presently) special - maxlevel may be reset for advantage...
      local first=thresdone[threscount]
      local firstpos=threspos[threshold[first]] or 1
      local last=maxlevel+1
      local lastpos=count+1
      for fillinlevel=first+1,last-1 do         
          threshold[fillinlevel]=findfirst(histo,math.floor(firstpos + (lastpos-firstpos)*(fillinlevel-first)/(last-first)))
      end
      
      local pattern = extractprofile(profile,maxlevel) -- now have pattern[x] = the actual fill for level x
      local orders={}
      for k,v in ipairs(values) do
          local x=1
          while v < threshold[x] do x=x+1 end
          orders[k] = pattern[x]
      end
   return orders --array of id's and color values
end
    
function replacetags(svginput,orders)
 -- this function gets an array "orders" which contains tags and hexadecimal values the tags should be set to IF they exist.  svginput is searched for tags with "id" fields and as they appear the "fill" contents are replaced with the "orders" value for that id.  Designed only for basic tagging within the id field, not when it's done at the front of the file.
   local ids={}
   local fills={}
   local index=1
   local repdebug="replacetags debug output:" .. svginput
   svgnew=svginput
   for w in string.gmatch(svginput, "\".-\"") do
       repdebug=repdebug.."w="..w
      local fill = string.match(w, "fill:(.-);") or ""
       repdebug=repdebug..fill
      local id = string.match(w, "id=\"(.-)\"") or ""
       repdebug=repdebug..id
      if (id ~= "") then
         if (fill ~= "") then
            if orders[id] then
               wnew = string.gsub (w, fill, orders[id]) -- first replace just this fill in this element
               svgnew = string.gsub (svgnew, w, wnew) -- then replace this exact element in the new svg
            end -- if no orders for id, don't touch anything, so no else
         else 
            local style = string.match(w, "(style=\")") or ""
            if (style ~= "") then -- shoehorn new fill at beginning of "style="
               wnew = string.gsub (w, style, style .. "fill:" .. orders[id] .. ";")
               svgnew = string.gsub (svgnew, w, wnew)
            else -- no style at all!
               local indent=""
               local insert=""
               insert, indent = string.match(w,"((%s*)id=\")") -- this had better not be nil...!
               wnew = string.gsub (w, insert, (indent or "") .. "style=\"fill:" .. orders [id] .. ";\"\n" .. (indent or ""))
            end -- end case of no style
         end -- end case of no fill
      end -- no else for id="" - if we don't have a name for the curve we can't easily figure out what it is, though in theory there could also be features to replace things by color without knowing what they're called                  
   end
   return svgnew,repdebug
end


function p.display(frame)
   output="" -- only a debug function for now
   local args=frame.args
   local parent=frame:getParent()
   local pargs=parent.args
   local svginput=args.svginput or pargs.svginput or "ERROR"
   local ids={}
   local fills={}
   local index=1
   local reverse={}
   for w in string.gmatch(svginput, "<.->") do
      local fill = string.match(w, "fill:(.-);") or ""
      local id = string.match(w, "id=\"(.-)\"") or ""
      if fill ~= "" then output = output .. id .. " = " .. fill .. "\n" end
      if (fill ~= "" and id ~= "") then 
          ids[index]=id
          fills[index]=fill
          index=index+1
          if (reverse[fill]) then 
              reverse[fill] = reverse[fill] .. ", " .. id
          else reverse[fill] = ":" .. id
          end
      end
   end
   output = output .. "\n " -- break before reverse lookup table output
   for k,v in pairs(reverse) do output = output .. "\n " .. k .. v end
   return output
end

function p.testextract(frame)
   local profile=frame.args.profile
   local levels=frame.args.levels
   local a={}
   a=extractprofile(profile, levels)
   return a[7]
end

function p.makesvg(frame)
   local args=frame.args
   local parent=frame:getParent()
   local pargs=parent.args
   local svginput=pargs.svginput or args.svginput or "ERROR"
   local profile=pargs.profile or args.profile or "==1== 000000" -- just paints all black
   local presets=pargs.presets or args.presets or ""
   local datafile=pargs.datafile or args.datafile or ""
   local z=1
   local preset=""
   local preposn=1
   local override={}
   local debug=""
   local repdebug=""
   debug = debug .. presets
   for preset in string.gmatch(presets,"([%d\?]+)") do
       if preset ~= "?" then override[z]=preset; debug=debug .. "preset" .. z .. preset end
       z=z+1
   end
   svgoutput,repdebug=replacetags(svginput,createorders(datafile,profile,override))
   return ((repdebug or "") .. debug .. "\n" .. "<pre>" .. svgoutput .. "</pre>") -- pre tags to keep output from being wikimunged!
end

return p