Module:Mapframe: Difference between revisions
Jump to navigation
Jump to search
>TheDJ (strip all control characters from the json and replace them with an empty space. control characters are not allowed inside json string value and not important for the structure... See also: https://phabricator.wikimedia.org/T214984) |
m (1 revision imported) |
||
(29 intermediate revisions by 10 users not shown) | |||
Line 1: | Line 1: | ||
-- Note: Originally written on English Wikipedia at https://en.wikipedia.org/wiki/Module:Mapframe | -- 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 = {} | 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) | -- Template parameter names (unnumbered versions only) | ||
Line 18: | Line 24: | ||
strokeColor = { "stroke-color", "stroke-colour" }, | strokeColor = { "stroke-color", "stroke-colour" }, | ||
strokeWidth = "stroke-width", | strokeWidth = "stroke-width", | ||
strokeOpacity = "stroke-opacity", | |||
fill = "fill", | |||
fillOpacity = "fill-opacity", | |||
coord = "coord", | coord = "coord", | ||
marker = "marker", | marker = "marker", | ||
markerColor = { "marker-color", "marker-colour" }, | 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", | text = "text", | ||
icon = "icon", | icon = "icon", | ||
Line 28: | Line 43: | ||
frameWidth = "frame-width", | frameWidth = "frame-width", | ||
frameHeight = "frame-height", | frameHeight = "frame-height", | ||
frameCoordinates = { "frame-coordinates", "frame-coord" }, | frameCoordinates = { "frame-coordinates", "frame-coord" }, | ||
frameLatitude = { "frame-lat", "frame-latitude" }, | frameLatitude = { "frame-lat", "frame-latitude" }, | ||
frameLongitude = { "frame-long", "frame-longitude" }, | frameLongitude = { "frame-long", "frame-longitude" }, | ||
frameAlign = "frame-align" | 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 | -- Names of other templates this module can extract coordinates from | ||
L10n.template = { | L10n.template = { | ||
Coord | 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 | -- Error messages | ||
L10n.error = { | L10n.error = { | ||
badDisplayPara = "Invalid display parameter", | 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 ) .. "=", | 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" | 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" | |||
} | } | ||
Line 49: | Line 88: | ||
L10n.str = { | L10n.str = { | ||
-- valid values for display parameter, e.g. (|display=inline) or (|display=title) or (|display=inline,title) or (|display=title,inline) | -- valid values for display parameter, e.g. (|display=inline) or (|display=title) or (|display=inline,title) or (|display=title,inline) | ||
inline = "inline", | inline = "inline", | ||
title = "title", | title = "title", | ||
dsep = ",", -- separator between inline and title (comma in the example above) | dsep = ",", -- separator between inline and title (comma in the example above) | ||
Line 59: | Line 98: | ||
data = "data", -- geoJSON data page on Commons | data = "data", -- geoJSON data page on Commons | ||
point = "point", -- single point feature (coordinates) | 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 | -- valid values for icon, frame, and plain parameters | ||
Line 97: | Line 141: | ||
frameWidth = "300", | frameWidth = "300", | ||
frameHeight = "200", | frameHeight = "200", | ||
frameAlign = "right", | |||
markerColor = "5E74F3", | markerColor = "5E74F3", | ||
markerSize = nil, | |||
strokeColor = "#ff0000", | strokeColor = "#ff0000", | ||
strokeWidth = 6 | 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 #### | -- #### End of L10n settings #### | ||
function getParameterValue(args, param_id, suffix) | --[[---------------------------------------------------------------------------- | ||
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 '' | suffix = suffix or '' | ||
if type( L10n.para[param_id] ) ~= 'table' then | if type( L10n.para[param_id] ) ~= 'table' then | ||
Line 115: | Line 181: | ||
end | end | ||
return nil | return nil | ||
end | end | ||
function | --[[ | ||
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 = {} | local cleanArgs = {} | ||
for key, val in pairs(argsTable) do | for key, val in pairs(argsTable) do | ||
Line 124: | Line 194: | ||
val = val:match('^%s*(.-)%s*$') | val = val:match('^%s*(.-)%s*$') | ||
if val ~= '' then | if val ~= '' then | ||
cleanArgs[key] = val | -- control characters inside json need to be escaped, but stripping them is simpler | ||
-- See also T214984 | |||
-- However, *don't* strip control characters from wikitext or you'll break strip markers | |||
cleanArgs[key] = (not util.matchesParam('text', key)) and val:gsub('%c',' ') or val | |||
end | end | ||
else | else | ||
Line 133: | Line 206: | ||
end | end | ||
function isAffirmed(val) | --[[ | ||
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 | if not(val) then return false end | ||
return string.find(L10n.str.affirmedWords, ' '..val..' ', 1, true ) and true or false | return string.find(L10n.str.affirmedWords, ' '..val..' ', 1, true ) and true or false | ||
end | end | ||
function isDeclined(val) | --[[ | ||
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 | if not(val) then return false end | ||
return string.find(L10n.str.declinedWords , ' '..val..' ', 1, true ) and true or false | return string.find(L10n.str.declinedWords , ' '..val..' ', 1, true ) and true or false | ||
end | end | ||
function | --[[ | ||
Check if the name of a template matches the known coord templates or wrappers | |||
return | (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 | 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 | for _, v in pairs(L10n.template.coord) do | ||
table.insert(L10n.template.titles, mw.title.new(v, 'Template')) | |||
end | end | ||
end | |||
if | for _, templateTitle in pairs(L10n.template.titles) do | ||
if mw.title.equals(inputTitle, templateTitle) then | |||
return true | |||
end | |||
end | end | ||
-- | return false | ||
local | end | ||
return | |||
--[[ | |||
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 | end | ||
function | --[[ | ||
local parts = mw.text.split((mw.ustring.match(coords,'[_%.%d]+[NS][_%.%d]+[EW]') or ''), '_') | 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¶ms=1_2_N_3_4_W_ or | |||
//tools.wmflabs.org/geohack/geohack.php?pagename=Example¶ms=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_d = tonumber(parts[1]) | ||
Line 195: | Line 393: | ||
end | end | ||
function wikidataCoords(item_id) | --[[ | ||
if not(mw.wikibase.isValidEntityId(item_id) | 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) | error(L10n.error.noCoords, 0) | ||
end | end | ||
Line 203: | Line 409: | ||
error(L10n.error.wikidataCoords, 0) | error(L10n.error.wikidataCoords, 0) | ||
end | end | ||
local hasNoValue = ( coordStatements[1].mainsnak and coordStatements[1].mainsnak.snaktype == 'novalue' ) | local hasNoValue = ( coordStatements[1].mainsnak and (coordStatements[1].mainsnak.snaktype == 'novalue' or coordStatements[1].mainsnak.snaktype == 'somevalue') ) | ||
if hasNoValue then | if hasNoValue then | ||
error(L10n.error.wikidataCoords, 0) | error(L10n.error.wikidataCoords, 0) | ||
Line 211: | Line 417: | ||
end | end | ||
function | --[[ | ||
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 coords, lat, long | ||
local frame = mw.getCurrentFrame() | local frame = mw.getCurrentFrame() | ||
if getParameterValue(args, 'coord') then | if util.getParameterValue(args, 'coord') then | ||
coords = frame:preprocess( getParameterValue(args, 'coord') ) | coords = frame:preprocess( util.getParameterValue(args, 'coord') ) | ||
lat, long = parseCoords(coords) | lat, long = util.parseCoords(coords) | ||
else | else | ||
lat, long = wikidataCoords(getParameterValue(args, 'id') or mw.wikibase.getEntityIdForCurrentPage()) | lat, long = util.wikidataCoords(util.getParameterValue(args, 'id') or mw.wikibase.getEntityIdForCurrentPage()) | ||
end | end | ||
if plainOutput then | if plainOutput then | ||
Line 226: | Line 603: | ||
end | end | ||
function | --[[ | ||
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 = {} | local data = {} | ||
if getParameterValue(contentArgs, 'type') == L10n.str.point then | 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.type = "Feature" | ||
data.geometry = { | data.geometry = { | ||
type = "Point", | type = isCircle and "LineString" or "Point", | ||
coordinates = | coordinates = isCircle and make.circleCoords(contentArgs) or make.coords(contentArgs) | ||
} | } | ||
data.properties = { | data.properties = { | ||
title = getParameterValue(contentArgs, 'title') or mw.getCurrentFrame():getParent():getTitle() | 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 | else | ||
data.type = "ExternalData" | data.type = "ExternalData" | ||
if getParameterValue(contentArgs, 'type') == L10n.str.data or getParameterValue(contentArgs, 'from') then | if util.getParameterValue(contentArgs, 'type') == L10n.str.data or util.getParameterValue(contentArgs, 'from') then | ||
data.service = "page" | data.service = "page" | ||
elseif getParameterValue(contentArgs, 'type') == L10n.str.line then | elseif util.getParameterValue(contentArgs, 'type') == L10n.str.line then | ||
data.service = "geoline" | data.service = "geoline" | ||
elseif getParameterValue(contentArgs, 'type') == L10n.str.shape then | elseif util.getParameterValue(contentArgs, 'type') == L10n.str.shape then | ||
data.service = "geoshape" | data.service = "geoshape" | ||
elseif getParameterValue(contentArgs, 'type') == L10n.str.shapeInverse then | elseif util.getParameterValue(contentArgs, 'type') == L10n.str.shapeInverse then | ||
data.service = "geomask" | data.service = "geomask" | ||
end | end | ||
if getParameterValue(contentArgs, 'id') or (not (getParameterValue(contentArgs, 'from')) and mw.wikibase.getEntityIdForCurrentPage()) then | if util.getParameterValue(contentArgs, 'id') or (not (util.getParameterValue(contentArgs, 'from')) and mw.wikibase.getEntityIdForCurrentPage()) then | ||
data.ids = getParameterValue(contentArgs, 'id') or mw.wikibase.getEntityIdForCurrentPage() | data.ids = util.getParameterValue(contentArgs, 'id') or mw.wikibase.getEntityIdForCurrentPage() | ||
else | else | ||
data.title = getParameterValue(contentArgs, 'from') | data.title = util.getParameterValue(contentArgs, 'from') | ||
end | end | ||
data.properties = { | data.properties = { | ||
stroke = getParameterValue(contentArgs, 'strokeColor') or L10n.defaults.strokeColor, | stroke = util.getParameterValue(contentArgs, 'strokeColor') or L10n.defaults.strokeColor, | ||
["stroke-width"] = tonumber(getParameterValue(contentArgs, 'strokeWidth')) or L10n.defaults.strokeWidth | ["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 | end | ||
data.properties.title = getParameterValue(contentArgs, 'title') or mw. | data.properties.title = util.getParameterValue(contentArgs, 'title') or mw.title.getCurrentTitle().text | ||
if getParameterValue(contentArgs, 'description') then | if util.getParameterValue(contentArgs, 'description') then | ||
data.properties.description = getParameterValue(contentArgs, 'description') | data.properties.description = util.getParameterValue(contentArgs, 'description') | ||
end | end | ||
Line 273: | Line 726: | ||
end | end | ||
function | --[[ | ||
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 = {} | local attribs = {} | ||
if getParameterValue(args, 'zoom') then | if util.getParameterValue(args, 'zoom') then | ||
attribs.zoom = getParameterValue(args, 'zoom') | attribs.zoom = util.getParameterValue(args, 'zoom') | ||
end | end | ||
if isDeclined(getParameterValue(args, 'icon')) then | if util.isDeclined(util.getParameterValue(args, 'icon')) then | ||
attribs.class = "no-icon" | attribs.class = "no-icon" | ||
end | end | ||
if getParameterValue(args, 'type') == L10n.str.point then | if util.getParameterValue(args, 'type') == L10n.str.point and not coordsDerivedFromFeatures then | ||
local lat, long = | local lat, long = make.coords(args, 'plainOutput') | ||
attribs.latitude = tostring(lat) | attribs.latitude = tostring(lat) | ||
attribs.longitude = tostring(long) | attribs.longitude = tostring(long) | ||
end | end | ||
if isAffirmed(getParameterValue(args, 'frame')) and not(isTitle) then | if util.isAffirmed(util.getParameterValue(args, 'frame')) and not(isTitle) then | ||
attribs.width = getParameterValue(args, 'frameWidth') or L10n.defaults.frameWidth | attribs.width = util.getParameterValue(args, 'frameWidth') or L10n.defaults.frameWidth | ||
attribs.height = getParameterValue(args, 'frameHeight') or L10n.defaults.frameHeight | attribs.height = util.getParameterValue(args, 'frameHeight') or L10n.defaults.frameHeight | ||
if getParameterValue(args, 'frameCoordinates') then | if util.getParameterValue(args, 'frameCoordinates') then | ||
local frameLat, frameLong = parseCoords(getParameterValue(args, 'frameCoordinates')) | local frameLat, frameLong = util.parseCoords(util.getParameterValue(args, 'frameCoordinates')) | ||
attribs.latitude = frameLat | attribs.latitude = frameLat | ||
attribs.longitude = frameLong | attribs.longitude = frameLong | ||
else | else | ||
if getParameterValue(args, 'frameLatitude') then | if util.getParameterValue(args, 'frameLatitude') then | ||
attribs.latitude = getParameterValue(args, 'frameLatitude') | attribs.latitude = util.getParameterValue(args, 'frameLatitude') | ||
end | end | ||
if getParameterValue(args, 'frameLongitude') then | if util.getParameterValue(args, 'frameLongitude') then | ||
attribs.longitude = getParameterValue(args, 'frameLongitude') | attribs.longitude = util.getParameterValue(args, 'frameLongitude') | ||
end | end | ||
end | end | ||
if not attribs.latitude and not attribs.longitude then | if not attribs.latitude and not attribs.longitude and not coordsDerivedFromFeatures then | ||
local success, lat, long = pcall(wikidataCoords, getParameterValue(args, 'id') or mw.wikibase.getEntityIdForCurrentPage()) | local success, lat, long = pcall(util.wikidataCoords, util.getParameterValue(args, 'id') or mw.wikibase.getEntityIdForCurrentPage()) | ||
if success then | if success then | ||
attribs.latitude = tostring(lat) | attribs.latitude = tostring(lat) | ||
Line 308: | Line 768: | ||
end | end | ||
end | end | ||
if getParameterValue(args, 'frameAlign') then | if util.getParameterValue(args, 'frameAlign') then | ||
attribs.align = getParameterValue(args, 'frameAlign') | attribs.align = util.getParameterValue(args, 'frameAlign') | ||
end | end | ||
if isAffirmed(getParameterValue(args, 'plain')) then | if util.isAffirmed(util.getParameterValue(args, 'plain')) then | ||
attribs.frameless = "1" | attribs.frameless = "1" | ||
else | else | ||
attribs.text = getParameterValue(args, 'text') or L10n.defaults.text | attribs.text = util.getParameterValue(args, 'text') or L10n.defaults.text | ||
end | end | ||
else | else | ||
attribs.text = getParameterValue(args, 'text') or L10n.defaults.text | attribs.text = util.getParameterValue(args, 'text') or L10n.defaults.text | ||
end | end | ||
return attribs | return attribs | ||
end | end | ||
function | --[[ | ||
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 = { | local spanAttribs = { | ||
style = "font-size: small;", | style = "font-size: small;", | ||
Line 331: | Line 798: | ||
end | end | ||
function | --[[ | ||
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' | local tagName = 'maplink' | ||
if getParameterValue(args, 'frame') then | if util.getParameterValue(args, 'frame') then | ||
tagName = 'mapframe' | tagName = 'mapframe' | ||
end | end | ||
return mw.text.tag(tagName, | 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 | end | ||
--[[---------------------------------------------------------------------------- | |||
Package to be exported, i.e. methods which will available to templates and | |||
other modules. | |||
----------------------------------------------------------------------------]]-- | |||
local p = {} | local p = {} | ||
Line 345: | Line 982: | ||
function p.main(frame) | function p.main(frame) | ||
local parent = frame.getParent(frame) | local parent = frame.getParent(frame) | ||
local output = p._main(parent.args) | -- 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) | return frame:preprocess(output) | ||
end | end | ||
-- Entry | -- Entry points for modules | ||
function p._main(_args) | function p._main(_args) | ||
local args = | local args = util.trimArgs(_args) | ||
local tagContent = | local tagContent = make.content(args) | ||
local display = mw.text.split(getParameterValue(args, 'display') or L10n.defaults.display, '%s*' .. L10n.str.dsep .. '%s*') | 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 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 displayInline = display[1] == L10n.str.inline or display[2] == L10n.str.inline | ||
Line 361: | Line 1,013: | ||
local output | local output | ||
if displayInTitle and displayInline then | if displayInTitle and displayInline then | ||
output = | output = make.titleOutput(args, tagContent) .. make.inlineOutput(args, tagContent) | ||
elseif displayInTitle then | elseif displayInTitle then | ||
output = | output = make.titleOutput(args, tagContent) | ||
elseif displayInline then | elseif displayInline then | ||
output = | output = make.inlineOutput(args, tagContent) | ||
else | else | ||
error(L10n.error.badDisplayPara) | error(L10n.error.badDisplayPara) | ||
Line 371: | Line 1,023: | ||
return output | 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 | end | ||
return p | return p |
Latest revision as of 10:57, 6 September 2021
Documentation for this module may be created at Module:Mapframe/doc
-- 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 -- "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 `{{{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(val) == 'string' then val = val:match('^%s*(.-)%s*$') if val ~= '' then -- control characters inside json need to be escaped, but stripping them is simpler -- See also T214984 -- However, *don't* strip control characters from wikitext or you'll break strip markers cleanArgs[key] = (not util.matchesParam('text', key)) and val:gsub('%c',' ') or val 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¶ms=1_2_N_3_4_W_ or //tools.wmflabs.org/geohack/geohack.php?pagename=Example¶ms=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