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 = &quot;270&quot;
 local DEFAULT_FRAME_HEIGHT = &quot;200&quot;
 local DEFAULT_ZOOM = 10
 local DEFAULT_GEOMASK_STROKE_WIDTH = &quot;1&quot;
 local DEFAULT_GEOMASK_STROKE_COLOR = &quot;#777777&quot;
 local DEFAULT_GEOMASK_FILL = &quot;#888888&quot;
 local DEFAULT_GEOMASK_FILL_OPACITY = &quot;0.5&quot;
 local DEFAULT_SHAPE_STROKE_WIDTH = &quot;3&quot;
 local DEFAULT_SHAPE_STROKE_COLOR = &quot;#FF0000&quot;
 local DEFAULT_SHAPE_FILL = &quot;#606060&quot;
 local DEFAULT_SHAPE_FILL_OPACITY = &quot;0.5&quot;
 local DEFAULT_LINE_STROKE_WIDTH = &quot;5&quot;
 local DEFAULT_LINE_STROKE_COLOR = &quot;#FF0000&quot;
 local DEFAULT_MARKER_COLOR = &quot;#5E74F3&quot;
 
 
 -- 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, &quot;[Pp]%d+&quot;) then
 		return &quot;property&quot;
 	elseif mw.ustring.match(id, &quot;[Qq]%d+&quot;) then
 		return &quot;item&quot;
 	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 &quot;&quot;)) -- true of false or nil
 	local onByDefault = (explicitlyOn == nil) and yesno(mw.text.trim(frame.args.onByDefault or &quot;&quot;), 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 &quot;mapframe-&quot;, remove that prefix from those that remain
 	local fixedArgs = {}
 	for name, val in pairs(args) do
 		local fixedName = string.match(name, &quot;^mapframe%-(.+)$&quot; )
 		if fixedName then
 			fixedArgs[fixedName] = val
 		-- allow coord, coordinates, etc to be unprefixed
 		elseif name == &quot;coordinates&quot; or name == &quot;coord&quot; or name == &quot;coordinate&quot; and not fixedArgs.coord then
 			fixedArgs.coord = val
 		-- allow id, qid to be unprefixed, map to id (if not already present)
 		elseif name == &quot;id&quot; or name == &quot;qid&quot; 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 &quot;&quot; end
 	local args = argsFromAuto(frame)
 	if args.caption then
 		return args.caption
 	elseif args.switcher then 
 		return &quot;&quot;
 	end
 	local maskItem
 	local maskType = idType(args.geomask)
 	if maskType == 'item' then
 		maskItem = args.geomask
 	elseif maskType == &quot;property&quot; then
 		maskItem = relatedEntity(args.id or mw.wikibase.getEntityIdForCurrentPage(), args.geomask)
 	end
 	local maskItemLabel = maskItem and mw.wikibase.getLabel( maskItem )
 	return maskItemLabel and &quot;Location in &quot;..maskItemLabel or &quot;&quot;
 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 &quot;&quot; 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 = &quot;inline&quot;
 	args.frame = &quot;yes&quot;
 	args.plain = &quot;yes&quot;
 	args[&quot;frame-width&quot;]  = config[&quot;frame-width&quot;] or config.width or DEFAULT_FRAME_WIDTH
 	args[&quot;frame-height&quot;] = config[&quot;frame-height&quot;] or config.height or DEFAULT_FRAME_HEIGHT
 	args[&quot;frame-align&quot;]  = &quot;center&quot;
 
 	args[&quot;frame-coord&quot;] = config[&quot;frame-coordinates&quot;] or config[&quot;frame-coord&quot;] or &quot;&quot;
 	-- Note: config[&quot;coordinates&quot;] or config[&quot;coord&quot;] 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[&quot;frame-lat&quot;]    = config[&quot;frame-lat&quot;] or config[&quot;frame-latitude&quot;] or &quot;&quot;
 	args[&quot;frame-long&quot;]   = config[&quot;frame-long&quot;] or config[&quot;frame-longitude&quot;] or &quot;&quot;
 
 	-- Calculate zoom from length or area (converted to km or km2)
 	if config.length_km then
 		args.zoom = getZoom(config.length_km, 'km')
 	elseif config.length_mi then
 		args.zoom = getZoom(config.length_mi, 'mi')
 	elseif config.area_km2 then
 		args.zoom = getZoom(config.area_km2, 'km2')
 	elseif config.area_mi2 then
 		args.zoom = getZoom(config.area_mi2, 'mi2')
 	else
 		args.zoom = config.zoom or DEFAULT_ZOOM
 	end
 
 	-- Conditionals: whether point, geomask should be shown
 	local hasOsmRelationId = hasWikidataProperty(wikidataId, 'P402') -- P402 is OSM relation ID
 	local shouldShowPointMarker;
 	if config.point == &quot;on&quot; then
 		shouldShowPointMarker = true 
 	elseif config.point == &quot;none&quot; 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 == &quot;zooms&quot; 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 = &quot;zoomed in, zoomed midway, zoomed out&quot;
 		args.zoom = string.format(&quot;SWITCH:%d,%d,%d&quot;, maxZoom, midZoom, minZoom)
 	elseif config.switcher == &quot;auto&quot; 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, &quot;P276&quot;) or  relatedEntity(wikidataId, &quot;P131&quot;) 
 		local maskLabel = mw.wikibase.getLabel(maskItemId)
 		while maskItemId and maskLabel and mw.text.trim(maskLabel) ~= &quot;&quot; do
 			table.insert(maskLabels, maskLabel)
 			table.insert(maskItems, maskItemId)
 			maskItemId = maskItemId and relatedEntity(maskItemId, &quot;P131&quot;)
 			maskLabel = maskItemId and mw.wikibase.getLabel(maskItemId)
 		end
 		if #maskLabels &gt; 1 then
 			args.switch = table.concat(maskLabels, &quot;###&quot;)
 			maskItem = &quot;SWITCH:&quot; .. table.concat(maskItems, &quot;,&quot;)
 		elseif #maskLabels == 1 then
 			maskItem = maskItemId[1]
 		end
 	elseif config.switcher == &quot;geomasks&quot; 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, &quot;###&quot;, 0, true ) and &quot;###&quot;) or
 			(mw.ustring.find(config.geomask, &quot;;&quot;, 0, true ) and &quot;;&quot;) or &quot;,&quot;
 		local pattern = &quot;%s*&quot;..separator..&quot;%s*&quot;
 		local maskItems = mw.text.split(mw.ustring.gsub(config.geomask, &quot;SWITCH:&quot;, &quot;&quot;), pattern)
 		local maskLabels = {}
 		if #maskItems &gt; 1 then
 			for i, item in ipairs(maskItems) do
 				table.insert(maskLabels, mw.wikibase.getLabel(item))
 			end
 			args.switch = table.concat(maskLabels, &quot;###&quot;)
 			maskItem = &quot;SWITCH:&quot; .. table.concat(maskItems, &quot;,&quot;)
 		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 == &quot;property&quot; 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[&quot;type&quot;..argNumber] = &quot;shape-inverse&quot;
 		args[&quot;id&quot;..argNumber] = maskItem
 		args[&quot;stroke-width&quot;..argNumber] = config[&quot;geomask-stroke-width&quot;] or DEFAULT_GEOMASK_STROKE_WIDTH
 		args[&quot;stroke-color&quot;..argNumber] = config[&quot;geomask-stroke-color&quot;] or config[&quot;geomask-stroke-colour&quot;] or DEFAULT_GEOMASK_STROKE_COLOR
 		args[&quot;fill&quot;..argNumber] = config[&quot;geomask-fill&quot;] or DEFAULT_GEOMASK_FILL
 		args[&quot;fill-opacity&quot;..argNumber] = config[&quot;geomask-fill-opacity&quot;] 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[&quot;frame-coord&quot;] = nil
 			args[&quot;frame-lat&quot;] = nil
 			args[&quot;frame-long&quot;] = nil 	
 			local maskArea = getStatementValue( getBestStatement(maskItem, 'P2046') )
 		end
 		incrementArgNumber()
 		-- Hack to fix phab:T255932
 		if not args.zoom then
 			args[&quot;type&quot;..argNumber] = &quot;line&quot;
 			args[&quot;id&quot;..argNumber] = maskItem
 			args[&quot;stroke-width&quot;..argNumber] = 0
 			incrementArgNumber()
 		end
 	end
 	
 	-- Shape (or shape-inverse)
 	if useWikidata and shouldShowShape then
 		args[&quot;type&quot;..argNumber] = shapeType
 		if config.id then args[&quot;id&quot;..argNumber] = config.id end
 		args[&quot;stroke-width&quot;..argNumber] = config[&quot;shape-stroke-width&quot;] or config[&quot;stroke-width&quot;] or DEFAULT_SHAPE_STROKE_WIDTH
 		args[&quot;stroke-color&quot;..argNumber] = config[&quot;shape-stroke-color&quot;] or config[&quot;shape-stroke-colour&quot;] or config[&quot;stroke-color&quot;] or config[&quot;stroke-colour&quot;] or DEFAULT_SHAPE_STROKE_COLOR
 		args[&quot;fill&quot;..argNumber] = config[&quot;shape-fill&quot;] or DEFAULT_SHAPE_FILL
 		args[&quot;fill-opacity&quot;..argNumber] = config[&quot;shape-fill-opacity&quot;] or DEFAULT_SHAPE_FILL_OPACITY
 		incrementArgNumber()
 	end
 	
 	-- Line
 	if useWikidata and shouldShowLine then
 		args[&quot;type&quot;..argNumber] = &quot;line&quot;
 		if config.id then args[&quot;id&quot;..argNumber] = config.id end
 		args[&quot;stroke-width&quot;..argNumber] = config[&quot;line-stroke-width&quot;] or config[&quot;stroke-width&quot;] or DEFAULT_LINE_STROKE_WIDTH
 		args[&quot;stroke-color&quot;..argNumber] = config[&quot;line-stroke-color&quot;] or config[&quot;line-stroke-colour&quot;] or config[&quot;stroke-color&quot;] or config[&quot;stroke-colour&quot;] or DEFAULT_LINE_STROKE_COLOR
 		incrementArgNumber()
 	end
 
 	-- Point
 	if shouldShowPointMarker then
 		args[&quot;type&quot;..argNumber] = &quot;point&quot;
 		if config.id then args[&quot;id&quot;..argNumber] = config.id end
 		if config.coord then args[&quot;coord&quot;..argNumber] = config.coord end
 		if config.marker then args[&quot;marker&quot;..argNumber] = config.marker end
 		args[&quot;marker-color&quot;..argNumber] = config[&quot;marker-color&quot;] or config[&quot;marker-colour&quot;] 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