Documentation for this module may be created at Module:Infobox mapframe/doc
local mf = require('Module:Mapframe')
local getArgs = require('Module:Arguments').getArgs
local yesno = require('Module:Yesno')
local infoboxImage = require('Module:InfoboxImage').InfoboxImage
-- Defaults
local DEFAULT_FRAME_WIDTH = "270"
local DEFAULT_FRAME_HEIGHT = "200"
local DEFAULT_ZOOM = 10
local DEFAULT_GEOMASK_STROKE_WIDTH = "1"
local DEFAULT_GEOMASK_STROKE_COLOR = "#777777"
local DEFAULT_GEOMASK_FILL = "#888888"
local DEFAULT_GEOMASK_FILL_OPACITY = "0.25"
local DEFAULT_SHAPE_STROKE_WIDTH = "2"
local DEFAULT_SHAPE_STROKE_COLOR = "#FF0000"
local DEFAULT_SHAPE_FILL = "#606060"
local DEFAULT_SHAPE_FILL_OPACITY = "0.1"
local DEFAULT_LINE_STROKE_WIDTH = "5"
local DEFAULT_LINE_STROKE_COLOR = "#FF0000"
local DEFAULT_MARKER_COLOR = "#5E74F3"
local util = {}
function util.noop(info)
local DEFAULT_NOOP_OUTPUT = ""
-- Uncomment this when debugging
-- DEFAULT_NOOP_OUTPUT = "debug: mapframe no-op: " .. info
-- mw.log(DEFAULT_NOOP_OUTPUT)
return DEFAULT_NOOP_OUTPUT
end
-- Trim whitespace from args, and remove empty args
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
cleanArgs[key] = val
end
else
cleanArgs[key] = val
end
end
return cleanArgs
end
function util.shouldAutoRun(frame)
-- Check if should be running
local pargs = frame.getParent(frame).args
local explicitlyOn = yesno(
mw.text.trim(pargs.mapframe or "")
)
if pargs.coordinates == "{{{coordinates}}}" then
explicitlyOn = false
end
if type(pargs.coordinates) == 'string'
and string.match(pargs.coordinates, 'coord.missing') then
explicitlyOn = false
end
local onByDefault = (explicitlyOn == nil)
and yesno(
mw.text.trim(frame.args.onByDefault or ""),
false
)
return explicitlyOn or onByDefault
end
function util.argsFromAuto(frame)
-- Get args from the frame (invoke call) and the parent (template call).
-- Frame arguments are default values which are overridden by parent values
-- when both are present.
local args = getArgs(frame, {
parentFirst = true
})
-- Discard args not prefixed with "mapframe-", remove that prefix
-- from those that remain.
local fixedArgs = {}
for name, val in pairs(args) do
local fixedName = string.match(
name,
"^mapframe%-(.+)$"
)
if fixedName then
fixedArgs[fixedName] = val
-- Allow coord, coordinates, etc. to be unprefixed.
elseif (name == "coordinates"
or name == "coord"
or name == "coordinate")
and not fixedArgs.coord then
fixedArgs.coord = val
if not fixedArgs['frame-coord'] then
fixedArgs['frame-coord'] = val
end
-- Allow id/qid to be unprefixed.
-- Note: Bharatpedia does not resolve these through Wikibase.
elseif (name == "id" or name == "qid")
and not fixedArgs.id then
fixedArgs.id = val
-- Allow captionstyle to be unprefixed,
-- for compatibility with Module:Infobox.
elseif name == "captionstyle"
and not fixedArgs.captionstyle then
fixedArgs.captionstyle = val
end
end
-- Parent-frame fallback: Module:Infobox sometimes expands this invoke
-- in a context where named article parameters are only on the parent.
if not fixedArgs.coord then
local parent = frame:getParent()
if parent and parent.args then
local raw = parent.args.coordinates
or parent.args.coord
or parent.args.coordinate
or parent.args['mapframe-coord']
or parent.args['mapframe-coordinates']
if type(raw) == 'string' then
raw = mw.text.trim(raw)
if raw ~= '' then
fixedArgs.coord = raw
fixedArgs['frame-coord'] = fixedArgs['frame-coord'] or raw
end
end
end
end
return fixedArgs
end
function util.parseCustomWikitext(customWikitext)
-- infoboxImage will format an image if given wikitext containing
-- an image, or else pass through the wikitext unmodified.
return infoboxImage({
args = {
image = customWikitext
}
})
end
function util.trackAndWarn(trackingCat, warning)
local title = mw.title.getCurrentTitle()
local results =
title
and title.namespace == 0
and trackingCat
and '[[Category:' .. trackingCat .. ']]'
or ''
if warning and title and title.namespace ~= 0 then
results = results
.. '<strong class="error">Infobox mapframe: '
.. mw.text.nowiki(tostring(warning))
.. '</strong>'
end
return results
end
-- Turn any {{Coord}} wikitext or expanded HTML into decimal lat, lon.
function util.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
coord = string.gsub(coord, '\127[^\127]*\127', ' ')
if not pcall(mw.ustring.len, coord) then
coord = string.gsub(coord, '[\128-\255]', ' ')
end
local htmlLat = mw.ustring.match(coord, 'class%s*=%s*["\']latitude["\'][^>]*>([^<]+)')
local htmlLon = mw.ustring.match(coord, 'class%s*=%s*["\']longitude["\'][^>]*>([^<]+)')
local function hemMul(val, hem, kind)
val = tonumber(val)
if not val then return nil end
if hem == 'S' or hem == 's' then val = -math.abs(val) end
if hem == 'W' or hem == 'w' then val = -math.abs(val) end
if kind == 'lat' and (hem == 'N' or hem == 'n') then val = math.abs(val) end
if kind == 'lon' and (hem == 'E' or hem == 'e') then val = math.abs(val) end
return val
end
if htmlLat and htmlLon then
local a = mw.ustring.match(htmlLat, '([+-]?[%.%d]+)')
local b = mw.ustring.match(htmlLon, '([+-]?[%.%d]+)')
local ah = mw.ustring.match(htmlLat, '([NSns])')
local bh = mw.ustring.match(htmlLon, '([EWew])')
if a and b then
return hemMul(a, ah, 'lat'), hemMul(b, bh, 'lon')
end
end
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:match('^[NSWEnswe]$') then
hems[#hems + 1] = part:upper()
elseif 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 hemMul(lat, hems[1], 'lat'), hemMul(lon, hems[2], 'lon')
end
end
end
local plain = string.gsub(coord, '<[^>]*>', ' ')
plain = string.gsub(plain, ' ', ' ')
plain = string.gsub(plain, '%s+', ' ')
plain = mw.text.trim(plain)
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 hemMul(values[1], values[2], 'lat'), hemMul(values[3], values[4], 'lon')
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
local lat = tonumber(values[1]) + tonumber(values[2]) / 60 + tonumber(values[3]) / 3600
local lon = tonumber(values[5]) + tonumber(values[6]) / 60 + tonumber(values[7]) / 3600
return hemMul(lat, values[4], 'lat'), hemMul(lon, values[8], 'lon')
end
end
local a, ah, b, bh = mw.ustring.match(
plain,
'([+-]?[%.%d]+)%s*°?%s*([NSns])%D+([+-]?[%.%d]+)%s*°?%s*([EWew])'
)
if a and b then
return hemMul(a, ah, 'lat'), hemMul(b, bh, 'lon')
end
local x, y = mw.ustring.match(plain, '([+-]?[%.%d]+)%s*[;,]%s*([+-]?[%.%d]+)')
if x and y then
return tonumber(x), tonumber(y)
end
x, y = mw.ustring.match(plain, '([+-]?[%.%d]+)%s+([+-]?[%.%d]+)')
if x and y then
x, y = tonumber(x), tonumber(y)
if x and y and x >= -90 and x <= 90 and y >= -180 and y <= 180 then
return x, y
end
end
return nil, nil
end
function util.resolveCoordPair(val)
local ok, lat, lon = pcall(util.parseLatLon, val)
if ok and lat and lon then
return lat, lon
end
if type(val) == 'string' and string.find(val, '{{', 1, true) then
local frame = mw.getCurrentFrame()
if frame then
local ok2, expanded = pcall(function() return frame:preprocess(val) end)
if ok2 then
ok, lat, lon = pcall(util.parseLatLon, expanded)
if ok and lat and lon then
return lat, lon
end
end
end
end
return nil, nil
end
function util.ternary(flag, other)
other = other or 'other'
flag = flag == 'none' and 'no' or flag
local yesNoOut = yesno(flag, other)
local yes = (yesNoOut == true)
local no = (yesNoOut == false)
return yes, no
end
local p = {}
p._caption = function(args)
if args.caption then
return args.caption
elseif args.switcher then
return util.noop("no caption or switcher")
end
-- Bharatpedia does not have Wikidata labels to generate
-- automatic "Location in ..." captions.
return util.noop("no caption")
end
p._main = function(_config)
-- Bharatpedia does not use an external entity database.
-- All map positioning must therefore come from explicit
-- template parameters.
local tracking = ''
-- `config` is the args passed to this module.
local config = util.trimArgs(_config)
-- Allow aliases for coordinates.
config.coord = config.coord or config.coordinates or config.coordinate
if config.coord and not config['frame-coord'] and not config['frame-coordinates'] then
config['frame-coord'] = config.coord
end
----------------------------------------------------------------------
-- COORDINATES
--
-- Unlike the Wikimedia version, this module does not look up
-- coordinates from Wikidata.
----------------------------------------------------------------------
if not config.coord
and not (
config['frame-coord']
or config['frame-coordinates']
)
and not (
config['frame-lat']
and config['frame-long']
)
and not (
config['frame-latitude']
and config['frame-longitude']
) then
return false, util.trackAndWarn(
'Pages using infobox mapframe with missing coordinates'
)
end
----------------------------------------------------------------------
-- MAPFRAME ARGUMENTS
----------------------------------------------------------------------
local args = {}
-- Defaults/overrides for infobox presentation.
args.display = "inline"
args.frame = "yes"
args.plain = "yes"
args["frame-width"] =
config["frame-width"]
or config.width
or DEFAULT_FRAME_WIDTH
args["frame-height"] =
config["frame-height"]
or config.height
or DEFAULT_FRAME_HEIGHT
args["frame-align"] = "center"
args["frame-coord"] =
config["frame-coordinates"]
or config["frame-coord"]
or config.coord
-- Deprecated latitude/longitude parameters.
args["frame-lat"] =
config["frame-lat"]
or config["frame-latitude"]
args["frame-long"] =
config["frame-long"]
or config["frame-longitude"]
----------------------------------------------------------------------
-- ZOOM
--
-- Use an explicitly supplied zoom first.
-- If absent, calculate one from dimensions using Infobox dim.
-- No Wikidata lookup is performed.
----------------------------------------------------------------------
local zoom = config.zoom
if not zoom then
local getZoom = require('Module:Infobox dim')._zoom
zoom = getZoom({
length_km = config.length_km,
length_mi = config.length_mi,
width_km = config.width_km,
width_mi = config.width_mi,
area_km2 = config.area_km2,
area_mi2 = config.area_mi2,
area_ha = config.area_ha,
area_acre = config.area_acre,
type = config.type,
population = config.population,
viewport_px = math.min(
tonumber(args["frame-width"])
or tonumber(DEFAULT_FRAME_WIDTH),
tonumber(args["frame-height"])
or tonumber(DEFAULT_FRAME_HEIGHT)
)
})
end
args.zoom = zoom or DEFAULT_ZOOM
----------------------------------------------------------------------
-- MARKER
----------------------------------------------------------------------
local forcePoint, suppressPoint =
util.ternary(config.point)
local forceMarker, suppressMarker =
util.ternary(config.marker, true)
forcePoint = forcePoint or forceMarker
suppressPoint = suppressPoint or suppressMarker
-- With explicit coordinates, show a point marker by default
-- unless it is explicitly suppressed.
local showMarker =
not suppressPoint
and (
forcePoint
or config.coord ~= nil
)
----------------------------------------------------------------------
-- SHAPE / LINE
--
-- The Wikimedia version inferred these from Wikidata geometry.
-- Bharatpedia requires them to be explicitly requested.
----------------------------------------------------------------------
local showShape = false
local showLine = false
local forceShape, suppressShape =
util.ternary(config.shape)
if forceShape then
showShape = true
elseif suppressShape then
showShape = false
end
local forceLine, suppressLine =
util.ternary(config.line)
if forceLine then
showLine = true
elseif suppressLine then
showLine = false
end
local shapeType =
config.shape == 'inverse'
and 'shape-inverse'
or 'shape'
----------------------------------------------------------------------
-- GEOMASK / SWITCHER
----------------------------------------------------------------------
local maskItem
if config.switcher == "zooms" then
-- Switch between zoom levels.
local maxZoom =
math.max(
tonumber(args.zoom) or DEFAULT_ZOOM,
3
)
local minZoom = 1
local midZoom =
math.floor(
(maxZoom + minZoom) / 2
)
args.switch =
"zoomed in, zoomed midway, zoomed out"
args.zoom =
string.format(
"SWITCH:%d,%d,%d",
maxZoom,
midZoom,
minZoom
)
elseif config.switcher == "auto" then
-- The Wikimedia version used Wikidata P276/P131
-- to determine this automatically.
--
-- Bharatpedia has no such dependency, so this mode
-- is intentionally disabled.
return false, util.trackAndWarn(
'Pages using infobox mapframe with unsupported automatic switcher'
)
elseif config.switcher == "geomasks"
and config.geomask then
-- Explicit geomask IDs are supported.
args.zoom = nil
local separator =
(
mw.ustring.find(
config.geomask,
"###",
0,
true
)
and "###"
)
or
(
mw.ustring.find(
config.geomask,
";",
0,
true
)
and ";"
)
or ","
local pattern =
"%s*" .. separator .. "%s*"
local maskItems =
mw.text.split(
mw.ustring.gsub(
config.geomask,
"SWITCH:",
""
),
pattern
)
if #maskItems > 1 then
-- No external labels are available,
-- so use the supplied IDs as switch labels.
args.switch =
table.concat(
maskItems,
"###"
)
maskItem =
"SWITCH:"
.. table.concat(
maskItems,
","
)
elseif #maskItems == 1 then
maskItem = maskItems[1]
end
end
----------------------------------------------------------------------
-- EXPLICIT GEOMASK
--
-- Property-based geomasks such as P131 are not supported because
-- they require an entity database.
----------------------------------------------------------------------
if not maskItem and config.geomask then
local geomask =
mw.text.trim(
config.geomask
)
if geomask ~= ''
and not mw.ustring.match(
geomask,
"^[Pp]%d+$"
) then
maskItem = geomask
end
end
----------------------------------------------------------------------
-- ENSURE SOMETHING CAN BE DRAWN
----------------------------------------------------------------------
if not maskItem
and not showShape
and not showLine
and not showMarker then
return false, util.trackAndWarn(
'Pages using infobox mapframe with no geometry',
'No geometry specified for mapframe'
)
end
----------------------------------------------------------------------
-- ARGUMENT NUMBERING
----------------------------------------------------------------------
local argNumber = ''
local function incrementArgNumber()
if argNumber == '' then
argNumber = 2
else
argNumber = argNumber + 1
end
end
----------------------------------------------------------------------
-- GEOMASK
----------------------------------------------------------------------
if maskItem then
args["type" .. argNumber] =
"shape-inverse"
args["id" .. argNumber] =
maskItem
args["stroke-width" .. argNumber] =
config["geomask-stroke-width"]
or DEFAULT_GEOMASK_STROKE_WIDTH
args["stroke-color" .. argNumber] =
config["geomask-stroke-color"]
or config["geomask-stroke-colour"]
or DEFAULT_GEOMASK_STROKE_COLOR
args["fill" .. argNumber] =
config["geomask-fill"]
or DEFAULT_GEOMASK_FILL
args["fill-opacity" .. argNumber] =
config["geomask-fill-opacity"]
or DEFAULT_SHAPE_FILL_OPACITY
-- Let Kartographer determine zoom and position
-- unless explicitly configured.
if not config.zoom
and not config.switcher then
args.zoom = nil
args["frame-coord"] = nil
args["frame-lat"] = nil
args["frame-long"] = nil
end
incrementArgNumber()
-- Hack retained from the original module.
if not args.zoom then
args["type" .. argNumber] =
"line"
args["id" .. argNumber] =
maskItem
args["stroke-width" .. argNumber] =
0
incrementArgNumber()
end
end
----------------------------------------------------------------------
-- SHAPE
----------------------------------------------------------------------
if showShape then
args["type" .. argNumber] =
shapeType
-- Explicit ID only.
if config.id then
args["id" .. argNumber] =
config.id
elseif config.from then
args["from" .. argNumber] =
config.from
end
args["stroke-width" .. argNumber] =
config["shape-stroke-width"]
or config["stroke-width"]
or DEFAULT_SHAPE_STROKE_WIDTH
args["stroke-color" .. argNumber] =
config["shape-stroke-color"]
or config["shape-stroke-colour"]
or config["stroke-color"]
or config["stroke-colour"]
or DEFAULT_SHAPE_STROKE_COLOR
args["fill" .. argNumber] =
config["shape-fill"]
or DEFAULT_SHAPE_FILL
args["fill-opacity" .. argNumber] =
config["shape-fill-opacity"]
or DEFAULT_SHAPE_FILL_OPACITY
incrementArgNumber()
end
----------------------------------------------------------------------
-- LINE
----------------------------------------------------------------------
if showLine then
args["type" .. argNumber] =
"line"
-- Explicit ID only.
if config.id then
args["id" .. argNumber] =
config.id
elseif config.from then
args["from" .. argNumber] =
config.from
end
args["stroke-width" .. argNumber] =
config["line-stroke-width"]
or config["stroke-width"]
or DEFAULT_LINE_STROKE_WIDTH
args["stroke-color" .. argNumber] =
config["line-stroke-color"]
or config["line-stroke-colour"]
or config["stroke-color"]
or config["stroke-colour"]
or DEFAULT_LINE_STROKE_COLOR
incrementArgNumber()
end
----------------------------------------------------------------------
-- POINT MARKER
----------------------------------------------------------------------
if showMarker then
args["type" .. argNumber] =
"point"
if config.id then
args["id" .. argNumber] =
config.id
end
if config.coord then
args["coord" .. argNumber] =
config.coord
end
if config.marker then
args["marker" .. argNumber] =
config.marker
end
args["marker-color" .. argNumber] =
config["marker-color"]
or config["marker-colour"]
or DEFAULT_MARKER_COLOR
incrementArgNumber()
end
----------------------------------------------------------------------
-- FRAME CENTERING
----------------------------------------------------------------------
if not maskItem
and not showShape
and not showLine
and showMarker then
if config.coord then
args["frame-coord"] =
args["frame-coord"]
or config.coord
end
end
----------------------------------------------------------------------
-- Normalise coordinates to plain decimals so Module:Mapframe does
-- not have to parse {{Coord}} HTML.
----------------------------------------------------------------------
local lat, lon = util.resolveCoordPair(
args.coord
or args['coord2']
or args['frame-coord']
or config.coord
or config['frame-coord']
)
if lat and lon then
local pair = string.format('%s, %s', lat, lon)
args.coord = pair
args['frame-coord'] = pair
args['frame-lat'] = tostring(lat)
args['frame-long'] = tostring(lon)
args['frame-latitude'] = tostring(lat)
args['frame-longitude'] = tostring(lon)
-- Numbered point feature used by Mapframe content builder
if args.type == 'point' then
args.coord = pair
end
if args.type2 == 'point' then
args.coord2 = pair
end
elseif not args['frame-lat'] or not args['frame-long'] then
return false, util.trackAndWarn(
'Pages using infobox mapframe with missing coordinates',
'Mapframe could not parse coordinates'
)
end
----------------------------------------------------------------------
-- RENDER MAP
----------------------------------------------------------------------
local mapframe =
args.switch
and mf.multi(args)
or mf._main(args)
return true, mapframe .. tracking
end
----------------------------------------------------------------------
-- ENTRY POINTS
----------------------------------------------------------------------
p.main = function(frame)
local parent =
frame.getParent(frame)
local parentArgs =
parent.args
local _, mapframe =
p._main(parentArgs)
return frame:preprocess(mapframe)
end
p.auto = function(frame)
if not util.shouldAutoRun(frame) then
return util.noop(
"auto should not autorun"
)
end
local args =
util.argsFromAuto(frame)
if args.custom then
return frame:preprocess(
util.parseCustomWikitext(
args.custom
)
)
end
local _, mapframe =
p._main(args)
return frame:preprocess(mapframe)
end
p.autocaption = function(frame)
if not util.shouldAutoRun(frame) then
return util.noop(
"autocaption should not autorun"
)
end
local args =
util.argsFromAuto(frame)
local caption =
p._caption(args)
return caption
end
p.autoWithCaption = function(frame)
if not util.shouldAutoRun(frame) then
return util.noop(
"autoWithCaption should not autorun"
)
end
local args =
util.argsFromAuto(frame)
local wikitext
local caption
local ok
if args.custom then
ok = true
wikitext =
util.parseCustomWikitext(
args.custom
)
else
ok, wikitext =
p._main(args)
end
if not ok then
return wikitext
end
wikitext =
frame:preprocess(
wikitext
)
caption =
p._caption(args)
local data =
mw.html.create()
:wikitext(wikitext)
data:tag('div')
:addClass('infobox-caption')
:cssText(args.captionstyle)
:wikitext(caption)
return tostring(data)
end
return p