--[[
Name: Astrolabe
Revision: $Rev: 107 $
$Date: 2009-08-05 08:34:29 +0100 (Wed, 05 Aug 2009) $
Author(s): Esamynn (esamynn at wowinterface.com)
Inspired By: Gatherer by Norganna
             MapLibrary by Kristofer Karlsson (krka at kth.se)
Documentation: http://wiki.esamynn.org/Astrolabe
SVN: http://svn.esamynn.org/astrolabe/
Description:
	This is a library for the World of Warcraft UI system to place
	icons accurately on both the Minimap and on Worldmaps.  
	This library also manages and updates the position of Minimap icons 
	automatically.  

Copyright (C) 2006-2008 James Carrothers

License:
	This library is free software; you can redistribute it and/or
	modify it under the terms of the GNU Lesser General Public
	License as published by the Free Software Foundation; either
	version 2.1 of the License, or (at your option) any later version.

	This library is distributed in the hope that it will be useful,
	but WITHOUT ANY WARRANTY; without even the implied warranty of
	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
	Lesser General Public License for more details.

	You should have received a copy of the GNU Lesser General Public
	License along with this library; if not, write to the Free Software
	Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA

Note:
	This library's source code is specifically designed to work with
	World of Warcraft's interpreted AddOn system.  You have an implicit
	licence to use this library with these facilities since that is its
	designated purpose as per:
	http://www.fsf.org/licensing/licenses/gpl-faq.html#InterpreterIncompat
]] -- WARNING!!!
-- DO NOT MAKE CHANGES TO THIS LIBRARY WITHOUT FIRST CHANGING THE LIBRARY_VERSION_MAJOR
-- STRING (to something unique) OR ELSE YOU MAY BREAK OTHER ADDONS THAT USE THIS LIBRARY!!!
local LIBRARY_VERSION_MAJOR = "Astrolabe-0.4"
local LIBRARY_VERSION_MINOR = tonumber(string.match("$Revision: 107 $", "(%d+)") or 1)

if not DongleStub then
    error(LIBRARY_VERSION_MAJOR .. " requires DongleStub.")
end
if not DongleStub:IsNewerVersion(LIBRARY_VERSION_MAJOR, LIBRARY_VERSION_MINOR) then
    return
end

local Astrolabe = {};

-- define local variables for Data Tables (defined at the end of this file)
local WorldMapSize, MinimapSize, ValidMinimapShapes;

function Astrolabe:GetVersion()
    return LIBRARY_VERSION_MAJOR, LIBRARY_VERSION_MINOR;
end

--------------------------------------------------------------------------------------------------------------
-- Config Constants
--------------------------------------------------------------------------------------------------------------

local configConstants = {
    MinimapUpdateMultiplier = true
}

-- this constant is multiplied by the current framerate to determine
-- how many icons are updated each frame
Astrolabe.MinimapUpdateMultiplier = 1;

--------------------------------------------------------------------------------------------------------------
-- Working Tables
--------------------------------------------------------------------------------------------------------------

Astrolabe.LastPlayerPosition = {0, 0, 0, 0};
Astrolabe.MinimapIcons = {};
Astrolabe.IconsOnEdge = {};
Astrolabe.IconsOnEdge_GroupChangeCallbacks = {};

Astrolabe.MinimapIconCount = 0
Astrolabe.ForceNextUpdate = false;
Astrolabe.IconsOnEdgeChanged = false;

-- This variable indicates whether we know of a visible World Map or not.  
-- The state of this variable is controlled by the AstrolabeMapMonitor library.  
Astrolabe.WorldMapVisible = false;

local AddedOrUpdatedIcons = {}
local MinimapIconsMetatable = {
    __index = AddedOrUpdatedIcons
}

--------------------------------------------------------------------------------------------------------------
-- Local Pointers for often used API functions
--------------------------------------------------------------------------------------------------------------

local twoPi = math.pi * 2;
local atan2 = math.atan2;
local sin = math.sin;
local cos = math.cos;
local abs = math.abs;
local sqrt = math.sqrt;
local min = math.min
local max = math.max
local yield = coroutine.yield
local next = next
local GetFramerate = GetFramerate

--------------------------------------------------------------------------------------------------------------
-- Internal Utility Functions
--------------------------------------------------------------------------------------------------------------

local function assert(level, condition, message)
    if not condition then
        error(message, level)
    end
end

local function argcheck(value, num, ...)
    assert(1, type(num) == "number", "Bad argument #2 to 'argcheck' (number expected, got " .. type(level) .. ")")

    for i = 1, select("#", ...) do
        if type(value) == select(i, ...) then
            return
        end
    end

    local types = strjoin(", ", ...)
    local name = string.match(debugstack(2, 2, 0), ": in function [`<](.-)['>]")
    error(string.format("Bad argument #%d to 'Astrolabe.%s' (%s expected, got %s)", num, name, types, type(value)), 3)
end

local function getContPosition(zoneData, z, x, y)
    if (z ~= 0) then
        zoneData = zoneData[z];
        x = x * zoneData.width + zoneData.xOffset;
        y = y * zoneData.height + zoneData.yOffset;
    else
        x = x * zoneData.width;
        y = y * zoneData.height;
    end
    return x, y;
end

--------------------------------------------------------------------------------------------------------------
-- General Utility Functions
--------------------------------------------------------------------------------------------------------------

function Astrolabe:ComputeDistance(c1, z1, x1, y1, c2, z2, x2, y2)
    --[[
	argcheck(c1, 2, "number");
	assert(3, c1 >= 0, "ComputeDistance: Illegal continent index to c1: "..c1);
	argcheck(z1, 3, "number", "nil");
	argcheck(x1, 4, "number");
	argcheck(y1, 5, "number");
	argcheck(c2, 6, "number");
	assert(3, c2 >= 0, "ComputeDistance: Illegal continent index to c2: "..c2);
	argcheck(z2, 7, "number", "nil");
	argcheck(x2, 8, "number");
	argcheck(y2, 9, "number");
	--]]

    z1 = z1 or 0;
    z2 = z2 or 0;

    local dist, xDelta, yDelta;
    if (c1 == c2 and z1 == z2) then
        -- points in the same zone
        local zoneData = WorldMapSize[c1];
        if (z1 ~= 0) then
            zoneData = zoneData[z1];
        end
        xDelta = (x2 - x1) * zoneData.width;
        yDelta = (y2 - y1) * zoneData.height;

    elseif (c1 == c2) then
        -- points on the same continent
        local zoneData = WorldMapSize[c1];
        x1, y1 = getContPosition(zoneData, z1, x1, y1);
        x2, y2 = getContPosition(zoneData, z2, x2, y2);
        xDelta = (x2 - x1);
        yDelta = (y2 - y1);

    elseif (c1 and c2) then
        local cont1 = WorldMapSize[c1];
        local cont2 = WorldMapSize[c2];
        if (cont1.parentContinent == cont2.parentContinent) then
            x1, y1 = getContPosition(cont1, z1, x1, y1);
            x2, y2 = getContPosition(cont2, z2, x2, y2);
            if (c1 ~= cont1.parentContinent) then
                x1 = x1 + cont1.xOffset;
                y1 = y1 + cont1.yOffset;
            end
            if (c2 ~= cont2.parentContinent) then
                x2 = x2 + cont2.xOffset;
                y2 = y2 + cont2.yOffset;
            end

            xDelta = x2 - x1;
            yDelta = y2 - y1;
        end

    end
    if (xDelta and yDelta) then
        dist = sqrt(xDelta * xDelta + yDelta * yDelta);
    end
    return dist, xDelta, yDelta;
end

function Astrolabe:TranslateWorldMapPosition(C, Z, xPos, yPos, nC, nZ)
    --[[
	argcheck(C, 2, "number");
	argcheck(Z, 3, "number", "nil");
	argcheck(xPos, 4, "number");
	argcheck(yPos, 5, "number");
	argcheck(nC, 6, "number");
	argcheck(nZ, 7, "number", "nil");
	--]]

    Z = Z or 0;
    nZ = nZ or 0;
    if (nC < 0) then
        return;
    end

    local zoneData;
    if (C == nC and Z == nZ) then
        return xPos, yPos;

    elseif (C == nC) then
        -- points on the same continent
        zoneData = WorldMapSize[C];
        xPos, yPos = getContPosition(zoneData, Z, xPos, yPos);
        if (nZ ~= 0) then
            zoneData = WorldMapSize[C][nZ];
            xPos = xPos - zoneData.xOffset;
            yPos = yPos - zoneData.yOffset;
        end

    elseif (C and nC) and (WorldMapSize[C].parentContinent == WorldMapSize[nC].parentContinent) then
        -- different continents, same world
        zoneData = WorldMapSize[C];
        local parentContinent = zoneData.parentContinent;
        xPos, yPos = getContPosition(zoneData, Z, xPos, yPos);
        if (C ~= parentContinent) then
            -- translate up to world map if we aren't there already
            xPos = xPos + zoneData.xOffset;
            yPos = yPos + zoneData.yOffset;
            zoneData = WorldMapSize[parentContinent];
        end
        if (nC ~= parentContinent) then
            -- translate down to the new continent
            zoneData = WorldMapSize[nC];
            xPos = xPos - zoneData.xOffset;
            yPos = yPos - zoneData.yOffset;
            if (nZ ~= 0) then
                zoneData = zoneData[nZ];
                xPos = xPos - zoneData.xOffset;
                yPos = yPos - zoneData.yOffset;
            end
        end

    else
        return;
    end

    return (xPos / zoneData.width), (yPos / zoneData.height);
end

-- *****************************************************************************
-- This function will do its utmost to retrieve some sort of valid position 
-- for the specified unit, including changing the current map zoom (if needed).  
-- Map Zoom is returned to its previous setting before this function returns.  
-- *****************************************************************************
function Astrolabe:GetUnitPosition(unit, noMapChange)
    local x, y = GetPlayerMapPosition(unit);
    if (x <= 0 and y <= 0) then
        if (noMapChange) then
            -- no valid position on the current map, and we aren't allowed
            -- to change map zoom, so return
            return;
        end
        local lastCont, lastZone = GetCurrentMapContinent(), GetCurrentMapZone();
        SetMapToCurrentZone();
        x, y = GetPlayerMapPosition(unit);
        if (x <= 0 and y <= 0) then
            SetMapZoom(GetCurrentMapContinent());
            x, y = GetPlayerMapPosition(unit);
            if (x <= 0 and y <= 0) then
                -- we are in an instance or otherwise off the continent map
                return;
            end
        end
        local C, Z = GetCurrentMapContinent(), GetCurrentMapZone();
        if (C ~= lastCont or Z ~= lastZone) then
            SetMapZoom(lastCont, lastZone); -- set map zoom back to what it was before
        end
        return C, Z, x, y;
    end
    return GetCurrentMapContinent(), GetCurrentMapZone(), x, y;
end

-- *****************************************************************************
-- This function will do its utmost to retrieve some sort of valid position 
-- for the specified unit, including changing the current map zoom (if needed).  
-- However, if a monitored WorldMapFrame (See AstrolabeMapMonitor.lua) is 
-- visible, then will simply return nil if the current zoom does not provide 
-- a valid position for the player unit.  Map Zoom is returned to its previous 
-- setting before this function returns, if it was changed.  
-- *****************************************************************************
function Astrolabe:GetCurrentPlayerPosition()
    local x, y = GetPlayerMapPosition("player");
    if (x <= 0 and y <= 0) then
        if (self.WorldMapVisible) then
            -- we know there is a visible world map, so don't cause 
            -- WORLD_MAP_UPDATE events by changing map zoom
            return;
        end
        local lastCont, lastZone = GetCurrentMapContinent(), GetCurrentMapZone();
        SetMapToCurrentZone();
        x, y = GetPlayerMapPosition("player");
        if (x <= 0 and y <= 0) then
            SetMapZoom(GetCurrentMapContinent());
            x, y = GetPlayerMapPosition("player");
            if (x <= 0 and y <= 0) then
                -- we are in an instance or otherwise off the continent map
                return;
            end
        end
        local C, Z = GetCurrentMapContinent(), GetCurrentMapZone();
        if (C ~= lastCont or Z ~= lastZone) then
            SetMapZoom(lastCont, lastZone); -- set map zoom back to what it was before
        end
        return C, Z, x, y;
    end
    return GetCurrentMapContinent(), GetCurrentMapZone(), x, y;
end

--------------------------------------------------------------------------------------------------------------
-- Working Table Cache System
--------------------------------------------------------------------------------------------------------------

local tableCache = {};
tableCache["__mode"] = "v";
setmetatable(tableCache, tableCache);

local function GetWorkingTable(icon)
    if (tableCache[icon]) then
        return tableCache[icon];
    else
        local T = {};
        tableCache[icon] = T;
        return T;
    end
end

--------------------------------------------------------------------------------------------------------------
-- Minimap Icon Placement
--------------------------------------------------------------------------------------------------------------

-- *****************************************************************************
-- local variables specifically for use in this section
-- *****************************************************************************
local minimapRotationEnabled = false;
local minimapShape = false;

local minimapRotationOffset = GetPlayerFacing();

local function placeIconOnMinimap(minimap, minimapZoom, mapWidth, mapHeight, icon, dist, xDist, yDist)
    local mapDiameter;
    if (Astrolabe.minimapOutside) then
        mapDiameter = MinimapSize.outdoor[minimapZoom];
    else
        mapDiameter = MinimapSize.indoor[minimapZoom];
    end
    local mapRadius = mapDiameter / 2;
    local xScale = mapDiameter / mapWidth;
    local yScale = mapDiameter / mapHeight;
    local iconDiameter = ((icon:GetWidth() / 2) + 3) * xScale;
    local iconOnEdge = nil;
    local isRound = true;

    if (minimapRotationEnabled) then
        local sinTheta = sin(minimapRotationOffset)
        local cosTheta = cos(minimapRotationOffset)
        --[[
		Math Note
		The math that is acutally going on in the next 3 lines is:
			local dx, dy = xDist, -yDist
			xDist = (dx * cosTheta) + (dy * sinTheta)
			yDist = -((-dx * sinTheta) + (dy * cosTheta))
		
		This is because the origin for map coordinates is the top left corner
		of the map, not the bottom left, and so we have to reverse the vertical 
		distance when doing the our rotation, and then reverse the result vertical 
		distance because this rotation formula gives us a result with the origin based 
		in the bottom left corner (of the (+, +) quadrant).  
		The actual code is a simplification of the above.  
		]]
        local dx, dy = xDist, yDist
        xDist = (dx * cosTheta) - (dy * sinTheta)
        yDist = (dx * sinTheta) + (dy * cosTheta)
    end

    if (minimapShape and not (xDist == 0 or yDist == 0)) then
        isRound = (xDist < 0) and 1 or 3;
        if (yDist < 0) then
            isRound = minimapShape[isRound];
        else
            isRound = minimapShape[isRound + 1];
        end
    end

    -- for non-circular portions of the Minimap edge
    if not (isRound) then
        dist = max(abs(xDist), abs(yDist))
    end

    if ((dist + iconDiameter) > mapRadius) then
        -- position along the outside of the Minimap
        iconOnEdge = true;
        local factor = (mapRadius - iconDiameter) / dist;
        xDist = xDist * factor;
        yDist = yDist * factor;
    end

    if (Astrolabe.IconsOnEdge[icon] ~= iconOnEdge) then
        Astrolabe.IconsOnEdge[icon] = iconOnEdge;
        Astrolabe.IconsOnEdgeChanged = true;
    end

    icon:ClearAllPoints();
    icon:SetPoint("CENTER", minimap, "CENTER", xDist / xScale, -yDist / yScale);
end

function Astrolabe:PlaceIconOnMinimap(icon, continent, zone, xPos, yPos)
    -- check argument types
    argcheck(icon, 2, "table");
    assert(3, icon.SetPoint and icon.ClearAllPoints, "Usage Message");
    argcheck(continent, 3, "number");
    argcheck(zone, 4, "number", "nil");
    argcheck(xPos, 5, "number");
    argcheck(yPos, 6, "number");

    -- if the positining system is currently active, just use the player position used by the last incremental (or full) update
    -- otherwise, make sure we base our calculations off of the most recent player position (if one is available)
    local lC, lZ, lx, ly;
    if (self.processingFrame:IsShown()) then
        lC, lZ, lx, ly = unpack(self.LastPlayerPosition);
    else
        lC, lZ, lx, ly = self:GetCurrentPlayerPosition();
        if (lC and lC >= 0) then
            local lastPosition = self.LastPlayerPosition;
            lastPosition[1] = lC;
            lastPosition[2] = lZ;
            lastPosition[3] = lx;
            lastPosition[4] = ly;
        else
            lC, lZ, lx, ly = unpack(self.LastPlayerPosition);
        end
    end

    local dist, xDist, yDist = self:ComputeDistance(lC, lZ, lx, ly, continent, zone, xPos, yPos);
    if not (dist) then
        -- icon's position has no meaningful position relative to the player's current location
        return -1;
    end

    local iconData = GetWorkingTable(icon);
    if (self.MinimapIcons[icon]) then
        self.MinimapIcons[icon] = nil;
    else
        self.MinimapIconCount = self.MinimapIconCount + 1
    end

    AddedOrUpdatedIcons[icon] = iconData
    iconData.continent = continent;
    iconData.zone = zone;
    iconData.xPos = xPos;
    iconData.yPos = yPos;
    iconData.dist = dist;
    iconData.xDist = xDist;
    iconData.yDist = yDist;

    minimapRotationEnabled = GetCVar("rotateMinimap") ~= "0"
    if (minimapRotationEnabled) then
        minimapRotationOffset = GetPlayerFacing();
    end

    -- check Minimap Shape
    minimapShape = GetMinimapShape and ValidMinimapShapes[GetMinimapShape()];

    -- place the icon on the Minimap and :Show() it
    local map = Minimap
    placeIconOnMinimap(map, map:GetZoom(), map:GetWidth(), map:GetHeight(), icon, dist, xDist, yDist);
    icon:Show()

    -- We know this icon's position is valid, so we need to make sure the icon placement system is active.  
    self.processingFrame:Show()

    return 0;
end

function Astrolabe:RemoveIconFromMinimap(icon)
    if not (self.MinimapIcons[icon]) then
        return 1;
    end
    AddedOrUpdatedIcons[icon] = nil
    self.MinimapIcons[icon] = nil;
    self.IconsOnEdge[icon] = nil;
    icon:Hide();

    local MinimapIconCount = self.MinimapIconCount - 1
    if (MinimapIconCount <= 0) then
        -- no icons left to manage
        self.processingFrame:Hide()
        MinimapIconCount = 0 -- because I'm paranoid
    end
    self.MinimapIconCount = MinimapIconCount

    return 0;
end

function Astrolabe:RemoveAllMinimapIcons()
    self:DumpNewIconsCache()
    local MinimapIcons = self.MinimapIcons;
    local IconsOnEdge = self.IconsOnEdge;
    for k, v in pairs(MinimapIcons) do
        MinimapIcons[k] = nil;
        IconsOnEdge[k] = nil;
        k:Hide();
    end
    self.MinimapIconCount = 0
    self.processingFrame:Hide()
end

local lastZoom; -- to remember the last seen Minimap zoom level

-- local variables to track the status of the two update coroutines
local fullUpdateInProgress = true
local resetIncrementalUpdate = false
local resetFullUpdate = false

-- Incremental Update Code
do
    -- local variables to track the incremental update coroutine
    local incrementalUpdateCrashed = true
    local incrementalUpdateThread

    local function UpdateMinimapIconPositions(self)
        yield()

        while (true) do
            self:DumpNewIconsCache() -- put new/updated icons into the main datacache

            resetIncrementalUpdate = false -- by definition, the incremental update is reset if it is here

            local C, Z, x, y = self:GetCurrentPlayerPosition();
            if (C and C >= 0) then
                local Minimap = Minimap;
                local lastPosition = self.LastPlayerPosition;
                local lC, lZ, lx, ly = unpack(lastPosition);

                minimapRotationEnabled = GetCVar("rotateMinimap") ~= "0"
                if (minimapRotationEnabled) then
                    minimapRotationOffset = GetPlayerFacing();
                end

                -- check current frame rate
                local numPerCycle = min(50, GetFramerate() * (self.MinimapUpdateMultiplier or 1))

                -- check Minimap Shape
                minimapShape = GetMinimapShape and ValidMinimapShapes[GetMinimapShape()];

                if (lC == C and lZ == Z and lx == x and ly == y) then
                    -- player has not moved since the last update
                    if (lastZoom ~= Minimap:GetZoom() or self.ForceNextUpdate or minimapRotationEnabled) then
                        local currentZoom = Minimap:GetZoom();
                        lastZoom = currentZoom;
                        local mapWidth = Minimap:GetWidth();
                        local mapHeight = Minimap:GetHeight();
                        numPerCycle = numPerCycle * 2
                        local count = 0
                        for icon, data in pairs(self.MinimapIcons) do
                            placeIconOnMinimap(Minimap, currentZoom, mapWidth, mapHeight, icon, data.dist, data.xDist,
                                data.yDist);

                            count = count + 1
                            if (count > numPerCycle) then
                                count = 0
                                yield()
                                -- check if the incremental update cycle needs to be reset 
                                -- because a full update has been run
                                if (resetIncrementalUpdate) then
                                    break
                                end
                            end
                        end
                        self.ForceNextUpdate = false;
                    end
                else
                    local dist, xDelta, yDelta = self:ComputeDistance(lC, lZ, lx, ly, C, Z, x, y);
                    if (dist) then
                        local currentZoom = Minimap:GetZoom();
                        lastZoom = currentZoom;
                        local mapWidth = Minimap:GetWidth();
                        local mapHeight = Minimap:GetHeight();
                        local count = 0
                        for icon, data in pairs(self.MinimapIcons) do
                            local xDist = data.xDist - xDelta;
                            local yDist = data.yDist - yDelta;
                            local dist = sqrt(xDist * xDist + yDist * yDist);
                            placeIconOnMinimap(Minimap, currentZoom, mapWidth, mapHeight, icon, dist, xDist, yDist);

                            data.dist = dist;
                            data.xDist = xDist;
                            data.yDist = yDist;

                            count = count + 1
                            if (count >= numPerCycle) then
                                count = 0
                                yield()
                                -- check if the incremental update cycle needs to be reset 
                                -- because a full update has been run
                                if (resetIncrementalUpdate) then
                                    break
                                end
                            end
                        end
                        if not (resetIncrementalUpdate) then
                            lastPosition[1] = C;
                            lastPosition[2] = Z;
                            lastPosition[3] = x;
                            lastPosition[4] = y;
                        end
                    else
                        self:RemoveAllMinimapIcons()
                        lastPosition[1] = C;
                        lastPosition[2] = Z;
                        lastPosition[3] = x;
                        lastPosition[4] = y;
                    end
                end
            else
                if not (self.WorldMapVisible) then
                    self.processingFrame:Hide();
                end
            end

            -- if we've been reset, then we want to start the new cycle immediately
            if not (resetIncrementalUpdate) then
                yield()
            end
        end
    end

    function Astrolabe:UpdateMinimapIconPositions()
        if (fullUpdateInProgress) then
            -- if we're in the middle a a full update, we want to finish that first
            self:CalculateMinimapIconPositions()
        else
            if (incrementalUpdateCrashed) then
                incrementalUpdateThread = coroutine.wrap(UpdateMinimapIconPositions)
                incrementalUpdateThread(self) -- initialize the thread
            end
            incrementalUpdateCrashed = true
            incrementalUpdateThread()
            incrementalUpdateCrashed = false
        end
    end
end

-- Full Update Code
do
    -- local variables to track the full update coroutine
    local fullUpdateCrashed = true
    local fullUpdateThread

    local function CalculateMinimapIconPositions(self)
        yield()

        while (true) do
            self:DumpNewIconsCache() -- put new/updated icons into the main datacache

            resetFullUpdate = false -- by definition, the full update is reset if it is here
            fullUpdateInProgress = true -- set the flag the says a full update is in progress

            local C, Z, x, y = self:GetCurrentPlayerPosition();
            if (C and C >= 0) then
                minimapRotationEnabled = GetCVar("rotateMinimap") ~= "0"
                if (minimapRotationEnabled) then
                    minimapRotationOffset = GetPlayerFacing();
                end

                -- check current frame rate
                local numPerCycle = GetFramerate() * (self.MinimapUpdateMultiplier or 1) * 2

                -- check Minimap Shape
                minimapShape = GetMinimapShape and ValidMinimapShapes[GetMinimapShape()];

                local currentZoom = Minimap:GetZoom();
                lastZoom = currentZoom;
                local Minimap = Minimap;
                local mapWidth = Minimap:GetWidth();
                local mapHeight = Minimap:GetHeight();
                local count = 0
                for icon, data in pairs(self.MinimapIcons) do
                    local dist, xDist, yDist = self:ComputeDistance(C, Z, x, y, data.continent, data.zone, data.xPos,
                        data.yPos);
                    if (dist) then
                        placeIconOnMinimap(Minimap, currentZoom, mapWidth, mapHeight, icon, dist, xDist, yDist);

                        data.dist = dist;
                        data.xDist = xDist;
                        data.yDist = yDist;
                    else
                        self:RemoveIconFromMinimap(icon)
                    end

                    count = count + 1
                    if (count >= numPerCycle) then
                        count = 0
                        yield()
                        -- check if we need to restart due to the full update being reset
                        if (resetFullUpdate) then
                            break
                        end
                    end
                end

                if not (resetFullUpdate) then
                    local lastPosition = self.LastPlayerPosition;
                    lastPosition[1] = C;
                    lastPosition[2] = Z;
                    lastPosition[3] = x;
                    lastPosition[4] = y;

                    resetIncrementalUpdate = true
                end
            else
                if not (self.WorldMapVisible) then
                    self.processingFrame:Hide();
                end
            end

            -- if we've been reset, then we want to start the new cycle immediately
            if not (resetFullUpdate) then
                fullUpdateInProgress = false
                yield()
            end
        end
    end

    function Astrolabe:CalculateMinimapIconPositions(reset)
        if (fullUpdateCrashed) then
            fullUpdateThread = coroutine.wrap(CalculateMinimapIconPositions)
            fullUpdateThread(self) -- initialize the thread
        elseif (reset) then
            resetFullUpdate = true
        end
        fullUpdateCrashed = true
        fullUpdateThread()
        fullUpdateCrashed = false

        -- return result flag
        if (fullUpdateInProgress) then
            return 1 -- full update started, but did not complete on this cycle

        else
            if (resetIncrementalUpdate) then
                return 0 -- update completed
            else
                return -1 -- full update did no occur for some reason
            end

        end
    end
end

function Astrolabe:GetDistanceToIcon(icon)
    local data = self.MinimapIcons[icon];
    if (data) then
        return data.dist, data.xDist, data.yDist;
    end
end

function Astrolabe:IsIconOnEdge(icon)
    return self.IconsOnEdge[icon];
end

function Astrolabe:GetDirectionToIcon(icon)
    local data = self.MinimapIcons[icon];
    if (data) then
        local dir = atan2(data.xDist, -(data.yDist))
        if (dir > 0) then
            return twoPi - dir;
        else
            return -dir;
        end
    end
end

function Astrolabe:Register_OnEdgeChanged_Callback(func, ident)
    -- check argument types
    argcheck(func, 2, "function");

    self.IconsOnEdge_GroupChangeCallbacks[func] = ident;
end

-- *****************************************************************************
-- INTERNAL USE ONLY PLEASE!!!
-- Calling this function at the wrong time can cause errors
-- *****************************************************************************
function Astrolabe:DumpNewIconsCache()
    local MinimapIcons = self.MinimapIcons
    for icon, data in pairs(AddedOrUpdatedIcons) do
        MinimapIcons[icon] = data
        AddedOrUpdatedIcons[icon] = nil
    end
    -- we now need to restart any updates that were in progress
    resetIncrementalUpdate = true
    resetFullUpdate = true
end

--------------------------------------------------------------------------------------------------------------
-- World Map Icon Placement
--------------------------------------------------------------------------------------------------------------

function Astrolabe:PlaceIconOnWorldMap(worldMapFrame, icon, continent, zone, xPos, yPos)
    -- check argument types
    argcheck(worldMapFrame, 2, "table");
    assert(3, worldMapFrame.GetWidth and worldMapFrame.GetHeight, "Usage Message");
    argcheck(icon, 3, "table");
    assert(3, icon.SetPoint and icon.ClearAllPoints, "Usage Message");
    argcheck(continent, 4, "number");
    argcheck(zone, 5, "number", "nil");
    argcheck(xPos, 6, "number");
    argcheck(yPos, 7, "number");

    local C, Z = GetCurrentMapContinent(), GetCurrentMapZone();
    local nX, nY = self:TranslateWorldMapPosition(continent, zone, xPos, yPos, C, Z);

    -- anchor and :Show() the icon if it is within the boundry of the current map, :Hide() it otherwise
    if (nX and nY and (0 < nX and nX <= 1) and (0 < nY and nY <= 1)) then
        icon:ClearAllPoints();
        icon:SetPoint("CENTER", worldMapFrame, "TOPLEFT", nX * worldMapFrame:GetWidth(), -nY * worldMapFrame:GetHeight());
        icon:Show();
    else
        icon:Hide();
    end
    return nX, nY;
end

--------------------------------------------------------------------------------------------------------------
-- Handler Scripts
--------------------------------------------------------------------------------------------------------------

function Astrolabe:OnEvent(frame, event)
    if (event == "MINIMAP_UPDATE_ZOOM") then
        -- update minimap zoom scale
        local Minimap = Minimap;
        local curZoom = Minimap:GetZoom();
        if (GetCVar("minimapZoom") == GetCVar("minimapInsideZoom")) then
            if (curZoom < 2) then
                Minimap:SetZoom(curZoom + 1);
            else
                Minimap:SetZoom(curZoom - 1);
            end
        end
        if (GetCVar("minimapZoom") + 0 == Minimap:GetZoom()) then
            self.minimapOutside = true;
        else
            self.minimapOutside = false;
        end
        Minimap:SetZoom(curZoom);

        -- re-calculate all Minimap Icon positions
        if (frame:IsVisible()) then
            self:CalculateMinimapIconPositions(true);
        end

    elseif (event == "PLAYER_LEAVING_WORLD") then
        frame:Hide(); -- yes, I know this is redunant
        self:RemoveAllMinimapIcons(); -- dump all minimap icons
        -- TODO: when I uncouple the point buffer from Minimap drawing,
        --       I should consider updating LastPlayerPosition here

    elseif (event == "PLAYER_ENTERING_WORLD") then
        frame:Show();
        if not (frame:IsVisible()) then
            -- do the minimap recalculation anyways if the OnShow script didn't execute
            -- this is done to ensure the accuracy of information about icons that were 
            -- inserted while the Player was in the process of zoning
            self:CalculateMinimapIconPositions(true);
        end

    elseif (event == "ZONE_CHANGED_NEW_AREA") then
        frame:Hide();
        frame:Show();

    end
end

function Astrolabe:OnUpdate(frame, elapsed)
    -- on-edge group changed call-backs
    if (self.IconsOnEdgeChanged) then
        self.IconsOnEdgeChanged = false;
        for func in pairs(self.IconsOnEdge_GroupChangeCallbacks) do
            pcall(func);
        end
    end

    self:UpdateMinimapIconPositions();
end

function Astrolabe:OnShow(frame)
    -- set the world map to a zoom with a valid player position
    if not (self.WorldMapVisible) then
        SetMapToCurrentZone();
    end
    local C, Z = Astrolabe:GetCurrentPlayerPosition();
    if (C and C >= 0) then
        SetMapZoom(C, Z);
    else
        frame:Hide();
        return
    end

    -- re-calculate minimap icon positions (if needed)
    if (next(self.MinimapIcons)) then
        self:CalculateMinimapIconPositions(true);
    else
        -- needed so that the cycle doesn't overwrite an updated LastPlayerPosition
        resetIncrementalUpdate = true;
    end

    if (self.MinimapIconCount <= 0) then
        -- no icons left to manage
        frame:Hide();
    end
end

function Astrolabe:OnHide(frame)
    -- dump the new icons cache here
    -- a full update will performed the next time processing is re-actived
    self:DumpNewIconsCache()
end

-- called by AstrolabMapMonitor when all world maps are hidden
function Astrolabe:AllWorldMapsHidden()
    if (IsLoggedIn()) then
        self.processingFrame:Hide();
        self.processingFrame:Show();
    end
end

--------------------------------------------------------------------------------------------------------------
-- Library Registration
--------------------------------------------------------------------------------------------------------------

local function activate(newInstance, oldInstance)
    if (oldInstance) then -- this is an upgrade activate
        if (oldInstance.DumpNewIconsCache) then
            oldInstance:DumpNewIconsCache()
        end
        for k, v in pairs(oldInstance) do
            if (type(v) ~= "function" and (not configConstants[k])) then
                newInstance[k] = v;
            end
        end
        -- sync up the current MinimapIconCount value
        local iconCount = 0
        for _ in pairs(newInstance.MinimapIcons) do
            iconCount = iconCount + 1
        end
        newInstance.MinimapIconCount = iconCount

        Astrolabe = oldInstance;
    else
        local frame = CreateFrame("Frame");
        newInstance.processingFrame = frame;

        newInstance.ContinentList = {GetMapContinents()};
        for C in pairs(newInstance.ContinentList) do
            local zones = {GetMapZones(C)};
            newInstance.ContinentList[C] = zones;
            for Z in ipairs(zones) do
                SetMapZoom(C, Z);
                zones[Z] = GetMapInfo();
            end
        end
    end
    configConstants = nil -- we don't need this anymore

    local frame = newInstance.processingFrame;
    frame:Hide();
    frame:SetParent("Minimap");
    frame:UnregisterAllEvents();
    frame:RegisterEvent("MINIMAP_UPDATE_ZOOM");
    frame:RegisterEvent("PLAYER_LEAVING_WORLD");
    frame:RegisterEvent("PLAYER_ENTERING_WORLD");
    frame:RegisterEvent("ZONE_CHANGED_NEW_AREA");
    frame:SetScript("OnEvent", function(frame, event, ...)
        Astrolabe:OnEvent(frame, event, ...);
    end);
    frame:SetScript("OnUpdate", function(frame, elapsed)
        Astrolabe:OnUpdate(frame, elapsed);
    end);
    frame:SetScript("OnShow", function(frame)
        Astrolabe:OnShow(frame);
    end);
    frame:SetScript("OnHide", function(frame)
        Astrolabe:OnHide(frame);
    end);

    setmetatable(Astrolabe.MinimapIcons, MinimapIconsMetatable)
end

DongleStub:Register(Astrolabe, activate)

--------------------------------------------------------------------------------------------------------------
-- Data
--------------------------------------------------------------------------------------------------------------

-- diameter of the Minimap in game yards at
-- the various possible zoom levels
MinimapSize = {
    indoor = {
        [0] = 300, -- scale
        [1] = 240, -- 1.25
        [2] = 180, -- 5/3
        [3] = 120, -- 2.5
        [4] = 80, -- 3.75
        [5] = 50 -- 6
    },
    outdoor = {
        [0] = 466 + 2 / 3, -- scale
        [1] = 400, -- 7/6
        [2] = 333 + 1 / 3, -- 1.4
        [3] = 266 + 2 / 6, -- 1.75
        [4] = 200, -- 7/3
        [5] = 133 + 1 / 3 -- 3.5
    }
}

ValidMinimapShapes = {
    -- { upper-left, lower-left, upper-right, lower-right }
    ["SQUARE"] = {false, false, false, false},
    ["CORNER-TOPLEFT"] = {true, false, false, false},
    ["CORNER-TOPRIGHT"] = {false, false, true, false},
    ["CORNER-BOTTOMLEFT"] = {false, true, false, false},
    ["CORNER-BOTTOMRIGHT"] = {false, false, false, true},
    ["SIDE-LEFT"] = {true, true, false, false},
    ["SIDE-RIGHT"] = {false, false, true, true},
    ["SIDE-TOP"] = {true, false, true, false},
    ["SIDE-BOTTOM"] = {false, true, false, true},
    ["TRICORNER-TOPLEFT"] = {true, true, true, false},
    ["TRICORNER-TOPRIGHT"] = {true, false, true, true},
    ["TRICORNER-BOTTOMLEFT"] = {true, true, false, true},
    ["TRICORNER-BOTTOMRIGHT"] = {false, true, true, true}
}

-- distances across and offsets of the world maps
-- in game yards
WorldMapSize = {
    -- World Map of Azeroth
    [0] = {
        parentContinent = 0,
        height = 31809.64857610083,
        width = 47714.278579261
    },
    -- Kalimdor
    { -- [1]
        parentContinent = 0,
        height = 24533.025279205,
        width = 36800.210572494,
        xOffset = -8590.40725049343,
        yOffset = 5628.692856102324,
        zoneData = {
            Ashenvale = {
                height = 3843.722331667447,
                width = 5766.728885829694,
                xOffset = 15366.76675592628,
                yOffset = 8126.925930315996
            },
            Aszhara = {
                height = 3381.22554790382,
                width = 5070.886912363937,
                xOffset = 20343.90431905976,
                yOffset = 7458.18074892042
            },
            AzuremystIsle = {
                height = 2714.563862990522,
                width = 4070.87719998905,
                xOffset = 9966.708003150136,
                yOffset = 5460.278492344226
            },
            Barrens = {
                height = 6756.201888541853,
                width = 10133.44231353798,
                xOffset = 14443.84040901447,
                yOffset = 11187.32063797497
            },
            BloodmystIsle = {
                height = 2174.984213312164,
                width = 3262.535628257626,
                xOffset = 9541.702868577344,
                yOffset = 3424.87645454774
            },
            Darkshore = {
                height = 4366.635262519317,
                width = 6550.07142937905,
                xOffset = 14125.0864431955,
                yOffset = 4466.535577798089
            },
            Darnassis = {
                height = 705.7244742259542,
                width = 1058.344272032854,
                xOffset = 14128.39058434346,
                yOffset = 2561.565704787642
            },
            Desolace = {
                height = 2997.895112390325,
                width = 4495.882561567951,
                xOffset = 12833.40568925697,
                yOffset = 12347.7291386989
            },
            Durotar = {
                height = 3524.975103516492,
                width = 5287.556393263762,
                xOffset = 19029.30641831177,
                yOffset = 10991.48866520544
            },
            Dustwallow = {
                height = 3499.975024621409,
                width = 5250.057340719384,
                xOffset = 18041.79555525144,
                yOffset = 14833.12785083746
            },
            Felwood = {
                height = 3833.30578953572,
                width = 5750.062500603067,
                xOffset = 15425.10050841866,
                yOffset = 5666.52696052216
            },
            Feralas = {
                height = 4633.300220031075,
                width = 6950.07476479001,
                xOffset = 11625.05968863682,
                yOffset = 15166.45859885191
            },
            Moonglade = {
                height = 1539.572004392277,
                width = 2308.359613919473,
                xOffset = 18448.04941280923,
                yOffset = 4308.203668830446
            },
            Mulgore = {
                height = 3424.975591847491,
                width = 5137.555664509726,
                xOffset = 15018.84638430639,
                yOffset = 13072.72374939454
            },
            Ogrimmar = {
                height = 935.4097495193695,
                width = 1402.619172463506,
                xOffset = 20747.42615230776,
                yOffset = 10525.94819392488
            },
            Silithus = {
                height = 2322.900917404436,
                width = 3483.371660836989,
                xOffset = 14529.25779832084,
                yOffset = 18758.10034739171
            },
            StonetalonMountains = {
                height = 3256.22685709556,
                width = 4883.386126224323,
                xOffset = 13820.91659894445,
                yOffset = 9883.163451643639
            },
            Tanaris = {
                height = 4599.967247105565,
                width = 6900.075410478199,
                xOffset = 17285.53717337067,
                yOffset = 18674.76738951156
            },
            Teldrassil = {
                height = 3393.725685278266,
                width = 5091.720159017256,
                xOffset = 13252.16118637725,
                yOffset = 968.6435100517717
            },
            TheExodar = {
                height = 704.6827795715492,
                width = 1056.782908333002,
                xOffset = 10533.08067788734,
                yOffset = 6276.205943683332
            },
            ThousandNeedles = {
                height = 2933.311990587089,
                width = 4400.046926738385,
                xOffset = 17500.12330544135,
                yOffset = 16766.44742072096
            },
            ThunderBluff = {
                height = 695.8286363220358,
                width = 1043.761162566134,
                xOffset = 16550.11296988024,
                yOffset = 13649.80296445508
            },
            UngoroCrater = {
                height = 2466.648940830691,
                width = 3700.039952384531,
                xOffset = 16533.4461782123,
                yOffset = 18766.43318304924
            },
            Winterspring = {
                height = 4733.299352687333,
                width = 7100.076688034489,
                xOffset = 17383.45536235255,
                yOffset = 4266.537029274375
            },
            -- Generated by WDM Collection
            Hyjal = {
                height = 2129.073486,
                width = 3195.891113,
                xOffset = 18205.577637,
                yOffset = 6714.003906
            },
            GMIsland = {
                height = 541.0,
                width = 828.0,
                xOffset = 318.599609,
                yOffset = -3725.099609
            },
            ShadowglenStart = {
                height = 966.666992,
                width = 1450.000977,
                xOffset = 15574.932617,
                yOffset = 1766.567383
            },
            ValleyofTrialsStart = {
                height = 900.0,
                width = 1350.0,
                xOffset = 20708.265625,
                yOffset = 12799.900391
            },
            CampNaracheStart = {
                height = 1177.082031,
                width = 1766.667969,
                xOffset = 16833.265625,
                yOffset = 15376.984375
            },
            AmmenValeStart = {
                height = 1212.5,
                width = 1818.75,
                xOffset = 12281.181641,
                yOffset = 6270.733406
            },
            Ogrimmar1_ = {
                height = 241.390015,
                width = 362.089844,
                xOffset = 21230.569824,
                yOffset = 10867.630371
            },
            ShadowthreadCave2_ = {
                height = 320.0,
                width = 480.0,
                xOffset = 15926.599609,
                yOffset = 1764.900391
            },
            FelRock3_ = {
                height = 186.400391,
                width = 279.600586,
                xOffset = 15816.799316,
                yOffset = 2588.200195
            },
            BanethilBarrowden4_ = {
                height = 153.339844,
                width = 230.009766,
                xOffset = 15381.594727,
                yOffset = 2918.230469
            },
            BanethilBarrowden5_ = {
                height = 253.339844,
                width = 380.009766,
                xOffset = 15331.594727,
                yOffset = 2868.230469
            },
            PalemaneRock6_ = {
                height = 233.339844,
                width = 350.01001,
                xOffset = 16486.594605,
                yOffset = 15063.230469
            },
            TheVentureCoMine7_ = {
                height = 494.0,
                width = 741.0,
                xOffset = 17884.599609,
                yOffset = 14235.900391
            },
            BurningBladeCoven8_ = {
                height = 177.333341,
                width = 266.0,
                xOffset = 21233.099609,
                yOffset = 12794.400058
            },
            TiragardeKeep10_ = {
                height = 83.334,
                width = 125.000977,
                xOffset = 22119.099121,
                yOffset = 13003.233383
            },
            TiragardeKeep11_ = {
                height = 83.334,
                width = 125.000977,
                xOffset = 22119.099121,
                yOffset = 13003.233383
            },
            SkullRock12_ = {
                height = 180.0,
                width = 270.0,
                xOffset = 21695.599609,
                yOffset = 11219.400391
            },
            TwilightsRun13_ = {
                height = 168.339844,
                width = 252.509903,
                xOffset = 16905.344605,
                yOffset = 18983.230469
            },
            TheSlitheringScar14_ = {
                height = 255.0,
                width = 382.5,
                xOffset = 18050.349609,
                yOffset = 20729.900391
            },
            TheNoxiousLair15_ = {
                height = 500.0,
                width = 750.0,
                xOffset = 19256.599609,
                yOffset = 20439.900391
            },
            TheGapingChasm16_ = {
                height = 590.0,
                width = 885.0,
                xOffset = 20679.099609,
                yOffset = 21699.900391
            },
            CavernsofTime17_ = {
                height = 738.339844,
                width = 1107.509766,
                xOffset = 21230.344727,
                yOffset = 20755.730469
            },
            CavernsofTime18_ = {
                height = 870.666504,
                width = 1306.0,
                xOffset = 20774.099609,
                yOffset = 20836.566895
            },
            DustwindCave19_ = {
                height = 172.0,
                width = 258.0,
                xOffset = 21689.099609,
                yOffset = 11828.650391
            },
            WailingCavernsBarrens20_ = {
                height = 380.0,
                width = 570.0,
                xOffset = 18969.099609,
                yOffset = 13289.900391
            },
            MaraudonOutside21_ = {
                height = 400.0,
                width = 600.0,
                xOffset = 13981.099609,
                yOffset = 14049.900391
            },
            MaraudonOutside22_ = {
                height = 373.34668,
                width = 560.02002,
                xOffset = 13901.5896,
                yOffset = 13892.227051
            },
            TidesHollow2_ = {
                height = 250.0,
                width = 375.0,
                xOffset = 10835.599609,
                yOffset = 7306.567391
            },
            StillpineHold3_ = {
                height = 316.660156,
                width = 474.990234,
                xOffset = 11714.154297,
                yOffset = 5675.737312
            }

        }
    },
    -- Eastern Kingdoms
    { -- [2]
        parentContinent = 0,
        height = 27149.795290881,
        width = 40741.175327834,
        xOffset = 18542.31220836664,
        yOffset = 3585.574573158966,
        zoneData = {
            Alterac = {
                height = 1866.674220583334,
                width = 2799.999464405289,
                xOffset = 17388.63468066122,
                yOffset = 9676.382149825957
            },
            Arathi = {
                height = 2400.009317069356,
                width = 3599.999517304195,
                xOffset = 19038.63447926361,
                yOffset = 11309.72195295708
            },
            Badlands = {
                height = 1658.340337615859,
                width = 2487.500569928747,
                xOffset = 20251.13345045087,
                yOffset = 17065.99453090572
            },
            BlastedLands = {
                height = 2233.342487048268,
                width = 3349.999380719363,
                xOffset = 19413.63423284709,
                yOffset = 21743.09620559562
            },
            BurningSteppes = {
                height = 1952.091015081907,
                width = 2929.167049647848,
                xOffset = 18438.63415866318,
                yOffset = 18207.66550773985
            },
            DeadwindPass = {
                height = 1666.673717206878,
                width = 2499.999255461505,
                xOffset = 19005.30099399293,
                yOffset = 21043.09319963172
            },
            DunMorogh = {
                height = 3283.346244075043,
                width = 4925.000979131685,
                xOffset = 16369.88372014602,
                yOffset = 15053.48652833869
            },
            Duskwood = {
                height = 1800.007435102674,
                width = 2699.999451812027,
                xOffset = 17338.63474984946,
                yOffset = 20893.09262994406
            },
            EasternPlaguelands = {
                height = 2687.510259086504,
                width = 4031.248684963022,
                xOffset = 20459.46800337001,
                yOffset = 7472.207074316265
            },
            Elwynn = {
                height = 2314.592478810788,
                width = 3470.832795915813,
                xOffset = 16636.55146195304,
                yOffset = 19116.00248086271
            },
            EversongWoods = {
                height = 3283.346090242183,
                width = 4925.00271131707,
                xOffset = 20259.46550654072,
                yOffset = 2534.687768168357
            },
            Ghostlands = {
                height = 2200.008615840919,
                width = 3300.001914001321,
                xOffset = 21055.29898547313,
                yOffset = 5309.698628620597
            },
            Hilsbrad = {
                height = 2133.341648261057,
                width = 3199.998998314975,
                xOffset = 17105.30161317513,
                yOffset = 10776.38647689923
            },
            Hinterlands = {
                height = 2566.6767425107,
                width = 3849.999302583992,
                xOffset = 19746.96759079755,
                yOffset = 9709.715638073398
            },
            Ironforge = {
                height = 527.6066263822604,
                width = 790.625237342102,
                xOffset = 18885.55918004965,
                yOffset = 15745.64757909506
            },
            LochModan = {
                height = 1839.589436540107,
                width = 2758.333078630792,
                xOffset = 20165.71744013867,
                yOffset = 15663.90644131906
            },
            Redridge = {
                height = 1447.921846941264,
                width = 2170.833008876805,
                xOffset = 19742.80073199006,
                yOffset = 19751.42200372843
            },
            SearingGorge = {
                height = 1487.505327445583,
                width = 2231.249676776115,
                xOffset = 18494.88412729142,
                yOffset = 17276.41249042905
            },
            SilvermoonCity = {
                height = 806.7736903384404,
                width = 1211.459296502504,
                xOffset = 22172.71642224908,
                yOffset = 3422.648306718702
            },
            Silverpine = {
                height = 2800.0110500699,
                width = 4199.999060067367,
                xOffset = 14721.96859379216,
                yOffset = 9509.714862642681
            },
            Stormwind = {
                height = 1158.337650999629,
                width = 1737.500553362899,
                xOffset = 16449.05109973784,
                yOffset = 19172.25293704512
            },
            Stranglethorn = {
                height = 4254.183097414531,
                width = 6381.247773741421,
                xOffset = 15951.13530113703,
                yOffset = 22345.18245588815
            },
            Sunwell = {
                height = 2218.75784157939,
                width = 3327.080984022923,
                xOffset = 21074.05125342849,
                yOffset = 7.594755912743345
            },
            SwampOfSorrows = {
                height = 1529.173582734637,
                width = 2293.750686253685,
                xOffset = 20394.88344424886,
                yOffset = 20797.25895394673
            },
            Tirisfal = {
                height = 3012.512329627232,
                width = 4518.747902731258,
                xOffset = 15138.6360714653,
                yOffset = 7338.872677268415
            },
            Undercity = {
                height = 640.1066040851099,
                width = 959.3745478926886,
                xOffset = 17298.77542115219,
                yOffset = 9298.435370484816
            },
            WesternPlaguelands = {
                height = 2866.677851772014,
                width = 4299.999720893135,
                xOffset = 17755.30124459509,
                yOffset = 7809.708293788776
            },
            Westfall = {
                height = 2333.342511708478,
                width = 3499.999662793482,
                xOffset = 15155.30169114852,
                yOffset = 20576.42535247717
            },
            Wetlands = {
                height = 2756.260945423485,
                width = 4135.416085415621,
                xOffset = 18561.55114967782,
                yOffset = 13324.31325114659
            },
            -- Generated by WDM Collection
            Northshire = {
                height = 645.833984,
                width = 968.75,
                xOffset = 17984.470703,
                yOffset = 19747.175781
            },
            ColdridgeValley = {
                height = 643.75,
                width = 964.583008,
                xOffset = 17192.803711,
                yOffset = 17138.84375
            },
            DeathknellStart = {
                height = 727.083008,
                width = 1089.583984,
                xOffset = 16024.053711,
                yOffset = 8905.510742
            },
            SunstriderIsleStart = {
                height = 1066.666992,
                width = 1600.0,
                xOffset = 21155.304687,
                yOffset = 2743.010742
            },
            Fargodeepmine1_ = {
                height = 160.0,
                width = 240.0,
                xOffset = 17890.220703,
                yOffset = 20876.34375
            },
            Fargodeepmine2_ = {
                height = 170.0,
                width = 255.0,
                xOffset = 17882.720703,
                yOffset = 20886.34375
            },
            EchoRidgeMine3_ = {
                height = 186.0,
                width = 279.0,
                xOffset = 18198.970703,
                yOffset = 19676.34375
            },
            GoldCoastQuarry4_ = {
                height = 175.0,
                width = 262.5,
                xOffset = 16075.720703,
                yOffset = 21606.34375
            },
            JangolodeMine5_ = {
                height = 184.666016,
                width = 277.0,
                xOffset = 16601.470703,
                yOffset = 20975.010742
            },
            ColdridgePass6_ = {
                height = 220.0,
                width = 330.0,
                xOffset = 17926.970703,
                yOffset = 17196.34375
            },
            TheGrizzledDen7_ = {
                height = 337.0,
                width = 505.5,
                xOffset = 18139.220703,
                yOffset = 16489.34375
            },
            FrostmaneHold8_ = {
                height = 234.391113,
                width = 341.883179,
                xOffset = 17323.910339,
                yOffset = 16661.129883
            },
            FrostmaneHovel9_ = {
                height = 177.660156,
                width = 266.490234,
                xOffset = 17663.725586,
                yOffset = 17622.013672
            },
            GnomereganEntrance10_ = {
                height = 467.231934,
                width = 730.487457,
                xOffset = 17089.04187,
                yOffset = 15939.22998
            },
            GolBolarQuarry11_ = {
                height = 249.339844,
                width = 374.009766,
                xOffset = 19714.96582,
                yOffset = 16663.173828
            },
            NightWebsHollow12_ = {
                height = 146.666748,
                width = 220.0,
                xOffset = 16151.970703,
                yOffset = 9053.010254
            },
            ScarletMonasteryEntrance13_ = {
                height = 136.660156,
                width = 204.990234,
                xOffset = 18831.975586,
                yOffset = 8227.513672
            },
            BlackrockMountain14_ = {
                height = 475.0,
                width = 712.5,
                xOffset = 18933.220703,
                yOffset = 18501.34375
            },
            BlackrockMountain15_ = {
                height = 170.0,
                width = 255.0,
                xOffset = 19316.970703,
                yOffset = 18656.34375
            },
            BlackrockMountain16_ = {
                height = 506.682617,
                width = 760.023987,
                xOffset = 18799.456726,
                yOffset = 18263.002441
            },
            DeadminesWestfall17_ = {
                height = 300.0,
                width = 450.0,
                xOffset = 16379.470703,
                yOffset = 22231.34375
            },
            Uldaman18_ = {
                height = 375.0,
                width = 562.5,
                xOffset = 20920.720703,
                yOffset = 17131.34375
            },
            JasperlodeMine19_ = {
                height = 215.5,
                width = 323.25,
                xOffset = 18617.845703,
                yOffset = 20161.34375
            },
            AmaniCatacombs1_ = {
                height = 200.0,
                width = 300.0,
                xOffset = 22951.970703,
                yOffset = 5866.34375
            }
        }
    },
    -- Outland
    { -- [3]
        parentContinent = 3,
        height = 11642.355227091,
        width = 17463.987300595,
        zoneData = {
            BladesEdgeMountains = {
                height = 3616.553525584605,
                width = 5424.971374542539,
                xOffset = 4150.184588602209,
                yOffset = 1412.982196881336
            },
            Hellfire = {
                height = 3443.64230460125,
                width = 5164.556244744065,
                xOffset = 7456.417231266903,
                yOffset = 4339.973859432732
            },
            Nagrand = {
                height = 3683.218433421437,
                width = 5524.971116484553,
                xOffset = 2700.192056890117,
                yOffset = 5779.512082963144
            },
            Netherstorm = {
                height = 3716.550667470386,
                width = 5574.970542741407,
                xOffset = 7512.666973902843,
                yOffset = 365.0979868806522
            },
            ShadowmoonValley = {
                height = 3666.551832578994,
                width = 5499.971055470069,
                xOffset = 8770.993482940312,
                yOffset = 7769.033432511459
            },
            ShattrathCity = {
                height = 870.8062268244973,
                width = 1306.243111124071,
                xOffset = 6860.744657085816,
                yOffset = 7295.086006462451
            },
            TerokkarForest = {
                height = 3599.887549731843,
                width = 5399.971897226099,
                xOffset = 5912.67529110344,
                yOffset = 6821.146327166267
            },
            Zangarmarsh = {
                height = 3351.978661481413,
                width = 5027.057239215307,
                xOffset = 3521.020775148071,
                yOffset = 3885.821395736634
            }
        }
    },
    -- Northrend
    { -- [4]
        parentContinent = 0,
        height = 11834.3119870532,
        width = 17751.3962441049,
        xOffset = 16020.94044398222,
        yOffset = 454.2451915717977,
        zoneData = {
            BoreanTundra = {
                height = 3843.764953143499,
                width = 5764.582303295793,
                xOffset = 646.3192474426043,
                yOffset = 5695.48114050537
            },
            CrystalsongForest = {
                height = 1814.590295101352,
                width = 2722.916513743646,
                xOffset = 7773.401390128443,
                yOffset = 4091.308181657137
            },
            Dalaran = {
                height = 553.3418567935553,
                width = 830.0149393375432,
                xOffset = 8164.641313001377,
                yOffset = 4526.723129621716
            },
            Dragonblight = {
                height = 3739.598062842169,
                width = 5608.332396545997,
                xOffset = 5590.068422600026,
                yOffset = 5018.394866268677
            },
            GrizzlyHills = {
                height = 3500.013689934217,
                width = 5249.998732532693,
                xOffset = 10327.56786162186,
                yOffset = 5076.728707808831
            },
            HrothgarsLanding = {
                height = 2452.093653509858,
                width = 3677.082560623348,
                xOffset = 6419.234857391856,
                yOffset = -187.8757232657943
            },
            HowlingFjord = {
                height = 4031.265457002443,
                width = 6045.831836878359,
                xOffset = 10615.0679627145,
                yOffset = 7476.73831512609
            },
            IcecrownGlacier = {
                height = 4181.266519840844,
                width = 6270.832975322177,
                xOffset = 3773.401695036191,
                yOffset = 1166.296622984233
            },
            LakeWintergrasp = {
                height = 1983.341134082619,
                width = 2974.99948105957,
                xOffset = 4887.98528918423,
                yOffset = 4876.727878058311
            },
            SholazarBasin = {
                height = 2904.178067737769,
                width = 4356.249510482578,
                xOffset = 2287.985538503677,
                yOffset = 3305.888591396293
            },
            TheStormPeaks = {
                height = 4741.684740041381,
                width = 7112.498187401986,
                xOffset = 7375.484940713573,
                yOffset = 395.46058562991
            },
            ZulDrak = {
                height = 3329.179762967791,
                width = 4993.749118857795,
                xOffset = 9817.151095677416,
                yOffset = 2924.637042390465
            }
        }
    }
}

local zeroData;
zeroData = {
    xOffset = 0,
    height = 0,
    yOffset = 0,
    width = 0,
    __index = function()
        return zeroData
    end
};
setmetatable(zeroData, zeroData);
setmetatable(WorldMapSize, zeroData);

for continent, zones in pairs(Astrolabe.ContinentList) do
    local mapData = WorldMapSize[continent];
    for index, mapName in pairs(zones) do
        if not (mapData.zoneData[mapName]) then
            -- WE HAVE A PROBLEM!!!
            ChatFrame1:AddMessage("Astrolabe is missing data for " .. select(index, GetMapZones(continent)) .. ".");
            mapData.zoneData[mapName] = zeroData;
        end
        mapData[index] = mapData.zoneData[mapName];
        mapData.zoneData[mapName] = nil;
    end
end

-- register this library with AstrolabeMapMonitor, this will cause a full update if PLAYER_LOGIN has already fired
local AstrolabeMapMonitor = DongleStub("AstrolabeMapMonitor");
AstrolabeMapMonitor:RegisterAstrolabeLibrary(Astrolabe, LIBRARY_VERSION_MAJOR);
