This module implements the {{Location map}} and {{Location map~}} templates. Please see the template pages for usage instructions.
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.
require('strict')local p = {}local getArgs = require('Module:Arguments').getArgslocal function round(n, decimals)local pow = 10^(decimals or 0)return math.floor(n * pow + 0.5) / powendfunction p.getMapParams(map, frame)if not map thenerror('The name of the location map definition to use must be specified', 2)endlocal moduletitle = mw.title.new('Module:Location map/data/' .. map)if not moduletitle thenerror(string.format('%q is not a valid name for a location map definition', map), 2)elseif moduletitle.exists thenlocal mapData = mw.loadData('Module:Location map/data/' .. map)return function(name, params)if name == nil thenreturn 'Module:Location map/data/' .. mapelseif mapData[name] == nil thenreturn ''elseif params thenreturn mw.message.newRawMessage(tostring(mapData[name]), unpack(params)):plain()elsereturn mapData[name]endendelseerror('Unable to find the specified location map definition: "Module:Location map/data/' .. map .. '" does not exist', 2)endendfunction p.data(frame, args, map)if not args thenargs = getArgs(frame, {frameOnly = true})endif not map thenmap = p.getMapParams(args[1], frame)endlocal params = {}for k,v in ipairs(args) doif k > 2 thenparams[k-2] = vendendreturn map(args[2], #params ~= 0 and params)endlocal 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 thenif degrees thenerror('Decimal and DMS degrees cannot both be provided for ' .. direction, 2)elseif minutes thenerror('Minutes can only be provided with DMS degrees for ' .. direction, 2)elseif seconds thenerror('Seconds can only be provided with DMS degrees for ' .. direction, 2)elseif hemisphere thenerror('A hemisphere can only be provided with DMS degrees for ' .. direction, 2)endlocal retval = tonumber(decimal)if retval thenreturn retvalenderror('The value "' .. decimal .. '" provided for ' .. direction .. ' is not valid', 2)elseif seconds and not minutes thenerror('Seconds were provided for ' .. direction .. ' without minutes also being provided', 2)elseif not degrees thenif minutes thenerror('Minutes were provided for ' .. direction .. ' without degrees also being provided', 2)elseif hemisphere thenerror('A hemisphere was provided for ' .. direction .. ' without degrees also being provided', 2)endreturn nilenddecimal = tonumber(degrees)if not decimal thenerror('The degree value "' .. degrees .. '" provided for ' .. direction .. ' is not valid', 2)elseif minutes and not tonumber(minutes) thenerror('The minute value "' .. minutes .. '" provided for ' .. direction .. ' is not valid', 2)elseif seconds and not tonumber(seconds) thenerror('The second value "' .. seconds .. '" provided for ' .. direction .. ' is not valid', 2)enddecimal = decimal + (minutes or 0)/60 + (seconds or 0)/3600if hemisphere thenlocal multiplier = hemisphereMultipliers[direction][hemisphere]if not multiplier thenerror('The hemisphere "' .. hemisphere .. '" provided for ' .. direction .. ' is not valid', 2)enddecimal = decimal * multiplierendreturn decimalend-- Finds a parameter in a transclusion of {{Coord}}.local function coord2text(para,coord) -- this should be changed for languages which do not use Arabic numerals or the degree signlocal lat, long = mw.ustring.match(coord,'<span class="p%-latitude latitude">([^<]+)</span><span class="p%-longitude longitude">([^<]+)</span>')if lat thenreturn tonumber(para == 'longitude' and long or lat)endlocal result = mw.text.split(mw.ustring.match(coord,'%-?[%.%d]+°[NS] %-?[%.%d]+°[EW]') or '', '[ °]')if para == 'longitude' then result = {result[3], result[4]} endif not tonumber(result[1]) or not result[2] thenmw.log('Malformed coordinates value')mw.logObject(para, 'para')mw.logObject(coord, 'coord')return error('Malformed coordinates value', 2)endreturn tonumber(result[1]) * hemisphereMultipliers[para][result[2]]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 thisfunction p.valueFunc(key, value)if value thenvalue = mw.text.trim(value)endif value ~= '' or key == 'caption' or key == 'maplink' thenreturn valueelseif key == 'useWikidata' thenreturn falseendendlocal function getContainerImage(args, map)if args.AlternativeMap thenreturn args.AlternativeMapelseif args.relief then local digits = mw.ustring.match(args.relief,'^[1-9][0-9]?$') or '1' -- image1 to image99 if map('image' .. digits) ~= '' then return map('image' .. digits) endend return map('image')endfunction p.top(frame, args, map)if not args thenargs = getArgs(frame, {frameOnly = true, valueFunc = p.valueFunc})endif not map thenmap = p.getMapParams(args[1], frame)endlocal widthlocal default_as_number = tonumber(mw.ustring.match(tostring(args.default_width),"%d*"))if not args.width thenwidth = round((default_as_number or 240) * (tonumber(map('defaultscale')) or 1))elseif mw.ustring.sub(args.width, -2) == 'px' thenwidth = mw.ustring.sub(args.width, 1, -3)elsewidth = args.widthendlocal 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 endlocal retval = frame:extensionTag{name = 'templatestyles', args = {src = 'Module:Location map/sandbox/styles.css'}}if args.float == 'center' thenretval = retval .. '<div class="center">'endif args.caption and args.caption ~= '' and args.border ~= 'infobox' thenretval = retval .. '<div class="locmap noviewer' .. (args.noresize ~= false and ' noresize' or '') .. ' thumb 'if args.float == '"left"' or args.float == 'left' thenretval = retval .. 'tleft'elseif args.float == '"center"' or args.float == 'center' or args.float == '"none"' or args.float == 'none' thenretval = retval .. 'tnone'elseretval = retval .. 'tright'endretval = retval .. '"><div class="thumbinner" style="width:' .. (width + 2) .. 'px'if args.border == 'none' thenretval = retval .. ';border:none'elseif args.border thenretval = retval .. ';border-color:' .. args.borderendretval = retval .. '"><div style="position:relative;width:' .. width .. 'px' .. (args.border ~= 'none' and ';border:1px solid lightgray">' or '">')elseretval = retval .. '<div class="locmap" style="width:' .. width .. 'px;'if args.float == '"left"' or args.float == 'left' thenretval = retval .. 'float:left;clear:left'elseif args.float == '"center"' or args.float == 'center' thenretval = retval .. 'float:none;clear:both;margin-left:auto;margin-right:auto'elseif args.float == '"none"' or args.float == 'none' thenretval = retval .. 'float:none;clear:none'elseretval = retval .. 'float:right;clear:right'endretval = retval .. '"><div style="width:' .. width .. 'px;padding:0"><div style="position:relative;width:' .. width .. 'px">'endlocal image = getContainerImage(args, map)local currentTitle = mw.title.getCurrentTitle()retval = string.format('%s[[File:%s|%spx|%s%s|class=notpageimage]]',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 ~= '' thenif (currentTitle.namespace == 0) and mw.ustring.find(args.caption, '##') thenretval = retval .. '[[Category:Pages using location map with a double number sign in the caption]]'endendif args.overlay_image thenreturn retval .. '<div style="position:absolute;top:0;left:0">[[File:' .. args.overlay_image .. '|' .. width .. 'px|class=notpageimage]]</div>'elsereturn retvalendendfunction p.bottom(frame, args, map)if not args thenargs = getArgs(frame, {frameOnly = true, valueFunc = p.valueFunc})endif not map thenmap = p.getMapParams(args[1], frame)endlocal retval = '</div>'local currentTitle = mw.title.getCurrentTitle()if not args.caption or args.border == 'infobox' thenif args.border thenretval = retval .. '<div style="padding-top:0.2em">'elseretval = retval .. '<div style="font-size:91%;padding-top:3px">'endretval = 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 imageretval = retval .. '<div class="thumbcaption"><div class="magnify">[[:File:' .. getContainerImage(args, map) .. '|class=notpageimage| ]]</div>' .. args.caption .. '</div>'endif args.switcherLabel thenretval = retval .. '<span class="switcher-label" style="display:none">' .. args.switcherLabel .. '</span>'elseif args.autoSwitcherLabel thenretval = retval .. '<span class="switcher-label" style="display:none">Show map of ' .. map('name') .. '</span>'endretval = retval .. '</div></div>'if args.caption_undefined thenmw.log('Removed parameter caption_undefined used.')local parent = frame:getParent()if parent thenmw.log('Parent is ' .. parent:getTitle())endmw.logObject(args, 'args')if currentTitle.namespace == 0 then retval = retval .. '[[Category:Location maps with removed parameters|caption_undefined]]'endendif map('skew') ~= '' or map('lat_skew') ~= '' or map('crosses180') ~= '' or map('type') ~= '' thenmw.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 .. ' ]]'endendif string.find(map('name'), '|', 1, true) thenmw.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]]'endendif args.float == 'center' thenretval = retval .. '</div>'endreturn retvalendlocal function markOuterDiv(x, y, imageDiv, labelDiv)return mw.html.create('div'):addClass('od'):addClass('notheme') -- T236137:cssText('top:' .. round(y, 3) .. '%;left:' .. round(x, 3) .. '%'):node(imageDiv):node(labelDiv)endlocal 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 thenbuilder:wikitext(string.format('[[File:%s|%dx%dpx|%s|link=%s%s|class=notpageimage]]',mark,marksize,marksize,label,link,alt and ('|alt=' .. alt) or ''))endreturn builderendlocal function markLabelDiv(label, label_size, label_width, position, background, x, marksize, top, compact)if tonumber(label_size) == 0 thenreturn mw.html.create('div'):addClass('l0'):wikitext(label)endlocal builder = mw.html.create('div'):cssText('font-size:' .. label_size .. '%;width:' .. label_width .. 'em')local distance = round(marksize / 2 + 1)if position == 'top' then -- specified topbuilder:addClass(compact and 'pvc' or 'pv'):cssText('bottom:' .. distance .. 'px;left:' .. (-label_width / 2) .. 'em')elseif position == 'bottom' then -- specified bottombuilder:addClass(compact and 'pvc' or '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 leftbuilder:addClass(compact and 'plc' or 'pl'):cssText('right:' .. distance .. 'px')else -- specified right or autodetected to rightbuilder:addClass(compact and 'prc' or 'pr'):cssText('left:' .. distance .. 'px')endif top thenbuilder:cssText('top:' .. top .. 'em')endendbuilder = builder:tag('div') -- not 'span' to avoid Linter errors if the label contains block content:wikitext(label)if background thenbuilder:cssText('background-color:' .. background)endreturn builder:done()endlocal function getX(longitude, left, right)local width = (right - left) % 360if width == 0 thenwidth = 360endlocal 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 shorterif distanceFromLeft - width / 2 >= 180 thendistanceFromLeft = distanceFromLeft - 360endreturn 100 * distanceFromLeft / widthendlocal function getY(latitude, top, bottom)return 100 * (top - latitude) / (top - bottom)endfunction p.mark(frame, args, map, ...)if not args thenargs = getArgs(frame, {wrappers = 'Template:Location map~'})endlocal mapnames = {}if not map thenif args[1] thenmap = {}for mapname in mw.text.gsplit(args[1], '#', true) domap[#map + 1] = p.getMapParams(mw.ustring.gsub(mapname, '^%s*(.-)%s*$', '%1'), frame)mapnames[#mapnames + 1] = mapnameendif #map == 1 then map = map[1] endelsemap = p.getMapParams('World', frame)args[1] = 'World'endendif type(map) == 'table' thenlocal outputs = {}local oldargs = args[1]for k,v in ipairs(map) doargs[1] = mapnames[k]outputs[k] = tostring(p.mark(frame, args, v))endargs[1] = oldargsreturn table.concat(outputs, '#PlaceList#') .. '#PlaceList#'endlocal containerArgs = ...local x, y, longitude, latitudelongitude = 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 thenreturn ''endendendlocal builder = mw.html.create()local currentTitle = mw.title.getCurrentTitle()if args.coordinates then--Temporarily removed to facilitate infobox conversion. See [[Wikipedia:Coordinates in infoboxes]]--if longitude or latitude then--error('Coordinates from [[Module:Coordinates]] and individual coordinates cannot both be provided')--endlongitude = coord2text('longitude', args.coordinates)latitude = coord2text('latitude', args.coordinates)elseif not longitude and not latitude and args.useWikidata then-- If they didn't provide either coordinate, try Wikidata. If they provided one but not the other, don't.local entity = mw.wikibase.getEntity()if entity and entity.claims and entity.claims.P625 and entity.claims.P625[1].mainsnak.snaktype == 'value' thenlocal value = entity.claims.P625[1].mainsnak.datavalue.valuelongitude, latitude = value.longitude, value.latitudeendif args.link and (currentTitle.namespace == 0) thenbuilder:wikitext('[[Category:Location maps with linked markers with coordinates from Wikidata]]')endendif not longitude thenerror('No value was provided for longitude')elseif not latitude thenerror('No value was provided for latitude')endif currentTitle.namespace > 0 thenif (not args.lon_deg) ~= (not args.lat_deg) thenbuilder:wikitext('[[Category:Location maps with different longitude and latitude precisions|Degrees]]')elseif (not args.lon_min) ~= (not args.lat_min) thenbuilder:wikitext('[[Category:Location maps with different longitude and latitude precisions|Minutes]]')elseif (not args.lon_sec) ~= (not args.lat_sec) thenbuilder:wikitext('[[Category:Location maps with different longitude and latitude precisions|Seconds]]')elseif (not args.lon_dir) ~= (not args.lat_dir) thenbuilder:wikitext('[[Category:Location maps with different longitude and latitude precisions|Hemisphere]]')elseif (not args.long) ~= (not args.lat) thenbuilder:wikitext('[[Category:Location maps with different longitude and latitude precisions|Decimal]]')endendif ((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 ~='')) thenbuilder:wikitext('[[Category:Location maps with negative degrees and minutes or seconds]]')endif ((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 ~= '')) thenbuilder:wikitext('[[Category:Location maps with negative degrees and minutes or seconds]]')endif (((tonumber(args.lat_min) or 0) < 0) or ((tonumber(args.lat_sec) or 0) < 0)) thenbuilder:wikitext('[[Category:Location maps with negative degrees and minutes or seconds]]')endif (((tonumber(args.lon_min) or 0) < 0) or ((tonumber(args.lon_sec) or 0) < 0)) thenbuilder:wikitext('[[Category:Location maps with negative degrees and minutes or seconds]]')endif args.skew or args.lon_shift or args.markhigh thenmw.log('Removed parameter used in invocation.')local parent = frame:getParent()if parent thenmw.log('Parent is ' .. parent:getTitle())endmw.logObject(args, 'args')if currentTitle.namespace == 0 thenlocal 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 ..' ]]')endendif map('x') ~= '' thenx = tonumber(mw.ext.ParserFunctions.expr(map('x', { latitude, longitude })))elsex = tonumber(getX(longitude, map('left'), map('right')))endif map('y') ~= '' theny = tonumber(mw.ext.ParserFunctions.expr(map('y', { latitude, longitude })))elsey = tonumber(getY(latitude, map('top'), map('bottom')))endif (x < 0 or x > 100 or y < 0 or y > 100) and not args.outside thenmw.log('Mark placed outside map boundaries without outside flag set. x = ' .. x .. ', y = ' .. y)local parent = frame:getParent()if parent thenmw.log('Parent is ' .. parent:getTitle())endmw.logObject(args, 'args')if currentTitle.namespace == 0 thenlocal key = currentTitle.prefixedTextbuilder:wikitext('[[Category:Location maps with marks outside map and outside parameter not set|' .. key .. ' ]]')endendlocal mark = args.mark or map('mark')if mark == '' thenmark = 'Red pog.svg'endlocal marksize = tonumber(args.marksize) or tonumber(map('marksize')) or 8local imageDiv = markImageDiv(mark, marksize, args.label or mw.title.getCurrentTitle().text, args.link or '', args.alt, args[2])local labelDivif args.label and args.position ~= 'none' thenlabelDiv = markLabelDiv(args.label, args.label_size or 91, args.label_width or 6, args.position, args.background, x, marksize, args.label_top, containerArgs and containerArgs.CompactLabels)endreturn builder:node(markOuterDiv(x, y, imageDiv, labelDiv))endlocal function switcherSeparate(s)if s == nil then return {} endlocal retval = {}for i in string.gmatch(s .. '#', '([^#]*)#') doi = mw.text.trim(i)retval[#retval + 1] = (i ~= '' and i)endreturn retvalendfunction p.main(frame, args, map)local caption_list = {}if not args thenargs = getArgs(frame, {wrappers = 'Template:Location map', valueFunc = p.valueFunc})endif args.useWikidata == nil thenargs.useWikidata = trueendif not map thenif args[1] thenmap = {}for mapname in string.gmatch(args[1], '[^#]+') domap[#map + 1] = p.getMapParams(mw.ustring.gsub(mapname, '^%s*(.-)%s*$', '%1'), frame)endif args['caption'] thenif args['caption'] == "" thenwhile #caption_list < #map docaption_list[#caption_list + 1] = args['caption']endelsefor caption in mw.text.gsplit(args['caption'], '##', true) docaption_list[#caption_list + 1] = captionendendendif #map == 1 then map = map[1] endelsemap = p.getMapParams('World', frame)endendif type(map) == 'table' thenlocal altmaps = switcherSeparate(args.AlternativeMap)if #altmaps > #map thenerror(string.format('%d AlternativeMaps were provided, but only %d maps were provided', #altmaps, #map))endlocal overlays = switcherSeparate(args.overlay_image)if #overlays > #map thenerror(string.format('%d overlay_images were provided, but only %d maps were provided', #overlays, #map))endif #caption_list > #map thenerror(string.format('%d captions were provided, but only %d maps were provided', #caption_list, #map))endlocal outputs = {}args.autoSwitcherLabel = truefor k,v in ipairs(map) doargs.AlternativeMap = altmaps[k]args.overlay_image = overlays[k]args.caption = caption_list[k]outputs[k] = p.main(frame, args, v)endreturn '<div class="switcher-container">' .. table.concat(outputs) .. '</div>'elsereturn p.top(frame, args, map) .. tostring( p.mark(frame, args, map) ) .. p.bottom(frame, args, map)endendreturn p