local r={}
local x=1
item=string.lower(fillCommas(item,max))
for t in string.gmatch(item,"[^,]+") do
r[x]=string.gsub(t,"%s+","")
if r[x]=='' then r[x]= nil else -- residual items might have commas
if x>max then r[max]=(r[max] or '')..', '..r[x] end
end
x=x+1
end
return r
end  
local function ParseShapeTypes (result,args,sgval) -- for use with compressed, comma-separated 'sg plus dots' parameters
-- shape table items and default values as set at top of page
shapeType="0", Name="initialSettings", Parent="0",
--sga items for the shape shape="circle", shapeSize="", shapeColor="blue", shapeAngle="0deg",
--sgb items for outline, outlineColor="darkblue", outline,
--sgc items label text textSZ="", textCL="white", textNG="0deg", textAT=attributes ("bold" and/or "italic")
--sgd items for dotTag tagSize="", tagColor="darkgrey", tagSpacer="", tagAngle="0deg",
--sge extension line textEW="", textEC="darkblue", textES="solid"
--sgf fx for text labels textSP="" textLH="100%" textOL="", textBG="paleground",  
---  
if args["sgn"..sgval] then
local sgname=string.match(args["sgn"..sgval],"(%w+)(.*)")
sgNames[sgname]=sgval
end
local parent= args["sgp"..sgval]
if parent then
parent=string.match(parent,"(%w+)(.*)")
local pos= string.find(parent,"%d+")
if pos == 1 then
parent=string.match(parent,"%d+")
else
parent=sgNames[parent] or '1'
end
end
if sgval~='H' then
if not parent or tonumber(parent) > tonumber(sgval) then
if sgval=="1" then parent="0" else parent="1" end
end
end
local itemTab, line, filename
result[sgval]={}
result[sgval].shapeType=sgval
line,filename=extractItem(args['sga'..sgval] or '','image:')
if sgval=='1' and not args.sga1 then line='circle,,blue,0deg' end -- ensure there is a parent=1 sga
result[sgval].shapeFile=filename or ''
-- sga= Attributes for shape
itemTab=splitItem(line,4)
result[sgval].shape = itemTab[1] or result[parent].shape
result[sgval].shapeSize=itemTab[2] or result[parent].shapeSize
result[sgval].shapeColor=itemTab[3] or result[parent].shapeColor
result[sgval].shapeAngle=itemCheck(itemTab[4],'deg') or result[parent].shapeAngle
-- sgb= Border outline attributes for shape
itemTab=splitItem(args['sgb'..sgval],3)
result[sgval].outlineWidth=itemCheck(itemTab[1],'px') or result[parent].outlineWidth
result[sgval].outlineColor=itemTab[2] or result[parent].outlineColor
result[sgval].outlineStyle=itemTab[3] or result[parent].outlineStyle
--sgc=character attributes for label
itemTab=splitItem(args['sgc'..sgval],4)
result[sgval].textSZ=itemCheck(itemTab[1],'px') or result[parent].textSZ -- size of text in px
result[sgval].textCL=itemTab[2] or result[parent].textCL -- colour for text
result[sgval].textNG=itemCheck(itemTab[3],'deg') or result[parent].textNG -- Angle for text
result[sgval].textAT=itemTab[4] or result[parent].textAT -- attributes bold, and/or italic
--sgd=dotTag attributes
itemTab=splitItem(args['sgd'..sgval],4)
result[sgval].tagSize=itemCheck(itemTab[1],'px') or result[parent].tagSize
result[sgval].tagColor=itemTab[2] or result[parent].tagColor
result[sgval].tagSpacer=itemCheck(itemTab[3],'px') or result[parent].tagSpacer
result[sgval].tagAngle=itemCheck(itemTab[4],'deg') or result[parent].tagAngle
--sge= extension line attributes
itemTab=splitItem(args['sge'..sgval],4)
result[sgval].textEW=itemCheck(itemTab[1],'px') or result[parent].textEW -- width
result[sgval].textEC=itemTab[2] or result[parent].textEC -- colour
result[sgval].textES=itemTab[3] or result[parent].textES -- style
--sgf= fx for label text
itemTab=splitItem(args['sgf'..sgval],4)
result[sgval].textSP=itemCheck(itemTab[1],'px') or result[parent].textSP -- spacing value for letters
result[sgval].textLH=itemCheck(itemTab[2],'%') or result[parent].textLH -- Angle for text
result[sgval].textOL=itemCheck(itemTab[3],'px') or result[parent].textOL -- width of text- line
result[sgval].textBG=itemTab[4] or result[parent].textBG -- color for text background  
return result
end  
local function round(x,dec)
-- x=number [, dec=integer] returns numeric value with upto dec decimals (all but first trailing zeros get truncated)
if (dec or 0)==0 then
return x>=0 and math.floor(x+0.5) or math.ceil(x-0.5) --this avoids .0 where dec=0
end
dec =10^(dec)
return x>=0 and math.floor(x*dec+0.5)/dec or math.ceil(x*dec-0.5)/dec
end  
local function maptogrid(t,r)
-- converts mercator projection longitude and latitude coordinates to x and y pixel coordinates, within a of given size, centre coordinates and zoom level.
t is a table of named indices: {lon, lat, lonbase, latbase, width, height, zoom}
output is two values, x and y, rounded to r decimal places--
local x=t.width/2 + ( ((math.rad(t.lon)*6378137) - (math.rad(t.lonbase)* 6378137) ) / (156543.03*math.cos(t.latbase/180)/(2^t.zoom) ) )*(1-(0.055*(t.latbase/90)))
local y=t.height/2 + ( ( (math.log(math.tan(math.rad(t.latbase)/2+math.pi/4))*6378137) - (math.log(math.tan(math.rad(t.lat)/2+math.pi/4))*6378137) ) / (156543.03*math.cos(t.latbase/180) / (2^t.zoom) ) )*(1-(0.055*(t.latbase/90) ) )
return round(x,r),round(y,r)
--source: python code at https://wiki.openstreetmap.org/wiki/Slippy_map_tilenames and https://wiki.openstreetmap.org/wiki/Mercator
---
width and height are the size, in pixels, of the map, which will be centerd around lonbase,latbase.
Method: Convert lon and lonbase to meter-offsets from coord(0,0), and subtract lonbase from lon,
zoom and latbase are used to scale the resulting meter-offset to pixels, and add it to width/2.
Convert lat and lat-base to meter-offsets from coord(0,0), subtract lat from latbase,
scale the resulting meter-offset to pixels, add it to height/2.
A correction factor '*(1-(0.055*(t.latbase/90) ) )' compensates for an error that seems to creep in towards
the edges of the map at higher latitudes. It was identified experimentally, and ensures a dot at the edge is
in the same place as if that location is positioned at the centre.  
Original Python code for lat,lon to x,y where 0,0 is the centre of the map
print('x=',width+(((math.radians(lon1) * 6378137)-
(math.radians(lonbase) * 6378137))/
(156543.03*math.cos(latbase/180)/(2**zoom))),' y=',height+((
(math.log(math.tan(math.pi / 4 + math.radians(latbase) / 2)) * 6378137)-
(math.log(math.tan(math.pi / 4 + math.radians(lat1) / 2)) * 6378137))/
(156543.03*math.cos(latbase/180)/(2**zoom)))
) --
end  
local function getScale(zoom, lat,magVal)
if magVal and magVal>1 and magVal <=2 then zoom=zoom+(magVal-1) end
local dist=(156543.03 * math.cos(math.rad(math.abs(lat))) / (2 ^ zoom))/17
local y
if dist < 1 then
y=(round(dist*10,1))
return tostring(y*100)..'m', tostring(round(y*109,0))..'yds'
elseif dist <18 then
y=(round(dist,0))
return tostring(y)..'km', tostring(round(y*0.621371,1))..'miles'
elseif dist <500 then
y=round(dist/10)
return tostring(y*10)..'km', tostring(round(y*6.21371,0))..'miles'
else
y=round(dist/100)
return tostring(y*100)..'km', tostring(round(y*62.1371,0))..'miles'
end
end  
local function ParseData (args,dotval) -- for use with compressed, comma-separated 'sg plus dots' parameters
-- takes a structured series of comma-separated items which get parsed as the following:
-- dot(n)= (sgNumber or Name), or (lat,lon), (dotTag)
-- dotlink(n) = single-parameter text to give wikilink and/or title used by tootlip, fullscreen dots and autocaption list
-- dotlabel(n) = 'label text',pos(,roght,top,bottom,centre,auto),(dx), (dy) pixel offsets, params, info
-- dotpic(n) = single parameter wikimedia filename for an image to use in photopanel and/or fullscreen dots
-- dotfeature(n)= 'mark-line' (,linewidth,style,gap) or 'photo-panel' (,image-dim,panel-width,panel-height), draws line to n-1
-- label is used if either a position and/or an x,y offset are not 0,0 ( if no label then dotTag will be put at at the x,y offset, or over the dot
-- label text can be autoaligned if x,y puts it or of the dot, or centered if above/below)
-- quote marks are not needed unless including commas within the label text