Module:Sandbox/User:Jakesterwars/Map

From Illerai

This is the current revision of this page, as edited by Mark (Sọ̀rọ̀ | contribs) at 22:24, 2 November 2024 (1 revision imported). The present address (URL) is a permanent link to this version.

(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)
Jump to navigation Jump to search

Documentation for this module may be created at Module:Sandbox/User:Jakesterwars/Map/doc

-- <nowiki>
local hc = require('Module:Paramtest').has_content

local p = {}

local zoomSizes = {
	{ 3, 8 },
	{ 2, 4 },
	{ 1, 2 },
	{ 0, 1 },
	{ -1, 1/2 },
	{ -2, 1/4 },
	{ -3, 1/8 }
}
-- Default size of maps (to calc zoom)
local default_size = 800 -- 800px for full screen
-- Map feature (overlay) types
local featureMap = {
	none = {},
	square = { square=true },
	rectangle = { square=true },
	polygon = { polygon=true },
	line = { line=true },
	lines = { line=true },
	circle = { circle=true },
	pin = { pins=true },
	pins = { pins=true },
	['pin-polygon'] = { polygon=true, pins=true },
	['pins-polygon'] = { polygon=true, pins=true },
	['pin-line'] = { line=true, pins=true },
	['pins-line'] = { line=true, pins=true },
	['pin-circle'] = { circle=true, pins=true },
	['pins-circle'] = { circle=true, pins=true }
}
-- Possible properties
local simplestyle = {'title', 'description', 'marker-size', 'marker-symbol', 'marker-color',
	'stroke', 'stroke-opacity', 'stroke-width', 'fill', 'fill-opacity'}
local properties = {
	polygon = { title=true, description=true, stroke=true, ['stroke-opacity']=true, ['stroke-width']=true, fill=true, ['fill-opacity']=true },
	line = { title=true, description=true, stroke=true, ['stroke-opacity']=true, ['stroke-width']=true },
	circle = { title=true, description=true, stroke=true, ['stroke-opacity']=true, ['stroke-width']=true, fill=true, ['fill-opacity']=true },
	pin = { title=true, description=true, icon=true, iconWikiLink=true, iconSize=true, iconAnchor=true, popupAnchor=true}
}

-- Create JSON
function toJSON(j)
	local json_good, json = pcall(mw.text.jsonEncode, j)--, mw.text.JSON_PRETTY)
	if json_good then
		return json
	end
	return error('Error converting to JSON')
end
-- Create map html element
function createMapElement(elem, args, json)
	local mapelem = mw.html.create(elem)
	mapelem:attr(args):newline():wikitext(toJSON(json)):newline()
	return mapelem
end
-- Create pin description
function parseDesc(args, pin, pgname, ptype)
	local desc = {}
	if ptype == 'item' then
		desc = {
			"'''Item''': ".. (args.name or pgname),
			"'''Quantity''': ".. (pin.qty or 1)
		}
		if pin.respawn then
			table.insert(desc, "'''Respawn time''': "..pin.respawn)
		elseif args.respawn then
			table.insert(desc, "'''Respawn time''': "..args.respawn)
		end
		if pin.notes then
			table.insert(desc, "'''Notes''': "..pin.notes)
		elseif args.notes then
			table.insert(desc, "'''Notes''': "..args.notes)
		end
	elseif ptype == 'monster' then
		table.insert(desc, "'''Monster''': " .. (args.name or pgname))
		if pin.levels then
			table.insert(desc, "'''Level(s)''': " .. pin.levels)
		elseif args.levels then
			table.insert(desc, "'''Level(s)''': " .. args.levels)
		end
	elseif ptype == 'npc' then
		table.insert(desc, "'''NPC''': "..(args.name or pgname))
		if pin.version then
			table.insert(desc, "'''Version''': "..pin.version)
		elseif args.version then
			table.insert(desc, "'''Version''': "..args.version)
		end
		if pin.npcid then
			table.insert(desc, "'''NPC ID''': "..pin.npcid)
		elseif args.npcid then
			table.insert(desc, "'''NPC ID''': "..args.npcid)
		end
		if pin.objectid then
			table.insert(desc, "'''Object ID''': "..pin.objectid)
		elseif args.objectid then
			table.insert(desc, "'''Object ID''': "..args.objectid)
		end
		if pin.respawn then
			table.insert(desc, "'''Respawn time''': "..pin.respawn)
		elseif args.respawn then
			table.insert(desc, "'''Respawn time''': "..args.respawn)
		end
		if pin.notes then
			table.insert(desc, "'''Notes''': "..pin.notes)
		elseif args.notes then
			table.insert(desc, "'''Notes''': "..args.notes)
		end
	elseif ptype == 'object' then
		table.insert(desc, "'''Object''': "..(args.name or pgname))
		if pin.version then
			table.insert(desc, "'''Version''': "..pin.version)
		elseif args.version then
			table.insert(desc, "'''Version''': "..args.version)
		end
		if pin.objectid then
			table.insert(desc, "'''Object ID''': "..pin.objectid)
		elseif args.objectid then
			table.insert(desc, "'''Object ID''': "..args.objectid)
		end
		if pin.npcid then
			table.insert(desc, "'''NPC ID''': "..pin.npcid)
		elseif args.npcid then
			table.insert(desc, "'''NPC ID''': "..args.npcid)
		end
		if pin.respawn then
			table.insert(desc, "'''Respawn time''': "..pin.respawn)
		elseif args.respawn then
			table.insert(desc, "'''Respawn time''': "..args.respawn)
		end
		if pin.notes then
			table.insert(desc, "'''Notes''': "..pin.notes)
		elseif args.notes then
			table.insert(desc, "'''Notes''': "..args.notes)
		end
	else
		if args.desc then
			table.insert(desc, args.desc)
		end
		if pin.desc then
			table.insert(desc, pin.desc)
		elseif pin.x and pin.y then
			table.insert(desc, 'X,Y: '..pin.x..','..pin.y)
		end
	end

	return table.concat(desc, '<br>')
end
-- Parse unnamed arguments (arg = pin)
function p.parseArgs(args, ptype)
	args.pins = {}
	local sep = args.sep or '%s*,%s*'
	local pgname = mw.title.getCurrentTitle().text
	local rng = {
		xmin = 10000000,
		xmax = -10000000,
		ymin = 10000000,
		ymax = -10000000
	}

	local i,cnt = 1,0
	while (args[i]) do
		local v = mw.text.trim(args[i])
		if hc(v) then
			local pin = {}
			for u in mw.text.gsplit(v, sep) do
				local _u = mw.text.split(u, '%s*:%s*')
				if _u[2] then
					local k = mw.text.trim(_u[1])
					if k == 'x' or k == 'y' then
						pin[k] = tonumber(mw.text.trim(_u[2]))
					else
						pin[k] = mw.text.trim(_u[2])
					end
				else
					if pin.x then
						pin.y = tonumber(_u[1])
					else
						pin.x = tonumber(_u[1])
					end
				end
			end

			if pin.x > rng.xmax then
				rng.xmax = pin.x
			end
			if pin.x < rng.xmin then
				rng.xmin = pin.x
			end
			if pin.y > rng.ymax then
				rng.ymax = pin.y
			end
			if pin.y <  rng.ymin then
				rng.ymin = pin.y
			end

			-- Pin size/location args
			if pin.iconSizeX and pin.iconSizeY then
				pin.iconSize = {pin.iconSizeX, pin.iconSizeY }
			elseif pin.iconSize then
				pin.iconSize = {pin.iconSize, pin.iconSize}
			end
			if pin.iconAnchorX and pin.iconAnchorY then
				pin.iconAnchor = {pin.iconAnchorX, pin.iconAnchorY }
			elseif pin.iconAnchor then
				pin.iconAnchor = {pin.iconAnchor, pin.iconAnchor}
			end
			if pin.popupAnchorX and pin.popupAnchorY then
				pin.popupAnchor = {pin.popupAnchorX, pin.popupAnchorY }
			elseif pin.popupAnchor then
				pin.popupAnchor = {pin.popupAnchor, pin.popupAnchor}
			end

			pin.desc = parseDesc(args, pin, pgname, ptype)
			
			table.insert( args.pins, pin)
			cnt =  cnt + 1
		end
		i =  i + 1
	end

	-- In no anonymous args then x,y are pin
	if cnt == 0 then
		local x = tonumber(args.x) or 3233 -- Default is Lumbridge loadstone
		local y = tonumber(args.y) or 3222
		rng.xmax = x
		rng.xmin = x
		rng.ymax = y
		rng.ymin = y
		local desc = parseDesc(args, {}, pgname, ptype)
		table.insert( args.pins, {x = x, y = y, desc = desc} )
		cnt = cnt + 1
	end

	local xrange = rng.xmax - rng.xmin
	local yrange = rng.ymax - rng.ymin

	if not tonumber(args.x) then
		args.x = math.floor(rng.xmin + xrange/2)
	end
	if not tonumber(args.y) then
		args.y = math.floor(rng.ymin + yrange/2)
	end
	-- Default range (1 pin) is 40
	if not tonumber(args.x_range) then
		if xrange > 0 then
			args.x_range = xrange
		else
			args.x_range = 40
		end
	end
	if not tonumber(args.y_range) then
		if yrange > 0 then
			args.y_range = yrange
		else
			args.y_range = 40
		end
	end
	-- Default square (1 pin) is 20
	if not tonumber(args.squareX) then
		if xrange > 0 then
			args.squareX = xrange
		else
			args.squareX = 20
		end
	end
	if not tonumber(args.squareY) then
		if yrange > 0 then
			args.squareY = yrange
		else
			args.squareY = 20
		end
	end

	args.pin_count = cnt

	return args
end
-- Add styles
function styles(ftjson, args, this, ptype)
	local props = properties[ptype]
	for i,v in pairs(args) do
		if props[i] then
			ftjson.properties[i] = v
		end
	end
	for i,v in pairs(this) do
		if props[i] then
			ftjson.properties[i] = v
		end
	end

	return ftjson
end

function p.map(frame)
	return p.buildMap(frame:getParent().args)	
end

-- Functions for templates --
function p.buildMap(arguments)
	local args = {}
	for i,v in pairs(arguments) do
		args[i] = v
	end
	-- Allow map/element type per template easily
	local inv_args = {}
	for i,v in pairs(arguments) do
		inv_args[i] = v
	end
	
	--[[ Each unnamed arg is 1 pin in format:
		x,y
		or
		x:#,y:#,desc:#
	]]
	args = p.parseArgs(args, args.ptype)

	if hc(args.iconSize) then
		if string.find(args.iconSize, ',') then
			local isize = mw.text.split(args.iconSize, '%s*,%s*')
			args.iconSize = { tonumber(isize[1]) or 25, tonumber(isize[2]) or 25}
		else
			args.iconSize = { tonumber(args.iconSize) or 25, tonumber(args.iconSize) or 25}
		end
	end
	if hc(args.iconAnchor) then
		if string.find(args.iconAnchor, ',') then
			local ianch = mw.text.split(args.iconAnchor, '%s*,%s*')
			args.iconAnchor = { tonumber(ianch[1]) or 0, tonumber(ianch[2]) or 0}
		else
			args.iconAnchor = { tonumber(args.iconAnchor) or 0, tonumber(args.iconAnchor) or 0}
		end
	end
	if hc(args.popupAnchor) then
		if string.find(args.popupAnchor, ',') then
			local panch = mw.text.split(args.popupAnchor, '%s*,%s*')
			args.popupAnchor = { tonumber(panch[1]) or 0, tonumber(panch[2]) or 0}
		else
			args.popupAnchor = { tonumber(args.popupAnchor) or 0, tonumber(args.popupAnchor) or 0}
		end
	end
	
	if args.showPins then
		if args.pin_count > 1 then
			if not hc(args.text) then
				args.text = 'Show exact spawns'
			end
			local capt = string.format('%s spawns', args.pin_count)
			if hc(args.caption) then
				capt = args.caption
			end
	
			args.etype = 'maplink'
			args.features = 'pins'
			local link = tostring(p.createMap(args))
			capt = capt .. link
	
			args.etype = 'mapframe'
			args.caption = ''
			args.features = 'square'
			local map = tostring(p.createMap(args))
	
			local classes = 'mw-kartographer-container thumb'
			if hc(args.align) then
				local align = string.lower(args.align)
				if align == 'left' then
					classes = classes..' tleft'
				elseif align == 'right' then
					classes = classes..' tright'
				else
					classes = classes..' center'
				end
			else
				classes = classes..' center'
			end
	
			local width = args.width or 300
			local ret = mw.html.create('div')
			ret:addClass(classes)
				:tag('div')
					:addClass('thumbinner')
					:css('width', width .. 'px')
					:node(map)
					:tag('div')
						:addClass('thumbcaption')
						:css('text-align', 'center')
						:node(capt)
	
			return ret
		end
	end

	if hc(inv_args.mtype) then
		args.features = string.lower(inv_args.mtype)
	end
	if hc(args.mtype) then
		args.features = string.lower(args.mtype)
	end
	if not args.features then
		args.features = 'none'
	end

	args.etype = 'mapframe'
	
	if hc(inv_args.type) then
		args.etype = string.lower(inv_args.type)
	end
	if hc(args.type) then
		args.etype = string.lower(args.type)
	end

	return p.createMap(args)
end

-- Function for creating map or link
function p.createMap(args)
	local x, y = args.x, args.y
	local opts = {
		x = x,
		y = y,
		width = args.width or 300,
		height = args.height or 300,
		mapID = args.mapID or 0, -- RuneScape Surface
		plane = tonumber(args.plane) or 0,
		zoom = args.zoom or 2,
		align = args.align or 'center'
	}
	if hc(args.group) then
		opts.group = args.group
	end

	-- mapframe, maplink
	local etype = 'mapframe'
	if hc(args.etype) then
		etype = args.etype
	end
	
	-- translate "centre" spelling for align
	if opts.align == 'centre' then
		opts.align = 'center'
	end

	-- Caption or link text
	if etype == 'maplink' then
		opts.text = args.text or 'Maplink'
		if string.find(opts.text,'[%[%]]') then 
			return error('Text cannot contain links')
		end
	elseif hc(args.caption) then
		opts.text = args.caption
	else
		opts.frameless = ''
	end

	local featColl, features = {}, {}
	if hc(args.features) then
		local _features = string.lower(args.features)
		features = featureMap[_features] or {}
	end
	if features.square then
		table.insert(featColl, p.featSquare(args, opts))
	elseif features.circle then
		table.insert(featColl, p.featCircle(args, opts))
	end
	if features.polygon then
		table.insert(featColl, p.featPolygon(args, opts))
	elseif features.line then
		table.insert(featColl, p.featLine(args, opts))
	end
	if features.pins then
		if not opts.group then
			opts.group = 'pins'
		end
		opts.icon = args.icon or 'greenPin'
		for _,pin in ipairs(args.pins) do
			table.insert(featColl, p.featPin(args, opts, pin))
		end
	end

	local json = {}
	if #featColl > 0 then
		json = {
			type = 'FeatureCollection',
			features = featColl
		}

		-- Zoom
		local width,height = opts.width, opts.height
		if etype == 'maplink' then
			width,height = default_size, default_size
		end
		local x_range = tonumber(args.squareX) or 40
		local y_range = tonumber(args.squareY) or 40
		if tonumber(args.r) then
			x_range = tonumber(args.r)
			y_range = tonumber(args.r)
		end
		if tonumber(args.x_range) then
			x_range = tonumber(args.x_range)
		end
		if tonumber(args.y_range) then
			y_range = tonumber(args.y_range)
		end

		local zoom = -3
		for i,v in ipairs(zoomSizes) do
			local sqsx, sqsy = width/v[2], height/v[2]
			if sqsx > x_range and sqsy > y_range then
				zoom = v[1]
				break
			end
		end
		if zoom > 2 then
			zoom = 2
		end

		if tonumber(args.zoom) then
			opts.zoom = args.zoom
		else
			opts.zoom = zoom
		end
	end
	local map = createMapElement(etype, opts, json)
	if args.nopreprocess then
		return map
	end
	return mw.getCurrentFrame():preprocess(tostring(map))
end

-- Create a square feature
function p.featSquare(args, opts)
	local x, y = args.x, args.y
	local squareX = tonumber(args.squareX) or 20
	local squareY = tonumber(args.squareY) or 20
	squareX = math.max(1, args.r or math.floor(squareX / 2))
	squareY = math.max(1, args.r or math.floor(squareY / 2))

	local ftjson = {
		type = 'Feature',
		properties = {['_']='_', mapID=opts.mapID, plane=opts.plane},
		geometry = {
			type = 'Polygon',
			coordinates = {
				{
					{ x-squareX, y-squareY },
					{ x-squareX, y+squareY },
					{ x+squareX, y+squareY },
					{ x+squareX, y-squareY }
				}
			}
		}
	}

	ftjson = styles(ftjson, args, {}, 'polygon')
	return ftjson
end

-- Create a polygon feature
function p.featPolygon(args, opts)
	local points, lastpoint = {}, {}
	for _,v in ipairs(args.pins) do
		table.insert(points, {v.x, v.y,})
		lastpoint = {v.x, v.y,}
	end
	-- Close polygon
	if not (points[1][1] == lastpoint[1] and points[1][2] == lastpoint[2]) then
		table.insert(points, {points[1][1], points[1][2]})
	end

	local ftjson = {
		type = 'Feature',
		properties = {['_']='_', mapID=opts.mapID, plane=opts.plane},
		geometry = {
			type = 'Polygon',
			coordinates = { points }
		}
	}

	ftjson = styles(ftjson, args, {}, 'polygon')
	return ftjson
end

-- Create a line feature
function p.featLine(args, opts)
	local points, lastpoint = {}, {}
	for _,v in ipairs(args.pins) do
		table.insert(points, {v.x, v.y,})
		lastpoint = {v.x, v.y,}
	end
	if hc(args.close) then
		-- Close line
		if not (points[1][1] == lastpoint[1] and points[1][2] == lastpoint[2]) then
			table.insert(points, {points[1][1], points[1][2]})
		end
	end

	local ftjson = {
		type = 'Feature',
		properties = {
			['_'] = '_',
			shape = 'Line',
			mapID = opts.mapID,
			plane = opts.plane
		},
		geometry = {
			type = 'LineString',
			coordinates = points
		}
	}

	ftjson = styles(ftjson, args, {}, 'line')
	return ftjson
end

-- Create a circle feature
function p.featCircle(args, opts)
	local rad = tonumber(args.r) or 10
	local ftjson = {
		type = 'Feature',
		properties = {
			['_']='_',
			shape = 'Circle',
			radius = rad,
			mapID = opts.mapID,
			plane = opts.plane
		},
		geometry = {
			type = 'Point',
			coordinates = {
				args.x, args.y, opts.plane
			}	
		}
	}

	ftjson = styles(ftjson, args, {}, 'circle')
	return ftjson
end

-- Create a pin feature
-- Pin types: greyPin, greenPin, redPin
function p.featPin(args, opts, pin)
	local desc = pin.desc or pin.x..', '..pin.y
	local ftjson = {
		type = 'Feature',
		properties = {
			providerID = 0,
			description = desc,
			mapID = opts.mapID,
			plane = opts.plane
		},
		geometry = {
			type = 'Point',
			coordinates = {
				pin.x, pin.y, opts.plane
			}
		}
	}

	ftjson = styles(ftjson, args, pin, 'pin')

	if not (ftjson.properties.icon or ftjson.properties.iconWikiLink) then
		ftjson.properties.icon = 'greenPin'
	end

	return ftjson
end

return p
-- </nowiki>