/**
 * @file llnetmap.cpp
 * @author James Cook
 * @brief Display of surrounding regions, objects, and agents.
 *
 * $LicenseInfo:firstyear=2001&license=viewerlgpl$
 * Second Life Viewer Source Code
 * Copyright (C) 2001-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 "llnetmap.h"

// Library includes (should move below)
#include "indra_constants.h"
#include "llavatarnamecache.h"
#include "llmath.h"
#include "llfloaterreg.h"
#include "llfocusmgr.h"
#include "lllocalcliprect.h"
#include "llrender.h"
#include "llresmgr.h"
#include "llui.h"
#include "lltooltip.h"

#include "llglheaders.h"

// Viewer includes
#include "llagent.h"
#include "llagentcamera.h"
#include "llappviewer.h" // for gDisconnected
#include "llcallingcard.h" // LLAvatarTracker
#include "llfloaterland.h"
#include "llfloaterworldmap.h"
#include "llparcel.h"
#include "lltracker.h"
#include "llsurface.h"
#include "llurlmatch.h"
#include "llurlregistry.h"
#include "llviewercamera.h"
#include "llviewercontrol.h"
#include "llviewerparcelmgr.h"
#include "llviewertexture.h"
#include "llviewertexturelist.h"
#include "llviewermenu.h"
#include "llviewerobjectlist.h"
#include "llviewerregion.h"
#include "llviewerwindow.h"
#include "llworld.h"
#include "llworldmapview.h"     // shared draw code

static LLDefaultChildRegistry::Register<LLNetMap> r1("net_map");

const F32 LLNetMap::MAP_SCALE_MIN = 32;
const F32 LLNetMap::MAP_SCALE_FAR = 32;
const F32 LLNetMap::MAP_SCALE_MEDIUM = 128;
const F32 LLNetMap::MAP_SCALE_CLOSE = 256;
const F32 LLNetMap::MAP_SCALE_VERY_CLOSE = 1024;
const F32 LLNetMap::MAP_SCALE_MAX = 4096;

const F32 MAP_SCALE_ZOOM_FACTOR = 1.04f; // Zoom in factor per click of scroll wheel (4%)
const F32 MIN_DOT_RADIUS = 3.5f;
const F32 DOT_SCALE = 0.75f;
const F32 MIN_PICK_SCALE = 2.f;
const S32 MOUSE_DRAG_SLOP = 2;      // How far the mouse needs to move before we think it's a drag

const F64 COARSEUPDATE_MAX_Z = 1020.0f;

LLNetMap::LLNetMap (const Params & p)
:   LLUICtrl (p),
    mBackgroundColor (p.bg_color()),
    mScale( MAP_SCALE_MEDIUM ),
    mPixelsPerMeter( MAP_SCALE_MEDIUM / REGION_WIDTH_METERS ),
    mObjectMapTPM(0.f),
    mObjectMapPixels(0.f),
    mCurPan(0.f, 0.f),
    mStartPan(0.f, 0.f),
    mPopupWorldPos(0.f, 0.f, 0.f),
    mMouseDown(0, 0),
    mPanning(false),
    mUpdateNow(false),
    mObjectImageCenterGlobal( gAgentCamera.getCameraPositionGlobal() ),
    mObjectRawImagep(),
    mObjectImagep(),
    mClosestAgentToCursor(),
    mClosestAgentAtLastRightClick(),
    mToolTipMsg()
{
    mScale = gSavedSettings.getF32("MiniMapScale");
    if (gAgent.isFirstLogin())
    {
        // *HACK: On first run, set this to false for new users, otherwise the
        // default is true to maintain consistent experience for existing
        // users.
        gSavedSettings.setBOOL("MiniMapRotate", false);
    }
    mPixelsPerMeter = mScale / REGION_WIDTH_METERS;
    mDotRadius = llmax(DOT_SCALE * mPixelsPerMeter, MIN_DOT_RADIUS);
}

LLNetMap::~LLNetMap()
{
    auto menu = static_cast<LLMenuGL*>(mPopupMenuHandle.get());
    if (menu)
    {
        menu->die();
        mPopupMenuHandle.markDead();
    }
}

BOOL LLNetMap::postBuild()
{
    LLUICtrl::CommitCallbackRegistry::ScopedRegistrar commitRegistrar;
    LLUICtrl::EnableCallbackRegistry::ScopedRegistrar enableRegistrar;

    enableRegistrar.add("Minimap.Zoom.Check", boost::bind(&LLNetMap::isZoomChecked, this, _2));
    commitRegistrar.add("Minimap.Zoom.Set", boost::bind(&LLNetMap::setZoom, this, _2));
    commitRegistrar.add("Minimap.Tracker", boost::bind(&LLNetMap::handleStopTracking, this, _2));
    commitRegistrar.add("Minimap.Center.Activate", boost::bind(&LLNetMap::activateCenterMap, this, _2));
    enableRegistrar.add("Minimap.MapOrientation.Check", boost::bind(&LLNetMap::isMapOrientationChecked, this, _2));
    commitRegistrar.add("Minimap.MapOrientation.Set", boost::bind(&LLNetMap::setMapOrientation, this, _2));
    commitRegistrar.add("Minimap.AboutLand", boost::bind(&LLNetMap::popupShowAboutLand, this, _2));

    LLMenuGL* menu = LLUICtrlFactory::getInstance()->createFromFile<LLMenuGL>("menu_mini_map.xml", gMenuHolder, LLViewerMenuHolderGL::child_registry_t::instance());
    mPopupMenuHandle = menu->getHandle();
    menu->setItemEnabled("Re-center map", false);
    return TRUE;
}

void LLNetMap::setScale( F32 scale )
{
    scale = llclamp(scale, MAP_SCALE_MIN, MAP_SCALE_MAX);
    mCurPan *= scale / mScale;
    mScale = scale;

    if (mObjectImagep.notNull())
    {
        F32 width = (F32)(getRect().getWidth());
        F32 height = (F32)(getRect().getHeight());
        F32 diameter = sqrt(width * width + height * height);
        F32 region_widths = diameter / mScale;
        F32 meters = region_widths * LLWorld::getInstance()->getRegionWidthInMeters();
        F32 num_pixels = (F32)mObjectImagep->getWidth();
        mObjectMapTPM = num_pixels / meters;
        mObjectMapPixels = diameter;
    }

    mPixelsPerMeter = mScale / REGION_WIDTH_METERS;
    mDotRadius = llmax(DOT_SCALE * mPixelsPerMeter, MIN_DOT_RADIUS);

    gSavedSettings.setF32("MiniMapScale", mScale);

    mUpdateNow = true;
}


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

void LLNetMap::draw()
{
    if (!LLWorld::instanceExists())
    {
        return;
    }
    LL_PROFILE_ZONE_SCOPED;
    static LLFrameTimer map_timer;
    static LLUIColor map_avatar_color = LLUIColorTable::instance().getColor("MapAvatarColor", LLColor4::white);
    static LLUIColor map_avatar_friend_color = LLUIColorTable::instance().getColor("MapAvatarFriendColor", LLColor4::white);
    static LLUIColor map_track_color = LLUIColorTable::instance().getColor("MapTrackColor", LLColor4::white);
    //static LLUIColor map_track_disabled_color = LLUIColorTable::instance().getColor("MapTrackDisabledColor", LLColor4::white);
    static LLUIColor map_frustum_color = LLUIColorTable::instance().getColor("MapFrustumColor", LLColor4::white);
    static LLUIColor map_parcel_outline_color = LLUIColorTable::instance().getColor("MapParcelOutlineColor", LLColor4(LLColor3(LLColor4::yellow), 0.5f));

    if (mObjectImagep.isNull())
    {
        createObjectImage();
    }

    static LLUICachedControl<bool> auto_center("MiniMapAutoCenter", true);
    bool auto_centering = auto_center && !mPanning;
    mCentering = mCentering && !mPanning;

    if (auto_centering || mCentering)
    {
        mCurPan = lerp(mCurPan, LLVector2(0.0f, 0.0f) , LLSmoothInterpolation::getInterpolant(0.1f));
    }
    bool centered = abs(mCurPan.mV[VX]) < 0.5f && abs(mCurPan.mV[VY]) < 0.5f;
    if (centered)
    {
        mCurPan.mV[0] = 0.0f;
        mCurPan.mV[1] = 0.0f;
        mCentering = false;
    }

    auto menu = static_cast<LLMenuGL*>(mPopupMenuHandle.get());
    if (menu)
    {
        bool can_recenter_map = !(centered || mCentering || auto_centering);
        menu->setItemEnabled("Re-center map", can_recenter_map);
    }
    updateAboutLandPopupButton();

    // Prepare a scissor region
    F32 rotation = 0;

    gGL.pushMatrix();
    gGL.pushUIMatrix();

    LLVector3 offset = gGL.getUITranslation();
    LLVector3 scale = gGL.getUIScale();

    gGL.loadIdentity();
    gGL.loadUIIdentity();

    gGL.scalef(scale.mV[0], scale.mV[1], scale.mV[2]);
    gGL.translatef(offset.mV[0], offset.mV[1], offset.mV[2]);

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

            gGL.matrixMode(LLRender::MM_MODELVIEW);

            // Draw background rectangle
            LLColor4 background_color = mBackgroundColor.get();
            gGL.color4fv( background_color.mV );
            gl_rect_2d(0, getRect().getHeight(), getRect().getWidth(), 0);
        }

        // region 0,0 is in the middle
        S32 center_sw_left = getRect().getWidth() / 2 + llfloor(mCurPan.mV[VX]);
        S32 center_sw_bottom = getRect().getHeight() / 2 + llfloor(mCurPan.mV[VY]);

        gGL.pushMatrix();

        gGL.translatef( (F32) center_sw_left, (F32) center_sw_bottom, 0.f);

        static LLUICachedControl<bool> rotate_map("MiniMapRotate", true);
        if( rotate_map )
        {
            // rotate subsequent draws to agent rotation
            rotation = atan2( LLViewerCamera::getInstance()->getAtAxis().mV[VX], LLViewerCamera::getInstance()->getAtAxis().mV[VY] );
            gGL.rotatef( rotation * RAD_TO_DEG, 0.f, 0.f, 1.f);
        }

        // figure out where agent is
        const S32 region_width = ll_round(LLWorld::getInstance()->getRegionWidthInMeters());
        const F32 scale_pixels_per_meter = mScale / region_width;

        for (LLWorld::region_list_t::const_iterator iter = LLWorld::getInstance()->getRegionList().begin();
             iter != LLWorld::getInstance()->getRegionList().end(); ++iter)
        {
            LLViewerRegion* regionp = *iter;
            // Find x and y position relative to camera's center.
            LLVector3 origin_agent = regionp->getOriginAgent();
            LLVector3 rel_region_pos = origin_agent - gAgentCamera.getCameraPositionAgent();
            F32 relative_x = rel_region_pos.mV[0] * scale_pixels_per_meter;
            F32 relative_y = rel_region_pos.mV[1] * scale_pixels_per_meter;

            // background region rectangle
            F32 bottom =    relative_y;
            F32 left =      relative_x;
            F32 top =       bottom + mScale ;
            F32 right =     left + mScale ;

            if (regionp == gAgent.getRegion())
            {
                gGL.color4f(1.f, 1.f, 1.f, 1.f);
            }
            else
            {
                gGL.color4f(0.8f, 0.8f, 0.8f, 1.f);
            }

            if (!regionp->isAlive())
            {
                gGL.color4f(1.f, 0.5f, 0.5f, 1.f);
            }



            // Draw using texture.
            gGL.getTexUnit(0)->bind(regionp->getLand().getSTexture());
            gGL.begin(LLRender::QUADS);
                gGL.texCoord2f(0.f, 1.f);
                gGL.vertex2f(left, top);
                gGL.texCoord2f(0.f, 0.f);
                gGL.vertex2f(left, bottom);
                gGL.texCoord2f(1.f, 0.f);
                gGL.vertex2f(right, bottom);
                gGL.texCoord2f(1.f, 1.f);
                gGL.vertex2f(right, top);
            gGL.end();

            // Draw water
            gGL.flush();
            {
                if (regionp->getLand().getWaterTexture())
                {
                    gGL.getTexUnit(0)->bind(regionp->getLand().getWaterTexture());
                    gGL.begin(LLRender::QUADS);
                        gGL.texCoord2f(0.f, 1.f);
                        gGL.vertex2f(left, top);
                        gGL.texCoord2f(0.f, 0.f);
                        gGL.vertex2f(left, bottom);
                        gGL.texCoord2f(1.f, 0.f);
                        gGL.vertex2f(right, bottom);
                        gGL.texCoord2f(1.f, 1.f);
                        gGL.vertex2f(right, top);
                    gGL.end();
                }
            }
            gGL.flush();
        }

        // Redraw object layer periodically
        if (mUpdateNow || (map_timer.getElapsedTimeF32() > 0.5f))
        {
            mUpdateNow = false;

            // Locate the centre of the object layer, accounting for panning
            LLVector3 new_center = globalPosToView(gAgentCamera.getCameraPositionGlobal());
            new_center.mV[VX] -= mCurPan.mV[VX];
            new_center.mV[VY] -= mCurPan.mV[VY];
            new_center.mV[VZ] = 0.f;
            mObjectImageCenterGlobal = viewPosToGlobal(llfloor(new_center.mV[VX]), llfloor(new_center.mV[VY]));

            // Create the base texture.
            U8 *default_texture = mObjectRawImagep->getData();
            memset( default_texture, 0, mObjectImagep->getWidth() * mObjectImagep->getHeight() * mObjectImagep->getComponents() );

            // Draw objects
            gObjectList.renderObjectsForMap(*this);

            mObjectImagep->setSubImage(mObjectRawImagep, 0, 0, mObjectImagep->getWidth(), mObjectImagep->getHeight());

            map_timer.reset();
        }

        LLVector3 map_center_agent = gAgent.getPosAgentFromGlobal(mObjectImageCenterGlobal);
        LLVector3 camera_position = gAgentCamera.getCameraPositionAgent();
        map_center_agent -= camera_position;
        map_center_agent.mV[VX] *= scale_pixels_per_meter;
        map_center_agent.mV[VY] *= scale_pixels_per_meter;

        gGL.getTexUnit(0)->bind(mObjectImagep);
        F32 image_half_width = 0.5f*mObjectMapPixels;
        F32 image_half_height = 0.5f*mObjectMapPixels;

        gGL.begin(LLRender::QUADS);
            gGL.texCoord2f(0.f, 1.f);
            gGL.vertex2f(map_center_agent.mV[VX] - image_half_width, image_half_height + map_center_agent.mV[VY]);
            gGL.texCoord2f(0.f, 0.f);
            gGL.vertex2f(map_center_agent.mV[VX] - image_half_width, map_center_agent.mV[VY] - image_half_height);
            gGL.texCoord2f(1.f, 0.f);
            gGL.vertex2f(image_half_width + map_center_agent.mV[VX], map_center_agent.mV[VY] - image_half_height);
            gGL.texCoord2f(1.f, 1.f);
            gGL.vertex2f(image_half_width + map_center_agent.mV[VX], image_half_height + map_center_agent.mV[VY]);
        gGL.end();

        for (LLWorld::region_list_t::const_iterator iter = LLWorld::getInstance()->getRegionList().begin();
             iter != LLWorld::getInstance()->getRegionList().end(); ++iter)
        {
            LLViewerRegion* regionp = *iter;
            regionp->renderPropertyLinesOnMinimap(scale_pixels_per_meter, map_parcel_outline_color.get().mV);
        }

        gGL.popMatrix();

        // Mouse pointer in local coordinates
        S32 local_mouse_x;
        S32 local_mouse_y;
        //localMouse(&local_mouse_x, &local_mouse_y);
        LLUI::getInstance()->getMousePositionLocal(this, &local_mouse_x, &local_mouse_y);
        mClosestAgentToCursor.setNull();
        F32 closest_dist_squared = F32_MAX; // value will be overridden in the loop
        F32 min_pick_dist_squared = (mDotRadius * MIN_PICK_SCALE) * (mDotRadius * MIN_PICK_SCALE);

        LLVector3 pos_map;
        uuid_vec_t avatar_ids;
        std::vector<LLVector3d> positions;
        bool unknown_relative_z;

        LLWorld::getInstance()->getAvatars(&avatar_ids, &positions, gAgentCamera.getCameraPositionGlobal());

        // Draw avatars
        for (U32 i = 0; i < avatar_ids.size(); i++)
        {
            LLUUID uuid = avatar_ids[i];
            // Skip self, we'll draw it later
            if (uuid == gAgent.getID()) continue;

            pos_map = globalPosToView(positions[i]);

            bool show_as_friend = (LLAvatarTracker::instance().getBuddyInfo(uuid) != NULL);

            LLColor4 color = show_as_friend ? map_avatar_friend_color : map_avatar_color;

            unknown_relative_z = positions[i].mdV[VZ] >= COARSEUPDATE_MAX_Z &&
                    camera_position.mV[VZ] >= COARSEUPDATE_MAX_Z;

            LLWorldMapView::drawAvatar(
                pos_map.mV[VX], pos_map.mV[VY],
                color,
                pos_map.mV[VZ], mDotRadius,
                unknown_relative_z);

            if(uuid.notNull())
            {
                bool selected = false;
                uuid_vec_t::iterator sel_iter = gmSelected.begin();
                for (; sel_iter != gmSelected.end(); sel_iter++)
                {
                    if(*sel_iter == uuid)
                    {
                        selected = true;
                        break;
                    }
                }
                if(selected)
                {
                    if( (pos_map.mV[VX] < 0) ||
                        (pos_map.mV[VY] < 0) ||
                        (pos_map.mV[VX] >= getRect().getWidth()) ||
                        (pos_map.mV[VY] >= getRect().getHeight()) )
                    {
                        S32 x = ll_round( pos_map.mV[VX] );
                        S32 y = ll_round( pos_map.mV[VY] );
                        LLWorldMapView::drawTrackingCircle( getRect(), x, y, color, 1, 10);
                    } else
                    {
                        LLWorldMapView::drawTrackingDot(pos_map.mV[VX],pos_map.mV[VY],color,0.f);
                    }
                }
            }

            F32 dist_to_cursor_squared = dist_vec_squared(LLVector2(pos_map.mV[VX], pos_map.mV[VY]),
                                          LLVector2(local_mouse_x,local_mouse_y));
            if(dist_to_cursor_squared < min_pick_dist_squared && dist_to_cursor_squared < closest_dist_squared)
            {
                closest_dist_squared = dist_to_cursor_squared;
                mClosestAgentToCursor = uuid;
            }
        }

        // Draw dot for autopilot target
        if (gAgent.getAutoPilot())
        {
            drawTracking( gAgent.getAutoPilotTargetGlobal(), map_track_color );
        }
        else
        {
            LLTracker::ETrackingStatus tracking_status = LLTracker::getTrackingStatus();
            if (  LLTracker::TRACKING_AVATAR == tracking_status )
            {
                drawTracking( LLAvatarTracker::instance().getGlobalPos(), map_track_color );
            }
            else if ( LLTracker::TRACKING_LANDMARK == tracking_status
                    || LLTracker::TRACKING_LOCATION == tracking_status )
            {
                drawTracking( LLTracker::getTrackedPositionGlobal(), map_track_color );
            }
        }

        // Draw dot for self avatar position
        LLVector3d pos_global = gAgent.getPositionGlobal();
        pos_map = globalPosToView(pos_global);
        S32 dot_width = ll_round(mDotRadius * 2.f);
        LLUIImagePtr you = LLWorldMapView::sAvatarYouLargeImage;
        if (you)
        {
            you->draw(ll_round(pos_map.mV[VX] - mDotRadius),
                      ll_round(pos_map.mV[VY] - mDotRadius),
                      dot_width,
                      dot_width);

            F32 dist_to_cursor_squared = dist_vec_squared(LLVector2(pos_map.mV[VX], pos_map.mV[VY]),
                                          LLVector2(local_mouse_x,local_mouse_y));
            if(dist_to_cursor_squared < min_pick_dist_squared && dist_to_cursor_squared < closest_dist_squared)
            {
                mClosestAgentToCursor = gAgent.getID();
            }
        }

        // Draw frustum
        F32 meters_to_pixels = mScale/ LLWorld::getInstance()->getRegionWidthInMeters();

        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 ctr_x = (F32)center_sw_left;
        F32 ctr_y = (F32)center_sw_bottom;

        const F32 steps_per_circle = 40.0f;
        const F32 steps_per_radian = steps_per_circle / F_TWO_PI;
        const F32 arc_start = -(horiz_fov / 2.0f) + F_PI_BY_TWO;
        const F32 arc_end = (horiz_fov / 2.0f) + F_PI_BY_TWO;
        const S32 steps = llmax(1, (S32)((horiz_fov * steps_per_radian) + 0.5f));

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

        if( rotate_map )
        {
            gGL.pushMatrix();
                gGL.translatef( ctr_x, ctr_y, 0 );
                gl_washer_segment_2d(far_clip_pixels, 0, arc_start, arc_end, steps, map_frustum_color(), map_frustum_color());
            gGL.popMatrix();
        }
        else
        {
            gGL.pushMatrix();
                gGL.translatef( ctr_x, ctr_y, 0 );
                // If we don't rotate the map, we have to rotate the frustum.
                gGL.rotatef( atan2( LLViewerCamera::getInstance()->getAtAxis().mV[VX], LLViewerCamera::getInstance()->getAtAxis().mV[VY] ) * RAD_TO_DEG, 0.f, 0.f, -1.f);
                gl_washer_segment_2d(far_clip_pixels, 0, arc_start, arc_end, steps, map_frustum_color(), map_frustum_color());
            gGL.popMatrix();
        }
    }

    gGL.popMatrix();
    gGL.popUIMatrix();

    LLUICtrl::draw();
}

void LLNetMap::reshape(S32 width, S32 height, BOOL called_from_parent)
{
    LLUICtrl::reshape(width, height, called_from_parent);
    createObjectImage();
}

LLVector3 LLNetMap::globalPosToView(const LLVector3d& global_pos)
{
    LLVector3d camera_position = gAgentCamera.getCameraPositionGlobal();

    LLVector3d relative_pos_global = global_pos - camera_position;
    LLVector3 pos_local;
    pos_local.setVec(relative_pos_global);  // convert to floats from doubles

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

    static LLUICachedControl<bool> rotate_map("MiniMapRotate", true);
    if( rotate_map )
    {
        F32 radians = atan2( LLViewerCamera::getInstance()->getAtAxis().mV[VX], LLViewerCamera::getInstance()->getAtAxis().mV[VY] );
        LLQuaternion rot(radians, LLVector3(0.f, 0.f, 1.f));
        pos_local.rotVec( rot );
    }

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

    return pos_local;
}

void LLNetMap::drawTracking(const LLVector3d& pos_global, const LLColor4& color,
                            BOOL draw_arrow )
{
    LLVector3 pos_local = globalPosToView(pos_global);
    if( (pos_local.mV[VX] < 0) ||
        (pos_local.mV[VY] < 0) ||
        (pos_local.mV[VX] >= getRect().getWidth()) ||
        (pos_local.mV[VY] >= getRect().getHeight()) )
    {
        if (draw_arrow)
        {
            S32 x = ll_round( pos_local.mV[VX] );
            S32 y = ll_round( pos_local.mV[VY] );
            LLWorldMapView::drawTrackingCircle( getRect(), x, y, color, 1, 10 );
            LLWorldMapView::drawTrackingArrow( getRect(), x, y, color );
        }
    }
    else
    {
        LLWorldMapView::drawTrackingDot(pos_local.mV[VX],
                                        pos_local.mV[VY],
                                        color,
                                        pos_local.mV[VZ]);
    }
}

bool LLNetMap::isMouseOnPopupMenu()
{
    auto menu = static_cast<LLMenuGL*>(mPopupMenuHandle.get());
    if (!menu || !menu->isOpen())
    {
        return false;
    }

    S32 popup_x;
    S32 popup_y;
    LLUI::getInstance()->getMousePositionLocal(menu, &popup_x, &popup_y);
    // *NOTE: Tolerance is larger than it needs to be because the context menu is offset from the mouse when the menu is opened from certain
    // directions. This may be a quirk of LLMenuGL::showPopup. -Cosmic,2022-03-22
    constexpr S32 tolerance = 10;
    // Test tolerance from all four corners, as the popup menu can appear from a different direction if there's not enough space.
    // Assume the size of the popup menu is much larger than the provided tolerance.
    // In practice, this is a [tolerance]px margin around the popup menu.
    for (S32 sign_x = -1; sign_x <= 1; sign_x += 2)
    {
        for (S32 sign_y = -1; sign_y <= 1; sign_y += 2)
        {
            if (menu->pointInView(popup_x + (sign_x * tolerance), popup_y + (sign_y * tolerance)))
            {
                return true;
            }
        }
    }
    return false;
}

void LLNetMap::updateAboutLandPopupButton()
{
    auto menu = static_cast<LLMenuGL*>(mPopupMenuHandle.get());
    if (!menu || !menu->isOpen())
    {
        return;
    }

    LLViewerRegion *region = LLWorld::getInstance()->getRegionFromPosGlobal(mPopupWorldPos);
    if (!region)
    {
        menu->setItemEnabled("About Land", false);
    }
    else
    {
        // Check if the mouse is in the bounds of the popup. If so, it's safe to assume no other hover function will be called, so the hover
        // parcel can be used to check if location-sensitive tooltip options are available.
        if (isMouseOnPopupMenu())
        {
            LLViewerParcelMgr::getInstance()->setHoverParcel(mPopupWorldPos);
            LLParcel *hover_parcel = LLViewerParcelMgr::getInstance()->getHoverParcel();
            bool      valid_parcel = false;
            if (hover_parcel)
            {
                valid_parcel = hover_parcel->getOwnerID().notNull();
            }
            menu->setItemEnabled("About Land", valid_parcel);
        }
    }
}

LLVector3d LLNetMap::viewPosToGlobal( S32 x, S32 y )
{
    x -= ll_round(getRect().getWidth() / 2 + mCurPan.mV[VX]);
    y -= ll_round(getRect().getHeight() / 2 + mCurPan.mV[VY]);

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

    F32 radians = - atan2( LLViewerCamera::getInstance()->getAtAxis().mV[VX], LLViewerCamera::getInstance()->getAtAxis().mV[VY] );

    static LLUICachedControl<bool> rotate_map("MiniMapRotate", true);
    if( rotate_map )
    {
        LLQuaternion rot(radians, LLVector3(0.f, 0.f, 1.f));
        pos_local.rotVec( rot );
    }

    pos_local *= ( LLWorld::getInstance()->getRegionWidthInMeters() / mScale );

    LLVector3d pos_global;
    pos_global.setVec( pos_local );
    pos_global += gAgentCamera.getCameraPositionGlobal();

    return pos_global;
}

BOOL LLNetMap::handleScrollWheel(S32 x, S32 y, S32 clicks)
{
    // note that clicks are reversed from what you'd think: i.e. > 0  means zoom out, < 0 means zoom in
    F32 new_scale = mScale * pow(MAP_SCALE_ZOOM_FACTOR, -clicks);
    F32 old_scale = mScale;

    setScale(new_scale);

    static LLUICachedControl<bool> auto_center("MiniMapAutoCenter", true);
    if (!auto_center)
    {
        // Adjust pan to center the zoom on the mouse pointer
        LLVector2 zoom_offset;
        zoom_offset.mV[VX] = x - getRect().getWidth() / 2;
        zoom_offset.mV[VY] = y - getRect().getHeight() / 2;
        mCurPan -= zoom_offset * mScale / old_scale - zoom_offset;
    }

    return true;
}

BOOL LLNetMap::handleToolTip(S32 x, S32 y, MASK mask)
{
    if (gDisconnected)
    {
        return false;
    }

    // If the cursor is near an avatar on the minimap, a mini-inspector will be
    // shown for the avatar, instead of the normal map tooltip.
    if (handleToolTipAgent(mClosestAgentToCursor))
    {
        return true;
    }

    // The popup menu uses the hover parcel when it is open and the mouse is on
    // top of it, with some additional tolerance. Returning early here prevents
    // fighting over that hover parcel when getting tooltip info in the
    // tolerance region.
    if (isMouseOnPopupMenu())
    {
        return false;
    }

    LLRect sticky_rect;
    S32 SLOP = 4;
    localPointToScreen(x - SLOP, y - SLOP, &(sticky_rect.mLeft), &(sticky_rect.mBottom));
    sticky_rect.mRight = sticky_rect.mLeft + 2 * SLOP;
    sticky_rect.mTop   = sticky_rect.mBottom + 2 * SLOP;

    std::string parcel_name_msg;
    std::string parcel_sale_price_msg;
    std::string parcel_sale_area_msg;
    std::string parcel_owner_msg;
    std::string region_name_msg;

    LLVector3d      posGlobal = viewPosToGlobal(x, y);
    LLViewerRegion *region    = LLWorld::getInstance()->getRegionFromPosGlobal(posGlobal);
    if (region)
    {
        std::string region_name = region->getName();
        if (!region_name.empty())
        {
            region_name_msg = mRegionNameMsg;
            LLStringUtil::format(region_name_msg, {{"[REGION_NAME]", region_name}});
        }

        // Only show parcel information in the tooltip if property lines are visible. Otherwise, the parcel the tooltip is referring to is
        // ambiguous.
        if (gSavedSettings.getBOOL("MiniMapShowPropertyLines"))
        {
            LLViewerParcelMgr::getInstance()->setHoverParcel(posGlobal);
            LLParcel *hover_parcel = LLViewerParcelMgr::getInstance()->getHoverParcel();
            if (hover_parcel)
            {
                std::string parcel_name = hover_parcel->getName();
                if (!parcel_name.empty())
                {
                    parcel_name_msg = mParcelNameMsg;
                    LLStringUtil::format(parcel_name_msg, {{"[PARCEL_NAME]", parcel_name}});
                }

                const LLUUID      parcel_owner          = hover_parcel->getOwnerID();
                std::string       parcel_owner_name_url = LLSLURL("agent", parcel_owner, "inspect").getSLURLString();
                static LLUrlMatch parcel_owner_name_url_match;
                LLUrlRegistry::getInstance()->findUrl(parcel_owner_name_url, parcel_owner_name_url_match);
                if (!parcel_owner_name_url_match.empty())
                {
                    parcel_owner_msg              = mParcelOwnerMsg;
                    std::string parcel_owner_name = parcel_owner_name_url_match.getLabel();
                    LLStringUtil::format(parcel_owner_msg, {{"[PARCEL_OWNER]", parcel_owner_name}});
                }

                if (hover_parcel->getForSale())
                {
                    const LLUUID auth_buyer_id = hover_parcel->getAuthorizedBuyerID();
                    const LLUUID agent_id      = gAgent.getID();
                    bool         show_for_sale = auth_buyer_id.isNull() || auth_buyer_id == agent_id || parcel_owner == agent_id;
                    if (show_for_sale)
                    {
                        S32 price        = hover_parcel->getSalePrice();
                        S32 area         = hover_parcel->getArea();
                        F32 cost_per_sqm = 0.0f;
                        if (area > 0)
                        {
                            cost_per_sqm = F32(price) / area;
                        }
                        std::string formatted_price          = LLResMgr::getInstance()->getMonetaryString(price);
                        std::string formatted_cost_per_meter = llformat("%.1f", cost_per_sqm);
                        parcel_sale_price_msg                = mParcelSalePriceMsg;
                        LLStringUtil::format(parcel_sale_price_msg,
                                             {{"[PRICE]", formatted_price}, {"[PRICE_PER_SQM]", formatted_cost_per_meter}});
                        std::string formatted_area = llformat("%d", area);
                        parcel_sale_area_msg       = mParcelSaleAreaMsg;
                        LLStringUtil::format(parcel_sale_area_msg, {{"[AREA]", formatted_area}});
                    }
                }
            }
        }
    }

    std::string tool_tip_hint_msg;
    if (gSavedSettings.getBOOL("DoubleClickTeleport"))
    {
        tool_tip_hint_msg = mAltToolTipHintMsg;
    }
    else if (gSavedSettings.getBOOL("DoubleClickShowWorldMap"))
    {
        tool_tip_hint_msg = mToolTipHintMsg;
    }

    LLStringUtil::format_map_t args;
    args["[PARCEL_NAME_MSG]"]       = parcel_name_msg.empty() ? "" : parcel_name_msg + '\n';
    args["[PARCEL_SALE_PRICE_MSG]"] = parcel_sale_price_msg.empty() ? "" : parcel_sale_price_msg + '\n';
    args["[PARCEL_SALE_AREA_MSG]"]  = parcel_sale_area_msg.empty() ? "" : parcel_sale_area_msg + '\n';
    args["[PARCEL_OWNER_MSG]"]      = parcel_owner_msg.empty() ? "" : parcel_owner_msg + '\n';
    args["[REGION_NAME_MSG]"]       = region_name_msg.empty() ? "" : region_name_msg + '\n';
    args["[TOOL_TIP_HINT_MSG]"]     = tool_tip_hint_msg.empty() ? "" : tool_tip_hint_msg + '\n';

    std::string msg                 = mToolTipMsg;
    LLStringUtil::format(msg, args);
    if (msg.back() == '\n')
    {
        msg.resize(msg.size() - 1);
    }
    LLToolTipMgr::instance().show(LLToolTip::Params().message(msg).sticky_rect(sticky_rect));

    return true;
}

BOOL LLNetMap::handleToolTipAgent(const LLUUID& avatar_id)
{
    LLAvatarName av_name;
    if (avatar_id.isNull() || !LLAvatarNameCache::get(avatar_id, &av_name))
    {
        return FALSE;
    }

    // only show tooltip if same inspector not already open
    LLFloater* existing_inspector = LLFloaterReg::findInstance("inspect_avatar");
    if (!existing_inspector
        || !existing_inspector->getVisible()
        || existing_inspector->getKey()["avatar_id"].asUUID() != avatar_id)
    {
        LLInspector::Params p;
        p.fillFrom(LLUICtrlFactory::instance().getDefaultParams<LLInspector>());
        p.message(av_name.getCompleteName());
        p.image.name("Inspector_I");
        p.click_callback(boost::bind(showAvatarInspector, avatar_id));
        p.visible_time_near(6.f);
        p.visible_time_far(3.f);
        p.delay_time(0.35f);
        p.wrap(false);

        LLToolTipMgr::instance().show(p);
    }
    return TRUE;
}

// static
void LLNetMap::showAvatarInspector(const LLUUID& avatar_id)
{
    LLSD params;
    params["avatar_id"] = avatar_id;

    if (LLToolTipMgr::instance().toolTipVisible())
    {
        LLRect rect = LLToolTipMgr::instance().getToolTipRect();
        params["pos"]["x"] = rect.mLeft;
        params["pos"]["y"] = rect.mTop;
    }

    LLFloaterReg::showInstance("inspect_avatar", params);
}

void LLNetMap::renderScaledPointGlobal( const LLVector3d& pos, const LLColor4U &color, F32 radius_meters )
{
    LLVector3 local_pos;
    local_pos.setVec( pos - mObjectImageCenterGlobal );

    S32 diameter_pixels = ll_round(2 * radius_meters * mObjectMapTPM);
    renderPoint( local_pos, color, diameter_pixels );
}


void LLNetMap::renderPoint(const LLVector3 &pos_local, const LLColor4U &color,
                           S32 diameter, S32 relative_height)
{
    if (diameter <= 0)
    {
        return;
    }

    const S32 image_width = (S32)mObjectImagep->getWidth();
    const S32 image_height = (S32)mObjectImagep->getHeight();

    S32 x_offset = ll_round(pos_local.mV[VX] * mObjectMapTPM + image_width / 2);
    S32 y_offset = ll_round(pos_local.mV[VY] * mObjectMapTPM + image_height / 2);

    if ((x_offset < 0) || (x_offset >= image_width))
    {
        return;
    }
    if ((y_offset < 0) || (y_offset >= image_height))
    {
        return;
    }

    U8 *datap = mObjectRawImagep->getData();

    S32 neg_radius = diameter / 2;
    S32 pos_radius = diameter - neg_radius;
    S32 x, y;

    if (relative_height > 0)
    {
        // ...point above agent
        S32 px, py;

        // vertical line
        px = x_offset;
        for (y = -neg_radius; y < pos_radius; y++)
        {
            py = y_offset + y;
            if ((py < 0) || (py >= image_height))
            {
                continue;
            }
            S32 offset = px + py * image_width;
            ((U32*)datap)[offset] = color.asRGBA();
        }

        // top line
        py = y_offset + pos_radius - 1;
        for (x = -neg_radius; x < pos_radius; x++)
        {
            px = x_offset + x;
            if ((px < 0) || (px >= image_width))
            {
                continue;
            }
            S32 offset = px + py * image_width;
            ((U32*)datap)[offset] = color.asRGBA();
        }
    }
    else
    {
        // ...point level with agent
        for (x = -neg_radius; x < pos_radius; x++)
        {
            S32 p_x = x_offset + x;
            if ((p_x < 0) || (p_x >= image_width))
            {
                continue;
            }

            for (y = -neg_radius; y < pos_radius; y++)
            {
                S32 p_y = y_offset + y;
                if ((p_y < 0) || (p_y >= image_height))
                {
                    continue;
                }
                S32 offset = p_x + p_y * image_width;
                ((U32*)datap)[offset] = color.asRGBA();
            }
        }
    }
}

void LLNetMap::createObjectImage()
{
    // Find the size of the side of a square that surrounds the circle that surrounds getRect().
    // ... which is, the diagonal of the rect.
    F32 width = (F32)getRect().getWidth();
    F32 height = (F32)getRect().getHeight();
    S32 square_size = ll_round( sqrt(width*width + height*height) );

    // Find the least power of two >= the minimum size.
    const S32 MIN_SIZE = 64;
    const S32 MAX_SIZE = 256;
    S32 img_size = MIN_SIZE;
    while( (img_size*2 < square_size ) && (img_size < MAX_SIZE) )
    {
        img_size <<= 1;
    }

    if( mObjectImagep.isNull() ||
        (mObjectImagep->getWidth() != img_size) ||
        (mObjectImagep->getHeight() != img_size) )
    {
        mObjectRawImagep = new LLImageRaw(img_size, img_size, 4);
        U8* data = mObjectRawImagep->getData();
        memset( data, 0, img_size * img_size * 4 );
        mObjectImagep = LLViewerTextureManager::getLocalTexture( mObjectRawImagep.get(), FALSE);
    }
    setScale(mScale);
    mUpdateNow = true;
}

BOOL LLNetMap::handleMouseDown(S32 x, S32 y, MASK mask)
{
    // Start panning
    gFocusMgr.setMouseCapture(this);

    mStartPan     = mCurPan;
    mMouseDown.mX = x;
    mMouseDown.mY = y;
    return true;
}

BOOL LLNetMap::handleMouseUp(S32 x, S32 y, MASK mask)
{
    if (abs(mMouseDown.mX - x) < 3 && abs(mMouseDown.mY - y) < 3)
    {
        handleClick(x, y, mask);
    }

    if (hasMouseCapture())
    {
        if (mPanning)
        {
            // restore mouse cursor
            S32 local_x, local_y;
            local_x          = mMouseDown.mX + llfloor(mCurPan.mV[VX] - mStartPan.mV[VX]);
            local_y          = mMouseDown.mY + llfloor(mCurPan.mV[VY] - mStartPan.mV[VY]);
            LLRect clip_rect = getRect();
            clip_rect.stretch(-8);
            clip_rect.clipPointToRect(mMouseDown.mX, mMouseDown.mY, local_x, local_y);
            LLUI::getInstance()->setMousePositionLocal(this, local_x, local_y);

            // finish the pan
            mPanning = false;

            mMouseDown.set(0, 0);
        }
        gViewerWindow->showCursor();
        gFocusMgr.setMouseCapture(NULL);
        return true;
    }

    return false;
}

BOOL LLNetMap::handleRightMouseDown(S32 x, S32 y, MASK mask)
{
    auto menu = static_cast<LLMenuGL*>(mPopupMenuHandle.get());
    if (menu)
    {
        mPopupWorldPos = viewPosToGlobal(x, y);
        menu->buildDrawLabels();
        menu->updateParent(LLMenuGL::sMenuContainer);
        menu->setItemEnabled("Stop Tracking", LLTracker::isTracking(0));
        LLMenuGL::showPopup(this, menu, x, y);
    }
    return TRUE;
}

BOOL LLNetMap::handleClick(S32 x, S32 y, MASK mask)
{
    // TODO: allow clicking an avatar on minimap to select avatar in the nearby avatar list
    // if(mClosestAgentToCursor.notNull())
    //     mNearbyList->selectUser(mClosestAgentToCursor);
    // Needs a registered observer i guess to accomplish this without using
    // globals to tell the mNearbyList in llpeoplepanel to select the user
    return TRUE;
}

BOOL LLNetMap::handleDoubleClick(S32 x, S32 y, MASK mask)
{
    LLVector3d pos_global = viewPosToGlobal(x, y);

    bool double_click_teleport = gSavedSettings.getBOOL("DoubleClickTeleport");
    bool double_click_show_world_map = gSavedSettings.getBOOL("DoubleClickShowWorldMap");

    if (double_click_teleport || double_click_show_world_map)
    {
        // If we're not tracking a beacon already, double-click will set one
        if (!LLTracker::isTracking(NULL))
        {
            LLFloaterWorldMap* world_map = LLFloaterWorldMap::getInstance();
            if (world_map)
            {
                world_map->trackLocation(pos_global);
            }
        }
    }

    if (double_click_teleport)
    {
        // If DoubleClickTeleport is on, double clicking the minimap will teleport there
        gAgent.teleportViaLocationLookAt(pos_global);
    }
    else if (double_click_show_world_map)
    {
        LLFloaterReg::showInstance("world_map");
    }
    return TRUE;
}

F32 LLNetMap::getScaleForName(std::string scale_name)
{
    if (scale_name == "very close")
    {
        return LLNetMap::MAP_SCALE_VERY_CLOSE;
    }
    else if (scale_name == "close")
    {
        return LLNetMap::MAP_SCALE_CLOSE;
    }
    else if (scale_name == "medium")
    {
        return LLNetMap::MAP_SCALE_MEDIUM;
    }
    else if (scale_name == "far")
    {
        return LLNetMap::MAP_SCALE_FAR;
    }
    return 0.0f;
}

// static
bool LLNetMap::outsideSlop( S32 x, S32 y, S32 start_x, S32 start_y, S32 slop )
{
    S32 dx = x - start_x;
    S32 dy = y - start_y;

    return (dx <= -slop || slop <= dx || dy <= -slop || slop <= dy);
}

BOOL LLNetMap::handleHover( S32 x, S32 y, MASK mask )
{
    if (hasMouseCapture())
    {
        if (mPanning || outsideSlop(x, y, mMouseDown.mX, mMouseDown.mY, MOUSE_DRAG_SLOP))
        {
            if (!mPanning)
            {
                // Just started panning. Hide cursor.
                mPanning = true;
                gViewerWindow->hideCursor();
            }

            LLVector2 delta(static_cast<F32>(gViewerWindow->getCurrentMouseDX()),
                            static_cast<F32>(gViewerWindow->getCurrentMouseDY()));

            // Set pan to value at start of drag + offset
            mCurPan += delta;

            gViewerWindow->moveCursorToCenter();
        }
    }

    if (mask & MASK_SHIFT)
    {
        // If shift is held, change the cursor to hint that the map can be
        // dragged. However, holding shift is not required to drag the map.
        gViewerWindow->setCursor( UI_CURSOR_TOOLPAN );
    }
    else
    {
        gViewerWindow->setCursor( UI_CURSOR_CROSS );
    }

    return TRUE;
}

bool LLNetMap::isZoomChecked(const LLSD &userdata)
{
    std::string level = userdata.asString();
    F32         scale = getScaleForName(level);
    return scale == mScale;
}

void LLNetMap::setZoom(const LLSD &userdata)
{
    std::string level = userdata.asString();
    F32         scale = getScaleForName(level);
    if (scale != 0.0f)
    {
        setScale(scale);
    }
}

void LLNetMap::handleStopTracking (const LLSD& userdata)
{
    auto menu = static_cast<LLMenuGL*>(mPopupMenuHandle.get());
    if (menu)
    {
        menu->setItemEnabled ("Stop Tracking", false);
        LLTracker::stopTracking (LLTracker::isTracking(NULL));
    }
}

void LLNetMap::activateCenterMap(const LLSD &userdata) { mCentering = true; }

bool LLNetMap::isMapOrientationChecked(const LLSD &userdata)
{
    const std::string command_name = userdata.asString();
    const bool        rotate_map   = gSavedSettings.getBOOL("MiniMapRotate");
    if (command_name == "north_at_top")
    {
        return !rotate_map;
    }

    if (command_name == "camera_at_top")
    {
        return rotate_map;
    }

    return false;
}

void LLNetMap::setMapOrientation(const LLSD &userdata)
{
    const std::string command_name = userdata.asString();
    if (command_name == "north_at_top")
    {
        gSavedSettings.setBOOL("MiniMapRotate", false);
    }
    else if (command_name == "camera_at_top")
    {
        gSavedSettings.setBOOL("MiniMapRotate", true);
    }
}

void LLNetMap::popupShowAboutLand(const LLSD &userdata)
{
    // Update parcel selection. It's important to deselect land first so the "About Land" floater doesn't refresh with the old selection.
    LLViewerParcelMgr::getInstance()->deselectLand();
    LLParcelSelectionHandle selection = LLViewerParcelMgr::getInstance()->selectParcelAt(mPopupWorldPos);
    gMenuHolder->setParcelSelection(selection);

    LLFloaterReg::showInstance("about_land", LLSD(), false);
}