-- <nowiki>
-- IMPORTANT: This module should be considered UNSTABLE and not be used widely
-- until further testing/bugfixing --
		Used to parse and convert between different RuneScape coordinate systems
		This is a metamodule for use in other LUA modules, and should not be
-- Usage -----------------------------------------------------------------------
		.create(input, mapid, smw)
			input - Set of coords to create from, can be a table or string.
			mapid - Id of map, used for 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:
			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, otherwise false
			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 coords 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
				return nil -- Return nil if a key wasn't found
		return ret
-- 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)
-- Calls tonumber() on all arguments
function tonumbers(...)
	local ret = {}
	for i = 1, #arg do
		table.insert(ret, tonumber(arg[i]))
	return unpack(ret)
-- 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 +
	local valx = t.dirx*(t.dx*60 +
	return {
-- 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
-- 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
-- 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
-- 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.dx, =
		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:preprocess(
			'|?Map coordinates|?Map name|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'
	return ctype and ret, ctype or nil
-- 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
	-- Parsing failed or nil/nonstring/nontable was passed in
	if input == nil or type(input) ~= 'table' or next(input) == nil then
		return nil
	self._pixel = setmetatable(extract(input, {'x', 'y'}) or {}, {
		__tostring = function(t)
			return string.format('%d, %d', t.x, t.y)
	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)
	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, or 0, t.diry == -1 and 'N' or 'S',
				t.dx or 0, or 0, t.dirx == -1 and 'W' or 'E')
	self.mapid = 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
	return self
-- 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))
	return self._pixel
-- Get dev coords
function Coordinates:dev()
	if next(self._dev) == nil then
		self._dev = mtCopy(self._dev, pixel2dev(self:pixel(), self.mapid))
	return self._dev
-- Get clue coords
function Coordinates:clue()
	if next(self._clue) == nil then
		self._clue = mtCopy(self._clue, pixel2clue(self:pixel()))
	return self._clue
return Coordinates

Ad blocker interference detected!

Wikia is a free-to-use site that makes money from advertising. We have a modified experience for viewers using ad blockers

Wikia is not accessible if you’ve made further modifications. Remove the custom ad blocker rule(s) and the page will load as expected.