# Module:Mapframe

-- Note: Originally written on English Wikipedia at https://en.wikipedia.org/wiki/Module:Mapframe  

---
        1. 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  
-- "Module:No globals" 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 -to-, 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" },  
stroke,  
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",  
= "",  
plain = "plain",  
frame,  
frame,  
frameCoordinates = { "-coordinates", "-coord" },  
frameLatitude = { "-lat", "-latitude" },  
frameLongitude = { "-long", "-longitude" },  
frame,  
switch = "switch",  
overlay = "overlay",  
overlayBorder = "overlay-",  
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/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, , and plain parameters  
affirmedWords = ' '..table.concat({  
"add",  
"added",  
"affirm",  
"affirmed",  
"include",  
"included",  
"on",  
"true",  
"yes",  
"y"  
}, ' ')..' ',  
declinedWords = ' '..table.concat({  
"decline",  
"declined",  
"exclude",  
"excluded",  
"false",  
"",  
"not",  
"no",  
"n",  
"off",  
"omit",  
"omitted",  
"remove",  
"removed"  
}, ' ')..' '  
}  

-- Default values for parameters  
L10n.defaults = {  
display = L10n.str.inline,  
text = "Map",  
frame,  
frame,  
frame,  
markerColor = "5E74F3",  
markerSize = nil,  
strokeColor = "#ff0000",  
strokeWidth = 6,  
edges = 32, -- number of edges used to approximate a circle  
overlayBorder = " solid white",  
overlayHorizontalAlignment = "",  
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  

--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, '  
--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 = mw.getCurrentFrame  
local sep = "________"  
local expandedContent = :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 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  

--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 = mw.getCurrentFrame  
if util.getParameterValue(args, 'coord') then  
coords = :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.  
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, '')) 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. = "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 = {,  
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, '') 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 "" or "" or ""  
@param {boolean} options.isThumbnail Display in a  
@param {string} options.width Width of , e.g. "200"  
@param {string} [options.caption] Caption wikitext for thumnail  
@retruns {string} swicther HTML  
]]--  
function make.switcherHtml(params, options)  
options = options or {}  
local = mw.getCurrentFrame  
local styles = :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 == "" or options.alignment == "" then  
container:addClass("float"..options.alignment)  
else -- alignment is ""  
container:addClass("")  
end  
for i = 1, #params do  
container  
:tag("div")  
:wikitext(params[i].map)  
:tag("span")  
:addClass("switcher-label")  
:css("display", "")  
: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("")  
if options.alignment == "" or options.alignment == "" then  
outerCountainer:addClass("t"..options.alignment)  
else -- alignment is ""  
outerCountainer  
:addClass("tnone")  
:addClass("")  
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 "" or "" or ""  
@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 style for the overlayed map, e.g. " solid white"  
@param {string} options.horizontalAlignment Horizontal alignment for overlay map, "" or ""  
@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  
@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 containerFloat..(options.align or "")  
if options.align == "" then  
containerFloat  
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 == "" then  
containerStyle["margin-"] = "auto"  
containerStyle["margin-"] = "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",  
= options. or " solid white"  
}  
if options.horizontalAlignment == "" then  
overlayStyle. = options.horizontalOffset .. "px"  
else  
overlayStyle. = 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("")  
if options.align == "" or options.align == "" then  
outerCountainer:addClass("t"..options.align)  
else -- alignment is ""  
outerCountainer  
:addClass("tnone")  
:addClass("")  
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  
local parent = .getParent  
-- 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 :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 will be used ( 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["-align"] or "",  
isThumbnail = (args. and not args.plain) and true or false,  
width = args["-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, "") 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,  
= 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