Modul:GraphHelfer

aus Wikipedia, der freien Enzyklopädie

Die Dokumentation für dieses Modul kann unter Modul:GraphHelfer/Doku erstellt werden

local p = {} --p stands for package

--[======[
Für Graph-Templates, welche MapData Eingabedaten skalieren
wollen, bevor sie von der graph-Erweiterung weiterverarbeitet
werden.  Nützlich um z.B. eine equirektanguläre Projektion um
einen Faktor, hier latScale bzw. lonScale, zu strecken oder zu stauchen.
Beispiel-Aufrufe:
  {{#invoke:GraphHelfer|scaleMapData|map=Germany.map|latScale=1.5}} oder
  {{#invoke:GraphHelfer|scaleMapData|map=Germany.map|latScale=1.5|latCen=51.179342979289}}
(ist latCen bzw. lonCen nicht angegeben, wird das Feld latitude bzw. longitude
der Eingabedaten verwendet, in der Annahme, das diese den Mittelpunkt angeben)

named args:  (one of map or maps must be passed, but not both)
--------------------------------------------------------------
map      - name of a map data page on commons, the source data
maps     - to supply more than one map data page, separated by colon ','
latScale - float scale factor for latitude values 
latCen   - the latitude center around which lat scaling is performed
           latCen is mandatory, if maps and latScale are supplied
           latCen is optional,  if map and latScale is supplied
lonScale - float scale factor for longitude values
lonCen   - the longitude center around which lon scaling is performed
           lonCen is mandatory, if maps and lonScale are supplied
           lonCen is optional,  if map and lonScale is supplied
--]======]
function p.scaleMapData( f )
	local ret = {}
	local maps = f.maps or mw.text.split(f.args.maps or f.args.map, ", *")
	
	for _, map in pairs(maps) do
		--local t = mw.title.new( map, "Data" )
		--local json = mw.text.jsonDecode(t:getContent())
		local json = mw.ext.data.get(map)
		local feats = json.data.features
		
		local scaleLL = function (ll)
			local lC
			if f.args.latScale then
				lC = f.args.latCen or json.latitude
				-- 1-based using jsonDecode, 0-based using mw.ext.data.get
				--v2[2] = f.args.latScale*(v2[2]-lC) + lC
				ll[1] = f.args.latScale*(ll[1]-lC) + lC
			end
			if f.args.lonScale then
				lC = f.args.lonCen or json.longitude
				-- 1-based using jsonDecode, 0-based using mw.ext.data.get
				--v2[1] = f.args.lonScale*(v2[1]-lC) + lC
				ll[0] = f.args.lonScale*(ll[0]-lC) + lC
			end
		end
		
		for _, feat in pairs(feats)	do
			if feat and feat.geometry then
				for _, v in pairs(feat.geometry.coordinates) do
					for _, v1 in pairs(v) do
						if feat.geometry.type == "MultiPolygon" then
							for _, v2 in pairs(v1) do
								scaleLL(v2)
							end
						end
						if feat.geometry.type == "Polygon" then
							scaleLL(v1)
						end
					end
				end
				ret[#ret+1] = feat
			end
		end
	end
	
	return mw.text.jsonEncode(ret)
end

--[======[
This function sets orientation values of place labels roughly
as documented in Vorlage:Positionskarte+/Doku#Position
--]======]
function PositionskartePlaces( p, bm )
	local places = (p:gsub("{{","(("):gsub("}}","))")
		:gsub("(%[%[[^]]+)|","%1[["):gsub("|",","):gsub("(%[%[[^[]+)%[%[","%1|")
	)
	
	local convs = {
		label = function ( v ) return (
			v:gsub("^%[%[?", ""):gsub("%]?%]$", ""):gsub("^.*|", "")
		) end,
		position = function ( v ) return tonumber(
			({ right= 3, bottom= 6, left= 9, top= 12, center= -1 })[v] or v
		) end,
		lat = function ( v ) return
			require("Modul:Coordinate").CoordinateLAT( v )
		end,
		long = function ( v ) return
			require("Modul:Coordinate").CoordinateLONG( v )
		end
	}
	
	local setdef = function ( t )
		local _t = {
			[ 1] = { align= "center", baseline= "bottom", dy= -4, dx= 12 },
			[ 2] = { align= "left", baseline= "bottom", dy= -3, dx= -12 },
			[ 3] = { align= "left", baseline= "middle", dy= 0, dx= 6 },
			[ 4] = { align= "left", baseline= "top", dy= 3, dx= -12 },
			[ 5] = { align= "center", baseline= "top", dy= 4, dx= 12 },
			[ 6] = { align= "center", baseline= "top", dy= 4, dx= 0 },
			[ 7] = { align= "center", baseline= "top", dy= 4, dx= -12 },
			[ 8] = { align= "right", baseline= "top", dy= 3, dx= 12 },
			[ 9] = { align= "right", baseline= "middle", dy= 0, dx= -6 },
			[10] = { align= "right", baseline= "bottom", dy= -3, dx= 12 },
			[11] = { align= "center", baseline= "bottom", dy= -4, dx= -12},
			[12] = { align= "center", baseline= "bottom", dy= -6, dx= 0 },
			[ 0] = { align= "center", baseline= "bottom", dy= -6, dx= 0 },
			[-1] = { align= "center", baseline= "middle", dy= 0, dx= 0 }
		}
		local inUpprHalf = (bm.y > t.lat and t.lat > (bm.y + (bm.y2-bm.y)/2))
		local inLeftHalf = (bm.x < t.long and t.long < (bm.x + (bm.x2-bm.x)/2))
		local _pn = inUpprHalf and (inLeftHalf and 4 or 8)
								or (inLeftHalf and 2 or 10)
		for k, v in pairs(_t[t.position or _pn]) do
			t[k] = type(v) == "number" and v + (tonumber(t[k]) or 0) or v
		end
		
		t.label_color = t.label_color or "blue"
		t.label_size = math.floor(11 * (tonumber(t.label_size) or 100)/100)
		t.angle = t.angle or 0
		
		return t
	end
	
	local ovl = {}
	for place in mw.text.gsplit(places, "((", true) do
		local p, n = place:gsub("\n", ""):gsub("%)%),? *$", "")
		if n > 0 then
			local t = {}
			for arg in mw.text.gsplit(p, ",", true) do
				local k, v = unpack(mw.text.split(arg, "=", true))
				if v then
					k = mw.text.trim(k)
					v = mw.text.trim(v)
					t[k] = convs[k] and convs[k](v) or v
				end
			end
			ovl[#ovl+1] = setdef(t)
		end
	end
	
	local ovlbr = {}
	for k, place in pairs(ovl) do
		if place.label:match("<br[^>]*>") then
			local fontsz = place.label_size
			local t = {}
			-- assume place.baseline == "top"
			for l in mw.text.gsplit(place.label, "<br[^>]*>") do
				t[#t+1] = {}
				for k, v in pairs(place) do
					t[#t][k] = v
				end
				t[#t].label = l
				t[#t].dy = t[#t].dy + fontsz * (#t-1)
			end
			-- offset if assumption was wrong
			local off = place.baseline == "bottom" and fontsz * (#t-1) or
					(place.baseline == "middle" and (fontsz * (#t-1)) / 2 or 0)
			for _, v in pairs(t) do
				v.dy = v.dy - off
				ovlbr[#ovlbr+1] = v -- record wrapped label fragment
			end
			ovl[k] = nil -- delete unwrapped label
		end
	end
	if #ovlbr > 0 then
		local t = {}
		for _, v in pairs(ovl) do t[#t+1] = v end
		for _, v in ipairs(ovlbr) do t[#t+1] = v end
		ovl = t
	end
	
	return ovl
end

--[======[
Work-in-Progress, do NOT use productively yet
Example call(s) to place on a wiki page for testing:

 {{#tag:graph|{{#invoke:GraphHelfer|Positionskarte|Deutschland|width=400|places=
   ((Deutschland,label=[[TSV Bad Königshofen|Bad Königshofen]],position=6,long=10/25//E,lat=50/18//N,region=DE-BY,marktarget=TSV Bad Königshofen))
   ((Deutschland,label=[[Werder Bremen|Bremen]],position=right,long=8/50/34.3/E,lat=53/3/49/N,region=DE-HB,marktarget=Werder Bremen))
 }}}}

 {{#tag:graph|{{#invoke:GraphHelfer|Positionskarte|Frankreich|width=600|maptype=relief|places= 
   ((label=Paris,position=bottom,long=2/21/6/E,lat=48/51/24/N))
   ((label=Metz,position=top,long=06/10/37/O,lat=49/07/11/N))
   ((label=Toulouse,position=left,long=01/26/31/O,lat=43/36/16/N))
   ((label=Brando,position=left,long=9/28/34/O,lat=42/46/34/N))
 }}}}
--]======]
function p.Positionskarte( f )
	local d = mw.loadData( "Modul:Positionskarte/Data" )
	local k, ktyp = f.args.karte or f.args[1], f.args.maptype or "default"
	local bm
	
	if type(d) == "table" and d[k] then
		d = d[k]
		bm = { x= d.left, y= d.top, x2= d.right, y2= d.bottom,
				img = d.image and d.image[ktyp] }
	else
		d = "Vorlage:Positionskarte " .. k
		bm = {
			x= tonumber(f:expandTemplate{ title = d, args = { "left" } }),
			y= tonumber(f:expandTemplate{ title = d, args = { "top" } }),
			x2= tonumber(f:expandTemplate{ title = d, args = { "right" } }),
			y2= tonumber(f:expandTemplate{ title = d, args = { "bottom" } }),
			img= f:expandTemplate{ title = d, args = { "image", ktyp } }
		}
	end
	bm.xdiff = math.abs(bm.x2-bm.x)
	bm.ydiff = math.abs(bm.y2-bm.y)
	
	local imgt = mw.title.new(bm.img, "File")
	local imgw = imgt.file and imgt.file.width
	local imgh = imgt.file and imgt.file.height
	local imgaspect = (imgw and imgh and imgw/imgh) or 1.0
	
	local gw = tonumber( f.args.width or 250 )
	local gh = math.floor( (gw / imgaspect) + 0.5 )
	bm.img = "wikifile:///File:" .. bm.img .. "?width=" .. gw .. "&height=" .. gh
	
	local graph =
		{
			width= gw,
			height= gh,
			padding= { left= 0, top= 0, bottom= 0, right= 0 },
			data= {
				{
					name= "basemap",
					values= { bm }
				},
				{
					name= "overlay",
					values= PositionskartePlaces( f.args.places or "", bm )
				}
			},
			scales= {
				{
					name= "x",
					type= "linear",
					domain= {bm.x, bm.x2},
					range= "width",
					zero= false
				},
				{
					name= "y",
					type= "linear",
					domain= {bm.y2, bm.y},
					range= "height",
					zero= false
				}
			},
			marks= {
				{
					type= "image",
					from= {data= "basemap"},
					properties= {
						enter= {
							url= {field= "img"},
							aspect= false, --always extend to group size
							x= {scale= "x", field= "x"},
							y= {scale= "y", field= "y"},
							x2= {scale= "x", field= "x2"},
							y2= {scale= "y", field= "y2"},
							align= {value= "left"},
							baseline= {value= "top"}
						}
					}
				},
				{
					type= "symbol",
					from= {data= "overlay"},
					properties= {
						enter= {
							x= {scale= "x",field= "long"},
							y= {scale= "y",field= "lat"},
							stroke= {value= "red"},
							fill= {value= "red"},
							size= {value= 30}
						}
					}
				},
				{
					type= "text",
					from= {data= "overlay"},
					properties= {
						enter= {
							x= {scale= "x",field= "long"},
							y= {scale= "y",field= "lat"},
							text= {field= "label"},
							fill= {field= "label_color"},
							fontSize= {field = "label_size"},
							fontWeight= {value= "normal"},
							align= {field= "align"},
							baseline= {field= "baseline"},
							dx= {field= "dx"},
							dy= {field= "dy"},
							angle= {field= "angle"}
						}
					}
				}
			}
		}
	
	if f.args.axes then
		graph.axes = {
			{type= "x", scale= "x"},
			{type= "y", scale= "y"}
		}
		graph.padding.left = 25
		graph.padding.bottom = 25
		if f.args.axes == "inset" then
			graph.width = graph.width - 25
			graph.height = graph.height - 25
		end
	end
	
	if f.args.mark then
		local marksz = tonumber(f.args.marksize) or 8
		local mark = #f.args.mark < 4 and "Red_pog.svg" or
			(f.args.mark:gsub("^File:",""):gsub("^Datei:",""):gsub(" ", "_"))
		mark = "wikifile:///File:"..mark.."?width="..marksz.."&height="..marksz
		local sym
		for _, v in ipairs(graph.marks) do
			if v.type == "symbol" then sym = v end
		end
		sym.type = "image"
		sym.properties.enter = {
			url= {value= mark},					--url= {field= "mark"},
			x= {scale= "x",field= "long"},
			y= {scale= "y",field= "lat"},
			width= {value= marksz},				--width= {field= "marksize"},
			height= {value= marksz},			--height= {field= "marksize"},
			align= {value= "center"},
			baseline= {value= "middle"}
		}
	end
	
	if f.args.mapdata and type(d) == "table" then
		local data = graph.data
		local marks = {}
		local _sty = {
			seas = { fill= "#C7EDFF" },
			states = { strokeWidth= "0.6" },
			neighs = { strokeWidth= "1.5", fill= "#ddd" }
		}
		setmetatable(_sty, { __index = function () return {} end })
		for md in mw.text.gsplit(f.args.mapdata, ',') do
			if d[md] then
				data[#data + 1] = {
					name = md,
					values = p.scaleMapData{ maps = d[md], args = {
						latScale = gh/gw * bm.xdiff/bm.ydiff,
						latCen = bm.y + (bm.y2-bm.y)/2,
					}},
					transform= {
						{
							type= "geopath",
							projection= "equirectangular",
							translate= { gw/2, gh/2 },
							center= {
								bm.x + (bm.x2-bm.x)/2,
								bm.y + (bm.y2-bm.y)/2
							},
							scale= 56.789 * math.min(gw/bm.xdiff, gh/bm.ydiff)
						}
					}
				}
				marks[#marks + 1] = {
					type= "path",
					from= {data= md},
					properties= {
						enter= {
							stroke= {value=
								d[md].stroke or _sty[md].stroke or "#aaa"},
							strokeWidth= {value=
								d[md].strokeWidth or _sty[md].strokeWidth or 1.0},
							fill= {value=
								d[md].fill or _sty[md].fill or "#FFFFEA"},
							path= {field= "layout_path"}
						}
					}
				}
			else
				if md == "seas" then
					marks[#marks + 1] = {
						type= "rect",
						properties= {
							enter= {
								x = {value= 0},
								y = {value= 0},
								x2 = {field= {group= "width"}},
								y2 = {field= {group= "height"}},
								strokeWidth= {value= 0},
								fill= {value= _sty[md].fill}
							}
						}
					}
				end
			end
		end
		for i = 2, #graph.marks do
			marks[#marks+1] = graph.marks[i]
		end
		graph.marks = {
			{
				type= "group",
				properties= {
					enter= {
						x= {value= 0},
						y= {value= 0},
						x2= {value= gw},
						y2= {value= gh},
						clip= {value= true}
					}
				},
				scales = graph.scales,
				axes = graph.axes,
				marks = marks
			}
		}
		graph.scales = nil
		graph.axes = nil
	end
	
	return mw.text.jsonEncode(graph)
end

return p