Module:Infobox mapframe

Usage

Module that automatically makes a mapframe suitable for an infobox automatically, with minimal user input.

Generic usage

Just use the template {{Infobox mapframe}}, following the documentation there. This module may also be imported to another Lua module.

Automatic maps in infoboxes

Example edits: Template:Infobox prison, Template:Infobox prison/doc

Edit the infobox template (or its sandbox). Add lines like the following examples to the infobox. Replace numbers (4, or 97 to 99), with the appropriate number based on how many other image or data parameters are already present.

If placing near the top of infobox If placing at/near the bottom of the infobox
| image4     = {{#invoke:Infobox mapframe|auto}}
| caption4   = {{#invoke:Infobox mapframe|autocaption}}
| header97   = {{#if:{{{mapframe|}}}|Location}}
| data98     = {{#invoke:Infobox mapframe|auto}}
| data99     = {{#invoke:Infobox mapframe|autocaption}}

If the template has a {{#invoke:Check for unknown parameters}}, add the following parameters:

| mapframe | mapframe-caption | mapframe-custom | mapframe-id | mapframe-coord | mapframe-wikidata | mapframe-point | mapframe-shape | mapframe-frame-width | mapframe-frame-height | mapframe-shape-fill | mapframe-shape-fill-opacity | mapframe-stroke-color | mapframe-stroke-colour | mapframe-stroke-width | mapframe-marker | mapframe-marker-color | mapframe-marker-colour | mapframe-geomask | mapframe-geomask-stroke-color | mapframe-geomask-stroke-colour | mapframe-geomask-stroke-width | mapframe-geomask-fill | mapframe-geomask-fill-opacity | mapframe-zoom | mapframe-length_km | mapframe-length_mi | mapframe-area_km2 | mapframe-area_mi2 | mapframe-frame-coordinates | mapframe-frame-coord | mapframe-switcher

Once this is done, the above parameters will be available to users of the template.

  • Defaults values for these parameters can also be specified in the #invoke calls above, e.g. {{#invoke:Infobox mapframe|auto|mapframe-marker=library}} means that the library marker will be used, unless a different value is passed in to the template.
  • The maps are off by default, which means maps will not be displayed unless |mapframe=yes is present in the template call. To turn maps on by default, in the #invoke calls above add |onByDefault=yes – which means maps will be displayed unless |mapframe=no is present in the template call. onByDefault can also be set to a conditional, such as if another parameters is present, e.g. |onByDefault={{#if:{{{pushpin_map|}}}|no|yes}}
  • Add the new parameters to the document. You can use {{Infobox mapframe/doc/parameters}}. Specify default values (if any are used in the #invoke calls) using by adding parameters in the form |parameter-name=value. The default output is shown here collapsed:

Template:Infobox mapframe/doc/parameters

|}

See also


local mf = require('Module:Mapframe')

-- 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_SHAPE_STROKE_WIDTH = "3"
local DEFAULT_SHAPE_STROKE_COLOR = "#FF0000"
local DEFAULT_SHAPE_FILL = "#606060"
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

local p = {}

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)
	
	-- Use wikidata by default
	local useWikidata = true
	-- Do not use wikidata when coords are specified, unless explicitly set
	if config.coord then
		useWikidata = config.wikidata and true or false
	end
	
	-- 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 DEFAULT_FRAME_WIDTH
	args["frame-height"] = config["frame-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 ""

	-- 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 = not(hasOsmRelationId) or (config.marker and config.marker ~= 'none') or (config.coordinates or config.coord)
	local maskItem
	local maskType = idType(config.geomask)
	if maskType == 'item' then
		maskItem = config.geomask
	elseif maskType == "property" then
		maskItem = relatedEntity(wikidataId, config.geomask)
	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
		-- Recalculate zoom based on mask area, if available, and not set in config
		if not config.zoom then
			local maskArea = getStatementValue( getBestStatement(maskItem, 'P2046') )
			if maskArea and maskArea.amount then
				if maskArea.unit == 'http://www.wikidata.org/entity/Q712226' then -- square kilometre
					args.zoom = getZoom(maskArea.amount, 'km2')
				elseif maskArea.unit == 'http://www.wikidata.org/entity/Q232291' then -- square mile
					args.zoom = getZoom(maskArea.amount, 'mi2')
				end
			end
		end
		incrementArgNumber()
	end
	
	-- Shape
	if useWikidata then
		args["type"..argNumber] = "shape"
		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
		incrementArgNumber()
	end
	
	-- Line
	if useWikidata 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 = mf._main(args)
	local tracking = hasOsmRelationId and '' or '[[Category:Infobox mapframe without OSM relation ID on Wikidata]]'
	return mapframe .. tracking
end

return p