Module:Infobox mapframe: Difference between revisions

From Ryukyusphere
(rm controversial warning message)
 
mNo edit summary
 
(One intermediate revision by one other user not shown)
Line 11: Line 11:
local DEFAULT_GEOMASK_STROKE_COLOR = "#777777"
local DEFAULT_GEOMASK_STROKE_COLOR = "#777777"
local DEFAULT_GEOMASK_FILL = "#888888"
local DEFAULT_GEOMASK_FILL = "#888888"
local DEFAULT_GEOMASK_FILL_OPACITY = "0.25"
local DEFAULT_GEOMASK_FILL_OPACITY = "0.5"
local DEFAULT_SHAPE_STROKE_WIDTH = "2"
local DEFAULT_SHAPE_STROKE_WIDTH = "3"
local DEFAULT_SHAPE_STROKE_COLOR = "#FF0000"
local DEFAULT_SHAPE_STROKE_COLOR = "#FF0000"
local DEFAULT_SHAPE_FILL = "#606060"
local DEFAULT_SHAPE_FILL = "#606060"
local DEFAULT_SHAPE_FILL_OPACITY = "0.1"
local DEFAULT_SHAPE_FILL_OPACITY = "0.5"
local DEFAULT_LINE_STROKE_WIDTH = "5"
local DEFAULT_LINE_STROKE_WIDTH = "5"
local DEFAULT_LINE_STROKE_COLOR = "#FF0000"
local DEFAULT_LINE_STROKE_COLOR = "#FF0000"
local DEFAULT_MARKER_COLOR = "#5E74F3"
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
-- Trim whitespace from args, and remove empty args
function util.trimArgs(argsTable)
function trimArgs(argsTable)
local cleanArgs = {}
local cleanArgs = {}
for key, val in pairs(argsTable) do
for key, val in pairs(argsTable) do
Line 48: Line 37:
end
end


function util.getBestStatement(item_id, property_id)
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
if not(item_id) or not(mw.wikibase.isValidEntityId(item_id)) or not(mw.wikibase.entityExists(item_id)) then
return false
return false
Line 63: Line 52:
end
end


function util.hasWikidataProperty(item_id, property_id)
function hasWikidataProperty(item_id, property_id)
return util.getBestStatement(item_id, property_id) and true or false
return getBestStatement(item_id, property_id) and true or false
end
end


function util.getStatementValue(statement)
function getStatementValue(statement)
return statement and statement.mainsnak and statement.mainsnak.datavalue and statement.mainsnak.datavalue.value or nil
return statement and statement.mainsnak and statement.mainsnak.datavalue and statement.mainsnak.datavalue.value or nil
end
end


function util.relatedEntity(item_id, property_id)
function relatedEntity(item_id, property_id)
local value = util.getStatementValue( util.getBestStatement(item_id, property_id) )
local value = getStatementValue( getBestStatement(item_id, property_id) )
return value and value.id or false
return value and value.id or false
end
end


function util.idType(id)
function idType(id)
if not id then  
if not id then  
return nil
return nil
Line 88: Line 77:
end
end


function util.shouldAutoRun(frame)
function getZoom(value, unit)
local length_km
if unit == 'km' then
length_km = tonumber(value)
elseif unit == 'mi' then
length_km = tonumber(value)*1.609344
elseif unit == 'km2' then
length_km = math.sqrt(tonumber(value))
elseif unit == 'mi2' then
length_km = math.sqrt(tonumber(value))*1.609344
end
-- max for zoom 2 is 6400km, for zoom 3 is 3200km, for zoom 4 is 1600km, etc
local zoom = math.floor(8 - (math.log10(length_km) - 2)/(math.log10(2)))
-- limit to values below 17
zoom = math.min(17, zoom)
-- take off 1 when calculated from area, to account for unusual shapes
if unit == 'km2' or unit == 'mi2' then
zoom = zoom - 1
end
-- minimum value is 1
return math.max(1, zoom)
end
 
function shouldAutoRun(frame)
-- Check if should be running
-- Check if should be running
local pargs = frame.getParent(frame).args
local explicitlyOn = yesno(mw.text.trim(frame.getParent(frame).args.mapframe or "")) -- true of false or nil
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
local onByDefault = (explicitlyOn == nil) and yesno(mw.text.trim(frame.args.onByDefault or ""), false) -- true or false
return explicitlyOn or onByDefault
return explicitlyOn or onByDefault
end
end


function util.argsFromAuto(frame)
function argsFromAuto(frame)
-- Get args from the frame (invoke call) and the parent (template call).
-- Get args from the frame (invoke call) and the parent (template call).
-- Frame arguments are default values which are overridden by parent values
-- Frame arguments are default values which are overridden by parent values
-- when both are present
-- when both are present
local args = getArgs(frame, {parentFirst = true})
local args = getArgs(frame, {parentFirst = true})
 
-- Discard args not prefixed with "mapframe-", remove that prefix from those that remain
-- Discard args not prefixed with "mapframe-", remove that prefix from those that remain
local fixedArgs = {}
local fixedArgs = {}
Line 115: Line 125:
elseif name == "id" or name == "qid" and not fixedArgs.id then
elseif name == "id" or name == "qid" and not fixedArgs.id then
fixedArgs.id = val
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
end
end
return fixedArgs
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 then
        local warn = require('Module:If preview')._warning
        results = results..warn({warning})
    end
    return results
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
end


local p = {}
local p = {}


 
p.autocaption = function(frame)
p._caption = function(args)
if not shouldAutoRun(frame) then return "" end
local args = argsFromAuto(frame)
if args.caption then
if args.caption then
return args.caption
return args.caption
elseif args.switcher then  
elseif args.switcher then  
return util.noop("no caption or switcher")
return ""
end
end
local maskItem
local maskItem
local maskType = util.idType(args.geomask)
local maskType = idType(args.geomask)
if maskType == 'item' then
if maskType == 'item' then
maskItem = args.geomask
maskItem = args.geomask
elseif maskType == "property" then
elseif maskType == "property" then
maskItem = util.relatedEntity(args.id or mw.wikibase.getEntityIdForCurrentPage(), args.geomask)
maskItem = relatedEntity(args.id or mw.wikibase.getEntityIdForCurrentPage(), args.geomask)
end
end
local maskItemLabel = maskItem and mw.wikibase.getLabel( maskItem )
local maskItemLabel = maskItem and mw.wikibase.getLabel( maskItem )
return maskItemLabel and "Location in "..maskItemLabel
return maskItemLabel and "Location in "..maskItemLabel or ""
or util.noop("missing maskItemLabel with type " .. (maskType or "nil") .. " and item " .. (maskItem or "nil"))
end
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


--A list of types for objects that are too small to allow Kartographer to take over zoom
p.auto = function(frame)
local tinyType = {
if not shouldAutoRun(frame) then return "" end
    landmark=true,
local args = argsFromAuto(frame)
    railwaystation=true,
if args.custom then
    edu=true,
return frame:preprocess(parseCustomWikitext(args.custom))
    pass=true,
    camera=true
}               
 
p._main = function(_config)
    -- accumulate tracking cats
    local tracking = ''
-- `config` is the args passed to this module
local config = util.trimArgs(_config)
    -- allow alias for config.coord
    config.coord = config.coord or config.coordinates
 
-- Require wikidata item, or specified coords
local wikidataId = config.id or mw.wikibase.getEntityIdForCurrentPage()
if not(wikidataId) and not(config.coord) then
return false, util.trackAndWarn('Pages using infobox mapframe with missing coordinates')
end
end
 
local mapframe = p._main(args)
-- Require coords (specified or from wikidata), so that map will be centred somewhere
return frame:preprocess(mapframe)
-- (P625 = coordinate location)
    local wdCoordinates = util.getStatementValue(util.getBestStatement(wikidataId, 'P625'))
    if not (config.coord or wdCoordinates) then
return false, util.trackAndWarn('Pages using infobox mapframe with missing coordinates')
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"]
-- 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"]
args["frame-long"]  = config["frame-long"] or config["frame-longitude"]
 
    -- if zoom isn't specified from config, first check wikidata
    local zoom = config.zoom or util.getStatementValue(util.getBestStatement(wikidataId, 'P6592'))
    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"]),
                        latitude=wdCoordinates and wdCoordinates.latitude})
    end
    args.zoom = zoom or DEFAULT_ZOOM
 
-- Use OSM relation ID if available; otherwise use geoshape if that is available
-- (geoshape is required for defunct entities, which are outside OSM's scope)
local hasOsmRelationId = util.hasWikidataProperty(wikidataId, 'P402') -- P402 is OSM relation ID
local hasGeoshape = util.hasWikidataProperty(wikidataId, 'P3896') -- P3896 is geoshape
local wikidataProvidesGeo = hasOsmRelationId or hasGeoshape
 
    -- determine marker argument value, determine whether to show marker
    local forcePoint, suppressPoint = util.ternary(config.point)
    local forceMarker, suppressMarker = util.ternary(config.marker,true)
 
    forcePoint = forcePoint or forceMarker
    suppressPoint = suppressPoint or suppressMarker
 
    local showMarker = not suppressPoint and (forcePoint or not wikidataProvidesGeo or config.coord)
   
    -- wikidata = "yes" turns on both shape and line
    -- wikidata = "no" turns off both shape and line
    -- otherwise show both if wikidata provides geo
    local forceWikidata, suppressWikidata = util.ternary(config.wikidata)
    local showShape = not suppressWikidata and (forceWikidata or wikidataProvidesGeo or not config.coord)
    local showLine = showShape
 
    -- determine shape parameter value, determine whether to show or suppress shape
    -- also determine whether to invert shape
    local forceShape, suppressShape = util.ternary(config.shape)
    showShape = wikidataId and not suppressShape and (forceShape or showShape)
local shapeType = config.shape == 'inverse' and 'shape-inverse' or 'shape'
 
    -- determine line parameter value, determine whether to show or suppress line
    local forceLine, suppressLine = util.ternary(config.line)
    showLine = wikidataId and not suppressLine and (forceLine or showLine)
 
    local maskItem
-- 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 = util.relatedEntity(wikidataId, "P276") or  util.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 util.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 = util.idType(config.geomask)
if maskType == 'item' then
maskItem = config.geomask
elseif maskType == "property" then
maskItem = util.relatedEntity(wikidataId, config.geomask)
end
end
 
    -- if asking for shape or line from Wikidata
    -- and if Wikidata actually has shape/line data (wikidataProvidesGeo=true)
    -- and if no geomask
    -- and if zoom not explicitly set
    -- and if the object size inferred from its type is not too small
    -- then let Kartographer "take over" zoom
    if (showLine or showShape) and wikidataProvidesGeo and not maskItem
      and not config.zoom and not (config.type and tinyType[config.type]) then
    args.zoom = nil
    end
 
    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
-- 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 = util.getStatementValue( util.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 showShape then
args["type"..argNumber] = shapeType
if hasGeoshape and not hasOsmRelationId then
args["from"..argNumber] = string.sub( util.getStatementValue( util.getBestStatement(wikidataId, 'P3896') ), 6)
elseif 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 showLine then
args["type"..argNumber] = "line"
if hasGeoshape and not hasOsmRelationId then
args["from"..argNumber] = string.sub( util.getStatementValue( util.getBestStatement(wikidataId, 'P3896') ), 6)
elseif 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 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
 
    -- if Wikidata doesn't link to OSM and the map has no mask or point,
    -- then center the map on the coordinates either from the infobox or from wikidata
    if not maskItem and not showMarker and not wikidataProvidesGeo then
        if config.coord then
            args["frame-coord"] = args["frame-coord"] or config.coord
        else
            args["frame-lat"] = args["frame-lat"] or wdCoordinates.latitude
            args["frame-long"] = args["frame-long"] or wdCoordinates.longitude
        end
        tracking = tracking..util.trackAndWarn('Pages using infobox mapframe with forced centering')
    end
 
    -- protect against nil frame arguments
    args["frame-coord"] = args["frame-coord"] or ""
    args["frame-lat"] = args["frame-lat"] or ""
    args["frame-long"] = args["frame-long"] or ""
 
local mapframe = args.switch and mf.multi(args) or mf._main(args)
tracking = tracking..((showLine or showShape) and not wikidataProvidesGeo
                          and util.trackAndWarn('Pages using infobox mapframe without shape links in Wikidata')
                          or '')
return true, mapframe.. tracking
end
end
-- Entry points


p.main = function(frame)
p.main = function(frame)
local parent = frame.getParent(frame)
local parent = frame.getParent(frame)
local parentArgs = parent.args
local parentArgs = parent.args
local _, mapframe = p._main(parentArgs)
local mapframe = p._main(parentArgs)
return frame:preprocess(mapframe)
return frame:preprocess(mapframe)
end
end


p.auto = function(frame)
p._main = function(_config)
if not util.shouldAutoRun(frame) then
return ""
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
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
return p

Latest revision as of 08:17, 18 January 2026

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.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 getZoom(value, unit)
	local length_km
	if unit == 'km' then
		length_km = tonumber(value)
	elseif unit == 'mi' then
		length_km = tonumber(value)*1.609344
	elseif unit == 'km2' then
		length_km = math.sqrt(tonumber(value))
	elseif unit == 'mi2' then
		length_km = math.sqrt(tonumber(value))*1.609344
	end
	-- max for zoom 2 is 6400km, for zoom 3 is 3200km, for zoom 4 is 1600km, etc
	local zoom = math.floor(8 - (math.log10(length_km) - 2)/(math.log10(2)))
	-- limit to values below 17
	zoom = math.min(17, zoom)
	-- take off 1 when calculated from area, to account for unusual shapes
	if unit == 'km2' or unit == 'mi2' then
		zoom = zoom - 1
	end
	-- minimum value is 1
	return math.max(1, zoom)
end

function shouldAutoRun(frame)
	-- Check if should be running
	local explicitlyOn = yesno(mw.text.trim(frame.getParent(frame).args.mapframe or "")) -- true of false or nil
	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)
	return ""
end

return p