Module:Coordinates

-- IMPORTANT: This module should be considered UNSTABLE and not be used widely until further testing/bugfixing -- --[[ Module:Coordinates    Used to parse and convert between different RuneScape coordinate systems.

-- Usage --- INVOKE: - The type of coords to convert to (pixel, dev or clue) (required) - The coords to convert or page to get coords from (required) - The mapid (opt - default: worldmap) Returns the converted coordinates Example:

LUA FUNCTIONS: .create(input, mapid, smw) input - Set of coords to create from, can be a table or string. mapid - Id of map, used for converting dev coords (opt - def: worldmap) smw - Whether to use SMW to get coords from page (opt - def: false)

Returns a Coordinates object with the following functions: :pixel Returns a table with pixel coordinates: x, y

Returns a table with dev coordinates: x1, y1, x2, y2

Returns a table with clue coordinates: dy, my, diry, dx, mx, dirx diry and dirx represent the direction in the Y and X axis for diry, -1 represents North, 1 represents South for dirx, -1 represents East, 1 represents West

Calling tostring on any of the above functions will return a string representation of the coordinates.

Coordinates object also has the following properties: mapid = mapid that was returned from SMW or passed in     fromsmw = true if the coords were parsed from SMW data, otherwise false

Example: local Coordinates = require('Module:Coordinates') local someCoords = Coordinates.create('31.4N, 18.2E') print(someCoords:pixel.x) --prints X pixel coord print(someCoords:pixel.y) --prints Y pixel coord print(someCoords:clue.dy) --prints Y-degrees clue coord print(tostring(someCoords:dev)) --print the dev coordinates as a string (See Module:CoordinateMap for a real example of this module in use) ]]--

-- Constants --- -- Dev coords offset data local DATA = mw.loadData('Module:Coordinates/data') -- magic numbers that are necessary for clue coords to work local OBSERVATORY_X = 1572 local OBSERVATORY_Y = 3993 local PIXELS_PER_MIN = 0.46875 --1.875 mins per tile / 4 pixels per tile

-- Private functions --- -- Shallow copy certain keys from a table -- ALL keys must be present in the table or returns nil function extract(t, keys) local ret = {} if type(keys) ~= 'table' then keys = {keys} end if type(t) == 'table' then for i = 1, #keys do     local v = t[keys[i]] if v ~= nil then ret[keys[i]] = v     else return nil -- Return nil if a key wasn't found end end return ret end end

-- Copy a table while preserving the original metatable -- Might be a better way to do this, idk function mtCopy(t, newt) local mt = {} for k, v in pairs(getmetatable(t)) do mt[k] = v end return setmetatable(newt, mt) end

-- Calls tonumber on all arguments function tonumbers(...) local ret = {} for i = 1, #arg do   table.insert(ret, tonumber(arg[i])) end return unpack(ret) end

-- Conversion functions -- -- The math shown below is probably awful -- Direct clue2dev, dev2clue functions would be useful for efficiency

-- Converts from clue coords to pixel coords -- Table must have elements: dy, my, diry, dx, mx, dirx -- diry should be 1 for E, -1 for W; dirx should be 1 for S, -1 for N function clue2pixel(t) local valy = t.diry*(t.dy*60 + t.my) local valx = t.dirx*(t.dx*60 + t.mx) return { x = OBSERVATORY_X + valx/PIXELS_PER_MIN, y = OBSERVATORY_Y + valy/PIXELS_PER_MIN } end

-- Converts from pixel coords to clue coords -- Table must have elements: x, y function pixel2clue(t) local fy = t.y - OBSERVATORY_Y local fx = t.x - OBSERVATORY_X local diry = fy > 0 and 1 or -1 local dirx = fx > 0 and 1 or -1 local by = math.abs(PIXELS_PER_MIN * fy) local bx = math.abs(PIXELS_PER_MIN * fx) return { dy = math.floor(by/60), my = by%60, diry = diry, dx = math.floor(bx/60), mx = bx%60, dirx = dirx } end

-- Converts from dev coords to pixel coords -- Table must have elements: x1, y1, x2, y2 -- Also requires mapid (default: 'worldmap') function dev2pixel(t, mapid) mapid = mapid or 'worldmap' if DATA[mapid] ~= nil then local offsetx = DATA[mapid].devoffsetx local offsety = DATA[mapid].devoffsety if offsetx ~= nil and offsety ~= nil then return { x = 4*(t.x1*64 + t.x2) - offsetx, y = -4*(t.y1*64 + t.y2) - offsety }   end end end

-- Converts from pixel to dev coords -- Table must have elements: x, y -- Also requires mapid (default: 'worldmap') function pixel2dev(t, mapid) mapid = mapname or 'worldmap' if DATA[mapid] ~= nil then local offsetx = DATA[mapid].devoffsetx local offsety = DATA[mapid].devoffsety if offsetx ~= nil and offsety ~= nil then local bx = (t.x + offsetx)/4 local by = (t.y + offsety)/-4 return { x1 = math.floor(bx/64), y1 = math.floor(by/64), x2 = bx%64, y2 = by%64 }   end end end

-- Parse a string representation of a set of coords into its constituent values --  str = the string to parse, --  smw = whether to try and get SMW data from the string if coords couldn't --         be parsed normally (default: false), -- Returns a table of parsed values, and the type of match that was made --  (type can be one of: 'clue', 'dev', 'pixel', 'smw') --  (returns nil if couldn't parse) -- E.G. local coords, type = parseString('123,456') -- result: type = 'pixel', coords = {x=123, y=456} function parseString(str, smw) local ret = {} str = string.gsub(str, '%s', '') -- Remove spaces local ctype if string.match(str, '%d+%.%d+[NS],%d+%.%d+[EW]') then -- Clue coords ret.dy, ret.my, ret.dx, ret.mx = tonumbers(string.match(str,'(%d+)%.(%d+)[NS],(%d+)%.(%d+)[EW]')) local diry, dirx = string.match(str,'%d+%.%d+([NS]),%d+%.%d+([EW])') ret.diry = diry == 'N' and -1 or 1 ret.dirx = dirx == 'W' and -1 or 1 ctype = 'clue' elseif string.match(str, '%d+,%d+,%d+,%d+') then -- Dev coords ret.x1, ret.y1, ret.x2, ret.y2 = tonumbers(string.match(str, '(%d+),(%d+),(%d+),(%d+)')) ctype = 'dev' elseif string.match(str, '%d+,%d+') then -- Pixel coords ret.x, ret.y = tonumbers(string.match(str, '(%d+),(%d+)')) ctype = 'pixel' elseif smw then -- Page reference local frame = mw.getCurrentFrame local smwresult = string.gsub(frame:callParserFunction('#show', { str, '?Map coordinates', '?Map', format = 'list', sep = ';' }), '%s', '') -- get smw data and strip whitespace local x, y, map = string.match(smwresult, '(%d+),(%d+).*%((%a+).*%)') if x and x ~=  and y and y ~=  and map and map ~= '' then ret.x = tonumber(x) ret.y = tonumber(y) ret.mapid = map ctype = 'smw' end end return ctype and ret, ctype or nil end

-- Public LUA Coordinates class local Coordinates = {} Coordinates.__index = Coordinates

-- Create a coordinates object with a given table or string -- returns nil if table or string couldn't be parsed function Coordinates.create(input, mapid, smw) local self = setmetatable({}, Coordinates) if type(input) == 'string' then -- Input is string local ctype input, ctype = parseString(input, smw) -- Convert to table self.fromsmw = (ctype and ctype == 'smw') and true or false end -- Parsing failed or nil/nonstring/nontable was passed in if input == nil or type(input) ~= 'table' or next(input) == nil then return nil end

self._pixel = setmetatable(extract(input, {'x', 'y'}) or {}, {   __tostring = function(t)      return string.format('%d, %d', t.x, t.y)    end  }) self._dev = setmetatable(extract(input, {'x1', 'y1', 'x2', 'y2'}) or {}, {   __tostring = function(t)      return string.format('%d, %d, %d, %d', t.x1, t.y1, t.x2, t.y2)   end  }) self._clue = setmetatable(   extract(input, {'dy', 'my', 'diry', 'dx', 'mx', 'dirx'}) or {},    {      __tostring = function(t)      return string.format('%d.%d %s, %d.%d %s', t.dy or 0, t.my or 0, t.diry == -1 and 'N' or 'S', t.dx or 0, t.mx or 0, t.dirx == -1 and 'W' or 'E')   end    }) self.mapid = extract(input, 'mapid') or mapid or 'worldmap'

-- If no full sets of coords could be parsed, return nil if not (next(self._pixel) or next(self._dev) or next(self._clue)) then return nil end return self end

-- Get pixel coords function Coordinates:pixel if next(self._pixel) == nil then if next(self._clue) ~= nil then self._pixel = mtCopy(self._pixel, clue2pixel(self._clue)) elseif next(self._dev) ~= nil then self._pixel = mtCopy(self._pixel, dev2pixel(self._dev, self.mapid)) end end return self._pixel end

-- Get dev coords function Coordinates:dev if next(self._dev) == nil then self._dev = mtCopy(self._dev, pixel2dev(self:pixel, self.mapid)) end return self._dev end

-- Get clue coords function Coordinates:clue if next(self._clue) == nil then self._clue = mtCopy(self._clue, pixel2clue(self:pixel)) end return self._clue end

-- Static INVOKE function -- -- Convert coords to a given type function Coordinates.convert(frame) if not frame or not frame.args then error('Coordinates.convert couldn\'t find the frame. This is an invoke '..     'function; if you\'re calling this function in LUA, you probably want '..      'the :pixel, :dev or :clue functions') end

local ctype = frame.args[1] local str = frame.args[2] local mapid = frame.args[3] or 'worldmap' if not ctype or (ctype ~= 'pixel' and ctype ~= 'dev' and ctype ~= 'clue') then error('Coordinates.convert requires a type passed as the first '..     'parameter. This can be one of: pixel, dev, clue') end if not str then error('Coordinates.convert requires a set of coordinates passed as the '..     'second parameter') end

local coords = Coordinates.create(str, mapid, true) if coords then if ctype == 'pixel' then return coords:pixel elseif ctype == 'dev' then return coords:dev elseif ctype == 'clue' then return coords:clue end else error(string.format('Coordinates.convert couldn\'t parse the '.. 'coordinates "%s"', str)) end end

-- Helper for getting dev coords function Coordinates.calculateDevCoords(frame) local rargs = (frame or {}).args or {} local coords = Coordinates.create(rargs[1], 'none'):pixel local ecoords = Coordinates.create(rargs[2], 'none'):pixel return mw.html.create('pre') :wikitext(string.format('devcoordsx = %s,\ndevcoordsy = %s', (coords.x - ecoords.x), (coords.y - ecoords.y))) end

return Coordinates