<?xml version="1.0"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en">
	<id>https://wiki.runerealm.org/index.php?action=history&amp;feed=atom&amp;title=Module%3AExchange%2FMaple_bird_house</id>
	<title>Module:Exchange/Maple bird house - Revision history</title>
	<link rel="self" type="application/atom+xml" href="https://wiki.runerealm.org/index.php?action=history&amp;feed=atom&amp;title=Module%3AExchange%2FMaple_bird_house"/>
	<link rel="alternate" type="text/html" href="https://wiki.runerealm.org/index.php?title=Module:Exchange/Maple_bird_house&amp;action=history"/>
	<updated>2026-04-30T22:43:55Z</updated>
	<subtitle>Revision history for this page on the wiki</subtitle>
	<generator>MediaWiki 1.41.1</generator>
	<entry>
		<id>https://wiki.runerealm.org/index.php?title=Module:Exchange/Maple_bird_house&amp;diff=35040&amp;oldid=prev</id>
		<title>Alex: Created page with &quot;local hc = require(&#039;Module:Paramtest&#039;).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 = &#039;mapframe&#039;,   width = 300,   height = 300,   zoom = 2,   mapID = 0, -- RuneScape surface   x = 3233, -- Lumbridge lodestone   y = 3222,   plane = 0,   align = &#039;center&#039;,   -- Feat...&quot;</title>
		<link rel="alternate" type="text/html" href="https://wiki.runerealm.org/index.php?title=Module:Exchange/Maple_bird_house&amp;diff=35040&amp;oldid=prev"/>
		<updated>2024-10-16T23:12:39Z</updated>

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