/**
 * @file llworldmapview.cpp
 * @brief LLWorldMapView class implementation
 *
 * $LicenseInfo:firstyear=2001&license=viewerlgpl$
 * Second Life Viewer Source Code
 * Copyright (C) 2010, Linden Research, Inc.
 *
 * 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;
 * version 2.1 of the License only.
 *
 * 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
 *
 * Linden Research, Inc., 945 Battery Street, San Francisco, CA  94111  USA
 * $/LicenseInfo$
 */

#include "llviewerprecompiledheaders.h"

#include "llworldmapview.h"

#include "indra_constants.h"
#include "llui.h"
#include "llmath.h"     // clampf()
#include "llregionhandle.h"
#include "lleventflags.h"
#include "llfloaterreg.h"
#include "llrender.h"
#include "lltooltip.h"

#include "llagent.h"
#include "llagentcamera.h"
#include "llcallingcard.h"
#include "llcommandhandler.h"
#include "llviewercontrol.h"
#include "llfloatermap.h"
#include "llfloaterworldmap.h"
#include "llfocusmgr.h"
#include "lllocalcliprect.h"
#include "lltextbox.h"
#include "lltextureview.h"
#include "lltracker.h"
#include "llviewercamera.h"
#include "llviewernetwork.h"
#include "llviewertexture.h"
#include "llviewertexturelist.h"
#include "llviewerregion.h"
#include "llviewerwindow.h"
#include "lltrans.h"

#include "llglheaders.h"

// # Constants
static const F32 MAP_DEFAULT_SCALE = 128.f;
static const F32 MAP_ITERP_TIME_CONSTANT = 0.75f;
static const F32 MAP_ZOOM_ACCELERATION_TIME = 0.3f;
static const F32 MAP_ZOOM_MAX_INTERP = 0.5f;
static const F32 MAP_SCALE_SNAP_THRESHOLD = 0.005f;

// Basically a C++ implementation of the OCEAN_COLOR defined in mapstitcher.py
// Please ensure consistency between those 2 files (TODO: would be better to get that color from an asset source...)
// OCEAN_COLOR = "#1D475F"
const F32 OCEAN_RED   = (F32)(0x1D)/255.f;
const F32 OCEAN_GREEN = (F32)(0x47)/255.f;
const F32 OCEAN_BLUE  = (F32)(0x5F)/255.f;

const F32 GODLY_TELEPORT_HEIGHT = 200.f;
const F32 BIG_DOT_RADIUS = 5.f;
BOOL LLWorldMapView::sHandledLastClick = FALSE;

LLUIImagePtr LLWorldMapView::sAvatarSmallImage = NULL;
LLUIImagePtr LLWorldMapView::sAvatarYouImage = NULL;
LLUIImagePtr LLWorldMapView::sAvatarYouLargeImage = NULL;
LLUIImagePtr LLWorldMapView::sAvatarLevelImage = NULL;
LLUIImagePtr LLWorldMapView::sAvatarAboveImage = NULL;
LLUIImagePtr LLWorldMapView::sAvatarBelowImage = NULL;
LLUIImagePtr LLWorldMapView::sAvatarUnknownImage = NULL;

LLUIImagePtr LLWorldMapView::sTelehubImage = NULL;
LLUIImagePtr LLWorldMapView::sInfohubImage = NULL;
LLUIImagePtr LLWorldMapView::sHomeImage = NULL;
LLUIImagePtr LLWorldMapView::sEventImage = NULL;
LLUIImagePtr LLWorldMapView::sEventMatureImage = NULL;
LLUIImagePtr LLWorldMapView::sEventAdultImage = NULL;

LLUIImagePtr LLWorldMapView::sTrackCircleImage = NULL;
LLUIImagePtr LLWorldMapView::sTrackArrowImage = NULL;

LLUIImagePtr LLWorldMapView::sClassifiedsImage = NULL;
LLUIImagePtr LLWorldMapView::sForSaleImage = NULL;
LLUIImagePtr LLWorldMapView::sForSaleAdultImage = NULL;

S32 LLWorldMapView::sTrackingArrowX = 0;
S32 LLWorldMapView::sTrackingArrowY = 0;
bool LLWorldMapView::sVisibleTilesLoaded = false;
F32 LLWorldMapView::sMapScaleSetting = MAP_DEFAULT_SCALE;
LLVector2 LLWorldMapView::sZoomPivot = LLVector2(0.0f, 0.0f);
LLFrameTimer LLWorldMapView::sZoomTimer = LLFrameTimer();

std::map<std::string,std::string> LLWorldMapView::sStringsMap;

// Fetch and draw info thresholds
const F32 DRAW_TEXT_THRESHOLD = 96.f;       // Don't draw text under that resolution value (res = width region in meters)
const S32 DRAW_SIMINFO_THRESHOLD = 3;       // Max level for which we load or display sim level information (level in LLWorldMipmap sense)
const S32 DRAW_LANDFORSALE_THRESHOLD = 2;   // Max level for which we load or display land for sale picture data (level in LLWorldMipmap sense)

// When on, draw an outline for each mipmap tile gotten from S3
#define DEBUG_DRAW_TILE 0


void LLWorldMapView::initClass()
{
    sAvatarSmallImage =     LLUI::getUIImage("map_avatar_8.tga");
    sAvatarYouImage =       LLUI::getUIImage("map_avatar_16.tga");
    sAvatarYouLargeImage =  LLUI::getUIImage("map_avatar_you_32.tga");
    sAvatarLevelImage =     LLUI::getUIImage("map_avatar_32.tga");
    sAvatarAboveImage =     LLUI::getUIImage("map_avatar_above_32.tga");
    sAvatarBelowImage =     LLUI::getUIImage("map_avatar_below_32.tga");
    sAvatarUnknownImage =   LLUI::getUIImage("map_avatar_unknown_32.tga");

    sHomeImage =            LLUI::getUIImage("map_home.tga");
    sTelehubImage =         LLUI::getUIImage("map_telehub.tga");
    sInfohubImage =         LLUI::getUIImage("map_infohub.tga");
    sEventImage =           LLUI::getUIImage("Parcel_PG_Dark");
    sEventMatureImage =     LLUI::getUIImage("Parcel_M_Dark");
    // To Do: update the image resource for adult events.
    sEventAdultImage =      LLUI::getUIImage("Parcel_R_Dark");

    sTrackCircleImage =     LLUI::getUIImage("map_track_16.tga");
    sTrackArrowImage =      LLUI::getUIImage("direction_arrow.tga");
    sClassifiedsImage =     LLUI::getUIImage("icon_top_pick.tga");
    sForSaleImage =         LLUI::getUIImage("icon_for_sale.tga");
    // To Do: update the image resource for adult lands on sale.
    sForSaleAdultImage =    LLUI::getUIImage("icon_for_sale_adult.tga");

    sStringsMap["loading"] = LLTrans::getString("texture_loading");
    sStringsMap["offline"] = LLTrans::getString("worldmap_offline");
}

// static
void LLWorldMapView::cleanupClass()
{
    sAvatarSmallImage = NULL;
    sAvatarYouImage = NULL;
    sAvatarYouLargeImage = NULL;
    sAvatarLevelImage = NULL;
    sAvatarAboveImage = NULL;
    sAvatarBelowImage = NULL;
    sAvatarUnknownImage = NULL;

    sTelehubImage = NULL;
    sInfohubImage = NULL;
    sHomeImage = NULL;
    sEventImage = NULL;
    sEventMatureImage = NULL;
    sEventAdultImage = NULL;

    sTrackCircleImage = NULL;
    sTrackArrowImage = NULL;
    sClassifiedsImage = NULL;
    sForSaleImage = NULL;
    sForSaleAdultImage = NULL;
}

LLWorldMapView::LLWorldMapView() :
    LLPanel(),
    mBackgroundColor(LLColor4(OCEAN_RED, OCEAN_GREEN, OCEAN_BLUE, 1.f)),
    mItemPicked(FALSE),
    mPanX(0.f),
    mPanY(0.f),
    mTargetPanX(0.f),
    mTargetPanY(0.f),
    mPanning(FALSE),
    mMouseDownPanX(0),
    mMouseDownPanY(0),
    mMouseDownX(0),
    mMouseDownY(0),
    mSelectIDStart(0),
    mMapScale(0.f),
    mTargetMapScale(0.f),
    mMapIterpTime(MAP_ITERP_TIME_CONSTANT)
{
    // LL_INFOS("WorldMap") << "Creating the Map -> LLWorldMapView::LLWorldMapView()" << LL_ENDL;

    clearLastClick();
}

BOOL LLWorldMapView::postBuild()
{
    mTextBoxNorth = getChild<LLTextBox> ("floater_map_north");
    mTextBoxEast = getChild<LLTextBox> ("floater_map_east");
    mTextBoxWest = getChild<LLTextBox> ("floater_map_west");
    mTextBoxSouth = getChild<LLTextBox> ("floater_map_south");
    mTextBoxSouthEast = getChild<LLTextBox> ("floater_map_southeast");
    mTextBoxNorthEast = getChild<LLTextBox> ("floater_map_northeast");
    mTextBoxSouthWest = getChild<LLTextBox> ("floater_map_southwest");
    mTextBoxNorthWest = getChild<LLTextBox> ("floater_map_northwest");

    mTextBoxNorth->setText(getString("world_map_north"));
    mTextBoxEast->setText(getString ("world_map_east"));
    mTextBoxWest->setText(getString("world_map_west"));
    mTextBoxSouth->setText(getString ("world_map_south"));
    mTextBoxSouthEast ->setText(getString ("world_map_southeast"));
    mTextBoxNorthEast ->setText(getString ("world_map_northeast"));
    mTextBoxSouthWest->setText(getString ("world_map_southwest"));
    mTextBoxNorthWest ->setText(getString("world_map_northwest"));

    mTextBoxNorth->reshapeToFitText();
    mTextBoxEast->reshapeToFitText();
    mTextBoxWest->reshapeToFitText();
    mTextBoxSouth->reshapeToFitText();
    mTextBoxSouthEast ->reshapeToFitText();
    mTextBoxNorthEast ->reshapeToFitText();
    mTextBoxSouthWest->reshapeToFitText();
    mTextBoxNorthWest ->reshapeToFitText();

    sZoomTimer.stop();
    setScale(sMapScaleSetting, true);

    return true;
}


LLWorldMapView::~LLWorldMapView()
{
    //LL_INFOS("WorldMap") << "Destroying the map -> LLWorldMapView::~LLWorldMapView()" << LL_ENDL;
    cleanupTextures();
}


// static
void LLWorldMapView::cleanupTextures()
{
}

void LLWorldMapView::zoom(F32 zoom)
{
    mTargetMapScale = scaleFromZoom(zoom);
    if (!sZoomTimer.getStarted() && mMapScale != mTargetMapScale)
    {
        sZoomPivot = LLVector2(0, 0);
        sZoomTimer.start();
    }
}

void LLWorldMapView::zoomWithPivot(F32 zoom, S32 x, S32 y)
{
    mTargetMapScale = scaleFromZoom(zoom);
    sZoomPivot      = LLVector2(x, y);
    if (!sZoomTimer.getStarted() && mMapScale != mTargetMapScale)
    {
        sZoomTimer.start();
    }
}

F32 LLWorldMapView::getZoom() { return LLWorldMapView::zoomFromScale(mMapScale); }

F32 LLWorldMapView::getScale() { return mMapScale; }

// static
void LLWorldMapView::setScaleSetting(F32 scaleSetting) { sMapScaleSetting = scaleSetting; }

// static
F32 LLWorldMapView::getScaleSetting() { return sMapScaleSetting; }

void LLWorldMapView::setScale(F32 scale, bool snap)
{
    if (scale != mMapScale)
    {
        F32 old_scale = mMapScale;

        mMapScale = scale;
        // Set the scale used when saving the setting
        sMapScaleSetting = scale;
        if (mMapScale <= 0.f)
        {
            mMapScale = 0.1f;
        }
        mMapIterpTime = MAP_ITERP_TIME_CONSTANT;
        F32 ratio = (scale / old_scale);
        mPanX *= ratio;
        mPanY *= ratio;
        mTargetPanX         = mPanX;
        mTargetPanY         = mPanY;
        sVisibleTilesLoaded = false;

        // If we are zooming relative to somewhere else rather than the center of the map, compensate for the difference in panning here
        if (!sZoomPivot.isExactlyZero())
        {
            LLVector2 relative_pivot;
            relative_pivot.mV[VX]     = sZoomPivot.mV[VX] - (getRect().getWidth() / 2.0);
            relative_pivot.mV[VY]     = sZoomPivot.mV[VY] - (getRect().getHeight() / 2.0);
            LLVector2 zoom_pan_offset = relative_pivot - (relative_pivot * scale / old_scale);
            mPanX += zoom_pan_offset.mV[VX];
            mPanY += zoom_pan_offset.mV[VY];
            mTargetPanX += zoom_pan_offset.mV[VX];
            mTargetPanY += zoom_pan_offset.mV[VY];
        }
    }

    if (snap)
    {
        mTargetMapScale = scale;
    }
}

// static
void LLWorldMapView::translatePan(S32 delta_x, S32 delta_y)
{
    mPanX += delta_x;
    mPanY += delta_y;
    mTargetPanX         = mPanX;
    mTargetPanY         = mPanY;
    sVisibleTilesLoaded = false;
}


// static
void LLWorldMapView::setPan(S32 x, S32 y, BOOL snap)
{
    mMapIterpTime = MAP_ITERP_TIME_CONSTANT;
    mTargetPanX = (F32) x;
    mTargetPanY = (F32) y;
    if (snap)
    {
        mPanX = mTargetPanX;
        mPanY = mTargetPanY;
    }
    sVisibleTilesLoaded = false;
}

// static
void LLWorldMapView::setPanWithInterpTime(S32 x, S32 y, BOOL snap, F32 interp_time)
{
    setPan(x, y, snap);
    mMapIterpTime = interp_time;
}

bool LLWorldMapView::showRegionInfo() { return (LLWorldMipmap::scaleToLevel(mMapScale) <= DRAW_SIMINFO_THRESHOLD ? true : false); }

///////////////////////////////////////////////////////////////////////////////////
// HELPERS

BOOL is_agent_in_region(LLViewerRegion* region, LLSimInfo* info)
{
    return (region && info && info->isName(region->getName()));
}

///////////////////////////////////////////////////////////////////////////////////

void LLWorldMapView::draw()
{
    static LLUIColor map_track_color = LLUIColorTable::instance().getColor("MapTrackColor", LLColor4::white);

    LLTextureView::clearDebugImages();

    F64 current_time = LLTimer::getElapsedSeconds();

    mVisibleRegions.clear();

    // animate pan if necessary
    mPanX = lerp(mPanX, mTargetPanX, LLSmoothInterpolation::getInterpolant(mMapIterpTime));
    mPanY = lerp(mPanY, mTargetPanY, LLSmoothInterpolation::getInterpolant(mMapIterpTime));

    //RN: snaps to zoom value because interpolation caused jitter in the text rendering
    if (!sZoomTimer.getStarted() && mMapScale != mTargetMapScale)
    {
        sZoomTimer.start();
    }
    bool snap_scale = false;
    F32 interp = llmin(MAP_ZOOM_MAX_INTERP, sZoomTimer.getElapsedTimeF32() / MAP_ZOOM_ACCELERATION_TIME);
    F32 current_zoom_val = zoomFromScale(mMapScale);
    F32 target_zoom_val = zoomFromScale(mTargetMapScale);
    F32 new_zoom_val = lerp(current_zoom_val, target_zoom_val, interp);
    if (abs(new_zoom_val - current_zoom_val) < MAP_SCALE_SNAP_THRESHOLD)
    {
        sZoomTimer.stop();
        snap_scale = true;
        new_zoom_val = target_zoom_val;
    }
    F32 map_scale = scaleFromZoom(new_zoom_val);
    setScale(map_scale, snap_scale);

    const S32 width = getRect().getWidth();
    const S32 height = getRect().getHeight();
    const F32 half_width = F32(width) / 2.0f;
    const F32 half_height = F32(height) / 2.0f;
    LLVector3d camera_global = gAgentCamera.getCameraPositionGlobal();

    S32 level = LLWorldMipmap::scaleToLevel(mMapScale);

    LLLocalClipRect clip(getLocalRect());
    gGL.getTexUnit(0)->unbind(LLTexUnit::TT_TEXTURE);

    gGL.matrixMode(LLRender::MM_MODELVIEW);

    // Draw background rectangle
    gGL.getTexUnit(0)->unbind(LLTexUnit::TT_TEXTURE);
    gGL.color4fv(mBackgroundColor.mV);
    gl_rect_2d(0, height, width, 0);

    // Draw the image tiles
    drawMipmap(width, height);

    // Draw per sim overlayed information (names, mature, offline...)
    for (LLWorldMap::sim_info_map_t::const_iterator it = LLWorldMap::getInstance()->getRegionMap().begin();
         it != LLWorldMap::getInstance()->getRegionMap().end(); ++it)
    {
        U64 handle = it->first;
        LLSimInfo* info = it->second;

        LLVector3d origin_global = from_region_handle(handle);

        // Find x and y position relative to camera's center.
        LLVector3d rel_region_pos = origin_global - camera_global;
        F32 relative_x = (rel_region_pos.mdV[0] / REGION_WIDTH_METERS) * mMapScale;
        F32 relative_y = (rel_region_pos.mdV[1] / REGION_WIDTH_METERS) * mMapScale;

        // Coordinates of the sim in pixels in the UI panel
        // When the view isn't panned, 0,0 = center of rectangle
        F32 bottom =    mPanY + half_height + relative_y;
        F32 left =      mPanX + half_width + relative_x;
        F32 top =       bottom + mMapScale ;
        F32 right =     left + mMapScale ;

        // Discard if region is outside the screen rectangle (not visible on screen)
        if ((top < 0.f)   || (bottom > height) ||
            (right < 0.f) || (left > width)       )
        {
            // Drop the "land for sale" fetching priority since it's outside the view rectangle
            info->dropImagePriority();
            continue;
        }

        // This list is used by other methods to know which regions are indeed displayed on screen

        mVisibleRegions.push_back(handle);

        // Update the agent count for that region if we're not too zoomed out already
        if (level <= DRAW_SIMINFO_THRESHOLD)
        {
            info->updateAgentCount(current_time);
        }

        if (info->isDown())
        {
            // Draw a transparent red square over down sims
            gGL.color4f(0.2f, 0.0f, 0.0f, 0.4f);

            gGL.getTexUnit(0)->unbind(LLTexUnit::TT_TEXTURE);
            gGL.begin(LLRender::QUADS);
                gGL.vertex2f(left, top);
                gGL.vertex2f(left, bottom);
                gGL.vertex2f(right, bottom);
                gGL.vertex2f(right, top);
            gGL.end();
        }
        else if (gSavedSettings.getBOOL("MapShowLandForSale") && (level <= DRAW_LANDFORSALE_THRESHOLD))
        {
            // Draw the overlay image "Land for Sale / Land for Auction"
            LLViewerFetchedTexture* overlayimage = info->getLandForSaleImage();
            if (overlayimage)
            {
                // Inform the fetch mechanism of the size we need
                S32 draw_size = ll_round(mMapScale);
                overlayimage->setKnownDrawSize(ll_round(draw_size * LLUI::getScaleFactor().mV[VX]), ll_round(draw_size * LLUI::getScaleFactor().mV[VY]));
                // Draw something whenever we have enough info
                if (overlayimage->hasGLTexture())
                {
                    gGL.getTexUnit(0)->bind(overlayimage);
                    gGL.color4f(1.f, 1.f, 1.f, 1.f);
                    gGL.begin(LLRender::QUADS);
                        gGL.texCoord2f(0.f, 1.f);
                        gGL.vertex3f(left, top, -0.5f);
                        gGL.texCoord2f(0.f, 0.f);
                        gGL.vertex3f(left, bottom, -0.5f);
                        gGL.texCoord2f(1.f, 0.f);
                        gGL.vertex3f(right, bottom, -0.5f);
                        gGL.texCoord2f(1.f, 1.f);
                        gGL.vertex3f(right, top, -0.5f);
                    gGL.end();
                }
            }
        }
        else
        {
            // If we're not displaying the "land for sale", drop its fetching priority
            info->dropImagePriority();
        }

        // Draw the region name in the lower left corner
        if (mMapScale >= DRAW_TEXT_THRESHOLD)
        {
            LLFontGL* font = LLFontGL::getFont(LLFontDescriptor("SansSerif", "Small", LLFontGL::BOLD));
            std::string mesg;
            if (info->isDown())
            {
                mesg = llformat( "%s (%s)", info->getName().c_str(), sStringsMap["offline"].c_str());
            }
            else
            {
                mesg = info->getName();
            }
            if (!mesg.empty())
            {
                font->renderUTF8(
                    mesg, 0,
                    llfloor(left + 3), llfloor(bottom + 2),
                    LLColor4::white,
                    LLFontGL::LEFT, LLFontGL::BASELINE, LLFontGL::NORMAL, LLFontGL::DROP_SHADOW,
                    S32_MAX, //max_chars
                    mMapScale, //max_pixels
                    NULL,
                    /*use_ellipses*/TRUE);
            }
        }
    }

    // Draw item infos if we're not zoomed out too much and there's something to draw
    if ((level <= DRAW_SIMINFO_THRESHOLD) && (gSavedSettings.getBOOL("MapShowInfohubs") ||
                                              gSavedSettings.getBOOL("MapShowTelehubs") ||
                                              gSavedSettings.getBOOL("MapShowLandForSale") ||
                                              gSavedSettings.getBOOL("MapShowEvents") ||
                                              gSavedSettings.getBOOL("ShowMatureEvents") ||
                                              gSavedSettings.getBOOL("ShowAdultEvents")))
    {
        drawItems();
    }

    // Draw the Home location (always)
    LLVector3d home;
    if (gAgent.getHomePosGlobal(&home))
    {
        drawImage(home, sHomeImage);
    }

    // Draw the current agent after all that other stuff.
    LLVector3d pos_global = gAgent.getPositionGlobal();
    drawImage(pos_global, sAvatarYouImage);

    LLVector3 pos_map = globalPosToView(pos_global);
    if (!pointInView(ll_round(pos_map.mV[VX]), ll_round(pos_map.mV[VY])))
    {
        drawTracking(pos_global,
                     lerp(LLColor4::yellow, LLColor4::orange, 0.4f),
                     TRUE,
                     "You are here",
                     "",
                     LLFontGL::getFontSansSerifSmall()->getLineHeight()); // offset vertically by one line, to avoid overlap with target tracking
    }

    // Draw the current agent viewing angle
    drawFrustum();

    // Draw icons for the avatars in each region.
    // Drawn this after the current agent avatar so one can see nearby people
    if (gSavedSettings.getBOOL("MapShowPeople") && (level <= DRAW_SIMINFO_THRESHOLD))
    {
        drawAgents();
    }

    // Always draw tracking information
    LLTracker::ETrackingStatus tracking_status = LLTracker::getTrackingStatus();
    if ( LLTracker::TRACKING_AVATAR == tracking_status )
    {
        drawTracking( LLAvatarTracker::instance().getGlobalPos(), map_track_color, TRUE, LLTracker::getLabel(), "" );
    }
    else if ( LLTracker::TRACKING_LANDMARK == tracking_status
              || LLTracker::TRACKING_LOCATION == tracking_status )
    {
        // While fetching landmarks, will have 0,0,0 location for a while,
        // so don't draw. JC
        LLVector3d pos_global = LLTracker::getTrackedPositionGlobal();
        if (!pos_global.isExactlyZero())
        {
            drawTracking( pos_global, map_track_color, TRUE, LLTracker::getLabel(), LLTracker::getToolTip() );
        }
    }
    else if (LLWorldMap::getInstance()->isTracking())
    {
        if (LLWorldMap::getInstance()->isTrackingInvalidLocation())
        {
            // We know this location to be invalid, draw a blue circle
            LLColor4 loading_color(0.0, 0.5, 1.0, 1.0);
            drawTracking( LLWorldMap::getInstance()->getTrackedPositionGlobal(), loading_color, TRUE, getString("InvalidLocation"), "");
        }
        else
        {
            // We don't know yet what that location is, draw a throbing blue circle
            double value = fmod(current_time, 2);
            value = 0.5 + 0.5*cos(value * F_PI);
            LLColor4 loading_color(0.0, F32(value/2), F32(value), 1.0);
            drawTracking( LLWorldMap::getInstance()->getTrackedPositionGlobal(), loading_color, TRUE, getString("Loading"), "");
        }
    }


    // turn off the scissor
    LLGLDisable no_scissor(GL_SCISSOR_TEST);

    updateDirections();

    LLView::draw();

    // Get sim info for all sims in view
    updateVisibleBlocks();
} // end draw()


//virtual
void LLWorldMapView::setVisible(BOOL visible)
{
    LLPanel::setVisible(visible);
    if (!visible)
    {
        // Drop the download of tiles and images priority to nil if we hide the map
        LLWorldMap::getInstance()->dropImagePriorities();
    }
}

void LLWorldMapView::drawMipmap(S32 width, S32 height)
{
    // Compute the level of the mipmap to use for the current scale level
    S32 level = LLWorldMipmap::scaleToLevel(mMapScale);
    // Set the tile boost level so that unused tiles get to 0
    LLWorldMap::getInstance()->equalizeBoostLevels();

    // Render whatever we already have loaded if we haven't the current level
    // complete and use it as a background (scaled up or scaled down)
    if (!sVisibleTilesLoaded)
    {
        // Note: the (load = false) parameter avoids missing tiles to be fetched (i.e. we render what we have, no more)
        // Check all the lower res levels and render them in reverse order (worse to best)
        // We need to traverse all the levels as the user can zoom in very fast
        for (S32 l = LLWorldMipmap::MAP_LEVELS; l > level; l--)
        {
            drawMipmapLevel(width, height, l, false);
        }
        // Skip the current level, as we'll do it anyway here under...

        // Just go one level down in res as it can really get too much stuff
        // when zooming out and too small to see anyway...
        if (level > 1)
        {
            drawMipmapLevel(width, height, level - 1, false);
        }
    }
    else
    {
        //LL_INFOS("WorldMap") << "Render complete, don't draw background..." << LL_ENDL;
    }

    // Render the current level
    sVisibleTilesLoaded = drawMipmapLevel(width, height, level);

    return;
}

// Return true if all the tiles required to render that level have been fetched or are truly missing
bool LLWorldMapView::drawMipmapLevel(S32 width, S32 height, S32 level, bool load)
{
    // Check input level
    llassert (level > 0);
    if (level <= 0)
        return false;

    // Count tiles hit and completed
    S32 completed_tiles = 0;
    S32 total_tiles = 0;

    // Size in meters (global) of each tile of that level
    S32 tile_width = LLWorldMipmap::MAP_TILE_SIZE * (1 << (level - 1));
    // Dimension of the screen in meter at that scale
    LLVector3d pos_SW = viewPosToGlobal(0, 0);
    LLVector3d pos_NE = viewPosToGlobal(width, height);
    // Add external band of tiles on the outskirt so to hit the partially displayed tiles right and top
    pos_NE[VX] += tile_width;
    pos_NE[VY] += tile_width;

    // Iterate through the tiles on screen: we just need to ask for one tile every tile_width meters
    U32 grid_x, grid_y;
    for (F64 index_y = pos_SW[VY]; index_y < pos_NE[VY]; index_y += tile_width)
    {
        for (F64 index_x = pos_SW[VX]; index_x < pos_NE[VX]; index_x += tile_width)
        {
            // Compute the world coordinates of the current point
            LLVector3d pos_global(index_x, index_y, pos_SW[VZ]);
            // Convert to the mipmap level coordinates for that point (i.e. which tile to we hit)
            LLWorldMipmap::globalToMipmap(pos_global[VX], pos_global[VY], level, &grid_x, &grid_y);
            // Get the tile. Note: NULL means that the image does not exist (so it's considered "complete" as far as fetching is concerned)
            LLPointer<LLViewerFetchedTexture> simimage = LLWorldMap::getInstance()->getObjectsTile(grid_x, grid_y, level, load);
            if (simimage)
            {
                // Checks that the image has a valid texture
                if (simimage->hasGLTexture())
                {
                    // Increment the number of completly fetched tiles
                    completed_tiles++;

                    // Convert those coordinates (SW corner of the mipmap tile) into world (meters) coordinates
                    pos_global[VX] = grid_x * REGION_WIDTH_METERS;
                    pos_global[VY] = grid_y * REGION_WIDTH_METERS;
                    // Now to screen coordinates for SW corner of that tile
                    LLVector3 pos_screen = globalPosToView (pos_global);
                    F32 left   = pos_screen[VX];
                    F32 bottom = pos_screen[VY];
                    // Compute the NE corner coordinates of the tile now
                    pos_global[VX] += tile_width;
                    pos_global[VY] += tile_width;
                    pos_screen = globalPosToView (pos_global);
                    F32 right  = pos_screen[VX];
                    F32 top    = pos_screen[VY];

                    // Draw the tile
                    LLGLSUIDefault gls_ui;
                    gGL.getTexUnit(0)->bind(simimage.get());
                    simimage->setAddressMode(LLTexUnit::TAM_CLAMP);

                    gGL.color4f(1.f, 1.0f, 1.0f, 1.0f);

                    gGL.begin(LLRender::QUADS);
                        gGL.texCoord2f(0.f, 1.f);
                        gGL.vertex3f(left, top, 0.f);
                        gGL.texCoord2f(0.f, 0.f);
                        gGL.vertex3f(left, bottom, 0.f);
                        gGL.texCoord2f(1.f, 0.f);
                        gGL.vertex3f(right, bottom, 0.f);
                        gGL.texCoord2f(1.f, 1.f);
                        gGL.vertex3f(right, top, 0.f);
                    gGL.end();
#if DEBUG_DRAW_TILE
                    drawTileOutline(level, top, left, bottom, right);
#endif // DEBUG_DRAW_TILE
                }
                //else
                //{
                //  Waiting for a tile -> the level is not complete
                //  LL_INFOS("WorldMap") << "Unfetched tile. level = " << level << LL_ENDL;
                //}
            }
            else
            {
                // Unexistent tiles are counted as "completed"
                completed_tiles++;
            }
            // Increment the number of tiles in that level / screen
            total_tiles++;
        }
    }
    return (completed_tiles == total_tiles);
}

// Draw lines (rectangle outline and cross) to visualize the position of the tile
// Used for debug only
void LLWorldMapView::drawTileOutline(S32 level, F32 top, F32 left, F32 bottom, F32 right)
{
    gGL.getTexUnit(0)->unbind(LLTexUnit::TT_TEXTURE);
    if (level == 1)
        gGL.color3f(1.f, 0.f, 0.f);     // red
    else if (level == 2)
        gGL.color3f(0.f, 1.f, 0.f);     // green
    else if (level == 3)
        gGL.color3f(0.f, 0.f, 1.f);     // blue
    else if (level == 4)
        gGL.color3f(1.f, 1.f, 0.f);     // yellow
    else if (level == 5)
        gGL.color3f(1.f, 0.f, 1.f);     // magenta
    else if (level == 6)
        gGL.color3f(0.f, 1.f, 1.f);     // cyan
    else if (level == 7)
        gGL.color3f(1.f, 1.f, 1.f);     // white
    else
        gGL.color3f(0.f, 0.f, 0.f);     // black
    gGL.begin(LLRender::LINE_STRIP);
        gGL.vertex2f(left, top);
        gGL.vertex2f(right, bottom);
        gGL.vertex2f(left, bottom);
        gGL.vertex2f(right, top);
        gGL.vertex2f(left, top);
        gGL.vertex2f(left, bottom);
        gGL.vertex2f(right, bottom);
        gGL.vertex2f(right, top);
    gGL.end();
}

void LLWorldMapView::drawGenericItems(const LLSimInfo::item_info_list_t& items, LLUIImagePtr image)
{
    LLSimInfo::item_info_list_t::const_iterator e;
    for (e = items.begin(); e != items.end(); ++e)
    {
        drawGenericItem(*e, image);
    }
}

void LLWorldMapView::drawGenericItem(const LLItemInfo& item, LLUIImagePtr image)
{
    drawImage(item.getGlobalPosition(), image);
}


void LLWorldMapView::drawImage(const LLVector3d& global_pos, LLUIImagePtr image, const LLColor4& color)
{
    LLVector3 pos_map = globalPosToView( global_pos );
    image->draw(ll_round(pos_map.mV[VX] - image->getWidth() /2.f),
                ll_round(pos_map.mV[VY] - image->getHeight()/2.f),
                color);
}

void LLWorldMapView::drawImageStack(const LLVector3d& global_pos, LLUIImagePtr image, U32 count, F32 offset, const LLColor4& color)
{
    LLVector3 pos_map = globalPosToView( global_pos );
    for(U32 i=0; i<count; i++)
    {
        image->draw(ll_round(pos_map.mV[VX] - image->getWidth() /2.f),
                    ll_round(pos_map.mV[VY] - image->getHeight()/2.f + i*offset),
                    color);
    }
}

void LLWorldMapView::drawItems()
{
    bool mature_enabled = gAgent.canAccessMature();
    bool adult_enabled = gAgent.canAccessAdult();

    BOOL show_mature = mature_enabled && gSavedSettings.getBOOL("ShowMatureEvents");
    BOOL show_adult = adult_enabled && gSavedSettings.getBOOL("ShowAdultEvents");

    for (handle_list_t::iterator iter = mVisibleRegions.begin(); iter != mVisibleRegions.end(); ++iter)
    {
        U64 handle = *iter;
        LLSimInfo* info = LLWorldMap::getInstance()->simInfoFromHandle(handle);
        if ((info == NULL) || (info->isDown()))
        {
            continue;
        }
        // Infohubs
        if (gSavedSettings.getBOOL("MapShowInfohubs"))
        {
            drawGenericItems(info->getInfoHub(), sInfohubImage);
        }
        // Telehubs
        if (gSavedSettings.getBOOL("MapShowTelehubs"))
        {
            drawGenericItems(info->getTeleHub(), sTelehubImage);
        }
        // Land for sale
        if (gSavedSettings.getBOOL("MapShowLandForSale"))
        {
            drawGenericItems(info->getLandForSale(), sForSaleImage);
            // for 1.23, we're showing normal land and adult land in the same UI; you don't
            // get a choice about which ones you want. If you're currently asking for adult
            // content and land you'll get the adult land.
            if (gAgent.canAccessAdult())
            {
                drawGenericItems(info->getLandForSaleAdult(), sForSaleAdultImage);
            }
        }
        // PG Events
        if (gSavedSettings.getBOOL("MapShowEvents"))
        {
            drawGenericItems(info->getPGEvent(), sEventImage);
        }
        // Mature Events
        if (show_mature)
        {
            drawGenericItems(info->getMatureEvent(), sEventMatureImage);
        }
        // Adult Events
        if (show_adult)
        {
            drawGenericItems(info->getAdultEvent(), sEventAdultImage);
        }
    }
}

void LLWorldMapView::drawAgents()
{
    static LLUIColor map_avatar_color = LLUIColorTable::instance().getColor("MapAvatarColor", LLColor4::white);

    for (handle_list_t::iterator iter = mVisibleRegions.begin(); iter != mVisibleRegions.end(); ++iter)
    {
        U64 handle = *iter;
        LLSimInfo* siminfo = LLWorldMap::getInstance()->simInfoFromHandle(handle);
        if ((siminfo == NULL) || (siminfo->isDown()))
        {
            continue;
        }
        LLSimInfo::item_info_list_t::const_iterator it = siminfo->getAgentLocation().begin();
        while (it != siminfo->getAgentLocation().end())
        {
            // Show Individual agents (or little stacks where real agents are)

            // Here's how we'd choose the color if info.mID were available but it's not being sent:
            // LLColor4 color = (agent_count == 1 && is_agent_friend(info.mID)) ? friend_color : avatar_color;
            drawImageStack(it->getGlobalPosition(), sAvatarSmallImage, it->getCount(), 3.f, map_avatar_color);
            ++it;
        }
    }
}

void LLWorldMapView::drawFrustum()
{
    // Draw frustum
    F32 meters_to_pixels = mMapScale/ REGION_WIDTH_METERS;

    F32 horiz_fov = LLViewerCamera::getInstance()->getView() * LLViewerCamera::getInstance()->getAspect();
    F32 far_clip_meters = LLViewerCamera::getInstance()->getFar();
    F32 far_clip_pixels = far_clip_meters * meters_to_pixels;

    F32 half_width_meters = far_clip_meters * tan( horiz_fov / 2 );
    F32 half_width_pixels = half_width_meters * meters_to_pixels;

    // Compute the frustum coordinates. Take the UI scale into account.
    F32 ctr_x = ((getLocalRect().getWidth() * 0.5f + mPanX)  * LLUI::getScaleFactor().mV[VX]);
    F32 ctr_y = ((getLocalRect().getHeight() * 0.5f + mPanY) * LLUI::getScaleFactor().mV[VY]);

    gGL.getTexUnit(0)->unbind(LLTexUnit::TT_TEXTURE);

    // Since we don't rotate the map, we have to rotate the frustum.
    gGL.pushMatrix();
    {
        gGL.translatef( ctr_x, ctr_y, 0 );

        // Draw triangle with more alpha in far pixels to make it
        // fade out in distance.
        gGL.begin( LLRender::TRIANGLES  );
        {
            // get camera look at and left axes
            LLVector3 at_axis = LLViewerCamera::instance().getAtAxis();
            LLVector3 left_axis = LLViewerCamera::instance().getLeftAxis();

            // grab components along XY plane
            LLVector2 cam_lookat(at_axis.mV[VX], at_axis.mV[VY]);
            LLVector2 cam_left(left_axis.mV[VX], left_axis.mV[VY]);

            // but, when looking near straight up or down...
            if (is_approx_zero(cam_lookat.magVecSquared()))
            {
                //...just fall back to looking down the x axis
                cam_lookat = LLVector2(1.f, 0.f); // x axis
                cam_left = LLVector2(0.f, 1.f); // y axis
            }

            // normalize to unit length
            cam_lookat.normVec();
            cam_left.normVec();

            gGL.color4f(1.f, 1.f, 1.f, 0.25f);
            gGL.vertex2f( 0, 0 );

            gGL.color4f(1.f, 1.f, 1.f, 0.02f);

            // use 2d camera vectors to render frustum triangle
            LLVector2 vert = cam_lookat * far_clip_pixels + cam_left * half_width_pixels;
            gGL.vertex2f(vert.mV[VX], vert.mV[VY]);

            vert = cam_lookat * far_clip_pixels - cam_left * half_width_pixels;
            gGL.vertex2f(vert.mV[VX], vert.mV[VY]);
        }
        gGL.end();
    }
    gGL.popMatrix();
}


LLVector3 LLWorldMapView::globalPosToView( const LLVector3d& global_pos )
{
    LLVector3d relative_pos_global = global_pos - gAgentCamera.getCameraPositionGlobal();
    LLVector3 pos_local;
    pos_local.setVec(relative_pos_global);  // convert to floats from doubles

    pos_local.mV[VX] *= mMapScale / REGION_WIDTH_METERS;
    pos_local.mV[VY] *= mMapScale / REGION_WIDTH_METERS;
    // leave Z component in meters


    pos_local.mV[VX] += getRect().getWidth() / 2 + mPanX;
    pos_local.mV[VY] += getRect().getHeight() / 2 + mPanY;

    return pos_local;
}


void LLWorldMapView::drawTracking(const LLVector3d& pos_global, const LLColor4& color, BOOL draw_arrow,
                                  const std::string& label, const std::string& tooltip, S32 vert_offset )
{
    LLVector3 pos_local = globalPosToView( pos_global );
    S32 x = ll_round( pos_local.mV[VX] );
    S32 y = ll_round( pos_local.mV[VY] );
    LLFontGL* font = LLFontGL::getFontSansSerifSmall();
    S32 text_x = x;
    S32 text_y = (S32)(y - sTrackCircleImage->getHeight()/2 - font->getLineHeight());

    if(    x < 0
        || y < 0
        || x >= getRect().getWidth()
        || y >= getRect().getHeight() )
    {
        if (draw_arrow)
        {
            drawTrackingCircle( getRect(), x, y, color, 3, 15 );
            drawTrackingArrow( getRect(), x, y, color );
            text_x = sTrackingArrowX;
            text_y = sTrackingArrowY;
        }
    }
    else if (LLTracker::getTrackingStatus() == LLTracker::TRACKING_LOCATION &&
        LLTracker::getTrackedLocationType() != LLTracker::LOCATION_NOTHING)
    {
        drawTrackingCircle( getRect(), x, y, color, 3, 15 );
    }
    else
    {
        drawImage(pos_global, sTrackCircleImage, color);
    }

    // clamp text position to on-screen
    const S32 TEXT_PADDING = DEFAULT_TRACKING_ARROW_SIZE + 2;
    S32 half_text_width = llfloor(font->getWidthF32(label) * 0.5f);
    text_x = llclamp(text_x, half_text_width + TEXT_PADDING, getRect().getWidth() - half_text_width - TEXT_PADDING);
    text_y = llclamp(text_y + vert_offset, TEXT_PADDING + vert_offset, getRect().getHeight() - font->getLineHeight() - TEXT_PADDING - vert_offset);

    if (label != "")
    {
        font->renderUTF8(
            label, 0,
            text_x,
            text_y,
            LLColor4::white, LLFontGL::HCENTER,
            LLFontGL::BASELINE, LLFontGL::NORMAL, LLFontGL::DROP_SHADOW);

        if (tooltip != "")
        {
            text_y -= font->getLineHeight();

            font->renderUTF8(
                tooltip, 0,
                text_x,
                text_y,
                LLColor4::white, LLFontGL::HCENTER,
                LLFontGL::BASELINE, LLFontGL::NORMAL, LLFontGL::DROP_SHADOW);
        }
    }
}

// If you change this, then you need to change LLTracker::getTrackedPositionGlobal() as well
LLVector3d LLWorldMapView::viewPosToGlobal( S32 x, S32 y )
{
    x -= llfloor((getRect().getWidth() / 2 + mPanX));
    y -= llfloor((getRect().getHeight() / 2 + mPanY));

    LLVector3 pos_local( (F32)x, (F32)y, 0.f );

    pos_local *= ( REGION_WIDTH_METERS / mMapScale );

    LLVector3d pos_global;
    pos_global.setVec( pos_local );
    pos_global += gAgentCamera.getCameraPositionGlobal();
    if(gAgent.isGodlike())
    {
        pos_global.mdV[VZ] = GODLY_TELEPORT_HEIGHT; // Godly height should always be 200.
    }
    else
    {
        pos_global.mdV[VZ] = gAgent.getPositionAgent().mV[VZ]; // Want agent's height, not camera's
    }

    return pos_global;
}


BOOL LLWorldMapView::handleToolTip( S32 x, S32 y, MASK mask )
{
    LLVector3d pos_global = viewPosToGlobal(x, y);
    U64 handle = to_region_handle(pos_global);
    std::string tooltip_msg;

    LLSimInfo* info = LLWorldMap::getInstance()->simInfoFromHandle(handle);
    if (info)
    {
        LLViewerRegion *region = gAgent.getRegion();

        std::string message = llformat("%s (%s)", info->getName().c_str(), info->getAccessString().c_str());

        if (!info->isDown())
        {
            S32 agent_count = info->getAgentCount();
            if (region && (region->getHandle() == handle))
            {
                ++agent_count; // Bump by 1 if we're here
            }

            // We may not have an agent count when the map is really
            // zoomed out, so don't display anything about the count. JC
            if (agent_count > 0)
            {
                LLStringUtil::format_map_t string_args;
                string_args["[NUMBER]"] = llformat("%d", agent_count);
                message += '\n';
                message += getString((agent_count == 1 ? "world_map_person" : "world_map_people") , string_args);
            }
        }
        tooltip_msg.assign( message );

        // Optionally show region flags
        std::string region_flags = info->getFlagsString();

        if (!region_flags.empty())
        {
            tooltip_msg += '\n';
            tooltip_msg += region_flags;
        }

        const S32 SLOP = 9;
        S32 screen_x, screen_y;

        localPointToScreen(x, y, &screen_x, &screen_y);
        LLRect sticky_rect_screen;
        sticky_rect_screen.setCenterAndSize(screen_x, screen_y, SLOP, SLOP);

        LLToolTipMgr::instance().show(LLToolTip::Params()
            .message(tooltip_msg)
            .sticky_rect(sticky_rect_screen));
    }
    return TRUE;
}

// Pass relative Z of 0 to draw at same level.
// static
static void drawDot(F32 x_pixels, F32 y_pixels,
             const LLColor4& color,
             F32 relative_z,
             F32 dot_radius,
             LLUIImagePtr dot_image)
{
    const F32 HEIGHT_THRESHOLD = 7.f;

    if(-HEIGHT_THRESHOLD <= relative_z && relative_z <= HEIGHT_THRESHOLD)
    {
        dot_image->draw(ll_round(x_pixels) - dot_image->getWidth()/2,
                        ll_round(y_pixels) - dot_image->getHeight()/2,
                        color);
    }
    else
    {
        // Draw V indicator for above or below
        // *TODO: Replace this vector drawing with icons

        F32 left =      x_pixels - dot_radius;
        F32 right =     x_pixels + dot_radius;
        F32 center = (left + right) * 0.5f;
        F32 top =       y_pixels + dot_radius;
        F32 bottom =    y_pixels - dot_radius;

        gGL.getTexUnit(0)->unbind(LLTexUnit::TT_TEXTURE);
        gGL.color4fv( color.mV );
        LLUI::setLineWidth(3.0f);
        F32 point = relative_z > HEIGHT_THRESHOLD ? top : bottom; // Y pos of the point of the V
        F32 back = relative_z > HEIGHT_THRESHOLD ? bottom : top; // Y pos of the ends of the V
        gGL.begin( LLRender::LINES );
            gGL.vertex2f(left, back);
            gGL.vertex2f(center, point);
            gGL.vertex2f(center, point);
            gGL.vertex2f(right, back);
        gGL.end();
        LLUI::setLineWidth(1.0f);
    }
}

// Pass relative Z of 0 to draw at same level.
// static
void LLWorldMapView::drawAvatar(F32 x_pixels,
                                F32 y_pixels,
                                const LLColor4& color,
                                F32 relative_z,
                                F32 dot_radius,
                                bool unknown_relative_z)
{
    const F32 HEIGHT_THRESHOLD = 7.f;
    LLUIImagePtr dot_image = sAvatarLevelImage;
    if (unknown_relative_z && llabs(relative_z) > HEIGHT_THRESHOLD)
    {
        dot_image = sAvatarUnknownImage;
    }
    else
    {
        if(relative_z < -HEIGHT_THRESHOLD)
        {
            dot_image = sAvatarBelowImage;
        }
        else if(relative_z > HEIGHT_THRESHOLD)
        {
            dot_image = sAvatarAboveImage;
        }
    }
    S32 dot_width = ll_round(dot_radius * 2.f);
    dot_image->draw(ll_round(x_pixels - dot_radius),
                    ll_round(y_pixels - dot_radius),
                    dot_width,
                    dot_width,
                    color);
}

// Pass relative Z of 0 to draw at same level.
// static
void LLWorldMapView::drawTrackingDot( F32 x_pixels,
                                      F32 y_pixels,
                                      const LLColor4& color,
                                      F32 relative_z,
                                      F32 dot_radius)
{
    drawDot(x_pixels, y_pixels, color, relative_z, dot_radius, sTrackCircleImage);
}


// Pass relative Z of 0 to draw at same level.
// static
void LLWorldMapView::drawIconName(F32 x_pixels,
                                  F32 y_pixels,
                                  const LLColor4& color,
                                  const std::string& first_line,
                                  const std::string& second_line)
{
    const S32 VERT_PAD = 8;
    S32 text_x = ll_round(x_pixels);
    S32 text_y = ll_round(y_pixels
                         - BIG_DOT_RADIUS
                         - VERT_PAD);

    // render text
    LLFontGL::getFontSansSerif()->renderUTF8(first_line, 0,
        text_x,
        text_y,
        color,
        LLFontGL::HCENTER,
        LLFontGL::TOP,
        LLFontGL::NORMAL,
        LLFontGL::DROP_SHADOW);

    text_y -= LLFontGL::getFontSansSerif()->getLineHeight();

    // render text
    LLFontGL::getFontSansSerif()->renderUTF8(second_line, 0,
        text_x,
        text_y,
        color,
        LLFontGL::HCENTER,
        LLFontGL::TOP,
        LLFontGL::NORMAL,
        LLFontGL::DROP_SHADOW);
}


//static
void LLWorldMapView::drawTrackingCircle( const LLRect& rect, S32 x, S32 y, const LLColor4& color, S32 min_thickness, S32 overlap )
{
    F32 start_theta = 0.f;
    F32 end_theta = F_TWO_PI;
    F32 x_delta = 0.f;
    F32 y_delta = 0.f;

    if (x < 0)
    {
        x_delta = 0.f - (F32)x;
        start_theta = F_PI + F_PI_BY_TWO;
        end_theta = F_TWO_PI + F_PI_BY_TWO;
    }
    else if (x > rect.getWidth())
    {
        x_delta = (F32)(x - rect.getWidth());
        start_theta = F_PI_BY_TWO;
        end_theta = F_PI + F_PI_BY_TWO;
    }

    if (y < 0)
    {
        y_delta = 0.f - (F32)y;
        if (x < 0)
        {
            start_theta = 0.f;
            end_theta = F_PI_BY_TWO;
        }
        else if (x > rect.getWidth())
        {
            start_theta = F_PI_BY_TWO;
            end_theta = F_PI;
        }
        else
        {
            start_theta = 0.f;
            end_theta = F_PI;
        }
    }
    else if (y > rect.getHeight())
    {
        y_delta = (F32)(y - rect.getHeight());
        if (x < 0)
        {
            start_theta = F_PI + F_PI_BY_TWO;
            end_theta = F_TWO_PI;
        }
        else if (x > rect.getWidth())
        {
            start_theta = F_PI;
            end_theta = F_PI + F_PI_BY_TWO;
        }
        else
        {
            start_theta = F_PI;
            end_theta = F_TWO_PI;
        }
    }

    F32 distance = sqrtf(x_delta * x_delta + y_delta * y_delta);

    distance = llmax(0.1f, distance);

    F32 outer_radius = distance + (1.f + (9.f * sqrtf(x_delta * y_delta) / distance)) * (F32)overlap;
    F32 inner_radius = outer_radius - (F32)min_thickness;

    F32 angle_adjust_x = asin(x_delta / outer_radius);
    F32 angle_adjust_y = asin(y_delta / outer_radius);

    if (angle_adjust_x)
    {
        if (angle_adjust_y)
        {
            F32 angle_adjust = llmin(angle_adjust_x, angle_adjust_y);
            start_theta += angle_adjust;
            end_theta -= angle_adjust;
        }
        else
        {
            start_theta += angle_adjust_x;
            end_theta -= angle_adjust_x;
        }
    }
    else if (angle_adjust_y)
    {
        start_theta += angle_adjust_y;
        end_theta -= angle_adjust_y;
    }

    gGL.matrixMode(LLRender::MM_MODELVIEW);
    gGL.pushMatrix();
    gGL.translatef((F32)x * LLUI::getScaleFactor().mV[VX], (F32)y * LLUI::getScaleFactor().mV[VY], 0.f);
    gl_washer_segment_2d(inner_radius, outer_radius, start_theta, end_theta, 40, color, color);
    gGL.popMatrix();

}

// static
void LLWorldMapView::drawTrackingArrow(const LLRect& rect, S32 x, S32 y,
                                       const LLColor4& color,
                                       S32 arrow_size)
{
    F32 x_center = (F32)rect.getWidth() / 2.f;
    F32 y_center = (F32)rect.getHeight() / 2.f;

    F32 x_clamped = (F32)llclamp( x, 0, rect.getWidth() - arrow_size );
    F32 y_clamped = (F32)llclamp( y, 0, rect.getHeight() - arrow_size );

    F32 slope = (F32)(y - y_center) / (F32)(x - x_center);
    F32 window_ratio = (F32)rect.getHeight() / (F32)rect.getWidth();

    if (llabs(slope) > window_ratio && y_clamped != (F32)y)
    {
        // clamp by y
        x_clamped = (y_clamped - y_center) / slope + x_center;
        // adjust for arrow size
        x_clamped  = llclamp(x_clamped , 0.f, (F32)(rect.getWidth() - arrow_size) );
    }
    else if (x_clamped != (F32)x)
    {
        // clamp by x
        y_clamped = (x_clamped - x_center) * slope + y_center;
        // adjust for arrow size
        y_clamped = llclamp( y_clamped, 0.f, (F32)(rect.getHeight() - arrow_size) );
    }

    // *FIX: deal with non-square window properly.
    // I do not understand what this comment means -- is it actually
    // broken or is it correctly dealing with non-square
    // windows. Phoenix 2007-01-03.
    S32 half_arrow_size = (S32) (0.5f * arrow_size);

    F32 angle = atan2( y + half_arrow_size - y_center, x + half_arrow_size - x_center);

    sTrackingArrowX = llfloor(x_clamped);
    sTrackingArrowY = llfloor(y_clamped);

    gl_draw_scaled_rotated_image(
        sTrackingArrowX,
        sTrackingArrowY,
        arrow_size, arrow_size,
        RAD_TO_DEG * angle,
        sTrackArrowImage->getImage(),
        color);
}

void LLWorldMapView::setDirectionPos( LLTextBox* text_box, F32 rotation )
{
    // Rotation is in radians.
    // Rotation of 0 means x = 1, y = 0 on the unit circle.


    F32 map_half_height = getRect().getHeight() * 0.5f;
    F32 map_half_width = getRect().getWidth() * 0.5f;
    F32 text_half_height = text_box->getRect().getHeight() * 0.5f;
    F32 text_half_width = text_box->getRect().getWidth() * 0.5f;
    F32 radius = llmin( map_half_height - text_half_height, map_half_width - text_half_width );

    text_box->setOrigin(
        ll_round(map_half_width - text_half_width + radius * cos( rotation )),
        ll_round(map_half_height - text_half_height + radius * sin( rotation )) );
}


void LLWorldMapView::updateDirections()
{
    S32 width = getRect().getWidth();
    S32 height = getRect().getHeight();

    S32 text_height = mTextBoxNorth->getRect().getHeight();
    S32 text_width = mTextBoxNorth->getRect().getWidth();

    const S32 PAD = 2;
    S32 top = height - text_height - PAD;
    S32 left = PAD*2;
    S32 bottom = PAD;
    S32 right = width - text_width - PAD;
    S32 center_x = width/2 - text_width/2;
    S32 center_y = height/2 - text_height/2;

    mTextBoxNorth->setOrigin( center_x, top );
    mTextBoxEast->setOrigin( right, center_y );
    mTextBoxSouth->setOrigin( center_x, bottom );
    mTextBoxWest->setOrigin( left, center_y );

    // These have wider text boxes
    text_width = mTextBoxNorthWest->getRect().getWidth();
    right = width - text_width - PAD;

    mTextBoxNorthWest->setOrigin(left, top);
    mTextBoxNorthEast->setOrigin(right, top);
    mTextBoxSouthWest->setOrigin(left, bottom);
    mTextBoxSouthEast->setOrigin(right, bottom);

//  S32 hint_width = mTextBoxScrollHint->getRect().getWidth();
//  mTextBoxScrollHint->setOrigin( width - hint_width - text_width - 2 * PAD,
//          PAD * 2 + text_height );
}


void LLWorldMapView::reshape( S32 width, S32 height, BOOL called_from_parent )
{
    LLView::reshape( width, height, called_from_parent );
}

bool LLWorldMapView::checkItemHit(S32 x, S32 y, LLItemInfo& item, LLUUID* id, bool track)
{
    LLVector3 pos_view = globalPosToView(item.getGlobalPosition());
    S32 item_x = ll_round(pos_view.mV[VX]);
    S32 item_y = ll_round(pos_view.mV[VY]);

    if (x < item_x - BIG_DOT_RADIUS) return false;
    if (x > item_x + BIG_DOT_RADIUS) return false;
    if (y < item_y - BIG_DOT_RADIUS) return false;
    if (y > item_y + BIG_DOT_RADIUS) return false;

    LLSimInfo* sim_info = LLWorldMap::getInstance()->simInfoFromHandle(item.getRegionHandle());
    if (sim_info)
    {
        if (track)
        {
            gFloaterWorldMap->trackLocation(item.getGlobalPosition());
        }
    }

    if (track)
    {
        gFloaterWorldMap->trackGenericItem(item);
    }

//  item.setSelected(true);
    *id = item.getUUID();

    return true;
}

// Handle a click, which might be on a dot
void LLWorldMapView::handleClick(S32 x, S32 y, MASK mask,
                                 S32* hit_type,
                                 LLUUID* id)
{
    LLVector3d pos_global = viewPosToGlobal(x, y);

    // *HACK: Adjust Z values automatically for liaisons & gods so
    // we swoop down when they click on the map. Sadly, the P2P
    // branch does not pay attention to this value; however, the
    // Distributed Messaging branch honors it.
    if(gAgent.isGodlike())
    {
        pos_global.mdV[VZ] = 200.0;
    }

    *hit_type = 0; // hit nothing

    LLWorldMap::getInstance()->cancelTracking();

    S32 level = LLWorldMipmap::scaleToLevel(mMapScale);
    // If the zoom level is not too far out already, test hits
    if (level <= DRAW_SIMINFO_THRESHOLD)
    {
        bool show_mature = gAgent.canAccessMature() && gSavedSettings.getBOOL("ShowMatureEvents");
        bool show_adult = gAgent.canAccessAdult() && gSavedSettings.getBOOL("ShowAdultEvents");

        // Test hits if trackable data are displayed, otherwise, we don't even bother
        if (gSavedSettings.getBOOL("MapShowEvents") || show_mature || show_adult || gSavedSettings.getBOOL("MapShowLandForSale"))
        {
            // Iterate through the visible regions
            for (handle_list_t::iterator iter = mVisibleRegions.begin(); iter != mVisibleRegions.end(); ++iter)
            {
                U64 handle = *iter;
                LLSimInfo* siminfo = LLWorldMap::getInstance()->simInfoFromHandle(handle);
                if ((siminfo == NULL) || (siminfo->isDown()))
                {
                    continue;
                }
                // If on screen check hits with the visible item lists
                if (gSavedSettings.getBOOL("MapShowEvents"))
                {
                    LLSimInfo::item_info_list_t::const_iterator it = siminfo->getPGEvent().begin();
                    while (it != siminfo->getPGEvent().end())
                    {
                        LLItemInfo event = *it;
                        if (checkItemHit(x, y, event, id, false))
                        {
                            *hit_type = MAP_ITEM_PG_EVENT;
                            mItemPicked = TRUE;
                            gFloaterWorldMap->trackEvent(event);
                            return;
                        }
                        ++it;
                    }
                }
                if (show_mature)
                {
                    LLSimInfo::item_info_list_t::const_iterator it = siminfo->getMatureEvent().begin();
                    while (it != siminfo->getMatureEvent().end())
                    {
                        LLItemInfo event = *it;
                        if (checkItemHit(x, y, event, id, false))
                        {
                            *hit_type = MAP_ITEM_MATURE_EVENT;
                            mItemPicked = TRUE;
                            gFloaterWorldMap->trackEvent(event);
                            return;
                        }
                        ++it;
                    }
                }
                if (show_adult)
                {
                    LLSimInfo::item_info_list_t::const_iterator it = siminfo->getAdultEvent().begin();
                    while (it != siminfo->getAdultEvent().end())
                    {
                        LLItemInfo event = *it;
                        if (checkItemHit(x, y, event, id, false))
                        {
                            *hit_type = MAP_ITEM_ADULT_EVENT;
                            mItemPicked = TRUE;
                            gFloaterWorldMap->trackEvent(event);
                            return;
                        }
                        ++it;
                    }
                }
                if (gSavedSettings.getBOOL("MapShowLandForSale"))
                {
                    LLSimInfo::item_info_list_t::const_iterator it = siminfo->getLandForSale().begin();
                    while (it != siminfo->getLandForSale().end())
                    {
                        LLItemInfo event = *it;
                        if (checkItemHit(x, y, event, id, true))
                        {
                            *hit_type = MAP_ITEM_LAND_FOR_SALE;
                            mItemPicked = TRUE;
                            return;
                        }
                        ++it;
                    }
                    // for 1.23, we're showing normal land and adult land in the same UI; you don't
                    // get a choice about which ones you want. If you're currently asking for adult
                    // content and land you'll get the adult land.
                    if (gAgent.canAccessAdult())
                    {
                        LLSimInfo::item_info_list_t::const_iterator it = siminfo->getLandForSaleAdult().begin();
                        while (it != siminfo->getLandForSaleAdult().end())
                        {
                            LLItemInfo event = *it;
                            if (checkItemHit(x, y, event, id, true))
                            {
                                *hit_type = MAP_ITEM_LAND_FOR_SALE_ADULT;
                                mItemPicked = TRUE;
                                return;
                            }
                            ++it;
                        }
                    }
                }
            }
        }
    }

    // If we get here, we haven't clicked on anything
    gFloaterWorldMap->trackLocation(pos_global);
    mItemPicked = FALSE;
    *id = LLUUID::null;
    return;
}


BOOL LLWorldMapView::handleMouseDown( S32 x, S32 y, MASK mask )
{
    gFocusMgr.setMouseCapture( this );

    mMouseDownPanX = ll_round(mPanX);
    mMouseDownPanY = ll_round(mPanY);
    mMouseDownX = x;
    mMouseDownY = y;
    sHandledLastClick = TRUE;
    return TRUE;
}

BOOL LLWorldMapView::handleMouseUp( S32 x, S32 y, MASK mask )
{
    if (hasMouseCapture())
    {
        if (mPanning)
        {
            // restore mouse cursor
            S32 local_x, local_y;
            local_x = mMouseDownX + llfloor(mPanX - mMouseDownPanX);
            local_y = mMouseDownY + llfloor(mPanY - mMouseDownPanY);
            LLRect clip_rect = getRect();
            clip_rect.stretch(-8);
            clip_rect.clipPointToRect(mMouseDownX, mMouseDownY, local_x, local_y);
            LLUI::getInstance()->setMousePositionLocal(this, local_x, local_y);

            // finish the pan
            mPanning = FALSE;

            mMouseDownX = 0;
            mMouseDownY = 0;
        }
        else
        {
            // ignore whether we hit an event or not
            S32 hit_type;
            LLUUID id;
            handleClick(x, y, mask, &hit_type, &id);
        }
        gViewerWindow->showCursor();
        gFocusMgr.setMouseCapture( NULL );
        return TRUE;
    }
    return FALSE;
}

void LLWorldMapView::updateVisibleBlocks()
{
    if (LLWorldMipmap::scaleToLevel(mMapScale) > DRAW_SIMINFO_THRESHOLD)
    {
        // If we're zoomed out too much, we just don't load all those sim info: too much!
        return;
    }

    // Load the blocks visible in the current World Map view

    // Get the World Map view coordinates and boundaries
    LLVector3d camera_global = gAgentCamera.getCameraPositionGlobal();
    const S32 width = getRect().getWidth();
    const S32 height = getRect().getHeight();
    const F32 half_width = F32(width) / 2.0f;
    const F32 half_height = F32(height) / 2.0f;

    // Compute center into sim grid coordinates
    S32 world_center_x = S32((-mPanX / mMapScale) + (camera_global.mdV[0] / REGION_WIDTH_METERS));
    S32 world_center_y = S32((-mPanY / mMapScale) + (camera_global.mdV[1] / REGION_WIDTH_METERS));

    // Compute the boundaries into sim grid coordinates
    S32 world_left   = world_center_x - S32(half_width  / mMapScale) - 1;
    S32 world_right  = world_center_x + S32(half_width  / mMapScale) + 1;
    S32 world_bottom = world_center_y - S32(half_height / mMapScale) - 1;
    S32 world_top    = world_center_y + S32(half_height / mMapScale) + 1;

    //LL_INFOS("WorldMap") << "LLWorldMapView::updateVisibleBlocks() : mMapScale = " << mMapScale << ", left = " << world_left << ", right = " << world_right << ", bottom  = " << world_bottom << ", top = " << world_top << LL_ENDL;
    LLWorldMap::getInstance()->updateRegions(world_left, world_bottom, world_right, world_top);
}

BOOL LLWorldMapView::handleHover( S32 x, S32 y, MASK mask )
{
    if (hasMouseCapture())
    {
        if (mPanning || llabs(x - mMouseDownX) > 1 || llabs(y - mMouseDownY) > 1)
        {
            // just started panning, so hide cursor
            if (!mPanning)
            {
                mPanning = TRUE;
                gViewerWindow->hideCursor();
            }

            F32 delta_x = (F32)(gViewerWindow->getCurrentMouseDX());
            F32 delta_y = (F32)(gViewerWindow->getCurrentMouseDY());

            // Set pan to value at start of drag + offset
            mPanX += delta_x;
            mPanY += delta_y;
            mTargetPanX = mPanX;
            mTargetPanY = mPanY;

            gViewerWindow->moveCursorToCenter();
        }

        // doesn't matter, cursor should be hidden
        gViewerWindow->setCursor(UI_CURSOR_CROSS );
        return TRUE;
    }
    else
    {
        // While we're waiting for data from the tracker, we're busy. JC
        LLVector3d pos_global = LLTracker::getTrackedPositionGlobal();
        if (LLTracker::isTracking(NULL)
            && pos_global.isExactlyZero())
        {
            gViewerWindow->setCursor( UI_CURSOR_WAIT );
        }
        else
        {
            gViewerWindow->setCursor( UI_CURSOR_CROSS );
        }
        LL_DEBUGS("UserInput") << "hover handled by LLWorldMapView" << LL_ENDL;
        return TRUE;
    }
}


BOOL LLWorldMapView::handleDoubleClick( S32 x, S32 y, MASK mask )
{
    if( sHandledLastClick )
    {
        S32 hit_type;
        LLUUID id;
        handleClick(x, y, mask, &hit_type, &id);

        switch (hit_type)
        {
        case MAP_ITEM_PG_EVENT:
        case MAP_ITEM_MATURE_EVENT:
        case MAP_ITEM_ADULT_EVENT:
            {
                LLFloaterReg::hideInstance("world_map");
                // This is an ungainly hack
                std::string uuid_str;
                S32 event_id;
                id.toString(uuid_str);
                uuid_str = uuid_str.substr(28);
                sscanf(uuid_str.c_str(), "%X", &event_id);
                // Invoke the event details floater if someone is clicking on an event.
                LLSD params(LLSD::emptyArray());
                params.append(event_id);
                LLCommandDispatcher::dispatch("event", params, LLSD(), LLGridManager::getInstance()->getGrid(), NULL, LLCommandHandler::NAV_TYPE_CLICKED, true);
                break;
            }
        case MAP_ITEM_LAND_FOR_SALE:
        case MAP_ITEM_LAND_FOR_SALE_ADULT:
            {
                LLVector3d pos_global = viewPosToGlobal(x, y);
                std::string sim_name;
                if (LLWorldMap::getInstance()->simNameFromPosGlobal(pos_global, sim_name))
                {
                    LLFloaterReg::hideInstance("world_map");
                    LLFloaterReg::showInstance("search", LLSD().with("category", "land").with("query", sim_name));
                }
                break;
            }
        case MAP_ITEM_CLASSIFIED:
            {
                LLFloaterReg::hideInstance("world_map");
                LLFloaterReg::showInstance("search", LLSD().with("category", "classifieds").with("query", id));
                break;
            }
        default:
            {
                if (LLWorldMap::getInstance()->isTracking())
                {
                    LLWorldMap::getInstance()->setTrackingDoubleClick();
                }
                else
                {
                    // Teleport if we got a valid location
                    LLVector3d pos_global = viewPosToGlobal(x,y);
                    LLSimInfo* sim_info = LLWorldMap::getInstance()->simInfoFromPosGlobal(pos_global);
                    if (sim_info && !sim_info->isDown())
                    {
                        gAgent.teleportViaLocation( pos_global );
                    }
                }
            }
        };

        return TRUE;
    }
    return FALSE;
}

// static
F32 LLWorldMapView::scaleFromZoom(F32 zoom) { return exp2(zoom) * 256.0f; }

// static
F32 LLWorldMapView::zoomFromScale(F32 scale) { return log2(scale / 256.f); }