view WorldPlan.lua @ 27:4a7e89bffbcb r27-beta

- Order Hall resource rewards now show numerals in quest pins. - Fixed an occasional lua error that occurs when opening the quest map for the first time. - Fixed another source of textures losing their masks.
author Nenue
date Thu, 27 Oct 2016 06:18:16 -0400
parents 08b03bcdfeac
children c1612c2c1840
line wrap: on
line source
-- Veneer
-- WorldPlan.lua
-- Created: 8/16/2016 8:19 AM
-- %file-revision%
--[[
 Summary:
 Adds reward icons to the world quest POI markers, and adds said markers to the continent map.

 Issues:
 Dalaran quests aren't visible until that map has been specifically viewed by the player.
--]]

WorldPlanCore = {}
WorldPlanPOIMixin = {}
WorldPlanFilterPinMixin = {}
local WorldPlanFlightMapMixin = setmetatable({}, {__tostring = function() return 'FlightMapHandler' end})
local WorldQuests = setmetatable({ QuestsByID = {}, freePins = {} }, {__tostring = function() return 'QuestHandler' end})
local FilterBar = setmetatable({ SummaryHeaders = {} },  {__tostring = function() return 'FilterBar' end})

local WorldPlan = WorldPlanCore
local QuestPOI = WorldPlanPOIMixin
local FilterPin = WorldPlanFilterPinMixin
local WP_VERSION = "1.0"

local db
local print = DEVIAN_WORKSPACE and function(...) _G.print('WP', ...) end or function() end
local qprint = DEVIAN_WORKSPACE and function(...) _G.print('POI', ...) end or function() end
local iprint = DEVIAN_WORKSPACE and function(...) _G.print('ItemScan', ...) end or function() end
local wqprint = DEVIAN_WORKSPACE and function(...) _G.print('WorldQuests', ...) end or function() end
local fbprint = DEVIAN_WORKSPACE and function(...) _G.print('FilterBar', ...) end or function() end

local wipe, tremove, tinsert, pairs, floor, tContains = table.wipe, table.remove, table.insert, pairs, floor, tContains
local TQ_GetQuestInfoByQuestID = C_TaskQuest.GetQuestInfoByQuestID -- Return the name of a quest with a given ID
local TQ_GetQuestsForPlayerByMapID = C_TaskQuest.GetQuestsForPlayerByMapID -- This function is not yet documented
local TQ_GetQuestTimeLeftMinutes = C_TaskQuest.GetQuestTimeLeftMinutes
local TQ_RequestPreloadRewardData = C_TaskQuest.RequestPreloadRewardData
local TQ_GetQuestLocation = C_TaskQuest.GetQuestLocation
local TQ_IsActive = C_TaskQuest.IsActive
local ITEM_QUALITY_COLORS = ITEM_QUALITY_COLORS
local WorldMap_DoesWorldQuestInfoPassFilters = WorldMap_DoesWorldQuestInfoPassFilters
local QuestMapFrame_IsQuestWorldQuest = QuestMapFrame_IsQuestWorldQuest
local GameTooltip = GameTooltip
local GetItemIcon = GetItemIcon


local GetMapInfo, QuestPOIGetIconInfo = GetMapInfo, QuestPOIGetIconInfo
local GetQuestTagInfo, HaveQuestData =  GetQuestTagInfo, HaveQuestData
local GetNumQuestLogRewards, GetNumQuestLogRewardCurrencies, GetQuestLogRewardMoney = GetNumQuestLogRewards, GetNumQuestLogRewardCurrencies, GetQuestLogRewardMoney
local GetQuestLogRewardInfo, GetQuestLogRewardCurrencyInfo, GetMoneyString = GetQuestLogRewardInfo, GetQuestLogRewardCurrencyInfo, GetMoneyString

local GetCurrentMapAreaID, GetMapNameByID, GetSuperTrackedQuestID = GetCurrentMapAreaID, GetMapNameByID, GetSuperTrackedQuestID
local MC_GetNumZones, MC_GetZoneInfo = C_MapCanvas.GetNumZones, C_MapCanvas.GetZoneInfo


local PinBaseIndex = 1600
local BROKEN_ISLES_ID, DALARAN_ID, AZSUNA_ID, VALSHARAH_ID, HIGHMOUNTAIN_ID, STORMHEIM_ID, SURAMAR_ID, EOA_ID = 1007, 1014, 1015,1018, 1024, 1017, 1033, 1096

-- maps where we do our own anchors
local CONTINENT_MAPS = { [BROKEN_ISLES_ID] = BROKEN_ISLES_ID, }
local WORLD_QUEST_MAPS = { [DALARAN_ID] = 'Dalaran70',  [AZSUNA_ID] = 'Azsuna',  [VALSHARAH_ID] = "Val'sharah",
  [HIGHMOUNTAIN_ID] = 'Highmountain', [STORMHEIM_ID] = 'Stormheim',  [SURAMAR_ID] = 'Suramar',  [EOA_ID] = 'EyeOfAszhara', }
-- default color templates
local ARTIFACT_COLOR = ITEM_QUALITY_COLORS[LE_ITEM_QUALITY_ARTIFACT]
local MONEY_COLOR = {hex ='|cFFFFFF00', r=1, g=1, b=0}
local COMMON_COLOR = ITEM_QUALITY_COLORS[LE_ITEM_QUALITY_COMMON]

local ICON_UNKNOWN = "Interface\\ICONS\\inv_misc_questionmark"
local ICON_MONEY = "Interface\\Buttons\\UI-GroupLoot-Coin-Up"

local POI_BORDER_MASK = "Interface\\Minimap\\UI-Minimap-Background"
local POI_BORDER_FILL = "Interface\\BUTTONS\\YELLOWORANGE64"
local POI_BORDER_BLUE = "Interface\\BUTTONS\\GRADBLUE"
local POI_BORDER_RED = "Interface\\BUTTONS\\RedGrad64"
local POI_BORDER_YELLOW = "Interface\\BUTTONS\\YELLOWORANGE64"
local POI_BORDER_GREEN = "Interface\\BUTTONS\\GREENGRAD64"

local REWARD_CASH = 1001
local REWARD_ARTIFACT_POWER = 1002
local REWARD_GEAR = 1003
local REWARD_CURRENCY = 1004
local REWARD_ITEM = 1005
local REWARD_REAGENT = 1006

local POI_DEFAULT_TYPE = {
  a = 1,
  r = 1, g = 1, b = 1,
  x = 0, y = 0,
  desaturated = true,
  pinMask = POI_BORDER_MASK,
  rewardMask = POI_BORDER_MASK,
  texture = POI_BORDER_FILL,
  continent = {
    PinSize = 18,
    Border = 3,
    TrackingBorder = 2,
    TagSize = 6,
    TimeleftStage = 3,
    showNumber = true,
  },
  zone = {
    PinSize = 22,
    Border = 3,
    TrackingBorder = 2,
    TagSize = 12,
    TimeleftStage = 3,
    showNumber = true,
  },
  minimized = {
    PinSize = 4,
    Border = 1,
    TrackingBorder = 2,
    NoIcon = true,
    TimeleftStage = 1,
    showNumber = false,
  }
}
local POI_REWARD_TYPE =  setmetatable({}, {
  __newindex = function(t, k, v)
    if type(v) == 'table' then
      setmetatable(v, {__index = POI_DEFAULT_TYPE})
    end
    rawset(t,k,v)
  end
})
local POI_FILTER_STYLE = setmetatable({
  continentBorder = 2,
  zoneBorder = 2,
}, {__index = POI_DEFAULT_TYPE})

local LE_QUEST_TAG_TYPE_PVP = LE_QUEST_TAG_TYPE_PVP
local LE_QUEST_TAG_TYPE_PET_BATTLE = LE_QUEST_TAG_TYPE_PET_BATTLE
local LE_QUEST_TAG_TYPE_DUNGEON = LE_QUEST_TAG_TYPE_DUNGEON
local LE_QUEST_TAG_TYPE_PROFESSION = LE_QUEST_TAG_TYPE_PROFESSION
local LE_QUEST_TAG_TYPE_NORMAL = LE_QUEST_TAG_TYPE_NORMAL

-- Pin color/display variables
POI_REWARD_TYPE[REWARD_ITEM] = {
  r = 1, g = 1, b = 1,
}
POI_REWARD_TYPE[REWARD_REAGENT] = {
  r = 0, g = 1, b = 1,
}
POI_REWARD_TYPE[REWARD_ARTIFACT_POWER] = {
  r = 1, g = .25, b = .5,
  hasNumeric = true,
  numberRGB = {1, 1, 1},
}
POI_REWARD_TYPE[REWARD_GEAR] = {
  r = .1, g = .2, b = 1,
}
POI_REWARD_TYPE[REWARD_CURRENCY] = {
  r = 1, g = 1, b = 0,
  hasNumeric = true,
  numberRGB = {1, 1, 1},
}
POI_REWARD_TYPE[REWARD_CASH] = {
  r = 0, g = 0, b = 0,
  --x = 0, y = -1,
  --mask = ICON_MONEY,
  --continentBorder = 1,
  --zoneBorder = 1,
}



local defaults = {
  defaultPinStyle = POI_DEFAULT_TYPE,
  rewardStyle = POI_REWARD_TYPE,
  filterStyle = POI_FILTER_STYLE,
  ShowAllProfessionQuests = false,
  DisplayContinentSummary = true,
  DisplayContinentPins = true,
  NotifyWhenNewQuests = true,
  EnablePins = true,
  FadeWhileGrouped = true,
}

-- Summary header structure
local POI_FILTER_OPTIONS = {
  { label = 'Filters', texture = "Interface\\WorldMap\\WorldMap-Icon" },
  { filterKey= 'rewardType', filterValue = REWARD_ARTIFACT_POWER, label = 'Artifact Power', texture = "Interface\\ICONS\\inv_7xp_inscription_talenttome01" },
  { filterKey= 'rewardType', filterValue = REWARD_CURRENCY,label = 'Currency', texture = "Interface\\ICONS\\inv_misc_elvencoins" },
  { filterKey= 'rewardType', filterValue = REWARD_ITEM, label = 'Item', texture = "Interface\\ICONS\\inv_crate_01" },
  { filterKey= 'rewardType', filterValue = REWARD_GEAR, label = 'Equipment', texture = "Interface\\ICONS\\garrison_bluearmorupgrade" },
  { filterKey= 'rewardType', filterValue = REWARD_REAGENT, label = 'Reagents', texture = 1417744 },
  { filterKey= 'rewardType', filterValue = REWARD_CASH, label = 'Reagents', texture = ICON_MONEY },
  { filterKey= 'worldQuestType', filterValue = LE_QUEST_TAG_TYPE_PVP, label = 'PvP', texture = "Interface\\Icons\\Ability_PVP_GladiatorMedallion", spacing = 10 },
  { filterKey= 'worldQuestType', filterValue = LE_QUEST_TAG_TYPE_PET_BATTLE, label = 'Pet Battle', texture = "Interface\\Icons\\PetJournalPortrait", },
  { filterKey= 'worldQuestType', filterValue = LE_QUEST_TAG_TYPE_DUNGEON, label = 'Dungeon', texture = "Interface\\LFGFRAME\\UI-LFR-PORTRAIT", },
  { filterKey= 'worldQuestType', filterValue = LE_QUEST_TAG_TYPE_PROFESSION, label = 'Profession', texture = "Interface\\ICONS\\70_professions_scroll_02", },
}
WorldPlanCore.BrokenIsleID = BROKEN_ISLES_ID
WorldPlanCore.FilterStyle = POI_FILTER_STYLE

WorldPlanCore.FilterOptions = {}
WorldPlanCore.UsedFilters = {}


-- operating flags
local superTrackedID
local currentMapName
local hasNewQuestPins
local isContinentMap
local numPins = 0
local QuestsByZone = {}
local QuestsByFaction = {}
local QuestsByReward = {}
local QuestsByTag = {}
local QuestsByID = {}
local QuestPositions = {}
local FilterInclusions = {rewardType = {}, worldQuestType = {}, factionID = {}}
local NotificationTypes = {}
local ZoneInfo = {}
local SummaryHeaders = {}

local FreePins = {}
local NumPinFrames = 1

local hasPendingQuestData
local notifyPlayed
local scanner, wmtt, WorldMapPOIFrame

WorldPlanCore.QuestsByID = QuestsByID
WorldPlanCore.QuestsByZone = QuestsByZone

local tasksQueue = {}
local function OnNext (func)
  if #tasksQueue == 0 then
    _G.WorldPlan:SetScript('OnUpdate', function()
      local func = tremove(tasksQueue, 1)
      if func then
        func()
      end
      if #tasksQueue == 0 then
        _G.WorldPlan:SetScript('OnUpdate', nil)
      end
    end)
  end
  print('inserting task #', #tasksQueue+1, func)
  tinsert(tasksQueue, func)
end

-- combines templates
local function DoMixins(frame,...)
  for i = 1, select('#', ...) do
    for k,v in pairs(select(i,...)) do
      frame[k] = v
    end
  end
  return frame
end

-- use tooltip object to extract item details
local ParseItemReward = function(questID)
  local rewardType = REWARD_ITEM
  local name, icon, quantity, quality, _, itemID = GetQuestLogRewardInfo(1, questID)
  if not itemID then
    return REWARD_ITEM
  end

  scanner:SetOwner(WorldPlan, "ANCHOR_NONE")
  scanner:SetItemByID(itemID)
  local ttl1 = _G['WorldPlanTooltipTextLeft1']
  local ttl2 = _G['WorldPlanTooltipTextLeft2']
  local ttl3 = _G['WorldPlanTooltipTextLeft3']
  local ttl4 = _G['WorldPlanTooltipTextLeft4']
  if ttl2 then
    local text = ttl2:GetText()
    -- Artifact Power
    if text and text:match("|cFFE6CC80") then
      --print('AP token!', text)
      local power
      if ttl4 then
        local text = ttl4:GetText()
        --print('tip line 4', text)
        if text then
          power = text:gsub("%p", ""):match("%d+")
          power = tonumber(power)
        end

      end
      rewardType = REWARD_ARTIFACT_POWER
      icon = "Interface\\ICONS\\inv_7xp_inscription_talenttome01"
      quantity = power
    elseif text and text:match("Item Level") then
      --print('equipment!', text)
      rewardType = REWARD_GEAR
      quantity = text:match("Item Level ([%d\+]+)")
    end
  end
  if ttl3 then
    local text = ttl3:GetText()
    -- Crafting Reagent
    if text and text:match("Crafting Reagent") then
      --print('reagent', text)
      rewardType = REWARD_REAGENT
    end
  end
  iprint('  item:', name, rewardType, icon, quantity)
  return rewardType, icon, quantity, name, itemID
end

-- update a masked texture without messing up its blending mask
local SetMaskedTexture = function(region, file, mask)
  mask = mask or POI_BORDER_MASK
  region:SetMask(nil)
  region:SetTexture(file)
  region:SetMask(mask)
end

-- tracking menu toggler
local DropDown_OnClick = function(self)
  local key = self.value
  if key then
    if WorldPlanData[key] then
      WorldPlanData[key] = nil
    else
      WorldPlanData[key] = true
    end
  end
  WorldPlan:RefreshAll()
end

function WorldPlan:print(...)
  local msg
  for i = 1, select('#', ...) do
    msg = (msg and (msg .. ' ') or '') .. tostring(select(i, ...))
  end
  DEFAULT_CHAT_FRAME:AddMessage("|cFF0088FFWorldPlan|r: " .. msg)
end

function WorldPlan:OnLoad ()
  WorldPlan = self
  scanner = _G.WorldPlanTooltip
  wmtt = _G.WorldMapTooltip
  WorldMapPOIFrame = _G.WorldMapPOIFrame

  WorldPlan:print('v'..WP_VERSION)

  self:RegisterEvent("QUESTLINE_UPDATE")
  self:RegisterEvent("QUEST_LOG_UPDATE")
  self:RegisterEvent("WORLD_MAP_UPDATE")
  self:RegisterEvent("WORLD_QUEST_COMPLETED_BY_SPELL")
  self:RegisterEvent("SUPER_TRACKED_QUEST_CHANGED")
  self:RegisterEvent("SKILL_LINES_CHANGED")
  self:RegisterEvent("ARTIFACT_XP_UPDATE")
  self:RegisterEvent("ADDON_LOADED")
  self:SetParent(WorldMapFrame)

  WorldPlan.modules = {
    WorldQuests, FilterBar, WorldPlanFlightMapMixin,
  }
end

function WorldPlan:OnShow()
  print(self:GetName()..':OnShow()')
  if self.isStale then
    self:RefreshAll()
  end

end

function WorldPlan:OnEvent (event, ...)
  print()
  print(event, self.initialized)
  if event == 'ADDON_LOADED' then
    local addon = ...
    if addon == "Blizzard_FlightMap" then
      print('do mixin junk')
      self.OnFlightMapLoaded()

    end
    if IsLoggedIn() and not self.initialized then
      self:Setup()
    end
  elseif event == 'WORLD_MAP_UPDATE' then
    self.currentMapID = GetCurrentMapAreaID()
    self:RefreshAll()
  else
    for i, module in ipairs(self.modules) do
      if module.OnEvent then
        print('forwarding to', tostring(module))
        module:OnEvent(event, ...)
      end
    end
  end
end


function WorldPlan:Setup ()
  if not WorldPlanData then
    WorldPlanData = {key = 0 }
  end
  WorldPlanData.key = (WorldPlanData.key or 0) + 1
  self.db = WorldPlanData
  self.db.WorldQuests = self.db.WorldQuests or {}
  db = self.db
  for k,v in pairs(defaults) do
    --[===[@non-debug@
    if not db[k] then
      db[k] = v
    end

    --@end-non-debug@]===]
    --@debug@
    db[k] = v
    --@end-debug@
  end

  self.currentMapID = GetCurrentMapAreaID()

  for i, module in ipairs(self.modules) do
    module.db = self.db
    if module.Setup then module:Setup() end
    if not module.RegisterEvent then
      module.RegisterEvent = self.RegisterEvent
    end
  end
  self.initialized = true

  hooksecurefunc("UIDropDownMenu_Initialize", self.OnDropDownInitialize)
end

function WorldPlan:RefreshAll (forced)
  if not self.initialized then
    return
  end

  POI_DEFAULT_TYPE = db.defaultPinStyle
  POI_REWARD_TYPE = db.rewardStyle
  POI_FILTER_STYLE = db.filterStyle


  for i, module in ipairs(self.modules) do
    if module.Reset then
      print(module, 'Reset()')
      module:Reset()
    end
  end

  for i, module in ipairs(self.modules) do
    if module.Refresh then
      print(module, 'Refresh()')
      module:Refresh()
    end
  end

  for i, module in ipairs(self.modules) do
    if module.Cleanup then
      print(module, 'Cleanup()')
      module:Cleanup()
    end
  end
end
function WorldPlan:UpdateAnchors ()
  for i, module in ipairs(self.modules) do
    if module.UpdateAnchors then
      module:UpdateAnchors()
    end
  end
end

-- insert visual options into the tracking button menu
WorldPlan.OnDropDownInitialize = function  (self, callback, dropType)
  if self ~= WorldMapFrameDropDown then
    return
  end
  local db = WorldPlan.db

  local info = UIDropDownMenu_CreateInfo()
  info.text = ""
  info.isTitle = true
  UIDropDownMenu_AddButton(info)
  info.text = "|cFF00AAFFWorldPlan|r"
  info.isTitle = true
  UIDropDownMenu_AddButton(info)
  info.isTitle = nil
  info.disabled = nil
  info.keepShownOnClick = true
  info.tooltipOnButton = 1

  info.text = "Enable"
  info.isNotRadio = true
  info.value = "EnablePins"
  info.checked = db.EnablePins
  info.tooltipTitle = "Enable World Quest Overlays"
  info.tooltipText = "Toggle the detail layers here."
  info.func = DropDown_OnClick
  UIDropDownMenu_AddButton(info)

  info.text = "Display All Profession Quests"
  info.isNotRadio = true
  info.value = "ShowAllProfessionQuests"
  info.checked = db.ShowAllProfessionQuests
  info.tooltipTitle = "Hidden Quests"
  info.tooltipText = "Display work order and profession-related quests that are skipped by the default UI."
  info.func = DropDown_OnClick
  UIDropDownMenu_AddButton(info)

  info.text = "Show Continent Pins"
  info.isNotRadio = true
  info.value = "DisplayContinentPins"
  info.checked = db.DisplayContinentPins
  info.tooltipTitle = "Continent Pins"
  info.tooltipText = "Display quest pins on the continent map (may get cramped)."
  info.func = DropDown_OnClick
  UIDropDownMenu_AddButton(info)

  info.text = "Show Summary"
  info.isNotRadio = true
  info.value = "DisplayContinentSummary"
  info.tooltipTitle = "Summary Bar"
  info.tooltipText = "Display a summary of active world quests. Note: requires directly viewing Broken Isle and Dalaran maps to gain complete info."
  info.checked = db.DisplayContinentSummary
  info.func = DropDown_OnClick
  UIDropDownMenu_AddButton(info)

  info.text = "Fade In Groups"
  info.isNotRadio = true
  info.value = "FadeWhileGrouped"
  info.tooltipTitle = "Group Fade"
  info.tooltipText = "Reduce pin alpha when grouped, so player dots are easier to see."
  info.checked = db.DisplayContinentSummary
  info.func = DropDown_OnClick
  UIDropDownMenu_AddButton(info)
end

function WorldQuests:Setup()
  -- refresh positions any time blizzard does so (i.e. mousewheel zoom)
  hooksecurefunc("WorldMapScrollFrame_ReanchorQuestPOIs", function()
    self:Refresh(true)
  end)

  -- hide the original world quest POIs
  hooksecurefunc("WorldMap_UpdateQuestBonusObjectives", function()
    for i = 1, NUM_WORLDMAP_TASK_POIS do
      local button = _G['WorldMapFrameTaskPOI'..i]
      if button and button.worldQuest then
        button:Hide()
      end
    end
  end)

end

function WorldQuests:OnEvent (event, ...)
  print('|cFFFFFF00'..tostring(self)..':OnEvent()'..event..'|r', GetTime(), ...)
  if event == 'QUEST_LOG_UPDATE' then
    local questID, added = ...
    if questID and added then
      self:GetPinByQuestID(questID)
    else
      self:GetPinsForMap()
    end
      print('WorldMapFrame', WorldMapFrame:IsVisible(), 'doRefresh:', hasNewQuestPins)
    if WorldMapFrame:IsVisible() and hasNewQuestPins then
      self:Refresh(true)
    end

  elseif event == 'WORLD_QUEST_COMPLETED_BY_SPELL' then
    local questID = ...
    if questID and QuestsByID[questID] then
      self:ReleasePin(QuestsByID[questID])
    end
  elseif event == 'SKILL_LINES_CHANGED' then
    self:Refresh(true)
  end
end

function WorldQuests:AcquirePin (questID, pinTable)
  local pin = QuestsByID[questID]
  local isNew = false
  if not pin then
    isNew = true
    local numFree = #self.freePins
    if numFree >= 1 then
      pin = tremove(self.freePins, numFree)
      --print('|cFF00FF00Re-using', pin:GetName())
    else
      local name = 'WorldPlanQuestMarker' .. NumPinFrames
      --print('|cFF00FF00Creating', name)
      pin = CreateFrame('Frame', name, WorldMapPOIFrame, 'WorldPlanQuestPin')
      pin:SetFrameStrata('HIGH')

      NumPinFrames = NumPinFrames + 1

      --pin.iconBorder:SetVertexColor(0,0,0,1)

    end
    QuestsByID[questID] = pin
    pin.isNew = true
    pin.currentWidth = nil

    -- used by TaskPOI_x scripts
    pin.questID = questID
    pin.worldQuest = true

    pin.Reset = function(self)
      WorldQuests:GetPinByQuestID(questID)
    end
  else
    --print('|cFF00FF00Using', pin:GetName())
  end

  -- set display flags accordingly
  if pinTable then
    for k,v in pairs(pinTable) do
      pin[k] = v
    end
  end
  pin.throttle = nil
  pin.timeThreschold = nil
  return pin, isNew
end

-- remove from index and add it to the recycling heap
function WorldQuests:ReleasePin (pin)

  local id = pin.questId
  if id then
    QuestsByID[id] = nil
    for i, zone in pairs(QuestsByZone) do
      print('-', i, zone[i])
      zone[id] = nil
    end
  end
  if pin.factionID then
    QuestsByFaction[pin.factionID][id] = nil
  end
  pin:Hide()
  pin:ClearAllPoints()
  tinsert(self.freePins, pin)
  print('|cFFFF4400Clearing out', pin:GetName(),id)
end

-- create of update quest pins for a map and its underlying zones
function WorldQuests:GetPinsForMap (mapID)
  local print = wqprint
  mapID = mapID or GetCurrentMapAreaID()
  superTrackedID = GetSuperTrackedQuestID()
  if not mapID then
    -- info not available yet
    return
  end
  if mapID == BROKEN_ISLES_ID then
    hasPendingQuestData = nil
    print('|cFF00FFFFRefreshQuestsForMap|r', mapID, GetMapNameByID(mapID), superTrackedID)
    self.fullSearch = true
    for i = 1, MC_GetNumZones(mapID) do
      local submapID, name, depth = MC_GetZoneInfo(mapID, i)
      self:GetPinsForMap(submapID)
    end
    self.fullSearch = nil
  elseif QuestsByZone[mapID] then
    local taskInfo = TQ_GetQuestsForPlayerByMapID(mapID)
    local quest = QuestsByZone[mapID]
    local numQuests = 0
    if taskInfo and #taskInfo >= 1 then
      print('|cFF00FFFFRefreshQuestsForMap|r', mapID, GetMapNameByID(mapID), #taskInfo)
      wipe(QuestsByZone[mapID])
      ZoneInfo[mapID] = taskInfo
      for taskID, info in pairs(taskInfo) do
        local questID = info.questId

        info.mapID = mapID
        QuestsByZone[mapID][questID] = self:GetPinByQuestID(questID, info)
        numQuests = numQuests + 1
      end
    end
  end
end

-- create or update the pin using the given questID and C_TaskQuest results
function WorldQuests:GetPinByQuestID (questID, taskInfo)

    local questTitle, rewardIcon, rewardName, rewardCount, rewardStyle, rewardType, itemID, quantity, quality, _
    local pin = self:AcquirePin(questID, taskInfo)

    if pin.isNew then
      if not hasNewQuestPins then
        print('triggering new quest pins event')
      end

      hasNewQuestPins = true
    end

    if not HaveQuestData(questID) then
      print('|cFFFF4400Retrieval failed.')
      TQ_RequestPreloadRewardData(questID)
      hasPendingQuestData = true
    else
      wqprint('|cFF00FF88Quest Data Received|r')
      pin.mapID = pin.mapID or C_TaskQuest.GetQuestZoneID(questID)

      -- set reward category
      local numRewards = GetNumQuestLogRewards(questID)
      local numCurrency = GetNumQuestLogRewardCurrencies(questID)
      local money = GetQuestLogRewardMoney(questID)
      if numRewards >= 1 then
        rewardType, rewardIcon, rewardCount, rewardName, itemID = ParseItemReward(questID)
      elseif numCurrency >= 1 then
        rewardName, rewardIcon, rewardCount = GetQuestLogRewardCurrencyInfo(1, questID)
        rewardType = REWARD_CURRENCY
      elseif money >= 1 then
        rewardIcon = ICON_MONEY
        rewardName = GetMoneyString(money)
        rewardType = REWARD_CASH
      end
      rewardStyle = POI_REWARD_TYPE[rewardType] or POI_DEFAULT_TYPE

      pin.itemNumber = rewardCount or pin.itemNumber
      pin.rewardType = rewardType or REWARD_ITEM
      pin.style = rewardStyle
      QuestsByID[questID] = pin

      -- title, faction, capped state
      local questTitle, factionID, capped = TQ_GetQuestInfoByQuestID(questID)
      if factionID then
        QuestsByFaction[factionID] = QuestsByFaction[factionID] or {}
        QuestsByFaction[factionID][questID] = pin
      end
      pin.factionID = factionID
      pin.capped = capped

      -- set tag details
      local tagID, tagName, worldQuestType, rarity, isElite, tradeskillLineIndex = GetQuestTagInfo(questID);
      local tagAtlas
      if worldQuestType == LE_QUEST_TAG_TYPE_PET_BATTLE then
        tagAtlas = "worldquest-icon-petbattle"
      elseif worldQuestType == LE_QUEST_TAG_TYPE_PVP then
        tagAtlas = "worldquest-icon-pvp-ffa"
      elseif worldQuestType == LE_QUEST_TAG_TYPE_PROFESSION then
        local id = tradeskillLineIndex and select(7, GetProfessionInfo(tradeskillLineIndex))
        if id then
          tagAtlas = WORLD_QUEST_ICONS_BY_PROFESSION[id]
        end
      elseif worldQuestType == LE_QUEST_TAG_TYPE_DUNGEON then
        tagAtlas = "worldquest-icon-dungeon"
      end

      pin.tagID = tagID
      pin.tagName = tagName
      pin.worldQuestType = worldQuestType
      pin.isElite = isElite
      pin.tradeskillLineIndex = tradeskillLineIndex
      pin.rarity = rarity
      pin.tagAtlas = tagAtlas

    -- flag unresolved info
    if not (rewardIcon and rewardName) then
      if not pin.isPending then
        pin.isPending = true
        TQ_RequestPreloadRewardData (questID)
        pin.rewardType = pin.rewardType or REWARD_ITEM
        pin.style = pin.style or POI_REWARD_TYPE[REWARD_ITEM]

        if not hasPendingQuestData then
          hasPendingQuestData = true
          PlaySoundKitID(229)
        end
        --WorldPlan:print('|cFFFFFF00'..tostring(pin.title)..'|r waiting on texture info')
      end
    else
      if (rewardIcon and rewardName) then
        --WorldPlan:print('|cFF00FF00'..tostring(pin.title)..'|r has info', rewardIcon, rewardName)
        pin.hasUpdate = true
      end
      pin.isPending = nil
    end

    pin.title = questTitle or "|cFFFF0000Retrieving..."
    pin.itemTexture = rewardIcon or pin.itemTexture
    pin.itemName = rewardName or pin.itemName

    qprint('  |cFF00FFFF'..questID..'|r:->', (HaveQuestData(questID) and "|cFF00FF00HaveQuestData" or "-"), (C_TaskQuest.IsActive(questID) and "|cFF88FF00IsActive|r" or ""))
    qprint(' ', pin.title, pin.itemTexture, 'rewardType:', pin.rewardType, 'tag:', pin.tagID, 'style', pin.style )
  end
  return QuestsByID[questID]
end

function WorldQuests:Refresh(forced)
  local print = wqprint
  print('|cFF00FF88'..tostring(self)..':Refresh()|r')
  if not WorldMapPOIFrame:IsVisible() then
    return
  end
  if forced then
    self:Reset()
  end
  self:UpdateAnchors()

  if forced then
    self:Cleanup ()
  end
end

-- prepares elements for a map update
function WorldQuests:Reset ()
  local print = wqprint
  print('|cFF00FF88'..tostring(self)..':Reset()|r')
  wipe(QuestPositions)
  wipe(QuestsByReward)
  wipe(QuestsByTag)
  for questID, pin in pairs(QuestsByID) do
    pin.used = nil
  end
end

-- update visibility states of all pins
function WorldQuests:UpdateAnchors (submapID)
  local print = wqprint
  local db = WorldPlan.db
  local mapFileName, textureHeight, textureWidth, isMicroDungeon, microDungeonMapName = GetMapInfo()
  if isMicroDungeon then
    return
  end

  local currentMap = GetCurrentMapAreaID()
  local submapID = submapID or currentMap

  if submapID == BROKEN_ISLES_ID and (not db.DisplayContinentPins) then
    print('not updating map for reasons')
    return
  end
  print('|cFF88FF00'..tostring(self)..':UpdateAnchors|r', submapID, GetMapNameByID(submapID), 'pin count:', numPins)
  local numZones = MC_GetNumZones(submapID)
  if numZones then
    for i = 1, numZones do
      local subMapID = MC_GetZoneInfo(submapID, i)
      self:UpdateAnchors(subMapID)
    end
  end
  local pins = QuestsByZone[submapID]

  if pins then
    local hostFrame = WorldMapPOIFrame
    local mapWidth, mapHeight = hostFrame:GetSize()
    for questID, pin in pairs(pins) do
      pin:IsShowable()
      if pin.used then
        pin:SetFrameLevel(PinBaseIndex+ (pin.whiteListed and 200 or 0) +numPins)
        print('level', PinBaseIndex+ (pin.whiteListed and 200 or 0) +numPins)
        pin:SetAnchor(WorldMapPOIFrame, currentMap, mapWidth, mapHeight)
        numPins = numPins + 1
      end
    end
  end
end

-- shows, animates, or hides pins based on their current visibility flags
local debug_show = {}
local debug_animate = {}
local debug_hide = {}
function WorldQuests:Cleanup ()
  local print = wqprint
  local showQuestPOI = db.EnablePins
  print('|cFFFFFF00'..tostring(self)..':Cleanup()|r')
  local mapID = GetCurrentMapAreaID()
  isContinentMap = (mapID == BROKEN_ISLES_ID)

  wipe(debug_show)
  wipe(debug_animate)
  wipe(debug_hide)
  -- continent or zone sizing
  local fadeGrouped = (db.FadeWhileGrouped and IsInGroup())

  numPins = 0
  for questID, pin in pairs(QuestsByID) do
    -- can we show it?
    if showQuestPOI and (pin.used) then

      if fadeGrouped then
        pin:SetAlpha(0.25)
      else
        pin:SetAlpha(1)
      end
      -- is it a new quest?
      if pin.isNew then
        if not pin.isAnimating then
          pin.isAnimating = true
          OnNext(function()
            pin:ShowNew()
          end)
          if not notifyPlayed then
            for k,v in pairs(NotificationTypes) do
              if v[pin[k]] then
                notifyPlayed = true
                PlaySoundKitID(23404)
              end
            end
          end
          tinsert(debug_animate,questID)
        else

          print('animating? ', questID, 'filtered:', pin.filtered)
        end
        -- trap new but animating pins here
      else
        -- hard show existing pin
        print('refresh #', questID, 'filtered:', pin.filtered)
        pin.hasUpdate = true
        pin:Show(true)
        tinsert(debug_show,questID)
      end
    else
      if pin:IsShown() then
        tinsert(debug_hide,questID)
      end
      pin.isAnimating = nil
      pin.FadeIn:Stop()
      pin:Hide()
    end
  end
  print('   adding:', table.concat(debug_animate, ',' ))
  print('  refresh:', table.concat(debug_show, ',' ))
  print('  hiding:', table.concat(debug_hide, ',' ))
  hasNewQuestPins = nil
  notifyPlayed = nil
end

-- data provider manipulations for the taxi map
WorldPlan.OnFlightMapLoaded = function()
  if true then return end
  -- todo: figure out how to layer inside the map canvas
  local res = {}
  local t = {}
  for k,v in pairs(FlightMapFrame) do
    tinsert(res, tostring(k))
  end

  table.sort(res)
  for i, k in ipairs(res) do
    print(k)
  end
  hooksecurefunc(FlightMapFrame, 'RefreshAll', function(self)
    print('|cFF0088FFWQDP RefreshAllData ', GetTime())

    WorldPlan:GetPinsForMap(self:GetMapID())

    for pin in self:EnumerateAllPins() do
      if pin.worldQuest then
        --print('got pin #', pin.questID)
        local wp = QuestsByID[pin.questID]
        if wp then
          wp:ClearAllPoints()
          wp:SetParent(FlightMapFrame.ScrollContainer)
          wp:SetFrameStrata('MEDIUM')
          wp:SetPoint('CENTER', pin, 'CENTER')
          wp:Show()
        end
      end
    end
  end)
end



local throttle = 0
local tooltip = CreateFrame ("GameTooltip", "VeneerWorldQuestsScanner", nil, "GameTooltipTemplate")
local tooltipLine1 = _G['VeneerWorldQuestsScannerTextLeft1']
local tooltipLine3 = _G['VeneerWorldQuestsScannerTextLeft3']
local GetTime, mod = GetTime, mod




function WorldQuests:FilterCheckByID(questID)
  local pin = WorldQuests:GetPinByQuestID(questID)
  return pin:IsShowable()
end


function QuestPOI:IsShowable ()
  local print = wqprint
  local db = WorldPlan.db
  local qType = self.worldQuestType
  local rType = self.rewardType
  self.filtered = nil
  self.used = true

  print('  |cFFFF4400IsShowable()|r', self.title)

  local isIncluded
  for filterKey, filterValues in pairs(WorldPlan.UsedFilters) do
    local controlValue = self[filterKey]
    if controlValue then
      local filterType = filterValues[controlValue]
      if filterType == true then
        isIncluded = true
        print('   include? ', filterKey, controlValue, filterType)
      end
    end
    self.filtered = (not isIncluded)
  end


  if not TQ_IsActive(self.questID) then
    self.used = nil
  end
  if qType == LE_QUEST_TAG_TYPE_PROFESSION then
    if not (db.ShowAllProfessionQuests or (self.tradeskillLineIndex and GetProfessionInfo(self.tradeskillLineIndex))) then
      self.used = nil
    end
  end
  return self.used, self.filtered
end

function QuestPOI:UpdateTimer (timeLeft, timeType)
  print('|cFF0088FFUpdatePinTimer()|r')
end

--- Fixes icons upon size update
function QuestPOI:UpdateSize (style, subStyle)
  self.style = self.style or POI_DEFAULT_TYPE
  style = style or self.style
  subStyle = subStyle or self.subStyle

  qprint('|cFF00FF88'..self:GetName()..'|r:UpdateSize()', style, subStyle)

  self.currentWidth = subStyle.PinSize
  self.borderSize = subStyle.Border
  self.trackingBorderSize = subStyle.TrackingBorder
  self.tagSize = subStyle.TagSize
  self.TimeleftStage = subStyle.TimeleftStage
  self.NoIcon = subStyle.NoIcon


  self:SetSize(self.currentWidth, self.currentWidth)

  local icon = self.icon
  local iconBorder = self.iconBorder
  local trackingBorder = self.supertrackBorder
  local tag = self.tagIcon
  local pinMask = style.pinMask
  local rewardMask = style.rewardMask

  if self.NoIcon then
    self.icon:Hide()
  else
    self.icon:Show()
    icon:SetMask(nil)
    icon:SetMask(rewardMask)
    icon:SetTexture(self.icon:GetTexture())
  end
  iconBorder:SetMask(nil)
  trackingBorder:SetMask(nil)


  local borderWidth = self.borderSize
  local trackingWidth = self.trackingBorderSize

  iconBorder:ClearAllPoints()
  iconBorder:SetPoint('BOTTOMLEFT', self, 'BOTTOMLEFT', -borderWidth + (style.x or 0), -borderWidth + (style.y or 0))
  iconBorder:SetPoint('TOPRIGHT', self, 'TOPRIGHT', borderWidth + (style.x or 0), borderWidth + (style.y or 0))

  trackingBorder:ClearAllPoints()
  trackingBorder:SetPoint('BOTTOMLEFT', iconBorder, 'BOTTOMLEFT', -trackingWidth, -trackingWidth)
  trackingBorder:SetPoint('TOPRIGHT', iconBorder, 'TOPRIGHT', trackingWidth, trackingWidth)

  if self.tagSize then
    tag:Show()
    tag:ClearAllPoints()
    tag:SetPoint('BOTTOMRIGHT', self, 'BOTTOMRIGHT', borderWidth, -borderWidth)
  else
    tag:Hide()
  end

  --qprint('using mask:', mask, self.name )
  iconBorder:SetMask(pinMask)
  trackingBorder:SetMask(pinMask)


end


function FilterBar:OnEvent(event)
  if event == 'QUEST_LOG_UPDATE' then
    self:Refresh()
  end
end

function FilterBar:PassesFilterSet(filterKey, pin)
  local passesFilter = true
  for filterKey, filters in pairs(QuestFilters) do
    for rewardType, value in pairs(QuestFilters[filterKey]) do
      if value == 1 and rewardType == pin[filterKey] then
        passesFilter = true
      elseif value == -1 and rewardType == pin[filterKey] then
        passesFilter = false
      end
    end
  end
  return passesFilter
end


local bountyIndex
local debug_headers = {}

function FilterBar:Setup()
  self:GetFilters()
end

function FilterBar:OnEvent(event,...)
  if event == 'QUEST_LOG_UPDATE' then
    self:Reset()
    self:Refresh()
  end
end

function FilterBar:GetFilters()

  local print = fbprint
  wipe(WorldPlan.FilterOptions)

  for index, info in ipairs(POI_FILTER_OPTIONS) do
    tinsert(WorldPlan.FilterOptions, info)
  end
  self.bounties, self.numBounties = GetQuestBountyInfoForMapID(WorldPlan.currentMapID)
  self.BountyFilters = {}
  for index, data in ipairs(self.bounties) do
    local info = self.BountyFilters[index]
    if not info then
      info  = {}
      self.BountyFilters[index] = info
    end

    local questTitle = GetQuestLogTitle(GetQuestLogIndexByID(data.questID))

    info.filterKey = 'factionID'
    info.filterValue = data.factionID
    info.label = questTitle
    info.texture = data.icon
    print('loading emissary', questTitle)
    tinsert(WorldPlan.FilterOptions, info)
    --{ filterKey= 'worldQuestType', filterValue = LE_QUEST_TAG_TYPE_PROFESSION, label = 'Profession', texture = "Interface\\LFGFRAME\\UI-LFR-PORTRAIT", },
  end
end

function FilterBar:Reset()
  self:GetFilters()
end

function FilterBar:Refresh(forced)
  local print = fbprint
  local blocks = self.SummaryHeaders
  local relativeFrame = WorldMapFrame.UIElementsFrame.TrackingOptionsButton
  local numHeaders = 0
  print('|cFF00FF88'..tostring(self)..':Refresh()|r', 'currentMap=',WorldPlan.currentMapID)


  local quests = QuestsByZone[WorldPlan.currentMapID] or QuestsByID


  for index, info in ipairs(WorldPlan.FilterOptions) do
    local numQuests = 0

    for questID, pin in pairs(quests) do
      if pin.used then
        if not info.filterKey then
          numQuests = numQuests + 1
        elseif pin[info.filterKey] == info.filterValue then
          numQuests = numQuests + 1
        end
      end
    end
    print(tostring(index).. ' ("'..tostring(info.label)..'" f('.. tostring(info.filterKey).. '='..tostring(info.filterValue) .. '), '..tostring(numQuests)..')')

    if numQuests >= 1 then
      numHeaders = numHeaders + 1
      local button = blocks[numHeaders]
      if not blocks[numHeaders] then
        button = CreateFrame('Button', 'WorldPlanFilterButton'..numHeaders, WorldMapScrollFrame, 'WorldPlanFilterPin')
        button.iconBorder:SetTexture(info.fill or POI_BORDER_FILL)
        button.iconBorder:SetMask(info.mask or POI_BORDER_MASK)
        button.iconBorder:SetDesaturated(info.desaturated)
        button.supertrackBorder:SetTexture(info.fill or POI_BORDER_FILL)
        button.supertrackBorder:SetMask(info.mask or POI_BORDER_MASK)
        button.supertrackBorder:SetDesaturated(true)
        blocks[numHeaders] = button
      end

      button:SetID(index)
      button.spacing = ((info.filterKey ~=  relativeFrame.filterKey) and 10) or 0
      button.relativeFrame = relativeFrame
      button:Refresh(info, (numHeaders == 1), numQuests)
      button:Show()
      relativeFrame = button
    end

  end

  self.numHeaders = numHeaders
  for i = numHeaders + 1, #WorldPlan.FilterOptions do
    if self.SummaryHeaders[i] then
      self.SummaryHeaders[i]:Hide()
    end
  end
end

function FilterBar:Cleanup()

  -- hide trailing buttons
end


function FilterPin:Refresh(info, isFirst, numQuests)
  local print = fbprint
  isFirst = isFirst or self.isFirst
  numQuests = numQuests or self.numQuests

  if info then
    self.isFirst = isFirst
    self.numQuests = numQuests
    self.filterKey = info.filterKey
    self.filterValue = info.filterValue
    self.tagID = info.tagID

    self.icon:ClearAllPoints()
    self.icon:SetTexture(info.texture)
    self.icon:SetAllPoints(self)
    self.supertrackBorder:Hide()
    self.label:SetText(numQuests)
    self:Show()
  end


  self.itemTexture = self.texture

  if isFirst then
    self:SetPoint('TOP', self.relativeFrame, 'BOTTOM', 0, -5)
  else
    self:SetPoint('TOPRIGHT', self.relativeFrame, 'BOTTOMRIGHT', 0, -(3*2 + 1 + (self.spacing or 0)))

  end
  print('anchor to', self.relativeFrame:GetName())

  local r, g, b, a = 1,1,1,1
  local used = WorldPlan.UsedFilters[self.filterKey]
  if used and self.filterKey then
    if used[self.filterValue] == true then
      r, g, b = 0, 1, 0
    elseif used[self.filterValue] == false then
      r, g, b = 1, 0, 0
    end
  end
  self.iconBorder:SetVertexColor(r, g, b, a)
  self:UpdateSize()
end

function FilterPin:OnLoad()
  self:RegisterForClicks('AnyUp')
  self:SetFrameStrata('HIGH')
  self:SetFrameLevel(151)
  self:SetScript('OnUpdate', nil)
  self.style = db.filterStyle
  self.subStyle = db.defaultPinStyle.continent
end

function FilterPin:OnUpdate ()

end

function FilterPin:OnLeave ()
  if GameTooltip:IsOwned(self) then
    GameTooltip:Hide()
  end
end

-- shift-click: reset filter
-- click: rotate through include(1), exclude(-1), ignore(nil)
function FilterPin:OnClick (button)
  local print = fbprint
  local filterKey = self.filterKey
  local filterValue = self.filterValue


  local operation = opPrefix
  local setInclude = (button == 'LeftButton')


  if not filterKey then
    -- resetting
    wipe(WorldPlan.UsedFilters)

  elseif IsShiftKeyDown() then
    WorldPlan.UsedFilters[filterKey] = nil
  else
    WorldPlan.UsedFilters[filterKey] = WorldPlan.UsedFilters[filterKey] or {}
    WorldPlan.UsedFilters[filterKey][filterValue] = setInclude
    print(filterKey, filterValue, '=', setInclude)

    for index, info in ipairs(WorldPlan.FilterOptions) do
      if info.filterKey == filterKey then
        if (not IsControlKeyDown()) and (filterValue ~= info.filterValue) then
          WorldPlan.UsedFilters[filterKey][info.filterValue] = (not setInclude)
          print(filterKey, info.filterValue, '=', WorldPlan.UsedFilters[filterKey][info.filterValue])
        end
      end
    end

  end
  print('|cFF00FF88Filter Update:', filterKey, filterValue, operation)
  WorldPlan:RefreshAll()
end

--%debug%
local SetTimedCallbackForAllPins = function(seconds, callback)
  C_Timer.After(seconds, function()
    for id, pin in pairs(QuestsByID) do
      callback(pin)
    end
  end)
end

SLASH_WORLDPLAN1 = "/worldplan"
SLASH_WORLDPLAN2 = "/wp"
SlashCmdList.WORLDPLAN = function()
  print('command pop')
  WorldPlan:GetPinsForMap()
  WorldPlan:RefreshPins()

  SetTimedCallbackForAllPins(0, function(self) self.FadeIn:Play() self.FlashIn:Play()  end)
  SetTimedCallbackForAllPins(5, function(self) self.PendingFade:Play() end)
  SetTimedCallbackForAllPins(8, function(self) self.PendingFade:Stop() end)
end
--%end-debug%

for mapID, mapName in pairs(WORLD_QUEST_MAPS) do
  QuestsByZone[mapID] = {}
end
for index, color in pairs(ITEM_QUALITY_COLORS) do
  POI_REWARD_TYPE[index] = {
    r = color.r, g = color.g, b = color.b,
    hex = color.hex,
  }
end