Module:Map

Documentation for this module may be created at Module: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 SMW string
function setSMW(obj, prop)
	mw.smw.set({[prop] = toJSON(obj)})
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 = {tonumber(pin.iconSizeX), tonumber(pin.iconSizeY) }
			elseif pin.iconSize then
				pin.iconSize = { tonumber(pin.iconSize), tonumber(pin.iconSize)}
			end
			if pin.iconAnchorX and pin.iconAnchorY then
				pin.iconAnchor = {tonumber(pin.iconAnchorX), tonumber(pin.iconAnchorY) }
			elseif pin.iconAnchor then
				pin.iconAnchor = {tonumber(pin.iconAnchor), tonumber(pin.iconAnchor)}
			end
			if pin.popupAnchorX and pin.popupAnchorY then
				pin.popupAnchor = {tonumber(pin.popupAnchorX), tonumber(pin.popupAnchorY) }
			elseif pin.popupAnchor then
				pin.popupAnchor = {tonumber(pin.popupAnchor), tonumber(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
	
	if args.smw == 'yes' or args.smw == 'y' or tonumber(args.smw) == 1 then
		args.smw = "Location JSON"
	elseif args.smw == 'hist' then
		args.smw = "Historic Location JSON"
	else
		args.smw = nil
	end

	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 locations'
			end
			local capt = string.format('%s locations', 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',
		icon = args.icon or 'greenPin'
	}
	if hc(args.group) then
		opts.group = args.group
	end
	if hc(args.show) then
		opts.show = args.show
	end
	
	-- plain map tiles
	if hc(args.plaintiles) then
		opts.plainTiles = 'true'
	end
	if hc(args.plainTiles) then
		opts.plainTiles = 'true'
	end
	
	-- other map tile version
	if hc(args.mapversion) then
		opts.mapVersion = args.mapversion
	end
	if hc(args.mapVersion) then
		opts.mapVersion = args.mapVersion
	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 = {}, {}
	local smwstr = ""
	if hc(args.features) then
		local _features = string.lower(args.features)
		features = featureMap[_features] or {}
	end
	if features.square then
		local feat = p.featSquare(args, opts)
		table.insert(featColl, feat)
		if args.smw then
			setSMW(feat, args.smw)
		end
	elseif features.circle then
		local feat = p.featCircle(args, opts)
		table.insert(featColl, feat)
		if args.smw then
			setSMW(feat, args.smw)
		end
	end
	if features.polygon then
		local feat = p.featPolygon(args, opts)
		table.insert(featColl, feat)
		if args.smw then
			setSMW(feat, args.smw)
		end
	elseif features.line then
		local feat = p.featLine(args, opts)
		table.insert(featColl, feat)
		if args.smw then
			setSMW(feat, args.smw)
		end
	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
			local feat = p.featPin(args, opts, pin)
			table.insert(featColl, feat)
			if args.smw then
				setSMW(feat, args.smw)
			end
		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 tostring(map) .. smwstr
	end
	return mw.getCurrentFrame():preprocess(tostring(map) .. smwstr)
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
			}
		}
	}
	if pin.iconWikiLink then
		pin.iconWikiLink = mw.ext.GloopTweaks.filepath(pin.iconWikiLink)
	end
	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>