モジュール:Mapframe/sandbox

-- Note: Originally written on English Wikipedia at https://en.wikipedia.org/wiki/Module:Mapframe
------------------------------------------------------------------------------
##### Localisation (L10n) settings #####
Replace values in quotes ("") with localised values
------------------------------------------------------------------------------
local L10n = {}
-- Modue dependencies
local transcluder -- local copy of https://www.mediawiki.org/wiki/Module:Transcluder loaded lazily
-- "strict" should not be used, at least until all other modules which require this module are not using globals.
-- Template parameter names (unnumbered versions only)
--   Specify each as either a single string, or a table of strings (aliases)
--   Aliases are checked left-to-right, i.e. `{ "one", "two" }` is equivalent to using `` in a template
L10n.para = {
	display		= "display",
	type		= "type",
	id              = { "id", "ids" },
	from		= "from",
	raw		= "raw",
	title		= "title",
	description	= "description",
	strokeColor     = { "stroke-color", "stroke-colour" },
	strokeWidth	= "stroke-width",
	strokeOpacity = "stroke-opacity",
	fill        = "fill",
	fillOpacity     = "fill-opacity",
	coord		= "coord",
	marker		= "marker",
	markerColor	= { "marker-color", "marker-colour" },
	markerSize = "marker-size",
	radius      = { "radius", "radius_m" },
	radiusKm    = "radius_km",
	radiusFt    = "radius_ft",
	radiusMi    = "radius_mi",
	edges       = "edges",
	text		= "text",
	icon		= "icon",
	zoom		= "zoom",
	frame		= "frame",
	plain		= "plain",
	frameWidth	= "frame-width",
	frameHeight	= "frame-height",
	frameCoordinates = { "frame-coordinates", "frame-coord" }, 
	frameLatitude	= { "frame-lat", "frame-latitude" },
	frameLongitude	= { "frame-long", "frame-longitude" },
	frameAlign	= "frame-align",
	switch           = "switch",
	overlay          = "overlay",
	overlayBorder    = "overlay-border",
	overlayHorizontalAlignment = "overlay-horizontal-alignment",
	overlayVerticalAlignment = "overlay-vertical-alignment",
	overlayHorizontalOffset = "overlay-horizontal-offset",
	overlayVerticalOffset = "overlay-vertical-offset"
}
-- Names of other templates this module depends on
L10n.template = {
	Coord		= "coord", -- lowercase template name
	Coord2		= "coord2" -- lowercase template name
}
-- Error messages
L10n.error = {
	badDisplayPara    = "無効な表示パラメータです",
	noCoords	      = "ウィキデータか|" .. ( type(L10n.para.coord)== 'table' and L10n.para.coord1 or L10n.para.coord ) .. "= に座標を指定してください",
	wikidataCoords    = "ウィキデータに座標がありません",
	noCircleCoords    = "円の中心座標が指定されていないかウィキデータにありません",
	negativeRadius    = "円の半径は正の数で指定してください",
	noRadius          = "円の半径が指定されていません",
	negativeEdges     = "円の端は正の数で指定してください",
	noSwitchPara      = "|" .. ( type(L10n.para.switch)== 'table' and L10n.para.switch1 or L10n.para.switch ) .. "= でのスイッチ値が1つしか見つかりません",
	oneSwitchLabel    = "|" .. ( type(L10n.para.switch)== 'table' and L10n.para.switch1 or L10n.para.switch ) .. "= でのラベルが1つしか見つかりません",
	noSwitchLists     = "SWITCH: listは最低1つ指定してください",
	switchMismatches  = "All SWITCH:のリストは同じ数である必要があります",
	
	 -- "%s" and "%d" tokens will be replaced with strings and numbers when used
	oneSwitchValue    = "|%s= でのスイッチ値が1つしか見つかりません",
	fewerSwitchLabels = "スイッチ値が%d個指定されていますが|" .. ( type(L10n.para.switch)== 'table' and L10n.para.switch1 or L10n.para.switch ) .. "= で指定された%d個のラベルのみです",
	noNamedCoords     = "%sで名前のある座標がありません"
}
-- Other strings
L10n.str = {
	-- valid values for display parameter, e.g. (|display=inline) or (|display=title) or (|display=inline,title) or (|display=title,inline)
	inline		= "inline",			
	title		= "title",	
	dsep		= ",",			-- separator between inline and title (comma in the example above)
	-- valid values for type paramter
	line		= "line",		-- geoline feature (e.g. a road)
	shape		= "shape",		-- geoshape feature (e.g. a state or province)
	shapeInverse	= "shape-inverse",	-- geomask feature (the inverse of a geoshape)
	data		= "data",		-- geoJSON data page on Commons
	point		= "point",		-- single point feature (coordinates)
	circle      = "circle",     -- circular area around a point
	named       = "named",      -- all named coordinates in an article or section
	
	-- Keyword to indicate a switch list. Must NOT use the special characters ^$()%.*+-?
	switch = "SWITCH",
	
	-- valid values for icon, frame, and plain parameters
	affirmedWords = ' '..table.concat({
		"add",
		"added",
		"affirm",
		"affirmed",
		"include",
		"included",
		"on",
		"true",
		"yes",
		"y"
	}, ' ')..' ',
	declinedWords = ' '..table.concat({
		"decline",
		"declined",
		"exclude",
		"excluded",
		"false",
		"none",
		"not",
		"no",
		"n",
		"off",
		"omit",
		"omitted",
		"remove",
		"removed"
	}, ' ')..' '
}
-- Default values for parameters
L10n.defaults = {
	display		= L10n.str.inline,
	text		= "地図",
	frameWidth	= "300",
	frameHeight	= "200",
frameAlign  = "right",
	marker		= "marker",		-- Do not translate. For valid alternate values, see https://www.mediawiki.org/wiki/Maps/Icons
	markerColor	= "5E74F3",
	markerSize	= nil,
	strokeColor	= "#ff0000",
	strokeWidth	= 6,
	edges = 32, -- number of edges used to approximate a circle
	overlayBorder = "1px solid white",
	overlayHorizontalAlignment = "right",
	overlayHorizontalOffset = "0",
	overlayVerticalAlignment = "bottom",
	overlayVerticalOffset = "0"
}
-- #### End of L10n settings ####
------------------------------------------------------------------------------
Utility methods
------------------------------------------------------------------------------
local util = {}
--
Looks up a parameter value based on the id (a key from the L10n.para table) and
optionally a suffix, for parameters that can be suffixed (e.g. type2 is type
with suffix 2).
@param {table} args  key-value pairs of parameter names and their values 
@param {string} param_id  id for parameter name (key from the L10n.para table)
@param {string} suffix  suffix for parameter name
@returns {string|nil} parameter value if found, or nil if not found 
--
function util.getParameterValue(args, param_id, suffix)
	suffix = suffix or 
	if type( L10n.paraparam_id ) ~= 'table' then
		return argsL10n.paraparam_id..suffix
	end
	for _i, paramAlias in ipairs(L10n.paraparam_id) do
		if argsparamAlias..suffix then
			return argsparamAlias..suffix
		end
	end
	return nil
end	
--
Trim whitespace from args, and remove empty args. Also fix control characters.
@param {table} argsTable
@returns {table} trimmed args table
--
function util.trimArgs(argsTable)
	local cleanArgs = {}
	for key, val in pairs(argsTable) do
		if type(val) == 'string' then
			val = val:match('^%s*(.-)%s*$')
			if val ~=  then
				-- control characters inside json need to be escaped, but stripping them is simpler
				-- See also T214984
				-- However, *don't* strip control characters from wikitext or you'll break strip markers
				cleanArgskey = (not util.matchesParam('text', key)) and val:gsub('%c',' ') or val
			end
		else
			cleanArgskey = val
		end
	end
	return cleanArgs
end
--nil} - an optional suffix to apply to the value(s) from the localization key
@returns {boolean} true if the name matches the parameter, false otherwise
--
function util.matchesParam(key, name, suffix)
	local param = L10n.parakey
	suffix = suffix or 
	if type(param) == 'table' then
		for _, v in pairs(param) do
			if (v .. suffix) == name then return true end
		end
		return false
	end
	return ((param .. suffix) == name)
end
--
Check if a value is affirmed (one of the values in L10n.str.affirmedWords)
@param {string} val  Value to be checked
@returns {boolean} true if affirmed, false otherwise
--
function util.isAffirmed(val)
	if not(val) then return false end
	return string.find(L10n.str.affirmedWords, ' '..val..' ', 1, true ) and true or false
end
--
Check if a value is declined (one of the values in L10n.str.declinedWords)
@param {string} val  Value to be checked
@returns {boolean} true if declined, false otherwise
--
function util.isDeclined(val)
	if not(val) then return false end
	return string.find(L10n.str.declinedWords , ' '..val..' ', 1, true ) and true or false
end
--
Recursively extract coord templates which have a name parameter.
@param {string} wikitext
@returns {table} table sequence of coord templates
--
function util.extractCoordTemplates(wikitext)
	local output = {}
	local templates = mw.ustring.gmatch(wikitext, '{%b{}}')
	local subtemplates = {}
	for template in templates do
		local name = mw.ustring.match(template, '|+")
		if mw.ustring.lower(mw.text.trim(name)) == L10n.template.Coord or mw.ustring.lower(mw.text.trim(name)) == L10n.template.Coord2 then
			if nameParam then table.insert(output, template) end
		elseif mw.ustring.find(template, L10n.template.Coord) or mw.ustring.find(template, L10n.template.Coord2) then
			local subOutput = util.extractCoordTemplates(mw.ustring.sub(template, 2))
			for _, t in pairs(subOutput) do
				table.insert(output, t)
			end
		end
	end
	-- ensure coords are not using title display
	for k, v in pairs(output) do
		outputk = mw.ustring.gsub(v, "|%s*display%s*=}+", "|display=inline")
	end
	return output
end
--nil} page  Page name, or name#section, to get named coordinates
from. If the name is omitted, i.e. #section or nil or empty string, then
the current page will be used.
@returns {table} sequence of {coord, name, description} tables where coord is
the coordinates in a format suitable for #util.parseCoords, name is a string,
and description is a string (coordinates in a format suitable for displaying
to the reader). If for some reason the name can't be found, the description
is nil and the name contains display-format coordinates.
@throws {L10n.error.noNamedCoords} if no named coordinates are found.
--
function util.getNamedCoords(page)
	if transcluder == nil then
		-- load Module:Transcluder lazily so it is only transcluded on pages that
		-- actually use named coordinates
		transcluder = require("Module:Transcluder")
	end
	local parts = mw.text.split(page or "", "#", true)
	local name = parts1 == "" and mw.title.getCurrentTitle().prefixedText or parts1
	local section = parts2
	local pageWikitext = transcluder.get(section and name.."#"..section or name)
	local coordTemplates = util.extractCoordTemplates(pageWikitext)
	if #coordTemplates == 0 then error(string.format(L10n.error.noNamedCoords, page or name), 0) end
	local frame = mw.getCurrentFrame()
	local sep = "________"
	local expandedContent = frame:preprocess(table.concat(coordTemplates, sep))
	local expandedTemplates = mw.text.split(expandedContent, sep)
	local namedCoords = {}
	for _, expandedTemplate in pairs(expandedTemplates) do
		local coord = mw.ustring.match(expandedTemplate, "(.-)")
		if coord then
			local name = mw.ustring.match(expandedTemplate, "(.-)") or coord
			local description = name ~= coord and coord
			local outputCoord
			if mw.ustring.find(coord, '度') then
				local coordJa = mw.text.split(coord, '緯経度 ')
				local i18nmap = {'東' = 'E', '南' = 'S', '西' = 'W', '北' = 'N'}
				outputCoord = coordJa2 .. '_' .. i18nmapcoordJa1 .. '_' .. coordJa5 .. '_' .. i18nmapcoordJa4
			else
				outputCoord = mw.ustring.gsub(coord, "° ", "_")
			end
			table.insert(namedCoords, {coord=outputCoord, name=name, description=description})
		end
	end
	if #namedCoords == 0 then error(string.format(L10n.error.noNamedCoords, page or name), 0) end
	return namedCoords
end
--
Parse coordinate values from the params passed in a GeoHack url (such as
//tools.wmflabs.org/geohack/geohack.php?pagename=Example&params=1_2_N_3_4_W_ or
//tools.wmflabs.org/geohack/geohack.php?pagename=Example&params=1.23_S_4.56_E_ )
or non-url string in the same format (such as `1_2_N_3_4_W_` or `1.23_S_4.56_E_`)
@param {string} coords  string containing coordinates
@returns {number, number} latitude, longitude
--
function util.parseCoords(coords)
	local coordsPatt
	if mw.ustring.find(coords, "params=", 1, true) then
		-- prevent false matches from page name, e.g. ?pagename=Lorem_S._Ipsum
		coordsPatt = 'params=(_%.%d+NS_%.%d+EW)'
	else
		-- not actually a geohack url, just the same format
		coordsPatt = '_%.%d+NS_%.%d+EW'
	end
	local parts = mw.text.split((mw.ustring.match(coords, coordsPatt) or ), '_')
	local lat_d = tonumber(parts1)
	local lat_m = tonumber(parts2) -- nil if coords are in decimal format
	local lat_s = lat_m and tonumber(parts3) -- nil if coords are either in decimal format or degrees and minutes only
	local lat = lat_d + (lat_m or 0)/60 + (lat_s or 0)/3600
	if parts#parts/2 == 'S' then
		lat = lat * -1
	end
	local long_d = tonumber(parts1+#parts/2)
	local long_m = tonumber(parts2+#parts/2) -- nil if coords are in decimal format
	local long_s = long_m and tonumber(parts3+#parts/2) -- nil if coords are either in decimal format or degrees and minutes only
	local long = long_d + (long_m or 0)/60 + (long_s or 0)/3600
	if parts#parts == 'W' then
		long = long * -1
	end
	return lat, long
end
--
Get coordinates from a Wikidata item
@param {string} item_id  Wikidata item id (Q number)
@returns {number, number} latitude, longitude
@throws {L10n.error.noCoords} if item_id is invalid or the item does not exist
@throws {L10n.error.wikidataCoords} if the the item does not have a P625
statement (coordinates), or it is set to "no value"
--
function util.wikidataCoords(item_id)
	if not(mw.wikibase.isValidEntityId(item_id)) or not(mw.wikibase.entityExists(item_id)) then
		error(L10n.error.noCoords, 0)
	end
	local coordStatements = mw.wikibase.getBestStatements(item_id, 'P625')
	if not coordStatements or #coordStatements == 0 then
		error(L10n.error.wikidataCoords, 0)
	end
	local hasNoValue = ( coordStatements1.mainsnak and (coordStatements1.mainsnak.snaktype == 'novalue' or coordStatements1.mainsnak.snaktype == 'somevalue') )
	if hasNoValue then
		error(L10n.error.wikidataCoords, 0)
	end
	local wdCoords = coordStatements1'mainsnakdatavaluevalue'
	return tonumber(wdCoords'latitude'), tonumber(wdCoords'longitude')
end
--
Creates a polygon that approximates a circle
@param {number} lat  Latitude
@param {number} long  Longitude
@param {number} radius  Radius in metres
@param {number} n  Number of edges for the polygon
@returns {table} sequence of {latitude, longitude} table sequences, where
latitude and longitude are both numbers
--
function util.circleToPolygon(lat, long, radius, n) -- n is number of edges
	-- Based on https://github.com/gabzim/circle-to-polygon, ISC licence
	
	local function offset(cLat, cLon, distance, bearing)
		local lat1 = math.rad(cLat)
		local lon1 = math.rad(cLon)
		local dByR = distance / 6378137 -- distance divided by 6378137 (radius of the earth) wgs84
		local lat = math.asin(
			math.sin(lat1) * math.cos(dByR) +
			math.cos(lat1) * math.sin(dByR) * math.cos(bearing)
		)
		local lon = lon1 + math.atan2(
			math.sin(bearing) * math.sin(dByR) * math.cos(lat1),
			math.cos(dByR) - math.sin(lat1) * math.sin(lat)
		)
		return {math.deg(lon), math.deg(lat)}
	end
	
	local coordinates = {};
	local i = 0;
	while i } key-value pairs of attribute names and values 
--
function make.tagAttribs(args, isTitle)
	local attribs = {}
	if util.getParameterValue(args, 'zoom') then
		attribs.zoom = util.getParameterValue(args, 'zoom')
	end
	if util.isDeclined(util.getParameterValue(args, 'icon')) then
		attribs.class = "no-icon"
	end
	if util.getParameterValue(args, 'type') == L10n.str.point and not coordsDerivedFromFeatures then
		local lat, long = make.coords(args, 'plainOutput')
		attribs.latitude = tostring(lat)
		attribs.longitude = tostring(long)
	end
	if util.isAffirmed(util.getParameterValue(args, 'frame')) and not(isTitle) then
		attribs.width = util.getParameterValue(args, 'frameWidth') or L10n.defaults.frameWidth
		attribs.height = util.getParameterValue(args, 'frameHeight') or L10n.defaults.frameHeight
		if util.getParameterValue(args, 'frameCoordinates') then
			local frameLat, frameLong = util.parseCoords(util.getParameterValue(args, 'frameCoordinates'))
			attribs.latitude = frameLat
			attribs.longitude = frameLong
		else
			if util.getParameterValue(args, 'frameLatitude') then
				attribs.latitude = util.getParameterValue(args, 'frameLatitude')
			end
			if util.getParameterValue(args, 'frameLongitude') then
				attribs.longitude = util.getParameterValue(args, 'frameLongitude')
			end
		end
		if not attribs.latitude and not attribs.longitude and not coordsDerivedFromFeatures then
			local success, lat, long = pcall(util.wikidataCoords, util.getParameterValue(args, 'id') or mw.wikibase.getEntityIdForCurrentPage())
			if success then
				attribs.latitude = tostring(lat)
				attribs.longitude = tostring(long)
			end
		end
		if util.getParameterValue(args, 'frameAlign') then
			attribs.align = util.getParameterValue(args, 'frameAlign')
		end
		if util.isAffirmed(util.getParameterValue(args, 'plain')) then
			attribs.frameless = "1"
		else
			attribs.text = util.getParameterValue(args, 'text') or L10n.defaults.text
		end
	else
		attribs.text = util.getParameterValue(args, 'text') or L10n.defaults.text
	end
	return attribs
end
--display=title are positioned).
@param {table} args
@param {string} tagContent  Content for the maplink tag
@returns {string}
--
function make.titleOutput(args, tagContent)
	local titleTag = mw.text.tag('maplink', make.tagAttribs(args, true), tagContent)
	local spanAttribs = {
		style = "font-size: small;",
		id = "coordinates"
	}
	return mw.text.tag('span', spanAttribs, titleTag)
end
--
Makes maplink or mapframe wikitext that will be located inline.
@param {table} args
@param {string} tagContent  Content for the maplink tag
@returns {string}
--
function make.inlineOutput(args, tagContent)
	local tagName = 'maplink'
	if util.getParameterValue(args, 'frame') then
		tagName = 'mapframe'
	end
	return mw.text.tag(tagName, make.tagAttribs(args), tagContent)
end
--
Makes the HTML required for the swicther to work, including the templatestyles
tag.
@param {table} params  table sequence of {map, label} tables
@param {string} params{}.map  Wikitext for mapframe map
@param {string} params{}.label  Label text for swicther option
@param {table} options
@param {string} options.alignment  "left" or "center" or "right"
@param {boolean} options.isThumbnail  Display in a thumbnail
@param {string} options.width  Width of frame, e.g. "200"
@param {string} options.caption  Caption wikitext for thumnail
@retruns {string} swicther HTML
--
function make.switcherHtml(params, options)
	if not options then option = {} end
	local frame = mw.getCurrentFrame()
	local styles = frame:extensionTag{
		name = "templatestyles",
		args = {src = "Template:Maplink/styles-multi.css"}
	}
	local container = mw.html.create("div")
		:addClass("switcher-container")
		:addClass("mapframe-multi-container")
	if options.alignment == "left" or options.alignment == "right" then
		container:addClass("float"..options.alignment)
	else -- alignment is "center"
		container:addClass("center")
	end
	for i = 1, #params do
		container
			:tag("div")
				:wikitext(paramsi.map)
				:tag("span")
					:addClass("switcher-label")
					:css("display", "none")
					:wikitext(mw.text.trim(paramsi.label))
	end
	if not options.isThumbnail then
		return styles .. tostring(container)
	end
	local classlist = container:getAttr("class")
	classlist = mw.ustring.gsub(classlist, "%a*"..options.alignment, "")
	container:attr("class", classlist)
	local outerCountainer = mw.html.create("div")
		:addClass("mapframe-multi-outer-container")
		:addClass("mw-kartographer-container")
		:addClass("thumb")
	if options.alignment == "left" or options.alignment == "right" then
		outerCountainer:addClass("t"..options.alignment)
	else -- alignment is "center"
		outerCountainer
			:addClass("tnone")
			:addClass("center")
	end
	outerCountainer
		:tag("div")
			:addClass("thumbinner")
			:css("width", options.width.."px")
			:node(container)
			:node(options.caption and mw.html.create("div")
				:addClass("thumbcaption")
				:wikitext(options.caption)
			)
	return styles .. tostring(outerCountainer)
end
--
Makes the HTML required for an overlay map to work
tag.
@param {string} overlayMap  wikitext for the overlay map
@param {string} baseMap  wikitext for the base map
@param {table} options  various styling/display options
@param {string} options.align  "left" or "center" or "right"
@param {string|number} options.width  Width of the base map, e.g. "300"
@param {string|number} options.width  Height of the base map, e.g. "200"
@param {string} options.border  Border style for the overlayed map, e.g. "1px solid white"
@param {string} options.horizontalAlignment  Horizontal alignment for overlay map, "left" or "right"
@param {string|number} options.horizontalOffset  Horizontal offset in pixels from the alignment edge, e.g "10"
@param {string} options.verticalAlignment  Vertical alignment for overlay map, "top" or "bottom"
@param {string|number} options.verticalOffset  Vertical offset in pixels from the alignment edge, e.g. is "10"
@param {boolean} options.isThumbnail  Display in a thumbnail
@param {string} options.caption  Caption wikitext for thumnail
@retruns {string} HTML for basemap with overlay
--
function make.overlayHtml(overlayMap, baseMap, options)
	if not options then options = {} end
	local containerFloatClass = "float"..(options.align or "none")
	if options.align == "center" then
		containerFloatClass = "center"
	end
	local containerStyle = {
		position = "relative",
		width = options.width .. "px",
		height = options.height .. "px",
		overflow = "hidden" -- mobile/minerva tends to add scrollbars for a couple of pixels
	}
	if options.align == "center" then
		containerStyle"margin-left" = "auto"
		containerStyle"margin-right" = "auto"
	end
	local container = mw.html.create("div")
		:addClass("mapframe-withOverlay-container")
		:addClass(containerFloatClass)
		:addClass("noresize")
		:css(containerStyle)
	
	local overlayStyle = {
		position = "absolute",
		"z-index" = "1",
		border = options.border or "1px solid white"
	}
	if options.horizontalAlignment == "right" then
		overlayStyle.right = options.horizontalOffset .. "px"
	else
		overlayStyle.left = options.horizontalOffset .. "px"
	end
	if options.verticalAlignment == "bottom" then
		overlayStyle.bottom = options.verticalOffset .. "px"
	else
		overlayStyle.top = options.verticalOffset .. "px"
	end
	local overlayDiv = mw.html.create("div")
		:css(overlayStyle)
		:wikitext(overlayMap)
	
	container
		:node(overlayDiv)
		:wikitext(baseMap)
		
	if not options.isThumbnail then
		return tostring(container)
	end
	local classlist = container:getAttr("class")
	classlist = mw.ustring.gsub(classlist, "%a*"..options.align, "")
	container:attr("class", classlist)
	local outerCountainer = mw.html.create("div")
		:addClass("mapframe-withOverlay-outerContainer")
		:addClass("mw-kartographer-container")
		:addClass("thumb")
	if options.align == "left" or options.align == "right" then
		outerCountainer:addClass("t"..options.align)
	else -- alignment is "center"
		outerCountainer
			:addClass("tnone")
			:addClass("center")
	end
	outerCountainer
		:tag("div")
			:addClass("thumbinner")
			:css("width", options.width.."px")
			:node(container)
			:node(options.caption and mw.html.create("div")
				:addClass("thumbcaption")
				:wikitext(options.caption)
			)
	return tostring(outerCountainer)
end
------------------------------------------------------------------------------
Package to be exported, i.e. methods which will available to templates and
other modules.
------------------------------------------------------------------------------
local p = {}
-- Entry point for templates
function p.main(frame)
	local parent = frame.getParent(frame)
	-- Check for overlay option
	local overlay = util.getParameterValue(parent.args, 'overlay')
	local hasOverlay = overlay and mw.text.trim(overlay) ~= ""
	-- Check for switch option
	local switch = util.getParameterValue(parent.args, 'switch')
	local isMulti = switch and mw.text.trim(switch) ~= ""
	-- Create output by choosing method to suit options
	local output
	if hasOverlay then
		output = p.withOverlay(parent.args)
	elseif isMulti then
		output = p.multi(parent.args)
	else
		output = p._main(parent.args)
	end
	-- Preprocess output before returning it
	return frame:preprocess(output)
end
-- Entry points for modules
function p._main(_args)
	local args = util.trimArgs(_args)

	local tagContent = make.content(args)
	local display = mw.text.split(util.getParameterValue(args, 'display') or L10n.defaults.display, '%s*' .. L10n.str.dsep .. '%s*')
	local displayInTitle = display1 ==  L10n.str.title or display2 ==  L10n.str.title
	local displayInline = display1 ==  L10n.str.inline or display2 ==  L10n.str.inline
	local output
	if displayInTitle and displayInline then
		output = make.titleOutput(args, tagContent) .. make.inlineOutput(args, tagContent)
	elseif displayInTitle then
		output = make.titleOutput(args, tagContent)
	elseif displayInline then
		output = make.inlineOutput(args, tagContent)
	else
		error(L10n.error.badDisplayPara)
	end
	return output
end
function p.multi(_args)
	local args = util.trimArgs(_args)
	if not argsL10n.para.switch then error(L10n.error.noSwitchPara, 0) end
	local switchParamValue = util.getParameterValue(args, 'switch')
	local switchLabels = util.tableFromList(switchParamValue)
	if #switchLabels == 1 then error(L10n.error.oneSwitchLabel, 0) end
	
	local mapframeArgs = {}
	local switchParams = {}
	for name, val in pairs(args) do
		-- Copy to mapframeArgs, if not the switch labels or a switch parameter
		if val ~= switchParamValue and not string.match(val, "^"..L10n.str.switch..":") then
			mapframeArgsname = val
		end
		-- Check if this is a param to switch. If so, store the name and switch
		-- values in switchParams table.
		local switchList = string.match(val, "^"..L10n.str.switch..":(.+)")
		if switchList ~= nil then
			local values = util.tableFromList(switchList)
			if #values == 1 then
				error(string.format(L10n.error.oneSwitchValue, name), 0)
			end
			switchParamsname = values
		end
	end
	if util.tableCount(switchParams) == 0 then
		error(L10n.error.noSwitchLists, 0)
	end
	local switchCount = util.subTablesCount(switchParams)
	if not switchCount then 
		error(L10n.error.switchMismatches, 0)
	elseif switchCount > #switchLabels then
		error(string.format(L10n.error.fewerSwitchLabels, switchCount, #switchLabels), 0)
	end
	
	-- Ensure a plain frame will be used (thumbnail will be built by the
	-- make.switcherHtml function if required, so that switcher options are
	-- inside the thumnail)
	mapframeArgs.plain = "yes"
	
	local switcher = {}
	for i = 1, switchCount do
		local label = switchLabelsi
		for name, values in pairs(switchParams) do
			mapframeArgsname = valuesi
		end
		table.insert(switcher, {
			map = p._main(mapframeArgs),
			label = "Show "..label
		})
	end
	return make.switcherHtml(switcher, {
		alignment = args"frame-align" or "right",
		isThumbnail = (args.frame and not args.plain) and true or false,
		width = args"frame-width" or L10n.defaults.frameWidth,
		caption = args.text
	})
end
function p.withOverlay(_args)
	-- Get and trim wikitext for overlay map
	local overlayMap = _args.overlay
	if type(overlayMap) == 'string' then
		overlayMap = overlayMap:match('^%s*(.-)%s*$')
	end
	local isThumbnail = (util.getParameterValue(_args, "frame") and not util.getParameterValue(_args, "plain")) and true or false
	-- Get base map using the _main function, as a plain map 
	local args = util.trimArgs(_args)
	args.plain = "yes"
	local basemap = p._main(args)
	-- Extract overlay options from args
	local overlayOptions = {
		width = util.getParameterValue(args, "frameWidth") or L10n.defaults.frameWidth,
		height = util.getParameterValue(args, "frameHeight") or L10n.defaults.frameHeight,
		align = util.getParameterValue(args, "frameAlign") or L10n.defaults.frameAlign,
		border = util.getParameterValue(args, "overlayBorder") or L10n.defaults.overlayBorder,
		horizontalAlignment = util.getParameterValue(args, "overlayHorizontalAlignment") or L10n.defaults.overlayHorizontalAlignment,
		horizontalOffset = util.getParameterValue(args, "overlayHorizontalOffset") or L10n.defaults.overlayHorizontalOffset,
		verticalAlignment = util.getParameterValue(args, "overlayVerticalAlignment") or L10n.defaults.overlayVerticalAlignment,
		verticalOffset = util.getParameterValue(args, "overlayVerticalOffset") or L10n.defaults.overlayVerticalOffset,
		isThumbnail = isThumbnail,
		caption = util.getParameterValue(args, "text") or L10n.defaults.text
	}
	-- Make the HTML for the overlaying maps
	return make.overlayHtml(overlayMap, basemap, overlayOptions)
end
return p