Module:Mapframe

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

-- Replace values in quotes ("") with localised values -- local L10n = {}
 * 1) Localisation (L10n) settings #####

-- 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 left-to-right, i.e. `{ "one", "two" }` is equivalent to using `` in a template L10n.para = { display		= "display", type		= "type", id             = { "id", "ids" }, from		= "from", raw		= "raw", title		= "title", description	= "description", strokeColor    = { "stroke-color", "stroke-colour" }, strokeWidth	= "stroke-width", strokeOpacity = "stroke-opacity", fill       = "fill", fillOpacity    = "fill-opacity", coord		= "coord", marker		= "marker", markerColor	= { "marker-color", "marker-colour" }, markerSize = "marker-size", radius     = { "radius", "radius_m" }, radiusKm   = "radius_km", radiusFt   = "radius_ft", radiusMi   = "radius_mi", edges      = "edges", text		= "text", icon		= "icon", zoom		= "zoom", frame		= "frame", plain		= "plain", frameWidth	= "frame-width", frameHeight	= "frame-height", frameCoordinates = { "frame-coordinates", "frame-coord" }, frameLatitude   = { "frame-lat", "frame-latitude" }, frameLongitude  = { "frame-long", "frame-longitude" }, frameAlign      = "frame-align", switch          = "switch", overlay         = "overlay", overlayBorder   = "overlay-border", overlayHorizontalAlignment = "overlay-horizontal-alignment", overlayVerticalAlignment = "overlay-vertical-alignment", overlayHorizontalOffset = "overlay-horizontal-offset", overlayVerticalOffset = "overlay-vertical-offset" }

-- Names of other templates this module can extract coordinates from L10n.template = { coord    = { -- The coord template, as well as templates with output that contains °N, °W "Coord", "Coord/sandbox", "NRHP row", "NRHP row/sandbox", "GratisdataCoord", "GratisdataCoord/sandbox", "Gratisdatacoord", "Gratisdata coord" } }

-- Error messages L10n.error = { badDisplayPara   = "Invalid display parameter", noCoords	     = "Coordinates must be specified on Gratisdata or in |" .. ( type(L10n.para.coord)== 'table' and L10n.para.coord[1] or L10n.para.coord ) .. "=",	gratisdataCoords = "Coordinates not found on Gratisdata", noCircleCoords   = "Circle centre coordinates must be specified, or available via Gratisdata", 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 = {}

--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, '{{([^}|]+)') 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

--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, "(.-) ") if coord then local name = (				-- name specified by a wrapper template, e.g Name				mw.ustring.match(expandedTemplate, "(.-) ") or				-- name passed into coord template				mw.ustring.match(expandedTemplate, "(.-) ") 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 Gratisdata item @param {string} item_id Gratisdata 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.gratisdataCoords} if the the item does not have a P134  statement (coordinates), or it is set to "no value" -- function util.gratisdataCoords(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, 'P134') if not coordStatements or #coordStatements == 0 then error(L10n.error.gratisdataCoords, 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.gratisdataCoords, 0) end local gdCoords = coordStatements[1]['mainsnak']['datavalue']['value'] return tonumber(gdCoords['latitude']), tonumber(gdCoords['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 "-- 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 Gratisdata 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.gratisdataCoords(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} 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.gratisdataCoords, util.getParameterValue(args, 'id') or mw.wikibase.getEntityIdForCurrentPage) if success then attribs.latitude = tostring(lat) attribs.longitude = tostring(long) end end if util.getParameterValue(args, 'frameAlign') then attribs.align = util.getParameterValue(args, 'frameAlign') end if util.isAffirmed(util.getParameterValue(args, 'plain')) then attribs.frameless = "1" else attribs.text = util.getParameterValue(args, 'text') or L10n.defaults.text end else attribs.text = util.getParameterValue(args, 'text') or L10n.defaults.text end return attribs end

--display=title are positioned). @param {table} args @param {string} tagContent Content for the maplink tag @returns {string} -- function make.titleOutput(args, tagContent) local titleTag = mw.text.tag('maplink', make.tagAttribs(args, true), tagContent) local spanAttribs = { style = "font-size: small;", id = "coordinates" }	return mw.text.tag('span', spanAttribs, titleTag) end

-- Makes maplink or mapframe wikitext that will be located inline. @param {table} args @param {string} tagContent Content for the maplink tag @returns {string} -- function make.inlineOutput(args, tagContent) local tagName = 'maplink' if util.getParameterValue(args, 'frame') then tagName = 'mapframe' end

return mw.text.tag(tagName, make.tagAttribs(args), tagContent) end

-- Makes the HTML required for the swicther to work, including the templatestyles tag. @param {table} params table sequence of {map, label} tables  @param {string} params{}.map  Wikitext for mapframe map  @param {string} params{}.label  Label text for swicther option @param {table} options  @param {string} options.alignment  "left" or "center" or "right"  @param {boolean} options.isThumbnail  Display in a thumbnail  @param {string} options.width  Width of frame, e.g. "200"  @param {string} [options.caption]  Caption wikitext for thumnail @retruns {string} swicther HTML -- function make.switcherHtml(params, options) 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

--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