بەڵگەدارکردنی مۆدیوول[ببینە] [دەستکاری بکە] [مێژوو] [پاکسازی]

This template adds a link, or frame, which will display map data – either from:

  • data stored in GeoJSON format on Commons
  • line/polygon features in OpenStreetMap.
  • coordinates, either supplied or from Wikidata
  • raw GeoJSON, preferably transcluded from another page

Note:

  • If you have a very large/slow map, you should use the mapframe extension tag directly instead of this template, as was done in Special:Diff/970846012. The reason for this is that using the template to call the extension tag will result in the map rendering counting as Lua time, which may result in time exceeded errors.
  • GeoJSON files cannot currently be directly uploaded. Instead, on Commons, create a page that is in the Data: namespace and ends in .map – see mw:Help:Map Data for further information.
  • To make OpenStreetMap (OSM) features available, add the Wikidata key to the OSM relation (and wait 1 or 2 days) – see mw:Help:Extension:Kartographer#External_data and mw:Help:Extension:Kartographer/OSM for further information.

Uses

This template is intended for linear or polygon features, including (but not limited to):

  • Roads
  • Rivers
  • Subway or train lines
  • Ship courses
  • Flight paths
  • Explorers journeys
  • Animal habitats
  • Countries and their subdvisions

and more... This template can also be used to display point features on a map, i.e. coordinates.

Usage

Basic usage

  • Data page on Commons (e.g. Data:ecos.fws.gov/Endangered habitat 58938/Plethodon neomexicanus.map):
    {{maplink|from=}} (specify .map data page in from=, excluding Data prefix)
  • Raw GeoJSON data (e.g. {{Wikipedia:Map data/Main Roads Western Australia/Forrest Highway}}):
    {{maplink|raw=}} (specify raw GeoJSON, preferably transcluded from a subpage of Wikipedia:Map data)
  • Line of a border or a linear feature from OSM:
    {{maplink|type=line|id=}} (Wikidata id only required if another article's Wikidata page is to be used)
  • Shape from OSM:
    {{maplink|type=shape|id=}} (Wikidata id only required if another article's Wikidata page is to be used)
  • Inverted shape from OSM:
    {{maplink|type=shape-inverse|id=}} (Wikidata id only required if another article's Wikidata page is to be used)
  • Point feature (coordinates):
    ...from Wikidata:
    {{maplink|type=point|id=}} (Wikidata id only required if another article's Wikidata page is to be used)
    ...from specified coordinates:
    {{maplink|type=point|coord=}} (specify coordinates using {{Coord}} template)
  • ھەڵەی زمانی لوا: Page 'Netherton Tunnel Branch Canal' not found. from a page or section ({{coord}} templates using the |name= parameter)
    ...from the current page: {{maplink|type=named}} or {{maplink|type=named|from=#Section}}
    ...from another page: {{maplink|type=named|from=Other page}} or {{maplink|type=named|from=Other page#Section}}
  • Circle (centred on a point's coordinates):
    ...from Wikidata:
    {{maplink|type=circle|radius=|id=}} (Wikidata id only required if another article's Wikidata page is to be used)
    ...from specified coordinates:
    {{maplink|type=circle|radius=|coord=}} (specify coordinates using {{Coord}} template)
  • To show in a frame, add the parameter |frame=yes.
Map
Mapframe example


  • Additional parameters are available to customise the displayed map.

Map
Full parameter set – data page

{{maplink
|display=
|from=
|zoom=
|text=
|icon=
}}

Full parameter set – raw data

{{maplink
|display=
|raw= <!-- preferably {{Wikipedia:Map data/subpage}} -->
|zoom=
|text=
|icon=
}}

Full parameter set – geoline/geoshape

{{maplink
|display=
|type=
|id=
|title=
|description=
|stroke-color=
|stroke-width=
|zoom=
|text=
|icon=
}}

Full parameter set – point features/coordinates

{{maplink
|display=
|type=point
|id=
|title=
|description=
|coord=
|marker=
|zoom=
|text=
|icon=
}}


Full parameter set – circles

{{maplink
|display=
|type=circle
|radius=  <!-- in metres, alternative parameters available for other units --> 
|id=
|title=
|description=
|coord=
|marker=
|zoom=
|text=
|icon=
}}

Parameters

For overall display

display
How the template is displayed. Valid values are:
inline - Only displays the inline link (you can forego this, as it is the default)
title - Only displays the title link
inline,title - Displays the inline link as well as the title link
title,inline - Same as above
text
Text to display for the link (or caption if using mapframe); default is "Map"
icon
Set to "no" to turn off the icon that appears next to the link
zoom
Zoom level, from 1 to 19 (larger numbers zoom in further); default is automatically determined by the map
(Note that automatically determined zoom level does not work for mapframe maps except when previewing the page – see phab:T193455)

Mapframe parameters (only work with inline display)

frame
Set to "yes" to show a map frame instead of a link
plain
Set to "yes" to show just the map without the surrounding frame, and without a caption (text parameter will be ignored)
frame-align
Alignment with respect to the page: left, center, or right; default is right
frame-width
Width of the map frame, default is 300
frame-height
Height of the map frame, default is 200
frame-coordinates
(or frame-coord)
Optional - alternate coordinates for initial placement of map. Use {{Coord}} template to specify coordinates (e.g. |frame-coordinates={{Coord|12.34|S|56.78|W}}). If not set, coordinates will be retrieved from Wikidata.
frame-latitude
(or frame-lat)
(ignored if |frame-coordinates= or |frame-coord= is set)
Optional - alternate latitude coordinate for initial placement of map.
frame-longitude
(or frame-long)
(ignored if |frame-coordinates= or |frame-coord= is set)
Optional - alternate longitude coordinate for initial placement of map.
تێبینی: If frame latitude and longitude are not set by parameters, then frame coordinates can be determined automatically, either:
  • Based on the line and shape objects (if any have been specified), or
  • From the point feature specified in |coord= (if set), or
  • Retrieved from Wikidata (if available – for either the item specified in |id= parameter, or for the page the template is placed on)

Raw GeoJSON

raw
Raw GeoJSON (preferably transcluded from a subpage of Wikipedia:Map data) as well as <maplink> functionality not available via the other parameters. See below for more information.

For each feature

Multiple features can be displayed on a single map. For each additional feature, add a sequential number to the end of the parameter name. For example, the second uses parameters like |type2=, |id2=, etc., the third feature |type3=, |id3=, etc., and so on.

type
Type of data. Valid values are:
line – a geoline feature from OSM (e.g. a road)
shape – a geoshape feature from OSM (e.g. a state or province)
shape-inverse – a geomask feature from OSM (the inverse of a geoshape - allows an unobstructed view of the map beneath)
data – a geoJSON data page on Commons (parameter is optional in this case)
point – a point feature (coordinates)
named – all named coordinates in an page or section ({{coord}} templates with |name= parameter)
circle – a circle centred on a point's coordinates
id
(or ids)
Wikidata id (Q-number) of the point feature, Wikidata id that the OSM line/shape feature is tagged with. Separate multiple ids with a comma. Defaults to the current page's Wikidata id (if |from= not set).
from
Source of map data:
  • If type is data or omitted: The JSON (.map) file from Commons to display (excluding the Data: prefix)
  • If type is named: Page name, or name#section, to get named coordinates from. If omitted, or specified as just #section, then current page will be used.
title
Title of the feature (for features from OSM), displayed when the feature is clicked on; default is the page title
description
Description of the feature (for features from OSM), displayed when the feature is clicked on; optional
stroke-color
(or stroke-colour)
Color code for the color used to draw the feature (for features from OSM); default is #ff0000 (red)
stroke-width
Width for the lines used to draw the feature (for features from OSM); default is 6
stroke-opacity
Opacity for the lines used to draw the feature (for features from OSM) – a number between 0 (fully transparent) and 1 (fully opaque); default is 1
fill
Color code for the fill color of the feature (for shape or shape-inverse features from OSM); default is #000000 (black). Note that a semi-transparent version of the specified color will be used by default, allowing the map below to still be seen.
fill-opacity
Opacity for the fill color of the feature (for shape or shape-inverse features from OSM) – a number between 0 (fully transparent) and 1 (fully opaque); default is 0.5
coord
Coordinates to display for a point feature. Use {{Coord}} template to specify coordinates (e.g. |coord={{Coord|12.34|S|56.78|W}}). If not set, coordinates will be retrieved from Wikidata.
marker
Name of Maki marker image to display at the point feature. See mw:Maps/Icons for options. If not set, a plain marker will be shown without an image.
marker-color
(or marker-colour)
Color code (Hex triplet) specifying marker background color. Default is 5E74F3 ( )
marker-size
Size of the marker. Can be small, medium (default) or large.
radius
(or radius_m), or alternative parameters radius_km, or radius_mi, or radius_ft
Radius of the circle. In metres by default, or in kilometers or miles or feet if the alternative parameters are used.
edges
Number of edges used to construct the circle. Default is 32. Higher numbers will result in a smoother circle but use more Lua processing resources.

Multiple map switcher

An option for Template:Switcher-style switching of multiple mapframes, where specified parameters like zoom, or frame coordinates, or a shape's item id are switched between different values. When using, ensure the number of values in each list are the same.

Instructions:

  1. Add parameter switch to specify labels for the different options: a list separated by either,, or ; if the labels contain commas, or ### if the labels contain semi-colons. Each label will be displayed with a prefix of "Show ".
    Example: |switch=zoomed out, zoomed mid, zoomed in
  2. For each parameter that is to be switched, set its value to SWITCH: followed by a list of values to switch between, again separated by , or ; or ###
    Example: |zoom=4,9,14
    Parameters frame, plain, and text should not be switched
  3. Make sure each list has the same number of entries. The position of value in a SWITCH: list corresponds to the same position in the list of labels set in |switch=.
Example:
Map
Map
Map
Boston
{{mapframe|frame=yes|type=point|id=Q100|text=Boston|zoom=SWITCH:4,9,14|switch=zoomed out, zoomed mid, zoomed in}}


Examples and tips

Multiple features

Multiple features allow you to:

  • Use external data from both Wikimedia Commons and OpenStreetMap together
  • Apply different styles to different external data features from OpenStreetMap
  • Include multiple links to external data from Wikimedia Commons
Map
Map
{{Maplink|frame=yes
|type=shape|id=Q3495894|title=Shape|description=Description of shape
|type2=line|id2=Q1318331|title2=Line|description2=Description of line
|type3=point|coord3={{coord|32|S|116|E}}|title3=Point|description3=Description of point
}}


Map

{{Maplink|frame=yes|plain=yes|frame-width=250|frame-height=280|zoom=6|frame-lat=29.185|frame-long=76.225
|type=shape-inverse|id=Q1174|title=Haryana
|type2=point|id2=Q200663|stroke-width=3|title2=Faridabad
}}


Infoboxes

Mapframes can be included in infoboxes. Using |plain=yes to remove the outer frame is recommended.

For simple cases with a single feature, {{infobox mapframe}} can be used instead (see that template's documentation for details). داڕێژە:Infobox Australian road

{{Infobox Australian road
| location = {{maplink |type=line |frame=yes |plain=yes |frame-align=center |frame-lat=-31.8262 |frame-long=115.7877 |zoom=9}}
  ...  (other parameters omitted)  ...
}}


The raw parameter

The "raw" parameter allows the same inputs as the full <maplink> function. It allows you to:

  • Use GeoJSON data directly
  • Use Wikidata queries to obtain information for the map
  • Use templates - if a map includes direct GeoJSON or many components, placing the data in a template will avoid cluttering the article text

See mw:Help:Extension:Kartographer for instructions.

Raw GeoJSON example

Route map

{{maplink|raw={{Wikipedia:Map data/Main Roads Western Australia/Forrest Highway}}|text=Route map}}

(Link to GeoJSON data subpage: Wikipedia:Map data/Main Roads Western Australia/Forrest Highway)

Raw Wikidata query examples

Map
Ten countries
{{maplink|frame=yes|text=Ten countries|raw={
  "type": "ExternalData",
  "service": "geoshape",
  "query": "
SELECT * WHERE {
  ?id wdt:P463 wd:Q1065 .
} LIMIT 10
"}
|frame-width=600|frame-height=400|frame-lat=25|frame-long=10|zoom=1
}}


Map
Map
{{maplink|frame=yes|raw={
  "type": "ExternalData",
  "service": "geoshape",
  "query": "
SELECT ?id ?head
  (SAMPLE(?img) as ?img_) 
  (min(?partyId) as ?party) 
  (if(?party = '0', '#800000', if(?party = '1', '#000080', '#008000')) as ?fill)
  (concat('[[', substr(str(?link),31,500),  '{{!}}', ?headLabel, ']]') as ?title)
  (concat(?stateLabel, '\\n', '[[File:', substr(str(?img_), 52, 500), '{{!}}200px]]') as ?description)
WHERE {  
  ?id wdt:P31 wd:Q35657 .
  ?id wdt:P6 ?head .
  ?head wdt:P102 ?party .
  BIND(if(?party = wd:Q29468, '0', if(?party = wd:Q29552, '1', '2')) as ?partyId) 
  SERVICE wikibase:label {
    bd:serviceParam wikibase:language 'en' .
    ?head rdfs:label ?headLabel .
    ?id rdfs:label ?stateLabel .
  }
  OPTIONAL {
    ?head wdt:P18 ?img .
  }
  ?link schema:about ?head .
  ?link schema:isPartOf <https://en.wikipedia.org/> .
} GROUP BY ?id ?head ?headLabel ?link ?stateLabel
"}|frame-width=600|frame-height=400|frame-lat=52|frame-long=-110|zoom=3}}


Gotchas

  • When retrieving lines or shapes from OSM, you can't style the fill color or the opacity level of fills or strokes (unless you use the |raw= parameter)
  • The following code utilising the |raw= parameter won't work because the curly brace to close the GeoJSON is adjacent to the curly braces to close the template:
{{Maplink|raw= {"type": "ExternalData","service": "geoshape","ids": "Q13518258"}}}
To fix it, put a space before the two template braces or move them to a separate line.

Tutorials

Redirects

  • {{Map link}}
  • {{Mapframe}}

See also

-- 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 `{{{one| {{{two|}}} }}}` 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 can extract coordinates from
L10n.template = {
	coord     = { -- The coord template, as well as templates with output that contains {{coord}}
		"Coord", "Coord/sandbox",
		"NRHP row", "NRHP row/sandbox",
		"WikidataCoord", "WikidataCoord/sandbox", "Wikidatacoord", "Wikidata coord"
	}
}

-- Error messages
L10n.error = {
	badDisplayPara    = "Invalid display parameter",
	noCoords	      = "Coordinates must be specified on Wikidata or in |" .. ( type(L10n.para.coord)== 'table' and L10n.para.coord[1] or L10n.para.coord ) .. "=",
	wikidataCoords    = "Coordinates not found on Wikidata",
	noCircleCoords    = "Circle centre coordinates must be specified, or available via Wikidata",
	negativeRadius    = "Circle radius must be a positive number",
	noRadius          = "Circle radius must be specified",
	negativeEdges     = "Circle edges must be a positive number",
	noSwitchPara      = "Found only one switch value in |" .. ( type(L10n.para.switch)== 'table' and L10n.para.switch[1] or L10n.para.switch ) .. "=",
	oneSwitchLabel    = "Found only one label in |" .. ( type(L10n.para.switch)== 'table' and L10n.para.switch[1] or L10n.para.switch ) .. "=",
	noSwitchLists     = "At least one parameter must have a SWITCH: list",
	switchMismatches  = "All SWITCH: lists must have the same number of values",

	 -- "%s" and "%d" tokens will be replaced with strings and numbers when used
	oneSwitchValue    = "Found only one switch value in |%s=",
	fewerSwitchLabels = "Found %d switch values but only %d labels in |" .. ( type(L10n.para.switch)== 'table' and L10n.para.switch[1] or L10n.para.switch ) .. "=",
	noNamedCoords     = "No named coordinates found in %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		= "Map",
	frameWidth	= "300",
	frameHeight	= "200",
	frameAlign  = "right",
	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.para[param_id] ) ~= 'table' then
		return args[L10n.para[param_id]..suffix]
	end
	for _i, paramAlias in ipairs(L10n.para[param_id]) do
		if args[paramAlias..suffix] then
			return args[paramAlias..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(key) == 'string'  and 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 (text or description parameters) or you'll break strip markers
				-- Alternatively it might be better to only strip control char from raw parameter content
				if util.matchesParam('text', key) or util.matchesParam('description', key, key:gsub('^%D+(%d+)$', '%1') ) then
					cleanArgs[key] = val
				else
					cleanArgs[key] = val:gsub('%c',' ')
				end
			end
		else
			cleanArgs[key] = val
		end
	end
	return cleanArgs
end

--[[
Check if a parameter name matches an unlocalized parameter key
@param {string} key - the unlocalized parameter name to search through
@param {string} name - the localized parameter name to check
@param {string|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.para[key]
	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

--[[
Check if the name of a template matches the known coord templates or wrappers
(in L10n.template.coord). The name is normalised when checked, so e.g. the names
"Coord", "coord", and "  Coord" all return true.
@param {string} name
@returns {boolean} true if it is a coord template or wrapper, false otherwise
]]--
function util.isCoordTemplateOrWrapper(name)
	name = mw.text.trim(name)
	local inputTitle = mw.title.new(name, 'Template')
	if not inputTitle then
		return false
	end

	-- Create (or reuse) mw.title objects for each known coord template/wrapper.
	-- Stored in L10n.template.title so that they don't need to be recreated
	-- each time this function is called
	if not L10n.template.titles then
		L10n.template.titles = {}
		for _, v in pairs(L10n.template.coord) do
			table.insert(L10n.template.titles, mw.title.new(v, 'Template'))
		end
	end

	for _, templateTitle in pairs(L10n.template.titles) do
		if mw.title.equals(inputTitle, templateTitle) then
			return true
		end
	end

	return 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 templateName = mw.ustring.match(template, '{{([^}|]+)')
		local nameParam = mw.ustring.match(template, "|%s*name%s*=%s*[^}|]+")
		if util.isCoordTemplateOrWrapper(templateName) then
			if nameParam then table.insert(output, template) end
		elseif mw.ustring.find(mw.ustring.sub(template, 2), "{{") 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
		output[k] = mw.ustring.gsub(v, "|%s*display%s*=[^|}]+", "|display=inline")
	end
	return output
end

--[[
Gets all named coordiates from a page or a section of a page.
@param {string|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 = parts[1] == "" and mw.title.getCurrentTitle().prefixedText or parts[1]
	local section = parts[2]
	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, "<span class=\"geo%-dec\".->(.-)</span>")
		if coord then
			local name = (
				-- name specified by a wrapper template, e.g [[Article|Name]]
				mw.ustring.match(expandedTemplate, "<span class=\"mapframe%-coord%-name\">(.-)</span>") or
				-- name passed into coord template
				mw.ustring.match(expandedTemplate, "<span class=\"fn org\">(.-)</span>") or
				-- default to the coordinates if the name can't be retrieved
				coord
			)
			local description = name ~= coord and coord
			local coord = mw.ustring.gsub(coord, "[° ]", "_")
			table.insert(namedCoords, {coord=coord, 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(parts[1])
	local lat_m = tonumber(parts[2]) -- nil if coords are in decimal format
	local lat_s = lat_m and tonumber(parts[3]) -- 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(parts[1+#parts/2])
	local long_m = tonumber(parts[2+#parts/2]) -- nil if coords are in decimal format
	local long_s = long_m and tonumber(parts[3+#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 (item_id and mw.wikibase.isValidEntityId(item_id) and 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 = ( coordStatements[1].mainsnak and (coordStatements[1].mainsnak.snaktype == 'novalue' or coordStatements[1].mainsnak.snaktype == 'somevalue') )
	if hasNoValue then
		error(L10n.error.wikidataCoords, 0)
	end
	local wdCoords = coordStatements[1]['mainsnak']['datavalue']['value']
	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 < n do
		table.insert(coordinates,
			offset(lat, long, radius, (2*math.pi*i*-1)/n)
		)
		i = i + 1
	end
	table.insert(coordinates, offset(lat, long, radius, 0))
	return coordinates
end


--[[
Get the number of key-value pairs in a table, which might not be a sequence.
@param {table} t
@returns {number} count of key-value pairs
]]--
function util.tableCount(t)
	local count = 0
	for k, v in pairs(t) do
		count = count + 1
	end
	return count
end

--[[
For a table where the values are all tables, returns either the util.tableCount
of the subtables if they are all the same, or nil if they are not all the same.
@param {table} t
@returns {number|nil} count of key-value pairs of subtable, or nil if subtables
  have different counts
]]--
function util.subTablesCount(t)
	local count = nil
	for k, v in pairs(t) do
		if count == nil then
			count = util.tableCount(v)
		elseif count ~= util.tableCount(v) then
			return nil
		end
	end
	return count
end

--[[
Splits a list into a table sequence. The items in the list may be separated by
commas, or by semicolons (if items may contain commas), or by "###" (if items
may contain semicolons).
@param {string} listString
@returns {table} sequence of list items
]]--
function util.tableFromList(listString)
	if type(listString) ~= "string" or listString == "" then return nil end
	local separator = (mw.ustring.find(listString, "###", 0, true ) and "###") or
		(mw.ustring.find(listString, ";", 0, true ) and ";") or ","
	local pattern = "%s*"..separator.."%s*"
	return mw.text.split(listString, pattern)
end

-- Boolean in outer scope indicating if Kartographer should be able to
-- automatically calculate coordinates (see phab:T227402)
local coordsDerivedFromFeatures = false;

--[[----------------------------------------------------------------------------
 Make methods: These take in a table of arguments, and return either a string
 or a table to be used in the eventual output.
----------------------------------------------------------------------------]]--
local make = {}

--[[
Makes content to go inside the maplink or mapframe tag.

@param {table} args
@returns {string} tag content
]]--
function make.content(args)
	if util.getParameterValue(args, 'raw') then
		coordsDerivedFromFeatures = true -- Kartographer should be able to automatically calculate coords from raw geoJSON
		return util.getParameterValue(args, 'raw')
	end

	local content = {}

    local argsExpanded = {}
    for k, v in pairs(args) do
		local index = string.match( k, '^[^0-9]+([0-9]*)$' )
		if index ~= nil then
			local indexNumber = ''
			if index ~= '' then
				indexNumber = tonumber(index)
			else
				indexNumber = 1
			end

			if argsExpanded[indexNumber] == nil then
				argsExpanded[indexNumber] = {}
			end
			argsExpanded[indexNumber][ string.gsub(k, index, '') ] = v
		end
    end

	for contentIndex, contentArgs in pairs(argsExpanded) do
		local argType = util.getParameterValue(contentArgs, "type")
		-- Kartographer automatically calculates coords if geolines/shapes are used (T227402)
		if not coordsDerivedFromFeatures then
			coordsDerivedFromFeatures = ( argType == L10n.str.line or argType == L10n.str.shape ) and true or false
		end
		if argType == L10n.str.named then
			local namedCoords = util.getNamedCoords(util.getParameterValue(contentArgs, "from"))
			local typeKey = type(L10n.para.type) == "table" and L10n.para.type[1] or L10n.para.type
			local coordKey = type(L10n.para.coord) == "table" and L10n.para.coord[1] or L10n.para.coord
			local titleKey = type(L10n.para.title) == "table" and L10n.para.title[1] or L10n.para.title
			local descKey = type(L10n.para.description) == "table" and L10n.para.description[1] or L10n.para.description
			for _, namedCoord in pairs(namedCoords) do
				contentArgs[typeKey] = "point"
				contentArgs[coordKey]  = namedCoord.coord
				contentArgs[titleKey]  = namedCoord.name
				contentArgs[descKey]  = namedCoord.description
				content[#content+1] = make.contentJson(contentArgs)
			end
		else
			content[#content + 1] = make.contentJson(contentArgs)
		end
	end

	--Single item, no array needed
	if #content==1 then return content[1] end

	--Multiple items get placed in a FeatureCollection
	local contentArray = '[\n' .. table.concat( content, ',\n') .. '\n]'
	return contentArray
end

--[[
Make coordinates from the coord arg, or the id arg, or the current page's
Wikidata item.
@param {table} args
@param {boolean} [plainOutput]
@returns {Mixed} Either:
  {number, number} latitude, longitude  if plainOutput is true; or
  {table} table sequence of longitude, then latitude (gives the required format
   for GeoJSON when encoded)
]]--
function make.coords(args, plainOutput)
	local coords, lat, long
	local frame = mw.getCurrentFrame()
	if util.getParameterValue(args, 'coord') then
		coords = frame:preprocess( util.getParameterValue(args, 'coord') )
		lat, long = util.parseCoords(coords)
	else
		lat, long = util.wikidataCoords(util.getParameterValue(args, 'id') or mw.wikibase.getEntityIdForCurrentPage())
	end
	if plainOutput then
		return lat, long
	end
	return {[0] = long, [1] = lat}
end

--[[
Makes a table of coordinates that approximate a circle.
@param {table} args
@returns {table} sequence of {latitude, longitude} table sequences, where
  latitude and longitude are both numbers
@throws {L10n.error.noCircleCoords} if centre coordinates are not specified
@throws {L10n.error.noRadius} if radius is not specified
@throws {L10n.error.negativeRadius} if radius is negative or zero
@throws {L10n.error.negativeEdges} if edges is negative or zero
]]--
function make.circleCoords(args)
	local lat, long = make.coords(args, true)
	local radius = util.getParameterValue(args, 'radius')
	if not radius then
		radius = util.getParameterValue(args, 'radiusKm') and tonumber(util.getParameterValue(args, 'radiusKm'))*1000
		if not radius then
			radius = util.getParameterValue(args, 'radiusMi') and tonumber(util.getParameterValue(args, 'radiusMi'))*1609.344
			if not radius then
				radius = util.getParameterValue(args, 'radiusFt') and tonumber(util.getParameterValue(args, 'radiusFt'))*0.3048
			end
		end
	end
	local edges = util.getParameterValue(args, 'edges') or L10n.defaults.edges
	if not lat or not long then
		error(L10n.error.noCircleCoords, 0)
	elseif not radius then
		error(L10n.error.noRadius, 0)
	elseif tonumber(radius) <= 0 then
		error(L10n.error.negativeRadius, 0)
	elseif tonumber(edges) <= 0 then
		error(L10n.error.negativeEdges, 0)
	end
	return util.circleToPolygon(lat, long, radius, tonumber(edges))
end

--[[
Makes JSON data for a feature
@param contentArgs  args for this feature. Keys must be the non-suffixed version
  of the parameter names, i.e. use type, stroke, fill,... rather than type3,
  stroke3, fill3,...
@returns {string} JSON encoded data
]]--
function make.contentJson(contentArgs)
	local data = {}

	if util.getParameterValue(contentArgs, 'type') == L10n.str.point or util.getParameterValue(contentArgs, 'type') == L10n.str.circle then
		local isCircle = util.getParameterValue(contentArgs, 'type') == L10n.str.circle
		data.type = "Feature"
		data.geometry = {
			type = isCircle and "LineString" or "Point",
			coordinates = isCircle and make.circleCoords(contentArgs) or make.coords(contentArgs)
		}
		data.properties = {
			title = util.getParameterValue(contentArgs, 'title') or mw.getCurrentFrame():getParent():getTitle()
		}
		if isCircle then
			-- TODO: This is very similar to below, should be extracted into a function
			data.properties.stroke = util.getParameterValue(contentArgs, 'strokeColor') or L10n.defaults.strokeColor
			data.properties["stroke-width"] = tonumber(util.getParameterValue(contentArgs, 'strokeWidth')) or L10n.defaults.strokeWidth
			local strokeOpacity = util.getParameterValue(contentArgs, 'strokeOpacity')
			if strokeOpacity then
				data.properties['stroke-opacity'] = tonumber(strokeOpacity)
			end
			local fill = util.getParameterValue(contentArgs, 'fill')
			if fill then
				data.properties.fill = fill
				local fillOpacity = util.getParameterValue(contentArgs, 'fillOpacity')
				data.properties['fill-opacity'] = fillOpacity and tonumber(fillOpacity) or 0.6
			end
		else -- is a point
			local markerSymbol = util.getParameterValue(contentArgs, 'marker') or L10n.defaults.marker
			-- allow blank to be explicitly specified, for overriding infoboxes or other templates with a default value
			if markerSymbol ~= "blank" then
				data.properties["marker-symbol"] = markerSymbol
			end
			data.properties["marker-color"] = util.getParameterValue(contentArgs, 'markerColor') or L10n.defaults.markerColor
			data.properties["marker-size"] = util.getParameterValue(contentArgs, 'markerSize') or L10n.defaults.markerSize
		end
	else
		data.type = "ExternalData"

		if util.getParameterValue(contentArgs, 'type') == L10n.str.data or util.getParameterValue(contentArgs, 'from') then
			data.service = "page"
		elseif util.getParameterValue(contentArgs, 'type') == L10n.str.line then
			data.service = "geoline"
		elseif util.getParameterValue(contentArgs, 'type') == L10n.str.shape then
			data.service = "geoshape"
		elseif util.getParameterValue(contentArgs, 'type') == L10n.str.shapeInverse then
			data.service = "geomask"
		end

		if util.getParameterValue(contentArgs, 'id') or (not (util.getParameterValue(contentArgs, 'from')) and mw.wikibase.getEntityIdForCurrentPage()) then
			data.ids = util.getParameterValue(contentArgs, 'id') or mw.wikibase.getEntityIdForCurrentPage()
		else
			data.title = util.getParameterValue(contentArgs, 'from')
		end

		data.properties = {
			stroke = util.getParameterValue(contentArgs, 'strokeColor') or L10n.defaults.strokeColor,
			["stroke-width"] = tonumber(util.getParameterValue(contentArgs, 'strokeWidth')) or L10n.defaults.strokeWidth
		}
		local strokeOpacity = util.getParameterValue(contentArgs, 'strokeOpacity')
		if strokeOpacity then
			data.properties['stroke-opacity'] = tonumber(strokeOpacity)
		end
		local fill = util.getParameterValue(contentArgs, 'fill')
		if fill and (data.service == "geoshape" or data.service == "geomask") then
			data.properties.fill = fill
			local fillOpacity = util.getParameterValue(contentArgs, 'fillOpacity')
			if fillOpacity then
				data.properties['fill-opacity'] = tonumber(fillOpacity)
			end
		end
	end

	data.properties.title = util.getParameterValue(contentArgs, 'title') or mw.title.getCurrentTitle().text
	if util.getParameterValue(contentArgs, 'description') then
		data.properties.description = util.getParameterValue(contentArgs, 'description')
	end

	return mw.text.jsonEncode(data)
end

--[[
Makes attributes for the maplink or mapframe tag.
@param {table} args
@param {boolean} [isTitle]  Tag is to be displayed in the title of page rather
  than inline
@returns {table<string,string>} 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

--[[
Makes maplink wikitext that will be located in the top-right of the title of the
page (the same place where coords with |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)
	options = options or {}
	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(params[i].map)
				:tag("span")
					:addClass("switcher-label")
					:css("display", "none")
					:wikitext(mw.text.trim(params[i].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)
	options = options or {}
	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 = display[1] ==  L10n.str.title or display[2] ==  L10n.str.title
	local displayInline = display[1] ==  L10n.str.inline or display[2] ==  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 args[L10n.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
			mapframeArgs[name] = 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
			switchParams[name] = 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 = switchLabels[i]
		for name, values in pairs(switchParams) do
			mapframeArgs[name] = values[i]
		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