Module:Infobox mapframe

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.5"

local DEFAULT_SHAPE_STROKE_WIDTH = "3"

local DEFAULT_SHAPE_STROKE_COLOR = "#FF0000"

local DEFAULT_SHAPE_FILL = "#606060"

local DEFAULT_SHAPE_FILL_OPACITY = "0.5"

local DEFAULT_LINE_STROKE_WIDTH = "5"

local DEFAULT_LINE_STROKE_COLOR = "#FF0000"

local DEFAULT_MARKER_COLOR = "#5E74F3"

-- Trim whitespace from args, and remove empty args

function 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 getBestStatement(item_id, property_id)

if not(item_id) or not(mw.wikibase.isValidEntityId(item_id)) or not(mw.wikibase.entityExists(item_id)) then

return false

end

local statements = mw.wikibase.getBestStatements(item_id, property_id)

if not statements or #statements == 0 then

return false

end

local hasNoValue = ( statements[1].mainsnak and statements[1].mainsnak.snaktype == 'novalue' )

if hasNoValue then

return false

end

return statements[1]

end

function hasWikidataProperty(item_id, property_id)

return getBestStatement(item_id, property_id) and true or false

end

function getStatementValue(statement)

return statement and statement.mainsnak and statement.mainsnak.datavalue and statement.mainsnak.datavalue.value or nil

end

function relatedEntity(item_id, property_id)

local value = getStatementValue( getBestStatement(item_id, property_id) )

return value and value.id or false

end

function idType(id)

if not id then

return nil

elseif mw.ustring.match(id, "[Pp]%d+") then

return "property"

elseif mw.ustring.match(id, "[Qq]%d+") then

return "item"

else

return nil

end

end

function shouldAutoRun(frame)

-- Check if should be running

local pargs = frame.getParent(frame).args

local explicitlyOn = yesno(mw.text.trim(pargs.mapframe or "")) -- true of false or nil

if pargs.coordinates == "{{{coordinates}}}" then explicitlyOn = false end

local onByDefault = (explicitlyOn == nil) and yesno(mw.text.trim(frame.args.onByDefault or ""), false) -- true or false

return explicitlyOn or onByDefault

end

function 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

-- allow id, qid to be unprefixed, map to id (if not already present)

elseif name == "id" or name == "qid" and not fixedArgs.id then

fixedArgs.id = val

end

end

return fixedArgs

end

local p = {}

p.autocaption = function(frame)

if not shouldAutoRun(frame) then return "" end

local args = argsFromAuto(frame)

if args.caption then

return args.caption

elseif args.switcher then

return ""

end

local maskItem

local maskType = idType(args.geomask)

if maskType == 'item' then

maskItem = args.geomask

elseif maskType == "property" then

maskItem = relatedEntity(args.id or mw.wikibase.getEntityIdForCurrentPage(), args.geomask)

end

local maskItemLabel = maskItem and mw.wikibase.getLabel( maskItem )

return maskItemLabel and "Location in "..maskItemLabel or ""

end

function 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

p.auto = function(frame)

if not shouldAutoRun(frame) then return "" end

local args = argsFromAuto(frame)

if args.custom then

return frame:preprocess(parseCustomWikitext(args.custom))

end

local mapframe = p._main(args)

return frame:preprocess(mapframe)

end

p.main = function(frame)

local parent = frame.getParent(frame)

local parentArgs = parent.args

local mapframe = p._main(parentArgs)

return frame:preprocess(mapframe)

end

p._main = function(_config)

-- `config` is the args passed to this module

local config = trimArgs(_config)

-- Require wikidata item, or specified coords

local wikidataId = config.id or mw.wikibase.getEntityIdForCurrentPage()

if not(wikidataId) and not(config.coord) then

return ''

end

-- Require coords (specified or from wikidata), so that map will be centred somewhere

-- (P625 = coordinate location)

local hasCoordinates = hasWikidataProperty(wikidataId, 'P625') or config.coordinates or config.coord

if not hasCoordinates then

return ''

end

-- `args` is the arguments which will be passed to the mapframe module

local args = {}

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

-- Note: config["coordinates"] or config["coord"] should not be used for the alignment of the frame;

-- see talk page ( https://en.wikipedia.org/wiki/Special:Diff/876492931 )

-- deprecated lat and long parameters

args["frame-lat"] = config["frame-lat"] or config["frame-latitude"] or ""

args["frame-long"] = config["frame-long"] or config["frame-longitude"] or ""

-- if zoom isn't specified from config:

local zoom = config.zoom

if not zoom then

-- Calculate zoom from length or area (converted to km or km2)

-- Zoom so that length or area is completely included in mapframe

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(args["frame-width"],args["frame-height"])})

end

args.zoom = zoom or DEFAULT_ZOOM

-- Conditionals: whether point, geomask should be shown

local hasOsmRelationId = hasWikidataProperty(wikidataId, 'P402') -- P402 is OSM relation ID

local shouldShowPointMarker;

if config.point == "on" then

shouldShowPointMarker = true

elseif config.point == "none" then

shouldShowPointMarker = false

else

shouldShowPointMarker = not(hasOsmRelationId) or (config.marker and config.marker ~= 'none') or (config.coordinates or config.coord)

end

local shouldShowShape = config.shape ~= 'none'

local shapeType = config.shape == 'inverse' and 'shape-inverse' or 'shape'

local shouldShowLine = config.line ~= 'none'

local maskItem

local useWikidata = wikidataId and true or false -- Use shapes/lines based on wikidata id, if there is one

-- But do not use wikidata when local coords are specified (and not turned off), unless explicitly set

if useWikidata and config.coord and shouldShowPointMarker then

useWikidata = config.wikidata and true or false

end

-- Switcher

if config.switcher == "zooms" then

-- switching between zoom levels

local maxZoom = math.max(tonumber(args.zoom), 3) -- what zoom would have otherwise been (if 3 or more, otherwise 3)

local minZoom = 1 -- completely zoomed out

local midZoom = math.floor((maxZoom + minZoom)/2) -- midway between maxn and min

args.switch = "zoomed in, zoomed midway, zoomed out"

args.zoom = string.format("SWITCH:%d,%d,%d", maxZoom, midZoom, minZoom)

elseif config.switcher == "auto" then

-- switching between P276 and P131 areas with recursive lookup, e.g. item's city,

-- that city's state, and that state's country

args.zoom = nil -- let kartographer determine the zoom

local maskLabels = {}

local maskItems = {}

local maskItemId = relatedEntity(wikidataId, "P276") or relatedEntity(wikidataId, "P131")

local maskLabel = mw.wikibase.getLabel(maskItemId)

while maskItemId and maskLabel and mw.text.trim(maskLabel) ~= "" do

table.insert(maskLabels, maskLabel)

table.insert(maskItems, maskItemId)

maskItemId = maskItemId and relatedEntity(maskItemId, "P131")

maskLabel = maskItemId and mw.wikibase.getLabel(maskItemId)

end

if #maskLabels > 1 then

args.switch = table.concat(maskLabels, "###")

maskItem = "SWITCH:" .. table.concat(maskItems, ",")

elseif #maskLabels == 1 then

maskItem = maskItemId[1]

end

elseif config.switcher == "geomasks" and config.geomask then

-- switching between items in geomask parameter

args.zoom = nil -- let kartographer determine the zoom

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)

local maskLabels = {}

if #maskItems > 1 then

for i, item in ipairs(maskItems) do

table.insert(maskLabels, mw.wikibase.getLabel(item))

end

args.switch = table.concat(maskLabels, "###")

maskItem = "SWITCH:" .. table.concat(maskItems, ",")

end

end

-- resolve geomask item id (if not using geomask switcher)

if not maskItem then --

local maskType = idType(config.geomask)

if maskType == 'item' then

maskItem = config.geomask

elseif maskType == "property" then

maskItem = relatedEntity(wikidataId, config.geomask)

end

end

-- Keep track of arg 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 it is explicitly set in config

if not config.zoom and not config.switcher then

args.zoom = nil

args["frame-coord"] = nil

args["frame-lat"] = nil

args["frame-long"] = nil

local maskArea = getStatementValue( getBestStatement(maskItem, 'P2046') )

end

incrementArgNumber()

-- Hack to fix phab:T255932

if not args.zoom then

args["type"..argNumber] = "line"

args["id"..argNumber] = maskItem

args["stroke-width"..argNumber] = 0

incrementArgNumber()

end

end

-- Shape (or shape-inverse)

if useWikidata and shouldShowShape then

args["type"..argNumber] = shapeType

if config.id then args["id"..argNumber] = config.id 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 useWikidata and shouldShowLine then

args["type"..argNumber] = "line"

if config.id then args["id"..argNumber] = config.id 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

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

local mapframe = args.switch and mf.multi(args) or mf._main(args)

local tracking = hasOsmRelationId and '' or 'Category:Infobox mapframe without OSM relation ID on Wikidata'

return mapframe .. tracking

end

return p