No edit summary
No edit summary
 
(12 intermediate revisions by 2 users not shown)
Line 1: Line 1:
require('Module:No globals')
require('strict')
 
local p = {}
local p = {}
local getArgs = require('Module:Arguments').getArgs
local getArgs = require('Module:Arguments').getArgs


Line 101: Line 99:
end
end


-- Finds a parameter in a transclusion of {{Coord}}.
local function applyHemisphere(value, hem, direction)
local function coord2text(para,coord) -- this should be changed for languages which do not use Arabic numerals or the degree sign
value = tonumber(value)
local result = mw.text.split(mw.ustring.match(coord,'%-?[%.%d]+°[NS] %-?[%.%d]+°[EW]') or '', '[ °]')
if not value then
if para == 'longitude' then result = {result[3], result[4]} end
return nil
if not tonumber(result[1]) or not result[2] then return error('Malformed coordinates value', 2) end
end
return tonumber(result[1]) * hemisphereMultipliers[para][result[2]]
if hem then
local multiplier = hemisphereMultipliers[direction][hem]
if multiplier then
value = value * (value < 0 and (multiplier < 0 and 1 or -1) or multiplier)
end
end
return value
end
 
local function dmsToDec(d, m, s, hem, direction)
d, m, s = tonumber(d), tonumber(m) or 0, tonumber(s) or 0
if not d then
return nil
end
local dec = math.abs(d) + m / 60 + s / 3600
if d < 0 then
dec = -dec
end
return applyHemisphere(dec, hem, direction)
end
 
-- Extract numeric latitude/longitude from any {{Coord}} form or expanded HTML.
local function parseLatLon(coord)
if type(coord) ~= 'string' or coord == '' then
return nil, nil
end
local okDecode, decoded = pcall(mw.text.decode, coord)
coord = okDecode and decoded or coord
-- Drop parser strip markers with byte-safe gsub (not mw.ustring).
coord = string.gsub(coord, '\127[^\127]*\127', ' ')
-- If the string is not valid UTF-8, strip high bytes so ustring can run.
if not pcall(mw.ustring.len, coord) then
coord = string.gsub(coord, '[\128-\255]', ' ')
end
-- Read class="latitude"/class="longitude" spans from raw HTML first.
local htmlLat = mw.ustring.match(coord, 'class%s*=%s*["\']latitude["\'][^>]*>([^<]+)')
local htmlLon = mw.ustring.match(coord, 'class%s*=%s*["\']longitude["\'][^>]*>([^<]+)')
if htmlLat and htmlLon then
local latN = mw.ustring.match(htmlLat, '([+-]?[%.%d]+)')
local lonN = mw.ustring.match(htmlLon, '([+-]?[%.%d]+)')
local latH = mw.ustring.match(htmlLat, '([NSns])')
local lonH = mw.ustring.match(htmlLon, '([EWew])')
if latN and lonN then
return applyHemisphere(latN, latH, 'latitude'), applyHemisphere(lonN, lonH, 'longitude')
end
end
local geoDec = mw.ustring.match(coord, 'class%s*=%s*["\']geo%-dec["\'][^>]*>([^<]+)')
if geoDec then
local a, ah, b, bh = mw.ustring.match(geoDec, '([+-]?[%.%d]+)%s*[°º˚∘deg]*%s*([NSns])%s+([+-]?[%.%d]+)%s*[°º˚∘deg]*%s*([EWew])')
if a and b then
return applyHemisphere(a, ah, 'latitude'), applyHemisphere(b, bh, 'longitude')
end
end
local geo = mw.ustring.match(coord, 'class%s*=%s*["\']geo["\'][^>]*>([^<]+)')
if geo then
local a, b = mw.ustring.match(geo, '([+-]?[%.%d]+)%s*;%s*([+-]?[%.%d]+)')
if a and b then
return tonumber(a), tonumber(b)
end
end
 
-- GeoHack params=26_50_15_N_75_45_33_E_ or 26.8375_N_75.7592_E_
local hack = mw.ustring.match(coord, 'params=([%d%._NSEWnswe]+)')
if hack then
local parts = mw.text.split(hack, '_', true)
local nums, hems = {}, {}
for _, part in ipairs(parts) do
if part == 'N' or part == 'S' or part == 'n' or part == 's'
or part == 'E' or part == 'W' or part == 'e' or part == 'w' then
hems[#hems + 1] = part:upper()
elseif part ~= '' and tonumber(part) then
nums[#nums + 1] = tonumber(part)
end
end
if #hems >= 2 and #nums >= 2 then
local lat, lon
if #nums == 2 then
lat, lon = nums[1], nums[2]
elseif #nums >= 6 then
lat = nums[1] + nums[2] / 60 + nums[3] / 3600
lon = nums[4] + nums[5] / 60 + nums[6] / 3600
elseif #nums == 4 then
lat = nums[1] + nums[2] / 60
lon = nums[3] + nums[4] / 60
end
if lat and lon then
return applyHemisphere(lat, hems[1], 'latitude'), applyHemisphere(lon, hems[2], 'longitude')
end
end
end
 
local function sg(s, pat, repl)
return (string.gsub(s, pat, repl))
end
local plain = sg(coord, '<[^>]*>', ' ')
plain = sg(plain, '&nbsp;', ' ')
plain = sg(plain, '&#160;', ' ')
plain = sg(plain, '\194\160', ' ')
plain = sg(plain, '\226\128[\139-\141]', '')
plain = sg(plain, '\239\187\191', '')
if not pcall(mw.ustring.len, plain) then
plain = sg(plain, '[\128-\255]', ' ')
else
local ok, out
ok, out = pcall(mw.ustring.gsub, plain, '[º˚∘]', '°')
if ok then plain = out end
ok, out = pcall(mw.ustring.gsub, plain, '[′’ʹ`´]', "'")
if ok then plain = out end
ok, out = pcall(mw.ustring.gsub, plain, '[″”]', '"')
if ok then plain = out end
end
plain = sg(plain, '%s+', ' ')
plain = mw.text.trim(plain)
 
-- Unexpanded {{Coord|...}} anywhere in the string
local body = mw.ustring.match(plain, '{{%s*[Cc]oord%s*|([^}]+)}')
if body then
local values = {}
for value in mw.text.gsplit(body, '|', true) do
value = mw.text.trim(value)
if value ~= '' and not mw.ustring.find(value, '=', 1, true) then
values[#values + 1] = value
end
end
if tonumber(values[1]) and tonumber(values[2]) and not values[3] then
return tonumber(values[1]), tonumber(values[2])
end
if tonumber(values[1]) and values[2] and values[2]:match('^[NSns]$')
and tonumber(values[3]) and values[4] and values[4]:match('^[EWew]$') then
return applyHemisphere(values[1], values[2]:upper(), 'latitude'),
applyHemisphere(values[3], values[4]:upper(), 'longitude')
end
if tonumber(values[1]) and tonumber(values[2]) and tonumber(values[3])
and values[4] and values[4]:match('^[NSns]$')
and tonumber(values[5]) and tonumber(values[6]) and tonumber(values[7])
and values[8] and values[8]:match('^[EWew]$') then
return dmsToDec(values[1], values[2], values[3], values[4]:upper(), 'latitude'),
dmsToDec(values[5], values[6], values[7], values[8]:upper(), 'longitude')
end
if tonumber(values[1]) and tonumber(values[2]) and values[3] and values[3]:match('^[NSns]$')
and tonumber(values[4]) and tonumber(values[5]) and values[6] and values[6]:match('^[EWew]$') then
return dmsToDec(values[1], values[2], 0, values[3]:upper(), 'latitude'),
dmsToDec(values[4], values[5], 0, values[6]:upper(), 'longitude')
end
end
 
local latD, latM, latS, latH, lonD, lonM, lonS, lonH = mw.ustring.match(
plain,
'([+-]?[%.%d]+)%s*°%s*([%.%d]*)%s*\'?%s*([%.%d]*)%s*"?%s*([NSns])%s*[,;/%s]+%s*' ..
'([+-]?[%.%d]+)%s*°%s*([%.%d]*)%s*\'?%s*([%.%d]*)%s*"?%s*([EWew])'
)
if latD and lonD then
return dmsToDec(latD, latM, latS, latH:upper(), 'latitude'),
dmsToDec(lonD, lonM, lonS, lonH:upper(), 'longitude')
end
 
local latDec, latHem, lonDec, lonHem = mw.ustring.match(
plain,
'([+-]?[%.%d]+)%s*°?%s*([NSns])%s*[,;/%s]+%s*([+-]?[%.%d]+)%s*°?%s*([EWew])'
)
if latDec and lonDec then
return applyHemisphere(latDec, latHem:upper(), 'latitude'),
applyHemisphere(lonDec, lonHem:upper(), 'longitude')
end
 
local geoLat, geoLon = mw.ustring.match(plain, '([+-]?[%.%d]+)%s*;%s*([+-]?[%.%d]+)')
if geoLat and geoLon then
return tonumber(geoLat), tonumber(geoLon)
end
 
local pairLat, pairLon = mw.ustring.match(plain, '([+-]?[%.%d]+)%s*[, ]+%s*([+-]?[%.%d]+)')
if pairLat and pairLon then
local lat, lon = tonumber(pairLat), tonumber(pairLon)
if lat and lon and lat >= -90 and lat <= 90 and lon >= -180 and lon <= 180 then
return lat, lon
end
end
 
-- Last resort: number + hemisphere pairs anywhere
local a, ah, b, bh = string.match(
plain,
'([+-]?[%d%.]+)%s*([NSns])%A+([+-]?[%d%.]+)%s*([EWew])'
)
if a and b then
return applyHemisphere(a, ah:upper(), 'latitude'),
applyHemisphere(b, bh:upper(), 'longitude')
end
 
-- Walk every number and N/S/E/W letter in order.
-- Handles 28|33|29|N|77|14|36|E and 28 33 29 N 77 14 36 E
-- even when degree symbols were stripped as invalid UTF-8.
local toks = {}
local i, n = 1, #plain
while i <= n do
local num, ni = string.match(plain, '^([+-]?%d+%.?%d*)()', i)
if num then
toks[#toks + 1] = num
i = ni
else
local hem, hi = string.match(plain, '^([NSEWnsew])()', i)
if hem then
toks[#toks + 1] = hem:upper()
i = hi
else
i = i + 1
end
end
end
local nsAt, ewAt
for idx, tok in ipairs(toks) do
if tok == 'N' or tok == 'S' then nsAt = idx end
if tok == 'E' or tok == 'W' then ewAt = idx end
end
if nsAt and ewAt then
local latParts, lonParts = {}, {}
for idx = nsAt - 1, 1, -1 do
if tonumber(toks[idx]) then
table.insert(latParts, 1, tonumber(toks[idx]))
if #latParts == 3 then break end
else
break
end
end
for idx = ewAt - 1, nsAt + 1, -1 do
if tonumber(toks[idx]) then
table.insert(lonParts, 1, tonumber(toks[idx]))
if #lonParts == 3 then break end
else
break
end
end
if #latParts >= 1 and #lonParts >= 1 then
local lat = latParts[1] + (latParts[2] or 0) / 60 + (latParts[3] or 0) / 3600
local lon = lonParts[1] + (lonParts[2] or 0) / 60 + (lonParts[3] or 0) / 3600
return applyHemisphere(lat, toks[nsAt], 'latitude'),
applyHemisphere(lon, toks[ewAt], 'longitude')
end
end
 
return nil, nil
end
 
local function coord2text(para, coord)
if type(coord) ~= 'string' or coord == '' then
return error('Malformed coordinates value', 2)
end
coord = string.gsub(coord, '&#123;', '{')
coord = string.gsub(coord, '&#125;', '}')
coord = string.gsub(coord, '&lt;', '<')
coord = string.gsub(coord, '&gt;', '>')
local ok, lat, lon = pcall(parseLatLon, coord)
if not ok then
lat, lon = nil, nil
end
if not lat or not lon then
local frame = mw.getCurrentFrame()
if frame and string.find(coord, '{{', 1, true) then
local okp, expanded = pcall(function() return frame:preprocess(coord) end)
if okp and expanded then
ok, lat, lon = pcall(parseLatLon, expanded)
if not ok then
lat, lon = nil, nil
end
end
end
end
if lat and lon then
return para == 'longitude' and lon or lat
end
mw.log('Malformed coordinates value')
mw.logObject(para, 'para')
mw.logObject(coord, 'coord')
return nil
end
end


Line 118: Line 388:
if value ~= '' or key == 'caption' or key == 'maplink' then
if value ~= '' or key == 'caption' or key == 'maplink' then
return value
return value
elseif key == 'useWikidata' then
return false
end
end
end
end
Line 126: Line 394:
if args.AlternativeMap then
if args.AlternativeMap then
return args.AlternativeMap
return args.AlternativeMap
elseif args.relief and map('image1') ~= '' then
elseif args.relief then
return map('image1')
        local digits = mw.ustring.match(args.relief,'^[1-9][0-9]?$') -- image1 to image99
else
        if digits then
return map('image')
local imageDigits = map('image' .. digits)
if imageDigits ~= '' then return imageDigits end
        end
local yesNo = require('Module:Yesno')
if yesNo(args.relief,true) then
local image1 = map('image1')
if image1 ~= '' then return image1 end
end
end
end
    return map('image')
end
end


Line 154: Line 430:
     width = round((default_as_number or 240) * (tonumber(map('defaultscale')) or 1))
     width = round((default_as_number or 240) * (tonumber(map('defaultscale')) or 1))
     width_as_number = tonumber(mw.ustring.match(tostring(width),"%d*")) or 0;
     width_as_number = tonumber(mw.ustring.match(tostring(width),"%d*")) or 0;
     end
     end
     if args.max_width ~= "" and args.max_width ~= nil then
     if args.max_width ~= "" and args.max_width ~= nil then
         -- check to see if width bigger than max_width
         -- check to see if width bigger than max_width
Line 162: Line 438:
         end
         end
     end
     end
local retval = frame:extensionTag{name = 'templatestyles', args = {src = 'Template:Location map/styles.css'}}
local retval = frame:extensionTag{name = 'templatestyles', args = {src = 'Module:Location map/styles.css'}}
if args.float == 'center' then
if args.float == 'center' then
retval = retval .. '<div class="center">'
retval = retval .. '<div class="center">'
end
end
if args.caption and args.caption ~= '' and args.border ~= 'infobox' then
if args.caption and args.caption ~= '' and args.border ~= 'infobox' then
retval = retval .. '<div class="locmap noviewer thumb '
retval = retval .. '<div class="locmap noresize thumb '
if args.float == '"left"' or args.float == 'left' then
if args.float == '"left"' or args.float == 'left' then
retval = retval .. 'tleft'
retval = retval .. 'floatleft'
elseif args.float == '"center"' or args.float == 'center' or args.float == '"none"' or args.float == 'none' then
elseif args.float == '"center"' or args.float == 'center' or args.float == '"none"' or args.float == 'none' then
retval = retval .. 'tnone'
retval = retval .. 'tnone'
else
else
retval = retval .. 'tright'
retval = retval .. 'floatright'
end
end
retval = retval .. '"><div class="thumbinner" style="width:' .. (width + 2) .. 'px'
retval = retval .. '"><div class="thumbinner" style="width:' .. (width + 2) .. 'px'
Line 198: Line 474:
local currentTitle = mw.title.getCurrentTitle()
local currentTitle = mw.title.getCurrentTitle()
retval = string.format(
retval = string.format(
'%s[[File:%s|%spx|%s%s]]',
'%s[[File:%s|%spx|%s%s|class=notpageimage noviewer]]',
retval,
retval,
image,
image,
Line 211: Line 487:
end
end
if args.overlay_image then
if args.overlay_image then
return retval .. '<div style="position:absolute;top:0;left:0">[[File:' .. args.overlay_image .. '|' .. width .. 'px]]</div>'
return retval .. '<div style="position:absolute;top:0;left:0">[[File:' .. args.overlay_image .. '|' .. width .. 'px|class=notpageimage noviewer]]</div>'
else
else
return retval
return retval
Line 230: Line 506:
retval = retval .. '<div style="padding-top:0.2em">'
retval = retval .. '<div style="padding-top:0.2em">'
else
else
retval = retval .. '<div style="font-size:90%;padding-top:3px">'
retval = retval .. '<div style="font-size:91%;padding-top:3px">'
end
end
retval = retval
retval = retval
Line 239: Line 515:
retval = retval .. '<div class="thumbcaption"><div class="magnify">[[:File:' .. getContainerImage(args, map) .. '| ]]</div>' .. args.caption .. '</div>'
retval = retval .. '<div class="thumbcaption"><div class="magnify">[[:File:' .. getContainerImage(args, map) .. '| ]]</div>' .. args.caption .. '</div>'
end
end
if args.switcherLabel then
if args.switcherLabel then
retval = retval .. '<span class="switcher-label" style="display:none">' .. args.switcherLabel .. '</span>'
retval = retval .. '<span class="switcher-label" style="display:none">' .. args.switcherLabel .. '</span>'
Line 245: Line 520:
retval = retval .. '<span class="switcher-label" style="display:none">Show map of ' .. map('name') .. '</span>'
retval = retval .. '<span class="switcher-label" style="display:none">Show map of ' .. map('name') .. '</span>'
end
end
 
retval = retval .. '</div></div>'
retval = retval .. '</div></div>'
if args.caption_undefined then
if args.caption_undefined then
Line 280: Line 555:
end
end


local function markOuterDiv(x, y, imageDiv, labelDiv)
local function markOuterDiv(x, y, imageDiv, labelDiv, label_size)
return mw.html.create('div')
return mw.html.create('div')
:addClass('od')
:addClass('od')
:cssText('top:' .. round(y, 3) .. '%;left:' .. round(x, 3) .. '%')
:addClass('notheme') -- T236137
:cssText('top:' .. round(y, 3) .. '%;left:' .. round(x, 3) .. '%;font-size:' .. label_size .. '%')
:node(imageDiv)
:node(imageDiv)
:node(labelDiv)
:node(labelDiv)
Line 295: Line 571:
if marksize ~= 0 then
if marksize ~= 0 then
builder:wikitext(string.format(
builder:wikitext(string.format(
'[[File:%s|%dx%dpx|%s|link=%s%s]]',
'[[File:%s|%dx%dpx|%s|link=%s%s|class=notpageimage noviewer]]',
mark,
mark,
marksize,
marksize,
Line 312: Line 588:
end
end
local builder = mw.html.create('div')
local builder = mw.html.create('div')
:cssText('font-size:' .. label_size .. '%;width:' .. label_width .. 'em')
:cssText('width:' .. label_width .. 'em')
local distance = round(marksize / 2 + 1)
local distance = round(marksize / 2 + 1)
if position == 'top' then -- specified top
if position == 'top' then -- specified top
Line 318: Line 594:
elseif position == 'bottom' then -- specified bottom
elseif position == 'bottom' then -- specified bottom
builder:addClass('pv'):cssText('top:' .. distance .. 'px;left:' .. (-label_width / 2) .. 'em')
builder:addClass('pv'):cssText('top:' .. distance .. 'px;left:' .. (-label_width / 2) .. 'em')
elseif position == 'left' or (tonumber(x) > 70 and position ~= 'right') then -- specified left or autodetected to left
builder:addClass('pl'):cssText('right:' .. distance .. 'px')
else -- specified right or autodetected to right
else -- specified right or autodetected to right
builder:addClass('pr'):cssText('left:' .. distance .. 'px')
builder:addClass('pr'):cssText('left:' .. distance .. 'px')
Line 327: Line 605:
end
end
return builder:done()
return builder:done()
end
local function getX(longitude, left, right)
local width = (right - left) % 360
if width == 0 then
width = 360
end
local distanceFromLeft = (longitude - left) % 360
-- the distance needed past the map to the right equals distanceFromLeft - width. the distance needed past the map to the left equals 360 - distanceFromLeft. to minimize page stretching, go whichever way is shorter
if distanceFromLeft - width / 2 >= 180 then
distanceFromLeft = distanceFromLeft - 360
end
return 100 * distanceFromLeft / width
end
local function getY(latitude, top, bottom)
return 100 * (top - latitude) / (top - bottom)
end
end


Line 368: Line 663:
end
end
end
end
end
end
local builder = mw.html.create()
local builder = mw.html.create()
local currentTitle = mw.title.getCurrentTitle()
local currentTitle = mw.title.getCurrentTitle()
if args.coordinates then
if args.coordinates then
-- Temporarily removed to facilitate infobox conversion. See [[Wikipedia:Coordinates in infoboxes]]
local okLon, lonVal = pcall(coord2text, 'longitude', args.coordinates)
 
local okLat, latVal = pcall(coord2text, 'latitude', args.coordinates)
-- if longitude or latitude then
if okLon then longitude = lonVal or longitude end
-- error('Coordinates from [[Module:Coordinates]] and individual coordinates cannot both be provided')
if okLat then latitude = latVal or latitude end
-- end
end
if args.link and (currentTitle.namespace == 0) then
if not longitude or not latitude then
builder:wikitext('[[Category:Location maps with linked markers with coordinates from Wikidata]]')
-- Broken or empty coordinates: omit the pin instead of erroring.
if currentTitle.namespace == 0 then
builder:wikitext('[[Category:Location maps with malformed coordinates]]')
end
end
return builder
end
end
if currentTitle.namespace > 0 then
if currentTitle.namespace > 0 then
Line 394: Line 691:
builder:wikitext('[[Category:Location maps with different longitude and latitude precisions|Decimal]]')
builder:wikitext('[[Category:Location maps with different longitude and latitude precisions|Decimal]]')
end
end
end
if ((tonumber(args.lat_deg) or 0) < 0) and ((tonumber(args.lat_min) or 0) ~= 0 or (tonumber(args.lat_sec) or 0) ~= 0 or (args.lat_dir and args.lat_dir ~='')) then
builder:wikitext('[[Category:Location maps with negative degrees and minutes or seconds]]')
end
if ((tonumber(args.lon_deg) or 0) < 0) and ((tonumber(args.lon_min) or 0) ~= 0 or (tonumber(args.lon_sec) or 0) ~= 0 or (args.lon_dir and args.lon_dir ~= '')) then
builder:wikitext('[[Category:Location maps with negative degrees and minutes or seconds]]')
end
if (((tonumber(args.lat_min) or 0) < 0) or ((tonumber(args.lat_sec) or 0) < 0)) then
builder:wikitext('[[Category:Location maps with negative degrees and minutes or seconds]]')
end
if (((tonumber(args.lon_min) or 0) < 0) or ((tonumber(args.lon_sec) or 0) < 0)) then
builder:wikitext('[[Category:Location maps with negative degrees and minutes or seconds]]')
end
end
if args.skew or args.lon_shift or args.markhigh then
if args.skew or args.lon_shift or args.markhigh then
Line 409: Line 718:
end
end
end
end
if map('x') ~= '' then
x = tonumber(mw.ext.ParserFunctions.expr(map('x', { latitude, longitude })))
else
x = tonumber(getX(longitude, map('left'), map('right')))
end
if map('y') ~= '' then
y = tonumber(mw.ext.ParserFunctions.expr(map('y', { latitude, longitude })))
else
y = tonumber(getY(latitude, map('top'), map('bottom')))
end
if (x < 0 or x > 100 or y < 0 or y > 100) and not args.outside then
mw.log('Mark placed outside map boundaries without outside flag set. x = ' .. x .. ', y = ' .. y)
local parent = frame:getParent()
if parent then
mw.log('Parent is ' .. parent:getTitle())
end
mw.logObject(args, 'args')
if currentTitle.namespace == 0 then
if currentTitle.namespace == 0 then
local key = currentTitle.prefixedText
local key = currentTitle.prefixedText
builder:wikitext('[[Category:Location maps with marks outside map and outside parameter not set|' .. key .. ' ]]')
builder:wikitext('[[Category:Location maps with marks outside map and outside parameter not set|' .. key .. ' ]]')
end
end
end
local mark = args.mark or map('mark')
local mark = args.mark or map('mark')
if mark == '' then
if mark == '' then
Line 419: Line 746:
local marksize = tonumber(args.marksize) or tonumber(map('marksize')) or 8
local marksize = tonumber(args.marksize) or tonumber(map('marksize')) or 8
local imageDiv = markImageDiv(mark, marksize, args.label or mw.title.getCurrentTitle().text, args.link or '', args.alt, args[2])
local imageDiv = markImageDiv(mark, marksize, args.label or mw.title.getCurrentTitle().text, args.link or '', args.alt, args[2])
local label_size = args.label_size or 91
local labelDiv
local labelDiv
if args.label and args.position ~= 'none' then
if args.label and args.position ~= 'none' then
labelDiv = markLabelDiv(args.label, args.label_size or 90, args.label_width or 6, args.position, args.background, x, marksize)
labelDiv = markLabelDiv(args.label, label_size, args.label_width or 6, args.position, args.background, x, marksize)
end
end
return builder:node(markOuterDiv(x, y, imageDiv, labelDiv))
return builder:node(markOuterDiv(x, y, imageDiv, labelDiv, label_size))
end
end


Line 434: Line 762:
end
end
return retval
return retval
end
function p.infobox_pushpin_map(frame)
--This function is designed to be invoked from an infobox template
--* Some parameters (generally starting with "pushpin_") are meant to be exposed as parameters
--  in the outer infobox, settable by editors
--* Others are meant to be "hidden" (e.g., only set inside the infobox template code)
-- First get the combined arguments from both parent and invocation frame
local raw_args = getArgs(frame,{parentFirst=true})
--If no map is supplied, return.
if not raw_args['pushpin_map'] then
return
end
-- Now get arguments from just the invocation frame
local frame_args = getArgs(frame,{frameOnly=true})
-- parsed_args will have the resulting merged parameters
local parsed_args = {}
-- Parse the arguments
parsed_args[1] = raw_args['pushpin_map']
parsed_args.float = frame_args.float or 'center'
parsed_args.border = frame_args.border or 'infobox'
parsed_args.default_width = frame_args.default_width
parsed_args.max_width = frame_args.max_width
parsed_args.coordinates = raw_args['coordinates']
parsed_args.caption = raw_args['pushpin_caption']
parsed_args.relief = raw_args['pushpin_relief']
parsed_args.label = raw_args['pushpin_label']
parsed_args.label_size = raw_args['pushpin_label_size']
parsed_args.position = raw_args['pushpin_label_position']
parsed_args.mark = raw_args['pushpin_mark']
parsed_args.marksize = raw_args['pushpin_mark_size']
parsed_args.alt = raw_args['pushpin_alt']
parsed_args.background = raw_args['pushpin_background']
parsed_args.width = raw_args['pushpin_map_size']
local map = {}
local caption_list = {}
for mapname in string.gmatch(parsed_args[1], '[^#]+') do
map[#map + 1] = p.getMapParams(mw.ustring.gsub(mapname, '^%s*(.-)%s*$', '%1'), frame)
end
if parsed_args['caption'] then
for caption in mw.text.gsplit(parsed_args['caption'], '##', true) do
caption_list[#caption_list + 1] = caption
end
end
if #map == 1 then
map = map[1]
if not parsed_args.caption then
parsed_args.caption = 'Location within ' .. map('name')
end
end
if type(map) == 'table' then
mw.log('----')
mw.logObject(map)
if #caption_list > #map then
error(string.format('%d captions were provided, but only %d maps were provided', #caption_list, #map))
elseif not parsed_args.caption then
-- Set a default caption for each of the multiple maps
for i=1, #map do
caption_list[#caption_list + 1] = 'Location within '.. map[i]('name')
end
end
local outputs = {}
parsed_args.autoSwitcherLabel = true
for k,v in ipairs(map) do
parsed_args.caption = caption_list[k]
outputs[k] = p.main(frame, parsed_args, v)
end
return '<div class="switcher-container">' .. table.concat(outputs) .. '</div>'
else
return p.top(frame, parsed_args, map) .. tostring( p.mark(frame, parsed_args, map) ) .. p.bottom(frame, parsed_args, map)
end
end
end


Line 440: Line 844:
if not args then
if not args then
args = getArgs(frame, {wrappers = 'Template:Location map', valueFunc = p.valueFunc})
args = getArgs(frame, {wrappers = 'Template:Location map', valueFunc = p.valueFunc})
end
if args.useWikidata == nil then
args.useWikidata = true
end
end
if not map then
if not map then

Latest revision as of 21:54, 28 September 2026

Usage[edit source]

This module implements the {{Location map}} and {{Location map~}} templates. Please see the template pages for usage instructions.

Known issues[edit source]

Since the introduction of support for different captions when multiple maps are utilized, an issue has been highlighted.
Some infobox templates use their caption parameter directly instead of passing it to this module. This results in the display of a "##" between the two captions.
If you are unable to edit the infobox template, contact Bellezzasolo.

Tracking/maintenance categories[edit source]

See also[edit source]


require('strict')
local p = {}
local getArgs = require('Module:Arguments').getArgs

local function round(n, decimals)
	local pow = 10^(decimals or 0)
	return math.floor(n * pow + 0.5) / pow
end

function p.getMapParams(map, frame)
	if not map then
		error('The name of the location map definition to use must be specified', 2)
	end
	local moduletitle = mw.title.new('Module:Location map/data/' .. map)
	if not moduletitle then
		error(string.format('%q is not a valid name for a location map definition', map), 2)
	elseif moduletitle.exists then
		local mapData = mw.loadData('Module:Location map/data/' .. map)
		return function(name, params)
			if name == nil then
				return 'Module:Location map/data/' .. map
			elseif mapData[name] == nil then
				return ''
			elseif params then
				return mw.message.newRawMessage(tostring(mapData[name]), unpack(params)):plain()
			else
				return mapData[name]
			end
		end
	else
		error('Unable to find the specified location map definition: "Module:Location map/data/' .. map .. '" does not exist', 2)
	end
end

function p.data(frame, args, map)
	if not args then
		args = getArgs(frame, {frameOnly = true})
	end
	if not map then
		map = p.getMapParams(args[1], frame)
	end
	local params = {}
	for k,v in ipairs(args) do
		if k > 2 then
			params[k-2] = v
		end
	end
	return map(args[2], #params ~= 0 and params)
end

local hemisphereMultipliers = {
	longitude = { W = -1, w = -1, E = 1, e = 1 },
	latitude = { S = -1, s = -1, N = 1, n = 1 }
}

local function decdeg(degrees, minutes, seconds, hemisphere, decimal, direction)
	if decimal then
		if degrees then
			error('Decimal and DMS degrees cannot both be provided for ' .. direction, 2)
		elseif minutes then
			error('Minutes can only be provided with DMS degrees for ' .. direction, 2)
		elseif seconds then
			error('Seconds can only be provided with DMS degrees for ' .. direction, 2)
		elseif hemisphere then
			error('A hemisphere can only be provided with DMS degrees for ' .. direction, 2)
		end
		local retval = tonumber(decimal)
		if retval then
			return retval
		end
		error('The value "' .. decimal .. '" provided for ' .. direction .. ' is not valid', 2)
	elseif seconds and not minutes then
		error('Seconds were provided for ' .. direction .. ' without minutes also being provided', 2)
	elseif not degrees then
		if minutes then
			error('Minutes were provided for ' .. direction .. ' without degrees also being provided', 2)
		elseif hemisphere then
			error('A hemisphere was provided for ' .. direction .. ' without degrees also being provided', 2)
		end
		return nil
	end
	decimal = tonumber(degrees)
	if not decimal then
		error('The degree value "' .. degrees .. '" provided for ' .. direction .. ' is not valid', 2)
	elseif minutes and not tonumber(minutes) then
		error('The minute value "' .. minutes .. '" provided for ' .. direction .. ' is not valid', 2)
	elseif seconds and not tonumber(seconds) then
		error('The second value "' .. seconds .. '" provided for ' .. direction .. ' is not valid', 2)
	end
	decimal = decimal + (minutes or 0)/60 + (seconds or 0)/3600
	if hemisphere then
		local multiplier = hemisphereMultipliers[direction][hemisphere]
		if not multiplier then
			error('The hemisphere "' .. hemisphere .. '" provided for ' .. direction .. ' is not valid', 2)
		end
		decimal = decimal * multiplier
	end
	return decimal
end

local function applyHemisphere(value, hem, direction)
	value = tonumber(value)
	if not value then
		return nil
	end
	if hem then
		local multiplier = hemisphereMultipliers[direction][hem]
		if multiplier then
			value = value * (value < 0 and (multiplier < 0 and 1 or -1) or multiplier)
		end
	end
	return value
end

local function dmsToDec(d, m, s, hem, direction)
	d, m, s = tonumber(d), tonumber(m) or 0, tonumber(s) or 0
	if not d then
		return nil
	end
	local dec = math.abs(d) + m / 60 + s / 3600
	if d < 0 then
		dec = -dec
	end
	return applyHemisphere(dec, hem, direction)
end

-- Extract numeric latitude/longitude from any {{Coord}} form or expanded HTML.
local function parseLatLon(coord)
	if type(coord) ~= 'string' or coord == '' then
		return nil, nil
	end
	local okDecode, decoded = pcall(mw.text.decode, coord)
	coord = okDecode and decoded or coord
	-- Drop parser strip markers with byte-safe gsub (not mw.ustring).
	coord = string.gsub(coord, '\127[^\127]*\127', ' ')
	-- If the string is not valid UTF-8, strip high bytes so ustring can run.
	if not pcall(mw.ustring.len, coord) then
		coord = string.gsub(coord, '[\128-\255]', ' ')
	end
	-- Read class="latitude"/class="longitude" spans from raw HTML first.
	local htmlLat = mw.ustring.match(coord, 'class%s*=%s*["\']latitude["\'][^>]*>([^<]+)')
	local htmlLon = mw.ustring.match(coord, 'class%s*=%s*["\']longitude["\'][^>]*>([^<]+)')
	if htmlLat and htmlLon then
		local latN = mw.ustring.match(htmlLat, '([+-]?[%.%d]+)')
		local lonN = mw.ustring.match(htmlLon, '([+-]?[%.%d]+)')
		local latH = mw.ustring.match(htmlLat, '([NSns])')
		local lonH = mw.ustring.match(htmlLon, '([EWew])')
		if latN and lonN then
			return applyHemisphere(latN, latH, 'latitude'), applyHemisphere(lonN, lonH, 'longitude')
		end
	end
	local geoDec = mw.ustring.match(coord, 'class%s*=%s*["\']geo%-dec["\'][^>]*>([^<]+)')
	if geoDec then
		local a, ah, b, bh = mw.ustring.match(geoDec, '([+-]?[%.%d]+)%s*[°º˚∘deg]*%s*([NSns])%s+([+-]?[%.%d]+)%s*[°º˚∘deg]*%s*([EWew])')
		if a and b then
			return applyHemisphere(a, ah, 'latitude'), applyHemisphere(b, bh, 'longitude')
		end
	end
	local geo = mw.ustring.match(coord, 'class%s*=%s*["\']geo["\'][^>]*>([^<]+)')
	if geo then
		local a, b = mw.ustring.match(geo, '([+-]?[%.%d]+)%s*;%s*([+-]?[%.%d]+)')
		if a and b then
			return tonumber(a), tonumber(b)
		end
	end

	-- GeoHack params=26_50_15_N_75_45_33_E_ or 26.8375_N_75.7592_E_
	local hack = mw.ustring.match(coord, 'params=([%d%._NSEWnswe]+)')
	if hack then
		local parts = mw.text.split(hack, '_', true)
		local nums, hems = {}, {}
		for _, part in ipairs(parts) do
			if part == 'N' or part == 'S' or part == 'n' or part == 's'
				or part == 'E' or part == 'W' or part == 'e' or part == 'w' then
				hems[#hems + 1] = part:upper()
			elseif part ~= '' and tonumber(part) then
				nums[#nums + 1] = tonumber(part)
			end
		end
		if #hems >= 2 and #nums >= 2 then
			local lat, lon
			if #nums == 2 then
				lat, lon = nums[1], nums[2]
			elseif #nums >= 6 then
				lat = nums[1] + nums[2] / 60 + nums[3] / 3600
				lon = nums[4] + nums[5] / 60 + nums[6] / 3600
			elseif #nums == 4 then
				lat = nums[1] + nums[2] / 60
				lon = nums[3] + nums[4] / 60
			end
			if lat and lon then
				return applyHemisphere(lat, hems[1], 'latitude'), applyHemisphere(lon, hems[2], 'longitude')
			end
		end
	end

	local function sg(s, pat, repl)
		return (string.gsub(s, pat, repl))
	end
	local plain = sg(coord, '<[^>]*>', ' ')
	plain = sg(plain, '&nbsp;', ' ')
	plain = sg(plain, '&#160;', ' ')
	plain = sg(plain, '\194\160', ' ')
	plain = sg(plain, '\226\128[\139-\141]', '')
	plain = sg(plain, '\239\187\191', '')
	if not pcall(mw.ustring.len, plain) then
		plain = sg(plain, '[\128-\255]', ' ')
	else
		local ok, out
		ok, out = pcall(mw.ustring.gsub, plain, '[º˚∘]', '°')
		if ok then plain = out end
		ok, out = pcall(mw.ustring.gsub, plain, '[′’ʹ`´]', "'")
		if ok then plain = out end
		ok, out = pcall(mw.ustring.gsub, plain, '[″”]', '"')
		if ok then plain = out end
	end
	plain = sg(plain, '%s+', ' ')
	plain = mw.text.trim(plain)

	-- Unexpanded {{Coord|...}} anywhere in the string
	local body = mw.ustring.match(plain, '{{%s*[Cc]oord%s*|([^}]+)}')
	if body then
		local values = {}
		for value in mw.text.gsplit(body, '|', true) do
			value = mw.text.trim(value)
			if value ~= '' and not mw.ustring.find(value, '=', 1, true) then
				values[#values + 1] = value
			end
		end
		if tonumber(values[1]) and tonumber(values[2]) and not values[3] then
			return tonumber(values[1]), tonumber(values[2])
		end
		if tonumber(values[1]) and values[2] and values[2]:match('^[NSns]$')
			and tonumber(values[3]) and values[4] and values[4]:match('^[EWew]$') then
			return applyHemisphere(values[1], values[2]:upper(), 'latitude'),
				applyHemisphere(values[3], values[4]:upper(), 'longitude')
		end
		if tonumber(values[1]) and tonumber(values[2]) and tonumber(values[3])
			and values[4] and values[4]:match('^[NSns]$')
			and tonumber(values[5]) and tonumber(values[6]) and tonumber(values[7])
			and values[8] and values[8]:match('^[EWew]$') then
			return dmsToDec(values[1], values[2], values[3], values[4]:upper(), 'latitude'),
				dmsToDec(values[5], values[6], values[7], values[8]:upper(), 'longitude')
		end
		if tonumber(values[1]) and tonumber(values[2]) and values[3] and values[3]:match('^[NSns]$')
			and tonumber(values[4]) and tonumber(values[5]) and values[6] and values[6]:match('^[EWew]$') then
			return dmsToDec(values[1], values[2], 0, values[3]:upper(), 'latitude'),
				dmsToDec(values[4], values[5], 0, values[6]:upper(), 'longitude')
		end
	end

	local latD, latM, latS, latH, lonD, lonM, lonS, lonH = mw.ustring.match(
		plain,
		'([+-]?[%.%d]+)%s*°%s*([%.%d]*)%s*\'?%s*([%.%d]*)%s*"?%s*([NSns])%s*[,;/%s]+%s*' ..
		'([+-]?[%.%d]+)%s*°%s*([%.%d]*)%s*\'?%s*([%.%d]*)%s*"?%s*([EWew])'
	)
	if latD and lonD then
		return dmsToDec(latD, latM, latS, latH:upper(), 'latitude'),
			dmsToDec(lonD, lonM, lonS, lonH:upper(), 'longitude')
	end

	local latDec, latHem, lonDec, lonHem = mw.ustring.match(
		plain,
		'([+-]?[%.%d]+)%s*°?%s*([NSns])%s*[,;/%s]+%s*([+-]?[%.%d]+)%s*°?%s*([EWew])'
	)
	if latDec and lonDec then
		return applyHemisphere(latDec, latHem:upper(), 'latitude'),
			applyHemisphere(lonDec, lonHem:upper(), 'longitude')
	end

	local geoLat, geoLon = mw.ustring.match(plain, '([+-]?[%.%d]+)%s*;%s*([+-]?[%.%d]+)')
	if geoLat and geoLon then
		return tonumber(geoLat), tonumber(geoLon)
	end

	local pairLat, pairLon = mw.ustring.match(plain, '([+-]?[%.%d]+)%s*[, ]+%s*([+-]?[%.%d]+)')
	if pairLat and pairLon then
		local lat, lon = tonumber(pairLat), tonumber(pairLon)
		if lat and lon and lat >= -90 and lat <= 90 and lon >= -180 and lon <= 180 then
			return lat, lon
		end
	end

	-- Last resort: number + hemisphere pairs anywhere
	local a, ah, b, bh = string.match(
		plain,
		'([+-]?[%d%.]+)%s*([NSns])%A+([+-]?[%d%.]+)%s*([EWew])'
	)
	if a and b then
		return applyHemisphere(a, ah:upper(), 'latitude'),
			applyHemisphere(b, bh:upper(), 'longitude')
	end

	-- Walk every number and N/S/E/W letter in order.
	-- Handles 28|33|29|N|77|14|36|E and 28 33 29 N 77 14 36 E
	-- even when degree symbols were stripped as invalid UTF-8.
	local toks = {}
	local i, n = 1, #plain
	while i <= n do
		local num, ni = string.match(plain, '^([+-]?%d+%.?%d*)()', i)
		if num then
			toks[#toks + 1] = num
			i = ni
		else
			local hem, hi = string.match(plain, '^([NSEWnsew])()', i)
			if hem then
				toks[#toks + 1] = hem:upper()
				i = hi
			else
				i = i + 1
			end
		end
	end
	local nsAt, ewAt
	for idx, tok in ipairs(toks) do
		if tok == 'N' or tok == 'S' then nsAt = idx end
		if tok == 'E' or tok == 'W' then ewAt = idx end
	end
	if nsAt and ewAt then
		local latParts, lonParts = {}, {}
		for idx = nsAt - 1, 1, -1 do
			if tonumber(toks[idx]) then
				table.insert(latParts, 1, tonumber(toks[idx]))
				if #latParts == 3 then break end
			else
				break
			end
		end
		for idx = ewAt - 1, nsAt + 1, -1 do
			if tonumber(toks[idx]) then
				table.insert(lonParts, 1, tonumber(toks[idx]))
				if #lonParts == 3 then break end
			else
				break
			end
		end
		if #latParts >= 1 and #lonParts >= 1 then
			local lat = latParts[1] + (latParts[2] or 0) / 60 + (latParts[3] or 0) / 3600
			local lon = lonParts[1] + (lonParts[2] or 0) / 60 + (lonParts[3] or 0) / 3600
			return applyHemisphere(lat, toks[nsAt], 'latitude'),
				applyHemisphere(lon, toks[ewAt], 'longitude')
		end
	end

	return nil, nil
end

local function coord2text(para, coord)
	if type(coord) ~= 'string' or coord == '' then
		return error('Malformed coordinates value', 2)
	end
	coord = string.gsub(coord, '&#123;', '{')
	coord = string.gsub(coord, '&#125;', '}')
	coord = string.gsub(coord, '&lt;', '<')
	coord = string.gsub(coord, '&gt;', '>')
	local ok, lat, lon = pcall(parseLatLon, coord)
	if not ok then
		lat, lon = nil, nil
	end
	if not lat or not lon then
		local frame = mw.getCurrentFrame()
		if frame and string.find(coord, '{{', 1, true) then
			local okp, expanded = pcall(function() return frame:preprocess(coord) end)
			if okp and expanded then
				ok, lat, lon = pcall(parseLatLon, expanded)
				if not ok then
					lat, lon = nil, nil
				end
			end
		end
	end
	if lat and lon then
		return para == 'longitude' and lon or lat
	end
	mw.log('Malformed coordinates value')
	mw.logObject(para, 'para')
	mw.logObject(coord, 'coord')
	return nil
end

-- effectively make removeBlanks false for caption and maplink, and true for everything else
-- if useWikidata is present but blank, convert it to false instead of nil
-- p.top, p.bottom, and their callers need to use this
function p.valueFunc(key, value)
	if value then
		value = mw.text.trim(value)
	end
	if value ~= '' or key == 'caption' or key == 'maplink' then
		return value
	end
end

local function getContainerImage(args, map)
	if args.AlternativeMap then
		return args.AlternativeMap
	elseif args.relief then
        local digits = mw.ustring.match(args.relief,'^[1-9][0-9]?$') -- image1 to image99
        if digits then
			local imageDigits = map('image' .. digits)
			if imageDigits ~= '' then return imageDigits end
        end
		local yesNo = require('Module:Yesno')
		if yesNo(args.relief,true) then
			local image1 = map('image1')
			if image1 ~= '' then return image1 end
		end
	end
    return map('image')
end

function p.top(frame, args, map)
	if not args then
		args = getArgs(frame, {frameOnly = true, valueFunc = p.valueFunc})
	end
	if not map then
		map = p.getMapParams(args[1], frame)
	end
	local width
	local default_as_number = tonumber(mw.ustring.match(tostring(args.default_width),"%d*"))
	if not args.width then
		width = round((default_as_number or 240) * (tonumber(map('defaultscale')) or 1))
	elseif mw.ustring.sub(args.width, -2) == 'px' then
		width = mw.ustring.sub(args.width, 1, -3)
	else
		width = args.width
	end
	local width_as_number = tonumber(mw.ustring.match(tostring(width),"%d*")) or 0;
    if width_as_number == 0 then
    	-- check to see if width is junk. If it is, then use default calculation
    	width = round((default_as_number or 240) * (tonumber(map('defaultscale')) or 1))
    	width_as_number = tonumber(mw.ustring.match(tostring(width),"%d*")) or 0;
    end
    if args.max_width ~= "" and args.max_width ~= nil then
        -- check to see if width bigger than max_width
        local max_as_number = tonumber(mw.ustring.match(args.max_width,"%d*")) or 0;
        if width_as_number>max_as_number and max_as_number>0 then
            width = args.max_width;
        end
    end
	local retval = frame:extensionTag{name = 'templatestyles', args = {src = 'Module:Location map/styles.css'}}
	if args.float == 'center' then
		retval = retval .. '<div class="center">'
	end
	if args.caption and args.caption ~= '' and args.border ~= 'infobox' then
		retval = retval .. '<div class="locmap noresize thumb '
		if args.float == '"left"' or args.float == 'left' then
			retval = retval .. 'floatleft'
		elseif args.float == '"center"' or args.float == 'center' or args.float == '"none"' or args.float == 'none' then
			retval = retval .. 'tnone'
		else
			retval = retval .. 'floatright'
		end
		retval = retval .. '"><div class="thumbinner" style="width:' .. (width + 2) .. 'px'
		if args.border == 'none' then
			retval = retval .. ';border:none'
		elseif args.border then
			retval = retval .. ';border-color:' .. args.border
		end
		retval = retval .. '"><div style="position:relative;width:' .. width .. 'px' .. (args.border ~= 'none' and ';border:1px solid lightgray">' or '">')
	else
		retval = retval .. '<div class="locmap" style="width:' .. width .. 'px;'
		if args.float == '"left"' or args.float == 'left' then
			retval = retval .. 'float:left;clear:left'
		elseif args.float == '"center"' or args.float == 'center' then
			retval = retval .. 'float:none;clear:both;margin-left:auto;margin-right:auto'
		elseif args.float == '"none"' or args.float == 'none' then
			retval = retval .. 'float:none;clear:none'
		else
			retval = retval .. 'float:right;clear:right'
		end
		retval = retval .. '"><div style="width:' .. width .. 'px;padding:0"><div style="position:relative;width:' .. width .. 'px">'
	end
	local image = getContainerImage(args, map)
	local currentTitle = mw.title.getCurrentTitle()
	retval = string.format(
		'%s[[File:%s|%spx|%s%s|class=notpageimage noviewer]]',
		retval,
		image,
		width,
		args.alt or ((args.label or currentTitle.text) .. ' is located in ' .. map('name')),
		args.maplink and ('|link=' .. args.maplink) or ''
	)
	if args.caption and args.caption ~= '' then
		if (currentTitle.namespace == 0) and mw.ustring.find(args.caption, '##') then
			retval = retval .. '[[Category:Pages using location map with a double number sign in the caption]]'
		end
	end
	if args.overlay_image then
		return retval .. '<div style="position:absolute;top:0;left:0">[[File:' .. args.overlay_image .. '|' .. width .. 'px|class=notpageimage noviewer]]</div>'
	else
		return retval
	end
end

function p.bottom(frame, args, map)
	if not args then
		args = getArgs(frame, {frameOnly = true, valueFunc = p.valueFunc})
	end
	if not map then
		map = p.getMapParams(args[1], frame)
	end
	local retval = '</div>'
	local currentTitle = mw.title.getCurrentTitle()
	if not args.caption or args.border == 'infobox' then
		if args.border then
			retval = retval .. '<div style="padding-top:0.2em">'
		else
			retval = retval .. '<div style="font-size:91%;padding-top:3px">'
		end
		retval = retval
		.. (args.caption or (args.label or currentTitle.text) .. ' (' .. map('name') .. ')')
		.. '</div>'
	elseif args.caption ~= ''  then
		-- This is not the pipe trick. We're creating a link with no text on purpose, so that CSS can give us a nice image
		retval = retval .. '<div class="thumbcaption"><div class="magnify">[[:File:' .. getContainerImage(args, map) .. '| ]]</div>' .. args.caption .. '</div>'
	end
	if args.switcherLabel then
		retval = retval .. '<span class="switcher-label" style="display:none">' .. args.switcherLabel .. '</span>'
	elseif args.autoSwitcherLabel then
		retval = retval .. '<span class="switcher-label" style="display:none">Show map of ' .. map('name') .. '</span>'
	end

	retval = retval .. '</div></div>'
	if args.caption_undefined then
		mw.log('Removed parameter caption_undefined used.')
		local parent = frame:getParent()
		if parent then
			mw.log('Parent is ' .. parent:getTitle())
		end
		mw.logObject(args, 'args')
		if currentTitle.namespace == 0 then
		    retval = retval .. '[[Category:Location maps with removed parameters|caption_undefined]]'
		end
	end
	if map('skew') ~= '' or map('lat_skew') ~= '' or map('crosses180') ~= '' or map('type') ~= '' then
		mw.log('Removed parameter used in map definition ' .. map())
		if currentTitle.namespace == 0 then
		    local key = (map('skew') ~= '' and 'skew' or '') ..
					(map('lat_skew') ~= '' and 'lat_skew' or '') ..
					(map('crosses180') ~= '' and 'crosses180' or '') ..
					(map('type') ~= '' and 'type' or '')
		    retval = retval .. '[[Category:Location maps with removed parameters|' .. key .. ' ]]'
		end
	end
	if string.find(map('name'), '|', 1, true) then
		mw.log('Pipe used in name of map definition ' .. map())
		if currentTitle.namespace == 0 then
		   retval = retval .. '[[Category:Location maps with a name containing a pipe]]'
		end
	end
	if args.float == 'center' then
		retval = retval .. '</div>'
	end
	return retval
end

local function markOuterDiv(x, y, imageDiv, labelDiv, label_size)
	return mw.html.create('div')
		:addClass('od')
		:addClass('notheme') -- T236137
		:cssText('top:' .. round(y, 3) .. '%;left:' .. round(x, 3) .. '%;font-size:' .. label_size .. '%')
		:node(imageDiv)
		:node(labelDiv)
end

local function markImageDiv(mark, marksize, label, link, alt, title)
	local builder = mw.html.create('div')
		:addClass('id')
		:cssText('left:-' .. round(marksize / 2) .. 'px;top:-' .. round(marksize / 2) .. 'px')
		:attr('title', title)
	if marksize ~= 0 then
		builder:wikitext(string.format(
			'[[File:%s|%dx%dpx|%s|link=%s%s|class=notpageimage noviewer]]',
			mark,
			marksize,
			marksize,
			label,
			link,
			alt and ('|alt=' .. alt) or ''
		))
	end
	return builder
end

local function markLabelDiv(label, label_size, label_width, position, background, x, marksize)
	if tonumber(label_size) == 0 then
		return mw.html.create('div'):addClass('l0'):wikitext(label)
	end
	local builder = mw.html.create('div')
		:cssText('width:' .. label_width .. 'em')
	local distance = round(marksize / 2 + 1)
	if position == 'top' then -- specified top
		builder:addClass('pv'):cssText('bottom:' .. distance .. 'px;left:' .. (-label_width / 2) .. 'em')
	elseif position == 'bottom' then -- specified bottom
		builder:addClass('pv'):cssText('top:' .. distance .. 'px;left:' .. (-label_width / 2) .. 'em')
	elseif position == 'left' or (tonumber(x) > 70 and position ~= 'right') then -- specified left or autodetected to left
		builder:addClass('pl'):cssText('right:' .. distance .. 'px')
	else -- specified right or autodetected to right
		builder:addClass('pr'):cssText('left:' .. distance .. 'px')
	end
	builder = builder:tag('div')
		:wikitext(label)
	if background then
		builder:cssText('background-color:' .. background)
	end
	return builder:done()
end

local function getX(longitude, left, right)
	local width = (right - left) % 360
	if width == 0 then
		width = 360
	end
	local distanceFromLeft = (longitude - left) % 360
	-- the distance needed past the map to the right equals distanceFromLeft - width. the distance needed past the map to the left equals 360 - distanceFromLeft. to minimize page stretching, go whichever way is shorter
	if distanceFromLeft - width / 2 >= 180 then
		distanceFromLeft = distanceFromLeft - 360
	end
	return 100 * distanceFromLeft / width
end

local function getY(latitude, top, bottom)
	return 100 * (top - latitude) / (top - bottom)
end

function p.mark(frame, args, map)
	if not args then
		args = getArgs(frame, {wrappers = 'Template:Location map~'})
	end
	local mapnames = {}
	if not map then
		if args[1] then
			map = {}
			for mapname in mw.text.gsplit(args[1], '#', true) do
				map[#map + 1] = p.getMapParams(mw.ustring.gsub(mapname, '^%s*(.-)%s*$', '%1'), frame)
				mapnames[#mapnames + 1] = mapname
			end
			if #map == 1 then map = map[1] end
		else
			map = p.getMapParams('World', frame)
			args[1] = 'World'
		end
	end
	if type(map) == 'table' then
		local outputs = {}
		local oldargs = args[1]
		for k,v in ipairs(map) do
			args[1] = mapnames[k]
			outputs[k] = tostring(p.mark(frame, args, v))
		end
		args[1] = oldargs
		return table.concat(outputs, '#PlaceList#') .. '#PlaceList#'
	end
	local x, y, longitude, latitude
	longitude = decdeg(args.lon_deg, args.lon_min, args.lon_sec, args.lon_dir, args.long, 'longitude')
	latitude = decdeg(args.lat_deg, args.lat_min, args.lat_sec, args.lat_dir, args.lat, 'latitude')
	if args.excludefrom then
		-- If this mark is to be excluded from certain maps entirely (useful in the context of multiple maps)
		for exclusionmap in mw.text.gsplit(args.excludefrom, '#', true) do
			-- Check if this map is excluded. If so, return an empty string.
			if args[1] == exclusionmap then
				return ''
			end
		end
	end
	local builder = mw.html.create()
	local currentTitle = mw.title.getCurrentTitle()
	if args.coordinates then
		local okLon, lonVal = pcall(coord2text, 'longitude', args.coordinates)
		local okLat, latVal = pcall(coord2text, 'latitude', args.coordinates)
		if okLon then longitude = lonVal or longitude end
		if okLat then latitude = latVal or latitude end
	end
	if not longitude or not latitude then
		-- Broken or empty coordinates: omit the pin instead of erroring.
		if currentTitle.namespace == 0 then
			builder:wikitext('[[Category:Location maps with malformed coordinates]]')
		end
		return builder
	end
	if currentTitle.namespace > 0 then
		if (not args.lon_deg) ~= (not args.lat_deg) then
			builder:wikitext('[[Category:Location maps with different longitude and latitude precisions|Degrees]]')
		elseif (not args.lon_min) ~= (not args.lat_min) then
			builder:wikitext('[[Category:Location maps with different longitude and latitude precisions|Minutes]]')
		elseif (not args.lon_sec) ~= (not args.lat_sec) then
			builder:wikitext('[[Category:Location maps with different longitude and latitude precisions|Seconds]]')
		elseif (not args.lon_dir) ~= (not args.lat_dir) then
			builder:wikitext('[[Category:Location maps with different longitude and latitude precisions|Hemisphere]]')
		elseif (not args.long) ~= (not args.lat) then
			builder:wikitext('[[Category:Location maps with different longitude and latitude precisions|Decimal]]')
		end
	end
	if ((tonumber(args.lat_deg) or 0) < 0) and ((tonumber(args.lat_min) or 0) ~= 0 or (tonumber(args.lat_sec) or 0) ~= 0 or (args.lat_dir and args.lat_dir ~='')) then
		builder:wikitext('[[Category:Location maps with negative degrees and minutes or seconds]]')
	end
	if ((tonumber(args.lon_deg) or 0) < 0) and ((tonumber(args.lon_min) or 0) ~= 0 or (tonumber(args.lon_sec) or 0) ~= 0 or (args.lon_dir and args.lon_dir ~= '')) then
		builder:wikitext('[[Category:Location maps with negative degrees and minutes or seconds]]')
	end
	if (((tonumber(args.lat_min) or 0) < 0) or ((tonumber(args.lat_sec) or 0) < 0)) then
		builder:wikitext('[[Category:Location maps with negative degrees and minutes or seconds]]')
	end
	if (((tonumber(args.lon_min) or 0) < 0) or ((tonumber(args.lon_sec) or 0) < 0)) then
		builder:wikitext('[[Category:Location maps with negative degrees and minutes or seconds]]')
	end
	if args.skew or args.lon_shift or args.markhigh then
		mw.log('Removed parameter used in invocation.')
		local parent = frame:getParent()
		if parent then
			mw.log('Parent is ' .. parent:getTitle())
		end
		mw.logObject(args, 'args')
		if currentTitle.namespace == 0 then
			local key = (args.skew and 'skew' or '') ..
						(args.lon_shift and 'lon_shift' or '') ..
						(args.markhigh and 'markhigh' or '')
			builder:wikitext('[[Category:Location maps with removed parameters|' .. key ..' ]]')
		end
	end
	if map('x') ~= '' then
		x = tonumber(mw.ext.ParserFunctions.expr(map('x', { latitude, longitude })))
	else
		x = tonumber(getX(longitude, map('left'), map('right')))
	end
	if map('y') ~= '' then
		y = tonumber(mw.ext.ParserFunctions.expr(map('y', { latitude, longitude })))
	else
		y = tonumber(getY(latitude, map('top'), map('bottom')))
	end
	if (x < 0 or x > 100 or y < 0 or y > 100) and not args.outside then
		mw.log('Mark placed outside map boundaries without outside flag set. x = ' .. x .. ', y = ' .. y)
		local parent = frame:getParent()
		if parent then
			mw.log('Parent is ' .. parent:getTitle())
		end
		mw.logObject(args, 'args')
		if currentTitle.namespace == 0 then
			local key = currentTitle.prefixedText
			builder:wikitext('[[Category:Location maps with marks outside map and outside parameter not set|' .. key .. ' ]]')
		end
	end
	local mark = args.mark or map('mark')
	if mark == '' then
		mark = 'Red pog.svg'
	end
	local marksize = tonumber(args.marksize) or tonumber(map('marksize')) or 8
	local imageDiv = markImageDiv(mark, marksize, args.label or mw.title.getCurrentTitle().text, args.link or '', args.alt, args[2])
	local label_size = args.label_size or 91
	local labelDiv
	if args.label and args.position ~= 'none' then
		labelDiv = markLabelDiv(args.label, label_size, args.label_width or 6, args.position, args.background, x, marksize)
	end
	return builder:node(markOuterDiv(x, y, imageDiv, labelDiv, label_size))
end

local function switcherSeparate(s)
	if s == nil then return {} end
	local retval = {}
	for i in string.gmatch(s .. '#', '([^#]*)#') do
		i = mw.text.trim(i)
		retval[#retval + 1] = (i ~= '' and i)
	end
	return retval
end

function p.infobox_pushpin_map(frame)
	--This function is designed to be invoked from an infobox template
	--* Some parameters (generally starting with "pushpin_") are meant to be exposed as parameters
	--   in the outer infobox, settable by editors
	--* Others are meant to be "hidden" (e.g., only set inside the infobox template code)
	-- First get the combined arguments from both parent and invocation frame
	local raw_args = getArgs(frame,{parentFirst=true})

	--If no map is supplied, return.
	if not raw_args['pushpin_map'] then
		return
	end
	-- Now get arguments from just the invocation frame
	local frame_args = getArgs(frame,{frameOnly=true})

	-- parsed_args will have the resulting merged parameters
	local parsed_args = {}
	-- Parse the arguments
	parsed_args[1] = raw_args['pushpin_map']
	parsed_args.float = frame_args.float or 'center'
	parsed_args.border = frame_args.border or 'infobox'
	parsed_args.default_width = frame_args.default_width
	parsed_args.max_width = frame_args.max_width
	parsed_args.coordinates = raw_args['coordinates']
	parsed_args.caption = raw_args['pushpin_caption']
	parsed_args.relief = raw_args['pushpin_relief']
	parsed_args.label = raw_args['pushpin_label']
	parsed_args.label_size = raw_args['pushpin_label_size']
	parsed_args.position = raw_args['pushpin_label_position']
	parsed_args.mark = raw_args['pushpin_mark']
	parsed_args.marksize = raw_args['pushpin_mark_size']
	parsed_args.alt = raw_args['pushpin_alt']
	parsed_args.background = raw_args['pushpin_background']
	parsed_args.width = raw_args['pushpin_map_size']
	local map = {}
	local caption_list = {}
	for mapname in string.gmatch(parsed_args[1], '[^#]+') do
		map[#map + 1] = p.getMapParams(mw.ustring.gsub(mapname, '^%s*(.-)%s*$', '%1'), frame)
	end

	if parsed_args['caption'] then
		for caption in mw.text.gsplit(parsed_args['caption'], '##', true) do
			caption_list[#caption_list + 1] = caption
		end
	end

	if #map == 1 then
		map = map[1]
		if not parsed_args.caption then
			parsed_args.caption = 'Location within ' .. map('name')
		end
	end
	if type(map) == 'table' then
		mw.log('----')
		mw.logObject(map)
		if #caption_list > #map then
			error(string.format('%d captions were provided, but only %d maps were provided', #caption_list, #map))
		elseif not parsed_args.caption then
			-- Set a default caption for each of the multiple maps
			for i=1, #map do
				caption_list[#caption_list + 1] = 'Location within '.. map[i]('name')
			end
		end

		local outputs = {}
		parsed_args.autoSwitcherLabel = true
		for k,v in ipairs(map) do
			parsed_args.caption = caption_list[k]
			outputs[k] = p.main(frame, parsed_args, v)
		end
		return '<div class="switcher-container">' .. table.concat(outputs) .. '</div>'
	else
		return p.top(frame, parsed_args, map) .. tostring( p.mark(frame, parsed_args, map) ) .. p.bottom(frame, parsed_args, map)
	end
end

function p.main(frame, args, map)
	local caption_list = {}
	if not args then
		args = getArgs(frame, {wrappers = 'Template:Location map', valueFunc = p.valueFunc})
	end
	if not map then
		if args[1] then
			map = {}
			for mapname in string.gmatch(args[1], '[^#]+') do
				map[#map + 1] = p.getMapParams(mw.ustring.gsub(mapname, '^%s*(.-)%s*$', '%1'), frame)
			end
			if args['caption'] then
				if args['caption'] == "" then
					while #caption_list < #map do
						caption_list[#caption_list + 1] = args['caption']
					end
				else
					for caption in mw.text.gsplit(args['caption'], '##', true) do
						caption_list[#caption_list + 1] = caption
					end
				end
			end
			if #map == 1 then map = map[1] end
		else
			map = p.getMapParams('World', frame)
		end
	end
	if type(map) == 'table' then
		local altmaps = switcherSeparate(args.AlternativeMap)
		if #altmaps > #map then
			error(string.format('%d AlternativeMaps were provided, but only %d maps were provided', #altmaps, #map))
		end
		local overlays = switcherSeparate(args.overlay_image)
		if #overlays > #map then
			error(string.format('%d overlay_images were provided, but only %d maps were provided', #overlays, #map))
		end
		if #caption_list > #map then
			error(string.format('%d captions were provided, but only %d maps were provided', #caption_list, #map))
		end
		local outputs = {}
		args.autoSwitcherLabel = true
		for k,v in ipairs(map) do
			args.AlternativeMap = altmaps[k]
			args.overlay_image = overlays[k]
			args.caption = caption_list[k]
			outputs[k] = p.main(frame, args, v)
		end
		return '<div class="switcher-container">' .. table.concat(outputs) .. '</div>'
	else
		return p.top(frame, args, map) .. tostring( p.mark(frame, args, map) ) .. p.bottom(frame, args, map)
	end
end

return p