ESOUI SVN TaosGroupUltimate

[/] [trunk/] [TaosGroupUltimate/] [libs/] [LibGPS/] [LibGPS.lua] - Rev 61

Compare with Previous | Blame | View Log

-- LibGPS2 & its files © sirinsidiator                          --
-- Distributed under The Artistic License 2.0 (see LICENSE)     --
------------------------------------------------------------------

local LIB_NAME = "LibGPS2"
local lib = LibStub:NewLibrary(LIB_NAME, 14)

if not lib then
    return
    -- already loaded and no upgrade necessary
end

local LMP = LibStub("LibMapPing", true)
if(not LMP) then
    error(string.format("[%s] Cannot load without LibMapPing", LIB_NAME))
end

local DUMMY_PIN_TYPE = LIB_NAME .. "DummyPin"
local LIB_IDENTIFIER_FINALIZE = LIB_NAME .. "_Finalize"
lib.LIB_EVENT_STATE_CHANGED = "OnLibGPS2MeasurementChanged"

local LOG_WARNING = "Warning"
local LOG_NOTICE = "Notice"
local LOG_DEBUG = "Debug"

local POSITION_MIN = 0.085
local POSITION_MAX = 0.915

local TAMRIEL_MAP_INDEX = 1

local rootMaps = lib.rootMaps or {}
lib.rootMaps = rootMaps

--lib.debugMode = 1 -- TODO
lib.mapMeasurements = lib.mapMeasurements or {}
local mapMeasurements = lib.mapMeasurements
lib.mapStack = lib.mapStack or {}
local mapStack = lib.mapStack
lib.suppressCount = lib.suppressCount or 0

local MAP_PIN_TYPE_PLAYER_WAYPOINT = MAP_PIN_TYPE_PLAYER_WAYPOINT
local currentWaypointX, currentWaypointY, currentWaypointMapId = 0, 0, nil
local needWaypointRestore = false
local orgSetMapToMapListIndex = nil
local orgSetMapToPlayerLocation = nil
local orgSetMapFloor = nil
local orgProcessMapClick = nil
local orgFunctions = {}
local measuring = false

SLASH_COMMANDS["/libgpsdebug"] = function(value)
    lib.debugMode = (tonumber(value) == 1)
    df("[%s] debug mode %s", LIB_NAME, lib.debugMode and "enabled" or "disabled")
end

local function LogMessage(type, message, ...)
    if not lib.debugMode then return end
    df("[%s] %s: %s", LIB_NAME, type, zo_strjoin(" ", message, ...))
end

local function GetAddon()
    local addOn
    local function errornous() addOn = 'a' + 1 end
    local function errorHandler(err) addOn = string.match(err, "'GetAddon'.+user:/AddOns/(.-:.-):") end
    xpcall(errornous, errorHandler)
    return addOn
end

local function FinalizeMeasurement()
    EVENT_MANAGER:UnregisterForUpdate(LIB_IDENTIFIER_FINALIZE)
    while lib.suppressCount > 0 do
        LMP:UnsuppressPing(MAP_PIN_TYPE_PLAYER_WAYPOINT)
        lib.suppressCount = lib.suppressCount - 1
    end
    if needWaypointRestore then
        LogMessage(LOG_DEBUG, "Update waypoint pin", LMP:GetMapPing(MAP_PIN_TYPE_PLAYER_WAYPOINT))
        LMP:RefreshMapPin(MAP_PIN_TYPE_PLAYER_WAYPOINT)
        needWaypointRestore = false
    end
    measuring = false
    CALLBACK_MANAGER:FireCallbacks(lib.LIB_EVENT_STATE_CHANGED, measuring)
end

local function HandlePingEvent(pingType, pingTag, x, y, isPingOwner)
    if(not isPingOwner or pingType ~= MAP_PIN_TYPE_PLAYER_WAYPOINT or not measuring) then return end
    -- we delay our handler until all events have been fired and so that other addons can react to it first in case they use IsMeasuring
    EVENT_MANAGER:UnregisterForUpdate(LIB_IDENTIFIER_FINALIZE)
    EVENT_MANAGER:RegisterForUpdate(LIB_IDENTIFIER_FINALIZE, 0, FinalizeMeasurement)
end

local function GetPlayerPosition()
    return GetMapPlayerPosition("player")
end

local function GetPlayerWaypoint()
    return LMP:GetMapPing(MAP_PIN_TYPE_PLAYER_WAYPOINT)
end

local function SetMeasurementWaypoint(x, y)
    -- this waypoint stays invisible for others
    lib.suppressCount = lib.suppressCount + 1
    LMP:SuppressPing(MAP_PIN_TYPE_PLAYER_WAYPOINT)
    LMP:SetMapPing(MAP_PIN_TYPE_PLAYER_WAYPOINT, MAP_TYPE_LOCATION_CENTERED, x, y)
end

local function SetPlayerWaypoint(x, y)
    LMP:SetMapPing(MAP_PIN_TYPE_PLAYER_WAYPOINT, MAP_TYPE_LOCATION_CENTERED, x, y)
end

local function RemovePlayerWaypoint()
    LMP:RemoveMapPing(MAP_PIN_TYPE_PLAYER_WAYPOINT)
end

local function GetReferencePoints()
    local x1, y1 = GetPlayerPosition()
    local x2, y2 = GetPlayerWaypoint()
    return x1, y1, x2, y2
end

local function IsMapMeasured(mapId)
    return (mapMeasurements[mapId or GetMapTileTexture()] ~= nil)
end

local function StoreTamrielMapMeasurements()
    -- no need to actually measure the world map
    if (orgSetMapToMapListIndex(TAMRIEL_MAP_INDEX) ~= SET_MAP_RESULT_FAILED) then
        local measurement = {
            scaleX = 1,
            scaleY = 1,
            offsetX = 0,
            offsetY = 0,
            mapIndex = TAMRIEL_MAP_INDEX,
            zoneIndex = GetCurrentMapZoneIndex()
        }
        mapMeasurements[GetMapTileTexture()] = measurement
        rootMaps[TAMRIEL_MAP_INDEX] = measurement
        return true
    end

    return false
end

local function CalculateMeasurements(mapId, localX, localY)
    -- select the map corner farthest from the player position
    local wpX, wpY = POSITION_MIN, POSITION_MIN
    -- on some maps we cannot set the waypoint to the map border (e.g. Aurdion)
    -- Opposite corner:
    if (localX < 0.5) then wpX = POSITION_MAX end
    if (localY < 0.5) then wpY = POSITION_MAX end

    SetMeasurementWaypoint(wpX, wpY)

    -- add local points to seen maps
    local measurementPositions = {}
    table.insert(measurementPositions, { mapId = mapId, pX = localX, pY = localY, wpX = wpX, wpY = wpY })

    -- switch to zone map in order to get the mapIndex for the current location
    local x1, y1, x2, y2
    while not(GetMapType() == MAPTYPE_ZONE and GetMapContentType() ~= MAP_CONTENT_DUNGEON) do
        if (MapZoomOut() ~= SET_MAP_RESULT_MAP_CHANGED) then break end
        -- collect measurements for all maps we come through on our way to the zone map
        x1, y1, x2, y2 = GetReferencePoints()
        table.insert(measurementPositions, { mapId = GetMapTileTexture(), pX = x1, pY = y1, wpX = x2, wpY = y2 })
    end

    -- some non-zone maps like Eyevea zoom directly to the Tamriel map
    local mapIndex = GetCurrentMapIndex() or TAMRIEL_MAP_INDEX
    local zoneIndex = GetCurrentMapZoneIndex()

    -- switch to world map so we can calculate the global map scale and offset
    if orgSetMapToMapListIndex(TAMRIEL_MAP_INDEX) == SET_MAP_RESULT_FAILED then
        -- failed to switch to the world map
        LogMessage(LOG_NOTICE, "Could not switch to world map")
        return
    end

    -- get the two reference points on the world map
    x1, y1, x2, y2 = GetReferencePoints()

    -- calculate scale and offset for all maps that we saw
    local scaleX, scaleY, offsetX, offsetY
    for _, m in ipairs(measurementPositions) do
        if (mapMeasurements[m.mapId]) then break end -- we always go up in the hierarchy so we can stop once a measurement already exists
        LogMessage(LOG_DEBUG, "Store map measurement for", m.mapId:sub(10, -7))
        scaleX = (x2 - x1) / (m.wpX - m.pX)
        scaleY = (y2 - y1) / (m.wpY - m.pY)
        offsetX = x1 - m.pX * scaleX
        offsetY = y1 - m.pY * scaleY
        if (math.abs(scaleX - scaleY) > 1e-3) then
            LogMessage(LOG_WARNING, "Current map measurement might be wrong", m.mapId:sub(10, -7), mapIndex, m.pX, m.pY, m.wpX, m.wpY, x1, y1, x2, y2, offsetX, offsetY, scaleX, scaleY)
        end

        -- store measurements
        mapMeasurements[m.mapId] = {
            scaleX = scaleX,
            scaleY = scaleY,
            offsetX = offsetX,
            offsetY = offsetY,
            mapIndex = mapIndex,
            zoneIndex = zoneIndex
        }
    end
    return mapIndex
end

local function StoreCurrentWaypoint()
    currentWaypointX, currentWaypointY = GetPlayerWaypoint()
    currentWaypointMapId = GetMapTileTexture()
end

local function ClearCurrentWaypoint()
    currentWaypointX, currentWaypointY = 0, 0, nil
end

local function GetExtraMapMeasurement(extraMapIndex)
    -- switch to the map
    orgSetMapToMapListIndex(extraMapIndex)
    local extraMapId = GetMapTileTexture()
    if(not IsMapMeasured(extraMapId)) then
        -- calculate the measurements of map without worrying about the waypoint
        local mapIndex = CalculateMeasurements(extraMapId, GetPlayerPosition())
        if (mapIndex ~= extraMapIndex) then
            local name = GetMapInfo(extraMapIndex)
            name = zo_strformat("<<C:1>>", name)
            LogMessage(LOG_WARNING, "CalculateMeasurements returned different index while measuring ", name, " map. expected:", extraMapIndex, "actual:", mapIndex)
            if (not IsMapMeasured(extraMapId)) then
                LogMessage(LOG_WARNING, "Failed to measure ", name, " map.")
                return
            end
        end
    end
    return mapMeasurements[extraMapId]
end

local function RestoreCurrentWaypoint()
    if(not currentWaypointMapId) then
        LogMessage(LOG_DEBUG, "Called RestoreCurrentWaypoint without calling StoreCurrentWaypoint.")
        return
    end

    local wasSet = false
    if (currentWaypointX ~= 0 or currentWaypointY ~= 0) then
        -- calculate waypoint position on the worldmap
        local measurements = mapMeasurements[currentWaypointMapId]
        local x = currentWaypointX * measurements.scaleX + measurements.offsetX
        local y = currentWaypointY * measurements.scaleY + measurements.offsetY

        for rootMapIndex, measurements in pairs(rootMaps) do
            if not measurements then
                measurements = GetExtraMapMeasurement(rootMapIndex)
                rootMaps[rootMapIndex] = measurements
            end
            if(measurements) then
                if(x > measurements.offsetX and x < (measurements.offsetX + measurements.scaleX) and
                    y > measurements.offsetY and y < (measurements.offsetY + measurements.scaleY)) then
                    if(orgSetMapToMapListIndex(rootMapIndex) ~= SET_MAP_RESULT_FAILED) then
                        -- calculate waypoint coodinates within root map
                        x = (x - measurements.offsetX) / measurements.scaleX
                        y = (y - measurements.offsetY) / measurements.scaleY
                        SetPlayerWaypoint(x, y)
                        wasSet = true
                        break
                    end
                end
            end
        end
        if (not wasSet) then
            LogMessage(LOG_DEBUG, "Cannot reset waypoint because it was outside of our reach")
        end
    end

    if(wasSet) then
        LogMessage(LOG_DEBUG, "Waypoint was restored, request pin update")
        needWaypointRestore = true -- we need to update the pin on the worldmap afterwards
    else
        RemovePlayerWaypoint()
    end
    ClearCurrentWaypoint()
end

local function ConnectToWorldMap()
    lib.panAndZoom = ZO_WorldMap_GetPanAndZoom()
    lib.mapPinManager = ZO_WorldMap_GetPinManager()
    if (_G[DUMMY_PIN_TYPE]) then return end
    ZO_WorldMap_AddCustomPin(DUMMY_PIN_TYPE, function(pinManager) end , nil, { level = 0, size = 0, texture = "" })
    ZO_WorldMap_SetCustomPinEnabled(_G[DUMMY_PIN_TYPE], false)
end

local function HookSetMapToFunction(funcName)
    local orgFunction = _G[funcName]
    orgFunctions[funcName] = orgFunction
    local function NewFunction(...)
        local result = orgFunction(...)
        if(result ~= SET_MAP_RESULT_MAP_FAILED and not IsMapMeasured()) then
            LogMessage(LOG_DEBUG, funcName)

            local success, mapResult = lib:CalculateMapMeasurements(false)
            if(mapResult ~= SET_MAP_RESULT_CURRENT_MAP_UNCHANGED) then
                result = mapResult
            end
            orgFunction(...)
        end
        -- All stuff is done before anyone triggers an "OnWorldMapChanged" event due to this result
        return result
    end
    _G[funcName] = NewFunction
end

local function HookSetMapToPlayerLocation()
    orgSetMapToPlayerLocation = SetMapToPlayerLocation
    orgFunctions["SetMapToPlayerLocation"] = orgSetMapToPlayerLocation
    local function NewSetMapToPlayerLocation(...)
        if not DoesUnitExist("player") then return SET_MAP_RESULT_MAP_FAILED end
        local result = orgSetMapToPlayerLocation(...)
        if(result ~= SET_MAP_RESULT_MAP_FAILED and not IsMapMeasured()) then
            LogMessage(LOG_DEBUG, "SetMapToPlayerLocation")

            local success, mapResult = lib:CalculateMapMeasurements(false)
            if(mapResult ~= SET_MAP_RESULT_CURRENT_MAP_UNCHANGED) then
                result = mapResult
            end
            orgSetMapToPlayerLocation(...)
        end
        -- All stuff is done before anyone triggers an "OnWorldMapChanged" event due to this result
        return result
    end
    SetMapToPlayerLocation = NewSetMapToPlayerLocation
end

local function HookSetMapToMapListIndex()
    orgSetMapToMapListIndex = SetMapToMapListIndex
    orgFunctions["SetMapToMapListIndex"] = orgSetMapToMapListIndex
    local function NewSetMapToMapListIndex(mapIndex)
        local result = orgSetMapToMapListIndex(mapIndex)
        if(result ~= SET_MAP_RESULT_MAP_FAILED and not IsMapMeasured()) then
            LogMessage(LOG_DEBUG, "SetMapToMapListIndex")

            local success, mapResult = lib:CalculateMapMeasurements(false)
            if(mapResult ~= SET_MAP_RESULT_CURRENT_MAP_UNCHANGED) then
                result = mapResult
            end
            orgSetMapToMapListIndex(mapIndex)
        end

        -- All stuff is done before anyone triggers an "OnWorldMapChanged" event due to this result
        return result
    end
    SetMapToMapListIndex = NewSetMapToMapListIndex
end

local function HookProcessMapClick()
    orgProcessMapClick = ProcessMapClick
    orgFunctions["ProcessMapClick"] = orgProcessMapClick
    local function NewProcessMapClick(...)
        local result = orgProcessMapClick(...)
        if(result ~= SET_MAP_RESULT_MAP_FAILED and not IsMapMeasured()) then
            LogMessage(LOG_DEBUG, "ProcessMapClick")
            local success, mapResult = lib:CalculateMapMeasurements(true)
            if(mapResult ~= SET_MAP_RESULT_CURRENT_MAP_UNCHANGED) then
                result = mapResult
            end
            -- Returning is done via clicking already
        end
        return result
    end
    ProcessMapClick = NewProcessMapClick
end

local function HookSetMapFloor()
    orgSetMapFloor = SetMapFloor
    orgFunctions["SetMapFloor"] = orgSetMapFloor
    local function NewSetMapFloor(...)
        local result = orgSetMapFloor(...)
        if result ~= SET_MAP_RESULT_MAP_FAILED and not IsMapMeasured() then
            LogMessage(LOG_DEBUG, "SetMapFloor")
            local success, mapResult = lib:CalculateMapMeasurements(true)
            if(mapResult ~= SET_MAP_RESULT_CURRENT_MAP_UNCHANGED) then
                result = mapResult
            end
            orgSetMapFloor(...)
        end
        return result
    end
    SetMapFloor = NewSetMapFloor
end

local function Initialize() -- wait until we have defined all functions
    --- Unregister handler from older libGPS ( < 3)
    EVENT_MANAGER:UnregisterForEvent("LibGPS2_SaveWaypoint", EVENT_PLAYER_DEACTIVATED)
    EVENT_MANAGER:UnregisterForEvent("LibGPS2_RestoreWaypoint", EVENT_PLAYER_ACTIVATED)

    --- Unregister handler from older libGPS ( <= 5.1)
    EVENT_MANAGER:UnregisterForEvent(LIB_NAME .. "_Init", EVENT_PLAYER_ACTIVATED)
    --- Unregister handler from older libGPS, as it is now managed by LibMapPing ( >= 6)
    EVENT_MANAGER:UnregisterForEvent(LIB_NAME .. "_UnmuteMapPing", EVENT_MAP_PING)

    if (lib.Unload) then
        -- Undo action from older libGPS ( >= 5.2)
        lib:Unload()
        if (lib.suppressCount > 0) then
            if lib.debugMode then zo_callLater(function() LogMessage(LOG_WARNING, "There is a measure in progress before loading is completed.") end, 2000) end
            FinalizeMeasurement()
        end
    end

    --- Register new Unload
    function lib:Unload()
        for funcName, func in pairs(orgFunctions) do
            _G[funcName] = func
        end

        LMP:UnregisterCallback("AfterPingAdded", HandlePingEvent)
        LMP:UnregisterCallback("AfterPingRemoved", HandlePingEvent)

        rootMaps, mapMeasurements, mapStack = nil, nil, nil
    end

    ConnectToWorldMap()

    HookSetMapToFunction("SetMapToQuestCondition")
    HookSetMapToFunction("SetMapToQuestStepEnding")
    HookSetMapToFunction("SetMapToQuestZone")
    HookSetMapToPlayerLocation()
    HookSetMapToMapListIndex()
    HookProcessMapClick()
    HookSetMapFloor()

    StoreTamrielMapMeasurements()

    local function addRootMap(zoneId)
        local mapIndex = GetMapIndexByZoneId(zoneId)
        if mapIndex then rootMaps[mapIndex] = false end
    end
    addRootMap(347) -- Coldhabour
    addRootMap(980) -- Clockwork City
    -- Any future extra dimension map here

    SetMapToPlayerLocation() -- initial measurement so we can get back to where we are currently

    LMP:RegisterCallback("AfterPingAdded", HandlePingEvent)
    LMP:RegisterCallback("AfterPingRemoved", HandlePingEvent)
end

------------------------ public functions ----------------------

--- Returns true as long as the player exists.
function lib:IsReady()
    return DoesUnitExist("player")
end

--- Returns true if the library is currently doing any measurements.
function lib:IsMeasuring()
    return measuring
end

--- Removes all cached measurement values.
function lib:ClearMapMeasurements()
    mapMeasurements = { }
end

--- Removes the cached measurement values for the map that is currently active.
function lib:ClearCurrentMapMeasurements()
    local mapId = GetMapTileTexture()
    mapMeasurements[mapId] = nil
end

--- Returns a table with the measurement values for the active map or nil if the measurements could not be calculated for some reason.
--- The table contains scaleX, scaleY, offsetX, offsetY and mapIndex.
--- scaleX and scaleY are the dimensions of the active map on the Tamriel map.
--- offsetX and offsetY are the offset of the top left corner on the Tamriel map.
--- mapIndex is the mapIndex of the parent zone of the current map.
function lib:GetCurrentMapMeasurements()
    local mapId = GetMapTileTexture()
    if (not mapMeasurements[mapId]) then
        -- try to calculate the measurements if they are not yet available
        lib:CalculateMapMeasurements()
    end
    return mapMeasurements[mapId]
end

--- Returns the mapIndex and zoneIndex of the parent zone for the currently set map.
--- return[1] number - The mapIndex of the parent zone
--- return[2] number - The zoneIndex of the parent zone
function lib:GetCurrentMapParentZoneIndices()
    local measurements = lib:GetCurrentMapMeasurements()
    local mapIndex = measurements.mapIndex
    if(not measurements.zoneIndex) then
        lib:PushCurrentMap()
        SetMapToMapListIndex(mapIndex)
        measurements.zoneIndex = GetCurrentMapZoneIndex()
        lib:PopCurrentMap()
    end
    local zoneIndex = measurements.zoneIndex
    return mapIndex, zoneIndex
end

--- Calculates the measurements for the current map and all parent maps.
--- This method does nothing if there is already a cached measurement for the active map.
--- return[1] boolean - True, if a valid measurement was calculated
--- return[2] SetMapResultCode - Specifies if the map has changed or failed during measurement (independent of the actual result of the measurement)
function lib:CalculateMapMeasurements(returnToInitialMap)
    -- cosmic map cannot be measured (GetMapPlayerWaypoint returns 0,0)
    if (GetMapType() == MAPTYPE_COSMIC) then return false, SET_MAP_RESULT_CURRENT_MAP_UNCHANGED end

    -- no need to take measurements more than once
    local mapId = GetMapTileTexture()
    if (mapMeasurements[mapId] or mapId == "") then return false end

    if (lib.debugMode) then
        LogMessage("Called from", GetAddon(), "for", mapId)
    end

    -- get the player position on the current map
    local localX, localY = GetPlayerPosition()
    if (localX == 0 and localY == 0) then
        -- cannot take measurements while player position is not initialized
        return false, SET_MAP_RESULT_CURRENT_MAP_UNCHANGED
    end

    returnToInitialMap = (returnToInitialMap ~= false)

    measuring = true
    CALLBACK_MANAGER:FireCallbacks(lib.LIB_EVENT_STATE_CHANGED, measuring)

    -- check some facts about the current map, so we can reset it later
    -- local oldMapIsZoneMap, oldMapFloor, oldMapFloorCount
    if returnToInitialMap then
        lib:PushCurrentMap()
    end

    local hasWaypoint = LMP:HasMapPing(MAP_PIN_TYPE_PLAYER_WAYPOINT)
    if(hasWaypoint) then StoreCurrentWaypoint() end

    local mapIndex = CalculateMeasurements(mapId, localX, localY)

    -- Until now, the waypoint was abused. Now the waypoint must be restored or removed again (not from Lua only).
    if(hasWaypoint) then
        RestoreCurrentWaypoint()
    else
        RemovePlayerWaypoint()
    end

    if (returnToInitialMap) then
        local result = lib:PopCurrentMap()
        return true, result
    end

    return true, (mapId == GetMapTileTexture()) and SET_MAP_RESULT_CURRENT_MAP_UNCHANGED or SET_MAP_RESULT_MAP_CHANGED
end

--- Converts the given map coordinates on the current map into coordinates on the Tamriel map.
--- Returns x and y on the world map and the mapIndex of the parent zone
--- or nil if the measurements of the active map are not available.
function lib:LocalToGlobal(x, y)
    local measurements = lib:GetCurrentMapMeasurements()
    if (measurements) then
        x = x * measurements.scaleX + measurements.offsetX
        y = y * measurements.scaleY + measurements.offsetY
        return x, y, measurements.mapIndex
    end
end

--- Converts the given global coordinates into a position on the active map.
--- Returns x and y on the current map or nil if the measurements of the active map are not available.
function lib:GlobalToLocal(x, y)
    local measurements = lib:GetCurrentMapMeasurements()
    if (measurements) then
        x = (x - measurements.offsetX) / measurements.scaleX
        y = (y - measurements.offsetY) / measurements.scaleY
        return x, y
    end
end

--- Converts the given map coordinates on the specified zone map into coordinates on the Tamriel map.
--- This method is useful if you want to convert global positions from the old LibGPS version into the new format.
--- Returns x and y on the world map and the mapIndex of the parent zone
--- or nil if the measurements of the zone map are not available.
function lib:ZoneToGlobal(mapIndex, x, y)
    lib:GetCurrentMapMeasurements()
    -- measurement done in here:
    SetMapToMapListIndex(mapIndex)
    x, y, mapIndex = lib:LocalToGlobal(x, y)
    return x, y, mapIndex
end

--- This function zooms and pans to the specified position on the active map.
function lib:PanToMapPosition(x, y)
    -- if we don't have access to the mapPinManager we cannot do anything
    if (not self.mapPinManager) then return end
    local mapPinManager = self.mapPinManager
    -- create dummy pin
    local pin = mapPinManager:CreatePin(_G[DUMMY_PIN_TYPE], "libgpsdummy", x, y)

    self.panAndZoom:PanToPin(pin)

    -- cleanup
    mapPinManager:RemovePins(DUMMY_PIN_TYPE)
end

local function FakeZO_WorldMap_IsMapChangingAllowed() return true end
local function FakeSetMapToMapListIndex() return SET_MAP_RESULT_MAP_CHANGED end
local FakeCALLBACK_MANAGER = { FireCallbacks = function() end }

--- This function sets the current map as player chosen so it won't switch back to the previous map.
function lib:SetPlayerChoseCurrentMap()
    -- replace the original functions
    local oldIsChangingAllowed = ZO_WorldMap_IsMapChangingAllowed
    ZO_WorldMap_IsMapChangingAllowed = FakeZO_WorldMap_IsMapChangingAllowed

    local oldSetMapToMapListIndex = SetMapToMapListIndex
    SetMapToMapListIndex = FakeSetMapToMapListIndex

    local oldCALLBACK_MANAGER = CALLBACK_MANAGER
    CALLBACK_MANAGER = FakeCALLBACK_MANAGER

    -- make our rigged call to set the player chosen flag
    ZO_WorldMap_SetMapByIndex()

    -- cleanup
    ZO_WorldMap_IsMapChangingAllowed = oldIsChangingAllowed
    SetMapToMapListIndex = oldSetMapToMapListIndex
    CALLBACK_MANAGER = oldCALLBACK_MANAGER
end

--- Sets the best matching root map: Tamriel, Cold Harbour or Clockwork City and what ever will come.
--- Returns SET_MAP_RESULT_FAILED, SET_MAP_RESULT_MAP_CHANGED depending on the result of the API calls.
function lib:SetMapToRootMap(x, y)
    local result = SET_MAP_RESULT_FAILED
    for rootMapIndex, measurements in pairs(rootMaps) do
        if (not measurements) then
            measurements = GetExtraMapMeasurement(rootMapIndex)
            rootMaps[rootMapIndex] = measurements
            result = SET_MAP_RESULT_MAP_CHANGED
        end
        if (measurements) then
            if (x > measurements.offsetX and x < (measurements.offsetX + measurements.scaleX) and
                y > measurements.offsetY and y < (measurements.offsetY + measurements.scaleY)) then
                if (orgSetMapToMapListIndex(rootMapIndex) ~= SET_MAP_RESULT_FAILED) then
                    return SET_MAP_RESULT_MAP_CHANGED
                end
            end
        end
    end
    return result
end

--- Repeatedly calls ProcessMapClick on the given global position starting on the Tamriel map until nothing more would happen.
--- Returns SET_MAP_RESULT_FAILED, SET_MAP_RESULT_MAP_CHANGED or SET_MAP_RESULT_CURRENT_MAP_UNCHANGED depending on the result of the API calls.
function lib:MapZoomInMax(x, y)
    local result = lib:SetMapToRootMap(x, y)

    if (result ~= SET_MAP_RESULT_FAILED) then
        local localX, localY = lib:GlobalToLocal(x, y)

        while WouldProcessMapClick(localX, localY) do
            result = orgProcessMapClick(localX, localY)
            if (result == SET_MAP_RESULT_FAILED) then break end
            localX, localY = lib:GlobalToLocal(x, y)
        end
    end

    return result
end

--- Stores information about how we can back to this map on a stack.
-- There is no panAndZoom:GetCurrentOffset(), yet
local function CalculateContainerAnchorOffsets()
    local containerCenterX, containerCenterY = ZO_WorldMapContainer:GetCenter()
    local scrollCenterX, scrollCenterY = ZO_WorldMapScroll:GetCenter()
    return containerCenterX - scrollCenterX, containerCenterY - scrollCenterY
end
function lib:PushCurrentMap()
    local wasPlayerLocation, targetMapTileTexture, currentMapFloor, currentMapFloorCount, currentMapIndex, zoom, offsetX, offsetY
    currentMapIndex = GetCurrentMapIndex()
    wasPlayerLocation = DoesCurrentMapMatchMapForPlayerLocation()
    targetMapTileTexture = GetMapTileTexture()
    currentMapFloor, currentMapFloorCount = GetMapFloorInfo()
    zoom = self.panAndZoom:GetCurrentZoom()
    offsetX, offsetY = CalculateContainerAnchorOffsets()

    mapStack[#mapStack + 1] = { wasPlayerLocation, targetMapTileTexture, currentMapFloor, currentMapFloorCount, currentMapIndex, zoom, offsetX, offsetY }
end

--- Switches to the map that was put on the stack last.
--- Returns SET_MAP_RESULT_FAILED, SET_MAP_RESULT_MAP_CHANGED or SET_MAP_RESULT_CURRENT_MAP_UNCHANGED depending on the result of the API calls.
function lib:PopCurrentMap()
    local result = SET_MAP_RESULT_FAILED
    local data = table.remove(mapStack, #mapStack)
    if(not data) then
        LogMessage(LOG_DEBUG, "PopCurrentMap failed. No data on map stack.")
        return result
    end

    local wasPlayerLocation, targetMapTileTexture, currentMapFloor, currentMapFloorCount, currentMapIndex, zoom, offsetX, offsetY = unpack(data)
    local currentTileTexture = GetMapTileTexture()
    if(currentTileTexture ~= targetMapTileTexture) then
        if(wasPlayerLocation) then
            result = orgSetMapToPlayerLocation()

        elseif(currentMapIndex ~= nil and currentMapIndex > 0) then -- set to a zone map
            result = orgSetMapToMapListIndex(currentMapIndex)

        else -- here is where it gets tricky
            local target = mapMeasurements[targetMapTileTexture]
            if(not target) then -- always just return to player map if we cannot restore the previous map.
                LogMessage(LOG_DEBUG, string.format("No measurement for \"%s\". Returning to player location.", targetMapTileTexture))
                return orgSetMapToPlayerLocation()
            end

            -- switch to the parent zone
            if(target.mapIndex == TAMRIEL_MAP_INDEX) then -- zone map has no mapIndex (e.g. Eyevea or Hew's Bane on first PTS patch for update 9)
                -- switch to the tamriel map just in case
                result = orgSetMapToMapListIndex(TAMRIEL_MAP_INDEX)
                if(result == SET_MAP_RESULT_FAILED) then return result end
                -- get global coordinates of target map center
                local x = target.offsetX + (target.scaleX / 2)
                local y = target.offsetY + (target.scaleY / 2)
                if(not WouldProcessMapClick(x, y)) then
                    LogMessage(LOG_DEBUG, string.format("Cannot process click at %s/%s on map \"%s\" in order to get to \"%s\". Returning to player location instead.", tostring(x), tostring(y), GetMapTileTexture(), targetMapTileTexture))
                    return orgSetMapToPlayerLocation()
                end
                result = orgProcessMapClick(x, y)
                if(result == SET_MAP_RESULT_FAILED) then return result end
            else
                result = orgSetMapToMapListIndex(target.mapIndex)
                if(result == SET_MAP_RESULT_FAILED) then return result end
            end

            -- switch to the sub zone
            currentTileTexture = GetMapTileTexture()
            if(currentTileTexture ~= targetMapTileTexture) then
                -- determine where on the zone map we have to click to get to the sub zone map
                -- get global coordinates of target map center
                local x = target.offsetX + (target.scaleX / 2)
                local y = target.offsetY + (target.scaleY / 2)
                -- transform to local coordinates
                local current = mapMeasurements[currentTileTexture]
                if(not current) then
                    LogMessage(LOG_DEBUG, string.format("No measurement for \"%s\". Returning to player location.", currentTileTexture))
                    return orgSetMapToPlayerLocation()
                end

                x = (x - current.offsetX) / current.scaleX
                y = (y - current.offsetY) / current.scaleY

                if(not WouldProcessMapClick(x, y)) then
                    LogMessage(LOG_DEBUG, string.format("Cannot process click at %s/%s on map \"%s\" in order to get to \"%s\". Returning to player location instead.", tostring(x), tostring(y), GetMapTileTexture(), targetMapTileTexture))
                    return orgSetMapToPlayerLocation()
                end
                result = orgProcessMapClick(x, y)
                if(result == SET_MAP_RESULT_FAILED) then return result end
            end

            -- switch to the correct floor (e.g. Elden Root)
            if (currentMapFloorCount > 0) then
                result = orgSetMapFloor(currentMapFloor)
            end
            if (result ~= SET_MAP_RESULT_FAILED) then
                lib.panAndZoom:SetCurrentZoom(zoom)
                lib.panAndZoom:SetCurrentOffset(offsetX, offsetY)
            end
        end
    else
        result = SET_MAP_RESULT_CURRENT_MAP_UNCHANGED
    end

    return result
end

Initialize()

Compare with Previous | Blame