Module:Map
Jump to navigation
Jump to search
Module documentation
This documentation is transcluded from Module:Map/doc. [edit] [history] [purge]
Module:Map's function map is invoked by Template:Map.
Module:Map requires Module:Paramtest.
Module:Map is required by Module:Clues.
Module:Map is required by Module:Sandbox/User:Als Toy Barn/ShopLocLine.
map(frame) | |||
The main entry point for templates and pages. Should only be called via {{#invoke}} outside of a module. | |||
Argument | Type | Description | Optional |
frame | frame object | The frame object automatically passed via {{#invoke}} . | |
Returns | string | A fully rendered map. |
buildMap(args) | |||
The main entry point for other modules. | |||
Argument | Type | Description | Optional |
args | table | Any map or feature arguments. | |
Returns | string | A fully rendered map. |
getMap(name, mapOpts) | |||
Pulls a map from SMW. | |||
Argument | Type | Description | Optional |
name | string | The page name to use in the look up. Append the version name the map is defined under (e.g., #Version1 or #Version2 ) if there is one. | |
mapOpts | table | Map options to control the map behaviour. | |
Returns | string | A fully rendered map. |
local hc = require('Module:Paramtest').has_content
-- Package
local p = {}
-- Feature functions
local feat = {}
local zoomRatios = {
{ 3, 8 },
{ 2, 4 },
{ 1, 2 },
{ 0, 1 },
{ -1, 1/2 },
{ -2, 1/4 },
{ -3, 1/8 }
}
-- Default arg values
local defaults = {
-- Map options
type = 'mapframe',
width = 300,
height = 300,
zoom = 2,
mapID = 0, -- RuneScape surface
x = 3233, -- Lumbridge lodestone
y = 3222,
plane = 0,
align = 'center',
-- Feature options
mtype = 'pin',
-- Rectangles, squares, circles
radius = 10,
-- Dots
fill = '#ffffff',
-- Pins
icon = 'greenPin',
iconSize = 25,
iconAnchor = 0,
popupAnchor = 0,
group = 'pins',
-- Text
position = 'top'
}
local mtypes = {
singlePoint = { pin=true, rectangle=true, square=true, circle=true, dot=true, text=true },
multiPoint = { polygon=true, line=true }
}
-- Named-only arguments
local namedOnlyArgs = { type=true, width=true, height=true, zoom=true, mapID=true, align=true, caption=true, text=true, nopreprocess=true, smw=true, smwName=true, plainTiles=true, mapVersion=true }
-- Anonymous feature options that should be removed for comma separation
local optsWithCommas = { 'iconSize', 'iconAnchor', 'popupAnchor' }
-- Optional feature properties
local properties = {
any = { title='string', desc='string' },
line = { stroke=true, ['stroke-opacity']=true, ['stroke-width']=true },
polygon = { stroke=true, ['stroke-opacity']=true, ['stroke-width']=true, fill=true, ['fill-opacity']=true },
dot = { fill=true },
pin = { icon=true, iconWikiLink=true, iconSize=true, iconAnchor=true, popupAnchor=true },
text = {}
}
-- Template entry point
function p.map(frame)
return p.buildMap(frame:getParent().args)
end
-- Module entry point to get completed map element
function p.buildMap(_args)
local args = {}
for k, v in pairs(_args) do
args[k] = v
end
local features, mapOpts = p.parseArgs(args)
return p.buildMapFromOpts(features, mapOpts)
end
-- Build full GeoJSON and insert into HTML
-- Can be used to turn Location JSON into completed map
function p.buildMapFromOpts(features, mapOpts)
local noPreprocess = mapOpts.nopreprocess
local collection = {}
if #features > 0 then
collection = {
type = 'FeatureCollection',
features = features
}
end
local map = createMapElement(mapOpts, collection)
if noPreprocess then
return tostring(map)
end
return mw.getCurrentFrame():preprocess(tostring(map))
end
-- Create map HTML element
function createMapElement(mapOpts, collection)
local mapElem = mw.html.create(mapOpts.type)
mapOpts.x = math.floor(mapOpts.x)
mapOpts.y = math.floor(mapOpts.y)
-- Remove unnecessary values
mapOpts.type = nil
mapOpts.range = nil
mapOpts.maxPinY = nil
mapOpts.nopreprocess = nil
mapElem:attr(mapOpts):newline():wikitext(toJSON(collection)):newline()
-- Set mapOpts in SMW so queries can rebuild with #buildMapFromOpts
-- Need to remove these opts just before setting
if hc(mapOpts.smw) then
local smwOpts = {
x = mapOpts.x,
y = mapOpts.y,
mapID = mapOpts.mapID,
plane = mapOpts.plane,
zoom = mapOpts.zoom
}
parseSMW(mapOpts, smwOpts)
end
return mapElem
end
-- Parse all arguments
function p.parseArgs(args)
local features = {}
local mapOpts = p.parseMapArgs(args)
-- Parse anon args and add features to table
local anonFeatures = p.parseAnonArgs(args, mapOpts)
combineTables(features, anonFeatures)
if #anonFeatures == 0 and hc(args.mtype) then
-- Parse named args and add feature to table
local namedFeature = p.parseNamedArgs(args, mapOpts)
table.insert(features, namedFeature)
end
if #features == 0 then
mapOpts.range = {
xMin = mapOpts.x or defaults.x,
xMax = mapOpts.x or defaults.x,
yMin = mapOpts.y or defaults.y,
yMax = mapOpts.y or defaults.y
}
end
calculateView(args, mapOpts)
return features, mapOpts
end
function calculateView(args, mapOpts)
if not tonumber(args.x) then
mapOpts.x = math.floor((mapOpts.range.xMax + mapOpts.range.xMin) / 2)
else
mapOpts.x = args.x
end
if not tonumber(args.y) then
mapOpts.y = math.floor((mapOpts.range.yMax + mapOpts.range.yMin) / 2)
else
mapOpts.y = args.y
end
local width, height = mapOpts.width, mapOpts.height
if args.type == 'maplink' then
width, height = 800, 800
mapOpts.width = nil
mapOpts.height = nil
end
if not tonumber(args.zoom) then
local zoom, ratio = defaults.zoom, 1
local xRange = mapOpts.range.xMax - mapOpts.range.xMin
local yRange = mapOpts.range.yMax - mapOpts.range.yMin
-- Ensure space between outer-most points and view border
local bufferX, bufferY = width / 25, height / 25
for _, v in ipairs(zoomRatios) do
local sizeX, sizeY = width / v[2], height / v[2]
-- Check if the dynamic sizes are greater than the buffered ranges
if sizeX > xRange + bufferX and sizeY > yRange + bufferY then
zoom = v[1]
ratio = v[2]
break
end
end
if mapOpts.maxPinY then
-- Default pin height relative to zoom 1
local pinHeight = 40
-- Northern-most pin Y plus its dynamic height
local maxPinHeightY = mapOpts.maxPinY + (pinHeight / ratio)
-- New Y range using this value
local yRangeMaxPin = maxPinHeightY - mapOpts.range.yMin
if maxPinHeightY > mapOpts.range.yMax then
-- Move the view up by half the pin's dynamic height
mapOpts.y = mapOpts.y + (pinHeight / ratio / 2)
-- Zoom out if new range is too big
if yRangeMaxPin + bufferY > height / ratio then
zoom = zoom - 1
end
end
end
if zoom > defaults.zoom then
zoom = defaults.zoom
end
mapOpts.zoom = zoom
end
end
function adjustRange(coords, mapOpts)
for _, v in ipairs(coords) do
if v[1] > mapOpts.range.xMax then
mapOpts.range.xMax = v[1]
end
if v[1] < mapOpts.range.xMin then
mapOpts.range.xMin = v[1]
end
if v[2] > mapOpts.range.yMax then
mapOpts.range.yMax = v[2]
end
if v[2] < mapOpts.range.yMin then
mapOpts.range.yMin = v[2]
end
end
end
-- Parse named map arguments
function p.parseMapArgs(args)
local opts = {
type = ternary(hc(args.type), args.type, defaults.type),
x = ternary(hc(args.x), args.x, defaults.x),
y = ternary(hc(args.y), args.y, defaults.y),
width = ternary(hc(args.width), args.width, defaults.width),
height = ternary(hc(args.height), args.height, defaults.height),
mapID = ternary(hc(args.mapID), args.mapID, defaults.mapID),
plane = ternary(hc(args.plane), args.plane, defaults.plane),
zoom = ternary(hc(args.zoom), args.zoom, defaults.zoom),
align = ternary(hc(args.align), args.align, defaults.align),
nopreprocess = args.nopreprocess,
smw = args.smw,
smwName = args.smwName,
range = {
xMin = 10000000,
xMax = -10000000,
yMin = 10000000,
yMax = -10000000
}
}
-- Feature grouping across map instances
if hc(args.group) then
opts.group = args.group
end
-- Plain map tiles
if hc(args.plainTiles) then
opts.plainTiles = 'true'
end
-- Alternate map tile version
if hc(args.mapVersion) then
opts.mapVersion = args.mapVersion
end
-- Map type
if hc(args.type) and args.type ~= 'mapframe' and args.type ~= 'maplink' then
mapError('Argument `type` must be either `mapframe`, `maplink`, or not provided')
end
-- Caption or link text
if args.type == 'maplink' then
if args.text then
if args.text:find('[%[%]]') then
mapError('Argument `text` cannot contain links')
end
opts.text = args.text
else
opts.text = 'Maplink'
end
elseif hc(args.caption) then
opts.text = args.caption
else
opts.frameless = ''
end
return opts
end
-- Parse named arguments
-- This is called per anon feature as well
function p.parseNamedArgs(_args, mapOpts)
local args = mw.clone(_args)
if not feat[args.mtype] then
mapError('Argument `mtype` has an unsupported value')
end
-- Use named X and Y as coords only if no other points
if #args.coords == 0 and args.x and args.y then
args.coords = { {
tonumber(args.x) or defaults.x,
tonumber(args.y) or defaults.y
} }
end
-- No feature if no coords
if not args.coords or #args.coords == 0 then
return nil
end
-- Save northern-most pin Y for later view adjustment
if args.mtype == 'pin' and not args.iconWikiLink then
if mapOpts.maxPinY then
if args.coords[1][2] > mapOpts.maxPinY then
mapOpts.maxPinY = args.coords[1][2]
end
else
mapOpts.maxPinY = args.coords[1][2]
end
end
-- Center all points of combo multi-point and line features
if (args.isInCombo and mtypes.multiPoint[args.mtype]) or args.mtype == 'line' then
for _, v in ipairs(args.coords) do
centeredCoords(v)
end
end
-- Handle range adjustment individually for these types
if not isCenteredPointFeature(args.mtype) then
adjustRange(args.coords, mapOpts)
end
if not mapOpts.group and args.mtype == 'pin' then
mapOpts.group = args.group or defaults.group
end
-- This key must match a key found in `defaults`
parseIconXYArg(args, 'iconSize')
parseIconXYArg(args, 'iconAnchor')
parseIconXYArg(args, 'popupAnchor')
args.desc = parseDesc(args)
local featJson = feat[args.mtype](args, mapOpts)
-- Set feature in SMW
if hc(mapOpts.smw) then
parseSMW(mapOpts, featJson)
end
return featJson
end
-- Parse icon X/Y argument
function parseIconXYArg(args, key)
if hc(args[key]) then
if args[key]:find(',') then
local xy = mw.text.split(args[key], '%s*,%s*')
args[key] = { tonumber(xy[1]) or defaults[key], tonumber(xy[2]) or defaults[key] }
else
args[key] = { tonumber(args[key]) or defaults[key], tonumber(args[key]) or defaults[key] }
end
elseif hc(args[key..'X']) and hc(args[key..'Y']) then
args[key] = { tonumber(args[key..'X']), tonumber(args[key..'Y']) }
end
end
-- Parse anonymous arguments and add to the features table
-- Note 1: Anon X/Y coords generate anon features
-- Note 2: "Repeatable" means a feature that can be created once for each X/Y
function p.parseAnonArgs(args, mapOpts)
local features = {}
local i = 1
-- Collect unusable anon coords for use by named feature
args.coords = {}
while args[i] do
local arg = mw.text.trim(args[i])
if hc(arg) then
local anonOpts = { coords = {} }
local rawOpts = {}
-- Track all X and Y to find mismatches
local xyCount = 0
-- Remove opts with commas manually
-- Big workaround for Lua not supporting positive lookaheads
for _, opt in ipairs(optsWithCommas) do
arg = arg:gsub(opt..':%d+,%d+', function(s)
table.insert(rawOpts, s)
return ''
end)
end
-- Temporarily replace escaped commas for use in text opts
arg = arg:gsub('\\,', '**')
-- Split arg into options by "," and put extra commas back
for opt in mw.text.gsplit(arg, '%s*,%s*') do
opt = opt:gsub('%*%*', ',')
table.insert(rawOpts, opt)
end
for _, opt in ipairs(rawOpts) do
if hc(opt) then
-- Temporarily replace escaped colons for use in text opts
opt = opt:gsub('\\:', '^^')
-- Split option into key/value by ":"
local kv = mw.text.split(opt, '%s*:%s*')
-- If option is a value with no key, assume it's a standalone X or Y
if #kv == 1 then
xyCount = xyCount + 1
addXYToCoords(anonOpts.coords, kv[1])
else
if namedOnlyArgs[kv[1]] then
mapError('Anonymous option `'..kv[1]..'` can only be used as a named argument')
-- Add X/Y pair
elseif tonumber(kv[1]) and tonumber(kv[2]) then
xyCount = xyCount + 2
table.insert(anonOpts.coords, { tonumber(kv[1]), tonumber(kv[2]) })
-- Add individual X or Y
elseif kv[1] == 'x' or kv[1] == 'y' then
xyCount = xyCount + 1
addXYToCoords(anonOpts.coords, kv[2])
else
-- Put extra colons back
kv[2] = kv[2]:gsub('%^%^', ':')
anonOpts[kv[1]] = mw.text.trim(kv[2])
end
end
end
end
if xyCount % 2 > 0 then
mapError('Feature contains mismatched coordinates')
end
-- Named args are applied to all anon features if not specified
-- An anon feature opts take precedence over named args
for k, v in pairs(args) do
if not tonumber(k) and
k ~= 'x' and k ~= 'y' and
not namedOnlyArgs[k] and
not anonOpts[k] then
anonOpts[k] = v
end
end
if not anonOpts.mtype then
if #anonOpts.coords > 0 then
-- Save coord without an mtype to apply to map view X/Y
table.insert(args.coords, anonOpts.coords[1])
end
elseif mtypes.singlePoint[anonOpts.mtype] then
if #anonOpts.coords == 0 then
mapError('Anonymous `'..anonOpts.mtype..'` feature must have at least one point')
end
addFeaturePerCoord(features, anonOpts, mapOpts)
elseif mtypes.multiPoint[anonOpts.mtype] then
parseMultiPointFeature(features, anonOpts, mapOpts, true, args)
elseif anonOpts.mtype:find('-') then
parseComboFeature(features, anonOpts, mapOpts, true, args)
end
end
i = i + 1
end
if #args.coords > 0 then
-- Use first coord without mtype as map view X/Y
if not args.mtype then
mapOpts.x = args.coords[1][1]
mapOpts.y = args.coords[1][2]
elseif mtypes.singlePoint[args.mtype] then
addFeaturePerCoord(features, args, mapOpts)
elseif mtypes.multiPoint[args.mtype] then
parseMultiPointFeature(features, args, mapOpts, false)
elseif args.mtype:find('-') then
parseComboFeature(features, args, mapOpts, false)
end
end
return features
end
-- Add individual X or Y to next coord set
-- Handles coords split by commas (e.g., `|1000,2000`)
function addXYToCoords(coords, value)
local xy = coords[#coords]
if xy and #xy == 1 then
local y = tonumber(value) or defaults.y
table.insert(xy, y)
else
local x = tonumber(value) or defaults.x
table.insert(coords, { x })
end
end
-- Parse opts to build multi-point feature
function parseMultiPointFeature(features, opts, mapOpts, isAnon, namedArgs)
-- Anon multi-point can't have 0 coords
if isAnon and #opts.coords == 0 then
mapError('Anonymous multi-point `'..opts.mtype..'` feature must have at least 1 point')
elseif isAnon and #opts.coords == 1 then
if not namedArgs.mtype then
mapError('Anonymous multi-point `'..opts.mtype..'` feature must have 2 or more points')
end
-- Single coord for multi-point isn't possible,
-- so save coord to apply to named feature
table.insert(namedArgs.coords, opts.coords[1])
-- Named multi-point can't have <2 coords
elseif not isAnon and #opts.coords < 2 then
mapError('Named multi-point `'..opts.mtype..'` feature must have 2 or more points')
else
local feature = p.parseNamedArgs(opts, mapOpts)
table.insert(features, feature)
end
end
-- Parse opts to build multi-point feature
function parseComboFeature(features, opts, mapOpts, isAnon, namedArgs)
local combo = mw.text.split(opts.mtype, '-')
if #combo ~= 2 or not mtypes.singlePoint[combo[1]] or not mtypes.multiPoint[combo[2]] then
mapError('Feature `'..opts.mtype..'` is not a single-point + multi-point combo')
end
if isAnon and #opts.coords == 0 then
mapError('Anonymous feature in `'..opts.mtype..'` combo must have at least 1 point')
elseif #opts.coords == 1 then
if isAnon then
if not namedArgs.mtype then
mapError('Anonymous feature `'..combo[2]..'` in `'..opts.mtype..'` combo must have 2 or more points')
else
-- Create single-point and also save to use with named multi-point
opts.mtype = combo[1]
local feature = p.parseNamedArgs(opts, mapOpts)
table.insert(features, feature)
table.insert(namedArgs.coords, opts.coords[1])
end
else
mapError('Named feature `'..combo[2]..'` in `'..opts.mtype..'` combo must have 2 or more points')
end
else
-- Create all anon single-points
if isAnon then
opts.mtype = combo[1]
addFeaturePerCoord(features, opts, mapOpts)
end
-- Create named multi-point
opts.mtype = combo[2]
opts.isInCombo = true
local feature = p.parseNamedArgs(opts, mapOpts)
table.insert(features, feature)
end
end
-- Add feature per coordinate provided
function addFeaturePerCoord(features, opts, mapOpts)
local tempOpts = mw.clone(opts)
for _, v in ipairs(opts.coords) do
tempOpts.coords = { v }
local feature = p.parseNamedArgs(tempOpts, mapOpts)
table.insert(features, feature)
end
end
function feat.rectangle(featOpts, mapOpts)
local x, y = featOpts.coords[1][1], featOpts.coords[1][2]
local r = tonumber(featOpts.r)
local rectX = tonumber(featOpts.rectX or featOpts.squareX) or defaults.radius * 2
local rectY = tonumber(featOpts.rectY or featOpts.squareY) or defaults.radius * 2
if hc(r) and r % 1 > 0 then
x = x + 0.5
y = y + 0.5
end
local rectXR = r or math.floor(rectX / 2) or defaults.radius
local rectYR = r or math.floor(rectY / 2) or defaults.radius
local xLeft = x - rectXR
local xRight = x + rectXR
local yTop = y + rectYR
local yBottom = y - rectYR
if rectX % 2 > 0 then
xRight = x + (rectXR + 1)
end
if rectY % 2 > 0 then
yTop = y + (rectYR + 1)
end
local corners = {
{ xLeft, yBottom },
{ xLeft, yTop },
{ xRight, yTop },
{ xRight, yBottom }
}
local featJson = {
type = 'Feature',
properties = {
mapID = featOpts.mapID or mapOpts.mapID,
plane = featOpts.plane or defaults.plane
},
geometry = {
type = 'Polygon',
coordinates = { corners }
}
}
adjustRange(corners, mapOpts)
setProperties(featJson, featOpts, 'polygon')
return featJson
end
-- Create a square/rectangle feature
function feat.square(featOpts, mapOpts)
return feat.rectangle(featOpts, mapOpts)
end
-- Create a polygon feature
function feat.polygon(featOpts, mapOpts)
local points = {}
local lastPoint = featOpts.coords[#featOpts.coords]
for _, v in ipairs(featOpts.coords) do
table.insert(points, { v[1], v[2] })
end
-- Close polygon
if not (points[1][1] == lastPoint[1] and points[1][2] == lastPoint[2]) then
table.insert(points, { points[1][1], points[1][2] })
end
local featJson = {
type = 'Feature',
properties = {
mapID = featOpts.mapID or mapOpts.mapID,
plane = featOpts.plane or defaults.plane
},
geometry = {
type = 'Polygon',
coordinates = { points }
}
}
setProperties(featJson, featOpts, 'polygon')
return featJson
end
-- Create a line feature
function feat.line(featOpts, mapOpts)
local featJson = {
type = 'Feature',
properties = {
shape = 'Line',
mapID = featOpts.mapID or mapOpts.mapID,
plane = featOpts.plane or defaults.plane
},
geometry = {
type = 'LineString',
coordinates = featOpts.coords
}
}
setProperties(featJson, featOpts, 'line')
return featJson
end
-- Create a circle feature
function feat.circle(featOpts, mapOpts)
local radius = tonumber(featOpts.r) or defaults.radius
local featJson = {
type = 'Feature',
properties = {
shape = 'Circle',
radius = radius,
mapID = featOpts.mapID or mapOpts.mapID,
plane = featOpts.plane or defaults.plane
},
geometry = {
type = 'Point',
coordinates = featOpts.coords[1]
}
}
local corners = {
{ featOpts.coords[1][1] - radius, featOpts.coords[1][2] - radius },
{ featOpts.coords[1][1] - radius, featOpts.coords[1][2] + radius },
{ featOpts.coords[1][1] + radius, featOpts.coords[1][2] - radius },
{ featOpts.coords[1][1] + radius, featOpts.coords[1][2] + radius }
}
adjustRange(corners, mapOpts)
setProperties(featJson, featOpts, 'polygon')
return featJson
end
-- Create a dot feature
function feat.dot(featOpts, mapOpts)
local featJson = {
type = 'Feature',
properties = {
shape = 'Dot',
mapID = featOpts.mapID or mapOpts.mapID,
plane = featOpts.plane or defaults.plane,
fill = featOpts.fill or defaults.fill,
},
geometry = {
type = 'Point',
coordinates = centeredCoords(featOpts.coords[1])
}
}
setProperties(featJson, featOpts, 'dot')
return featJson
end
-- Create a pin feature
function feat.pin(featOpts, mapOpts)
local featJson = {
type = 'Feature',
properties = {
providerID = 0,
mapID = featOpts.mapID or mapOpts.mapID,
plane = featOpts.plane or defaults.plane,
group = featOpts.group or defaults.group
},
geometry = {
type = 'Point',
coordinates = centeredCoords(featOpts.coords[1])
}
}
if hc(featOpts.iconWikiLink) then
featOpts.iconWikiLink = mw.ext.GloopTweaks.filepath(featOpts.iconWikiLink)
else
featOpts.icon = ternary(hc(featOpts.icon), featOpts.icon, defaults.icon)
end
setProperties(featJson, featOpts, 'pin')
return featJson
end
-- Create a text feature
function feat.text(featOpts, mapOpts)
if not featOpts.label then
mapError('Argument `label` missing on text feature')
end
local featJson = {
type = 'Feature',
properties = {
shape = 'Text',
label = featOpts.label,
direction = featOpts.position or defaults.position,
class = featOpts.class or 'lbl-bg-grey',
mapID = featOpts.mapID or mapOpts.mapID,
plane = featOpts.plane or defaults.plane
},
geometry = {
type = 'Point',
coordinates = centeredCoords(featOpts.coords[1])
}
}
setProperties(featJson, featOpts, 'text')
return featJson
end
-- Create feature description
function parseDesc(args)
local pageName = mw.title.getCurrentTitle().text
local coordsStr = 'X/Y: '..math.floor(args.coords[1][1])..','..math.floor(args.coords[1][2])
local coordsElem = mw.html.create('p')
:wikitext(coordsStr)
:attr('style', 'font-size:10px; margin:0px;')
if args.ptype == 'item' or
args.ptype == 'monster' or
args.ptype == 'npc' or
args.ptype == 'object' then
local tableElem = mw.html.create('table')
:addClass('wikitable')
:attr('style', 'font-size:12px; text-align:left; margin:0px; width:100%;')
if args.ptype == 'item' then
addTableRow(tableElem, 'Item', args.name or pageName)
addTableRow(tableElem, 'Quantity', args.qty or 1)
if hc(args.respawn) then
addTableRow(tableElem, 'Respawn time', args.respawn)
end
elseif args.ptype == 'monster' then
addTableRow(tableElem, 'Monster', args.name or pageName)
if hc(args.levels) then
addTableRow(tableElem, 'Level(s)', args.levels)
end
if hc(args.respawn) then
addTableRow(tableElem, 'Respawn time', args.respawn)
end
elseif args.ptype == 'npc' then
addTableRow(tableElem, 'NPC', args.name or pageName)
if hc(args.levels) then
addTableRow(tableElem, 'Level(s)', args.levels)
end
if hc(args.respawn) then
addTableRow(tableElem, 'Respawn time', args.respawn)
end
elseif args.ptype == 'object' then
addTableRow(tableElem, 'Object', args.name or pageName)
end
if hc(args.id) then
addTableRow(tableElem, 'ID', args.id)
end
return tostring(tableElem)..tostring(coordsElem)
end
local desc = ''
if hc(args.desc) then
desc = args.desc
end
return desc..tostring(coordsElem)
end
-- Add row to table element
function addTableRow(table, label, value)
local row = table:tag('tr')
row:tag('td'):wikitext("'''"..label.."'''")
row:tag('td'):wikitext(value)
end
-- Move coords to tile center
function centeredCoords(coords)
for k, v in ipairs(coords) do
coords[k] = v + 0.5
end
return coords
end
-- Set GeoJSON properties
-- If an option exists in the allowed feature props, add it
function setProperties(featJson, opts, mtype)
for k, v in pairs(opts) do
if properties[mtype][k] or properties.any[k] then
if k == 'desc' then
featJson.properties.description = v
else
-- If marked as string, use value as is, otherwise try number
featJson.properties[k] = ternary(properties.any[k] == 'string', v, tonumber(v) or v)
end
end
end
end
-- Parse SMW args
function parseSMW(args, data)
if args.smw:lower() == 'yes' then
if not hc(args.smwName) then
setSMW(data, 'Location JSON')
else
setSMW(data, 'Location JSON', args.smwName)
end
elseif args.smw:lower() == 'hist' then
setSMW(data, 'Historic Location JSON')
end
end
-- Create SMW entry
function setSMW(obj, prop, subobjectName)
if not subobjectName then
mw.smw.set({ [prop] = toJSON(obj) })
else
mw.smw.subobject({ [prop] = toJSON(obj) }, subobjectName)
end
end
-- Helper function to get rendered map through SMW lookup
function p.getMap(name, _mapOpts)
local features, mapOpts = {}, {}
local query = {
'?Location JSON',
'limit=1'
}
if name then
table.insert(query, string.format('[[%s]][[Location JSON::+]]', name))
else
return nil
end
local results = mw.smw.ask(query) or {}
local page = results[1]
local entries = page and page['Location JSON']
if not entries then
return nil
end
for _, entry in ipairs(entries) do
local data = mw.text.jsonDecode(entry)
if data.type then
table.insert(features, data)
else
mapOpts = data
end
end
if #features > 0 then
for k, v in pairs(_mapOpts or {}) do
mapOpts[k] = v
end
return p.buildMapFromOpts(features, mapOpts)
else
return nil
end
end
-- Test if feature is based on a center point with calculated size
function isCenteredPointFeature(mtype)
return
mtype == 'rectangle' or
mtype == 'square' or
mtype == 'circle'
end
-- Add all elements of table 2 to table 1
function combineTables(table1, table2)
for _, v in ipairs(table2) do
table.insert(table1, v)
end
end
-- Create JSON
function toJSON(val)
local good, json = pcall(mw.text.jsonEncode, val)
if good then
return json
end
mapError('Error converting value to JSON')
end
-- Makeshift ternary operator
function ternary(condition, a, b)
if condition then
return a
else
return b
end
end
-- Produce an error
function mapError(message)
error('[Module:Map] '..message, 0)
end
return p