|
|
Line 1: |
Line 1: |
| local mf = require('Module:Mapframe')
| |
| local getArgs = require('Module:Arguments').getArgs | | local getArgs = require('Module:Arguments').getArgs |
| local yesno = require('Module:Yesno') | | local yesno = require('Module:Yesno') |
| local infoboxImage = require('Module:InfoboxImage').InfoboxImage | | local infoboxImage = require('Module:InfoboxImage').InfoboxImage |
|
| |
|
| -- Defaults | | -- Default configurations |
| local DEFAULT_FRAME_WIDTH = "270" | | local DEFAULT_FRAME_WIDTH = "270" |
| local DEFAULT_FRAME_HEIGHT = "200" | | local DEFAULT_FRAME_HEIGHT = "200" |
| local DEFAULT_ZOOM = 10 | | 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 | | -- Trim whitespace from args, and remove empty args |
| function trimArgs(argsTable) | | local function trimArgs(argsTable) |
| local cleanArgs = {}
| | local cleanArgs = {} |
| for key, val in pairs(argsTable) do
| | for key, val in pairs(argsTable) do |
| if type(val) == 'string' then
| | if type(val) == 'string' then |
| val = val:match('^%s*(.-)%s*$')
| | val = val:match('^%s*(.-)%s*$') |
| if val ~= '' then
| | if val ~= '' then |
| cleanArgs[key] = val
| | cleanArgs[key] = val |
| end
| | end |
| else
| | else |
| cleanArgs[key] = val
| | cleanArgs[key] = val |
| end
| | end |
| end
| | end |
| return cleanArgs
| | 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 | | end |
|
| |
|
| function relatedEntity(item_id, property_id)
| | -- Convert latitude and longitude to tile coordinates |
| local value = getStatementValue( getBestStatement(item_id, property_id) )
| | local function latLonToTile(lat, lon, zoom) |
| return value and value.id or false
| | local n = 2 ^ zoom |
| end
| | local xtile = math.floor((lon + 180.0) / 360.0 * n) |
| | | local ytile = math.floor((1.0 - math.log(math.tan(lat * math.pi / 180.0) + 1 / math.cos(lat * math.pi / 180.0)) / math.pi) / 2.0 * n) |
| function idType(id) | | return xtile, ytile |
| 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 | | end |
|
| |
|
| local p = {} | | 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) | | p.main = function(frame) |
| local parent = frame.getParent(frame)
| | local args = getArgs(frame) |
| local parentArgs = parent.args
| | args = trimArgs(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 ""
| |
| | |
| -- 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 lat = tonumber(args.lat or args.latitude) |
| local hasOsmRelationId = hasWikidataProperty(wikidataId, 'P402') -- P402 is OSM relation ID
| | local lon = tonumber(args.lon or args.longitude) |
| local shouldShowPointMarker;
| | local zoom = tonumber(args.zoom) or DEFAULT_ZOOM |
| if config.point == "on" then
| | local width = args.width or DEFAULT_FRAME_WIDTH |
| shouldShowPointMarker = true
| | local height = args.height or DEFAULT_FRAME_HEIGHT |
| 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 not lat or not lon then |
| if shouldShowPointMarker then
| | return "Coordinates (lat, lon) must be specified." |
| args["type"..argNumber] = "point"
| | end |
| 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)
| | -- Convert lat/lon to tile coordinates for the given zoom level |
| local tracking = hasOsmRelationId and '' or '[[Category:Infobox mapframe without OSM relation ID on Wikidata]]'
| | local xtile, ytile = latLonToTile(lat, lon, zoom) |
| return mapframe .. tracking
| | |
| | -- Construct the OpenStreetMap tile URL |
| | local tileUrl = string.format("https://tile.openstreetmap.org/%d/%d/%d.png", zoom, xtile, ytile) |
| | |
| | -- Generate the HTML for the map frame |
| | local mapHtml = string.format( |
| | '<div class="mapframe" style="width: %dpx; height: %dpx;">' .. |
| | '<img src="%s" alt="Map view" style="width: 100%%; height: 100%%;">' .. |
| | '</div>', |
| | width, height, tileUrl |
| | ) |
| | |
| | return frame:preprocess(mapHtml) |
| end | | end |
|
| |
|
| return p | | return p |