--[[
Name: Astrolabe
Revision: $Rev: 105 $
$Date: 2009-04-15 05:21:53 +0200 (┼Ür, 15 kwi 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-Zygor'
local LIBRARY_VERSION_MINOR = tonumber(string.match('$Revision: 106 $', '(%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

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

-- protect against virtual continents
local function safe_SetMapZoom(c, z)
  if c >= 0 and c <= 4 then
    SetMapZoom(c, z)
  end
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 = GetCurrentMapContinentAndZone()
    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 = GetCurrentMapContinentAndZone()
    if C ~= lastCont or Z ~= lastZone then
      safe_SetMapZoom(lastCont, lastZone) -- set map zoom back to what it was before
    end
    return C, Z, x, y
  end
  local c, z = GetCurrentMapContinentAndZone()
  return c, z, 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 = GetCurrentMapContinentAndZone()
    if lastCont == -1 and lastZone == 0 and GetMapInfo() == 'ScarletEnclave' then
      lastCont = 5
      lastZone = 1
    end
    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
      safe_SetMapZoom(lastCont, lastZone) --set map zoom back to what it was before
    end
    return C, Z, x, y
  end
  local c, z = GetCurrentMapContinentAndZone()
  return c, z, x, y
end

function GetCurrentMapContinentAndZone()
  local c, z = GetCurrentMapContinent(), GetCurrentMapZone()
  if c == -1 and z == 0 and GetMapInfo() == 'ScarletEnclave' then
    c, z = 5, 1
  end
  return c, z
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]
    if GetMapInfo() == 'Dalaran' and GetCurrentMapDungeonLevel() == 2 then
      mapDiameter = mapDiameter * 1.5
    end
  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 = GetCurrentMapContinentAndZone()
  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
    safe_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

    newInstance.ContinentList[5] = { 'ScarletEnclave' }
  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,
      },
    },
  },
  -- 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,
      },
    },
  },
  -- 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,
      },
    },
  },
  -- Scarlet Enclave
  { -- [5]
    parentContinent = 0,
    height = 27149.795290881,
    width = 40741.175327834,
    xOffset = 19242.31220836664,
    yOffset = 4885.574573158966,
    zoneData = {
      ScarletEnclave = {
        height = 1333.510259086504,
        width = 2000.248684963022,
        xOffset = 22459.46800337001,
        yOffset = 7672.207074316265,
      },
    },
  },
}

-- move zoneData keys into zone-number-indexed entries.
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
    --print (index,mapName)
    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]
  end
  mapData.zoneData = nil
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)
