diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index ef95a8a3..245da98b 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -30,7 +30,7 @@ jobs:
             msvc_version: 2022
             qt_version: 6.6.0
             qt_arch: win64_msvc2019_64
-            qt_modules: qtimageformats
+            qt_modules: qtimageformats qtpositioning
             qt_tools: ''
             conan_arch: x86_64
             conan_compiler: Visual Studio
@@ -46,7 +46,7 @@ jobs:
             compiler: gcc
             qt_version: 6.6.0
             qt_arch: gcc_64
-            qt_modules: qtimageformats
+            qt_modules: qtimageformats qtpositioning
             qt_tools: ''
             conan_arch: x86_64
             conan_compiler: gcc
diff --git a/scwx-qt/res/textures/images/crosshairs-24.png b/scwx-qt/res/textures/images/crosshairs-24.png
new file mode 100644
index 00000000..f2a7ae2d
Binary files /dev/null and b/scwx-qt/res/textures/images/crosshairs-24.png differ
diff --git a/scwx-qt/res/textures/images/crosshairs-32.png b/scwx-qt/res/textures/images/crosshairs-32.png
new file mode 100644
index 00000000..6163b3ae
Binary files /dev/null and b/scwx-qt/res/textures/images/crosshairs-32.png differ
diff --git a/scwx-qt/scwx-qt.cmake b/scwx-qt/scwx-qt.cmake
index 650d94f7..6e38717e 100644
--- a/scwx-qt/scwx-qt.cmake
+++ b/scwx-qt/scwx-qt.cmake
@@ -25,6 +25,7 @@ find_package(QT NAMES Qt6
                         Network
                         OpenGL
                         OpenGLWidgets
+                        Positioning
                         Widgets REQUIRED)
 
 find_package(Qt${QT_VERSION_MAJOR}
@@ -33,6 +34,7 @@ find_package(Qt${QT_VERSION_MAJOR}
                         Network
                         OpenGL
                         OpenGLWidgets
+                        Positioning
                         Widgets
              REQUIRED)
 
@@ -55,6 +57,7 @@ set(HDR_GL source/scwx/qt/gl/gl.hpp
 set(SRC_GL source/scwx/qt/gl/gl_context.cpp
            source/scwx/qt/gl/shader_program.cpp)
 set(HDR_GL_DRAW source/scwx/qt/gl/draw/draw_item.hpp
+                source/scwx/qt/gl/draw/geo_icons.hpp
                 source/scwx/qt/gl/draw/geo_line.hpp
                 source/scwx/qt/gl/draw/placefile_icons.hpp
                 source/scwx/qt/gl/draw/placefile_images.hpp
@@ -64,6 +67,7 @@ set(HDR_GL_DRAW source/scwx/qt/gl/draw/draw_item.hpp
                 source/scwx/qt/gl/draw/placefile_triangles.hpp
                 source/scwx/qt/gl/draw/rectangle.hpp)
 set(SRC_GL_DRAW source/scwx/qt/gl/draw/draw_item.cpp
+                source/scwx/qt/gl/draw/geo_icons.cpp
                 source/scwx/qt/gl/draw/geo_line.cpp
                 source/scwx/qt/gl/draw/placefile_icons.cpp
                 source/scwx/qt/gl/draw/placefile_images.cpp
@@ -74,6 +78,7 @@ set(SRC_GL_DRAW source/scwx/qt/gl/draw/draw_item.cpp
                 source/scwx/qt/gl/draw/rectangle.cpp)
 set(HDR_MANAGER source/scwx/qt/manager/font_manager.hpp
                 source/scwx/qt/manager/placefile_manager.hpp
+                source/scwx/qt/manager/position_manager.hpp
                 source/scwx/qt/manager/radar_product_manager.hpp
                 source/scwx/qt/manager/radar_product_manager_notifier.hpp
                 source/scwx/qt/manager/resource_manager.hpp
@@ -83,6 +88,7 @@ set(HDR_MANAGER source/scwx/qt/manager/font_manager.hpp
                 source/scwx/qt/manager/update_manager.hpp)
 set(SRC_MANAGER source/scwx/qt/manager/font_manager.cpp
                 source/scwx/qt/manager/placefile_manager.cpp
+                source/scwx/qt/manager/position_manager.cpp
                 source/scwx/qt/manager/radar_product_manager.cpp
                 source/scwx/qt/manager/radar_product_manager_notifier.cpp
                 source/scwx/qt/manager/resource_manager.cpp
@@ -166,7 +172,8 @@ set(HDR_TYPES source/scwx/qt/types/alert_types.hpp
               source/scwx/qt/types/qt_types.hpp
               source/scwx/qt/types/radar_product_record.hpp
               source/scwx/qt/types/text_event_key.hpp
-              source/scwx/qt/types/text_types.hpp)
+              source/scwx/qt/types/text_types.hpp
+              source/scwx/qt/types/texture_types.hpp)
 set(SRC_TYPES source/scwx/qt/types/alert_types.cpp
               source/scwx/qt/types/github_types.cpp
               source/scwx/qt/types/imgui_font.cpp
@@ -174,7 +181,8 @@ set(SRC_TYPES source/scwx/qt/types/alert_types.cpp
               source/scwx/qt/types/map_types.cpp
               source/scwx/qt/types/radar_product_record.cpp
               source/scwx/qt/types/text_event_key.cpp
-              source/scwx/qt/types/text_types.cpp)
+              source/scwx/qt/types/text_types.cpp
+              source/scwx/qt/types/texture_types.cpp)
 set(HDR_UI source/scwx/qt/ui/about_dialog.hpp
            source/scwx/qt/ui/alert_dialog.hpp
            source/scwx/qt/ui/alert_dock_widget.hpp
@@ -503,6 +511,7 @@ endif()
 
 target_link_libraries(scwx-qt PUBLIC Qt${QT_VERSION_MAJOR}::Widgets
                                      Qt${QT_VERSION_MAJOR}::OpenGLWidgets
+                                     Qt${QT_VERSION_MAJOR}::Positioning
                                      Boost::json
                                      Boost::timer
                                      qmaplibregl
diff --git a/scwx-qt/scwx-qt.qrc b/scwx-qt/scwx-qt.qrc
index 298ef828..3e7e55dd 100644
--- a/scwx-qt/scwx-qt.qrc
+++ b/scwx-qt/scwx-qt.qrc
@@ -59,5 +59,6 @@
         res/palettes/wct/ZDR.pal
         res/textures/lines/default-1x7.png
         res/textures/lines/test-pattern.png
+        res/textures/images/crosshairs-24.png
     
 
diff --git a/scwx-qt/source/scwx/qt/gl/draw/geo_icons.cpp b/scwx-qt/source/scwx/qt/gl/draw/geo_icons.cpp
new file mode 100644
index 00000000..fb5dad75
--- /dev/null
+++ b/scwx-qt/source/scwx/qt/gl/draw/geo_icons.cpp
@@ -0,0 +1,894 @@
+#include 
+#include 
+#include 
+#include 
+#include 
+
+#include 
+
+#include 
+
+namespace scwx
+{
+namespace qt
+{
+namespace gl
+{
+namespace draw
+{
+
+static const std::string logPrefix_ = "scwx::qt::gl::draw::geo_icons";
+static const auto        logger_    = scwx::util::Logger::Create(logPrefix_);
+
+static constexpr std::size_t kNumRectangles        = 1;
+static constexpr std::size_t kNumTriangles         = kNumRectangles * 2;
+static constexpr std::size_t kVerticesPerTriangle  = 3;
+static constexpr std::size_t kVerticesPerRectangle = kVerticesPerTriangle * 2;
+static constexpr std::size_t kPointsPerVertex      = 9;
+static constexpr std::size_t kPointsPerTexCoord    = 3;
+static constexpr std::size_t kIconBufferLength =
+   kNumTriangles * kVerticesPerTriangle * kPointsPerVertex;
+static constexpr std::size_t kTextureBufferLength =
+   kNumTriangles * kVerticesPerTriangle * kPointsPerTexCoord;
+
+// Threshold, start time, end time
+static constexpr std::size_t kIntegersPerVertex_ = 3;
+
+struct IconInfo
+{
+   IconInfo(const std::string& iconSheet,
+            std::size_t        iconWidth,
+            std::size_t        iconHeight,
+            std::int32_t       hotX,
+            std::int32_t       hotY) :
+       iconSheet_ {iconSheet},
+       iconWidth_ {iconWidth},
+       iconHeight_ {iconHeight},
+       hotX_ {hotX},
+       hotY_ {hotY}
+   {
+   }
+
+   void UpdateTextureInfo();
+
+   std::string             iconSheet_;
+   std::size_t             iconWidth_;
+   std::size_t             iconHeight_;
+   std::int32_t            hotX_;
+   std::int32_t            hotY_;
+   util::TextureAttributes texture_ {};
+   std::size_t             rows_ {};
+   std::size_t             columns_ {};
+   std::size_t             numIcons_ {};
+   float                   scaledWidth_ {};
+   float                   scaledHeight_ {};
+};
+
+struct GeoIconDrawItem
+{
+   units::length::nautical_miles       threshold_ {};
+   std::chrono::sys_time startTime_ {};
+   std::chrono::sys_time endTime_ {};
+
+   boost::gil::rgba8_pixel_t modulate_ {255, 255, 255, 255};
+   double                    latitude_ {};
+   double                    longitude_ {};
+   double                    x_ {};
+   double                    y_ {};
+   units::degrees    angle_ {};
+   std::string               iconSheet_ {};
+   std::size_t               iconIndex_ {};
+   std::string               hoverText_ {};
+};
+
+class GeoIcons::Impl
+{
+public:
+   struct IconHoverEntry
+   {
+      std::shared_ptr di_;
+
+      glm::vec2 p_;
+      glm::vec2 otl_;
+      glm::vec2 otr_;
+      glm::vec2 obl_;
+      glm::vec2 obr_;
+   };
+
+   explicit Impl(const std::shared_ptr& context) :
+       context_ {context},
+       shaderProgram_ {nullptr},
+       uMVPMatrixLocation_(GL_INVALID_INDEX),
+       uMapMatrixLocation_(GL_INVALID_INDEX),
+       uMapScreenCoordLocation_(GL_INVALID_INDEX),
+       uMapDistanceLocation_(GL_INVALID_INDEX),
+       uSelectedTimeLocation_(GL_INVALID_INDEX),
+       vao_ {GL_INVALID_INDEX},
+       vbo_ {GL_INVALID_INDEX},
+       numVertices_ {0}
+   {
+   }
+
+   ~Impl() {}
+
+   void UpdateBuffers();
+   void UpdateTextureBuffer();
+   void Update(bool textureAtlasChanged);
+
+   std::shared_ptr context_;
+
+   bool visible_ {true};
+   bool dirty_ {false};
+   bool thresholded_ {false};
+   bool lastTextureAtlasChanged_ {false};
+
+   std::chrono::system_clock::time_point selectedTime_ {};
+
+   std::mutex iconMutex_;
+
+   boost::unordered_flat_map currentIconSheets_ {};
+   boost::unordered_flat_map newIconSheets_ {};
+
+   std::vector> currentIconList_ {};
+   std::vector> newIconList_ {};
+   std::vector> newValidIconList_ {};
+
+   std::vector currentIconBuffer_ {};
+   std::vector currentIntegerBuffer_ {};
+   std::vector newIconBuffer_ {};
+   std::vector newIntegerBuffer_ {};
+
+   std::vector textureBuffer_ {};
+
+   std::vector currentHoverIcons_ {};
+   std::vector newHoverIcons_ {};
+
+   std::shared_ptr shaderProgram_;
+   GLint                          uMVPMatrixLocation_;
+   GLint                          uMapMatrixLocation_;
+   GLint                          uMapScreenCoordLocation_;
+   GLint                          uMapDistanceLocation_;
+   GLint                          uSelectedTimeLocation_;
+
+   GLuint                vao_;
+   std::array vbo_;
+
+   GLsizei numVertices_;
+};
+
+GeoIcons::GeoIcons(const std::shared_ptr& context) :
+    DrawItem(context->gl()), p(std::make_unique(context))
+{
+}
+GeoIcons::~GeoIcons() = default;
+
+GeoIcons::GeoIcons(GeoIcons&&) noexcept            = default;
+GeoIcons& GeoIcons::operator=(GeoIcons&&) noexcept = default;
+
+void GeoIcons::set_selected_time(
+   std::chrono::system_clock::time_point selectedTime)
+{
+   p->selectedTime_ = selectedTime;
+}
+
+void GeoIcons::set_thresholded(bool thresholded)
+{
+   p->thresholded_ = thresholded;
+}
+
+void GeoIcons::Initialize()
+{
+   gl::OpenGLFunctions& gl = p->context_->gl();
+
+   p->shaderProgram_ = p->context_->GetShaderProgram(
+      {{GL_VERTEX_SHADER, ":/gl/geo_texture2d.vert"},
+       {GL_GEOMETRY_SHADER, ":/gl/threshold.geom"},
+       {GL_FRAGMENT_SHADER, ":/gl/texture2d_array.frag"}});
+
+   p->uMVPMatrixLocation_ = p->shaderProgram_->GetUniformLocation("uMVPMatrix");
+   p->uMapMatrixLocation_ = p->shaderProgram_->GetUniformLocation("uMapMatrix");
+   p->uMapScreenCoordLocation_ =
+      p->shaderProgram_->GetUniformLocation("uMapScreenCoord");
+   p->uMapDistanceLocation_ =
+      p->shaderProgram_->GetUniformLocation("uMapDistance");
+   p->uSelectedTimeLocation_ =
+      p->shaderProgram_->GetUniformLocation("uSelectedTime");
+
+   gl.glGenVertexArrays(1, &p->vao_);
+   gl.glGenBuffers(static_cast(p->vbo_.size()), p->vbo_.data());
+
+   gl.glBindVertexArray(p->vao_);
+   gl.glBindBuffer(GL_ARRAY_BUFFER, p->vbo_[0]);
+   gl.glBufferData(GL_ARRAY_BUFFER, 0u, nullptr, GL_DYNAMIC_DRAW);
+
+   // aLatLong
+   gl.glVertexAttribPointer(0,
+                            2,
+                            GL_FLOAT,
+                            GL_FALSE,
+                            kPointsPerVertex * sizeof(float),
+                            static_cast(0));
+   gl.glEnableVertexAttribArray(0);
+
+   // aXYOffset
+   gl.glVertexAttribPointer(1,
+                            2,
+                            GL_FLOAT,
+                            GL_FALSE,
+                            kPointsPerVertex * sizeof(float),
+                            reinterpret_cast(2 * sizeof(float)));
+   gl.glEnableVertexAttribArray(1);
+
+   // aModulate
+   gl.glVertexAttribPointer(3,
+                            4,
+                            GL_FLOAT,
+                            GL_FALSE,
+                            kPointsPerVertex * sizeof(float),
+                            reinterpret_cast(4 * sizeof(float)));
+   gl.glEnableVertexAttribArray(3);
+
+   // aAngle
+   gl.glVertexAttribPointer(4,
+                            1,
+                            GL_FLOAT,
+                            GL_FALSE,
+                            kPointsPerVertex * sizeof(float),
+                            reinterpret_cast(8 * sizeof(float)));
+   gl.glEnableVertexAttribArray(4);
+
+   gl.glBindBuffer(GL_ARRAY_BUFFER, p->vbo_[1]);
+   gl.glBufferData(GL_ARRAY_BUFFER, 0u, nullptr, GL_DYNAMIC_DRAW);
+
+   // aTexCoord
+   gl.glVertexAttribPointer(2,
+                            3,
+                            GL_FLOAT,
+                            GL_FALSE,
+                            kPointsPerTexCoord * sizeof(float),
+                            static_cast(0));
+   gl.glEnableVertexAttribArray(2);
+
+   gl.glBindBuffer(GL_ARRAY_BUFFER, p->vbo_[2]);
+   gl.glBufferData(GL_ARRAY_BUFFER, 0u, nullptr, GL_DYNAMIC_DRAW);
+
+   // aThreshold
+   gl.glVertexAttribIPointer(5, //
+                             1,
+                             GL_INT,
+                             0,
+                             static_cast(0));
+   gl.glEnableVertexAttribArray(5);
+
+   // aTimeRange
+   gl.glVertexAttribIPointer(6, //
+                             2,
+                             GL_INT,
+                             kIntegersPerVertex_ * sizeof(GLint),
+                             reinterpret_cast(1 * sizeof(GLint)));
+   gl.glEnableVertexAttribArray(6);
+
+   p->dirty_ = true;
+}
+
+void GeoIcons::Render(const QMapLibreGL::CustomLayerRenderParameters& params,
+                      bool textureAtlasChanged)
+{
+   if (!p->visible_)
+   {
+      if (textureAtlasChanged)
+      {
+         p->lastTextureAtlasChanged_ = true;
+      }
+
+      return;
+   }
+
+   std::unique_lock lock {p->iconMutex_};
+
+   if (!p->currentIconList_.empty())
+   {
+      gl::OpenGLFunctions& gl = p->context_->gl();
+
+      gl.glBindVertexArray(p->vao_);
+
+      p->Update(textureAtlasChanged);
+      p->shaderProgram_->Use();
+      UseRotationProjection(params, p->uMVPMatrixLocation_);
+      UseMapProjection(
+         params, p->uMapMatrixLocation_, p->uMapScreenCoordLocation_);
+
+      if (p->thresholded_)
+      {
+         // If thresholding is enabled, set the map distance
+         units::length::nautical_miles mapDistance =
+            util::maplibre::GetMapDistance(params);
+         gl.glUniform1f(p->uMapDistanceLocation_, mapDistance.value());
+      }
+      else
+      {
+         // If thresholding is disabled, set the map distance to 0
+         gl.glUniform1f(p->uMapDistanceLocation_, 0.0f);
+      }
+
+      // Selected time
+      std::chrono::system_clock::time_point selectedTime =
+         (p->selectedTime_ == std::chrono::system_clock::time_point {}) ?
+            std::chrono::system_clock::now() :
+            p->selectedTime_;
+      gl.glUniform1i(
+         p->uSelectedTimeLocation_,
+         static_cast(std::chrono::duration_cast(
+                               selectedTime.time_since_epoch())
+                               .count()));
+
+      // Interpolate texture coordinates
+      gl.glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
+      gl.glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
+
+      // Draw icons
+      gl.glDrawArrays(GL_TRIANGLES, 0, p->numVertices_);
+   }
+}
+
+void GeoIcons::Deinitialize()
+{
+   gl::OpenGLFunctions& gl = p->context_->gl();
+
+   gl.glDeleteVertexArrays(1, &p->vao_);
+   gl.glDeleteBuffers(static_cast(p->vbo_.size()), p->vbo_.data());
+
+   std::unique_lock lock {p->iconMutex_};
+
+   p->currentIconList_.clear();
+   p->currentIconSheets_.clear();
+   p->currentHoverIcons_.clear();
+   p->currentIconBuffer_.clear();
+   p->currentIntegerBuffer_.clear();
+   p->textureBuffer_.clear();
+}
+
+void IconInfo::UpdateTextureInfo()
+{
+   texture_ = util::TextureAtlas::Instance().GetTextureAttributes(iconSheet_);
+
+   if (iconWidth_ > 0 && iconHeight_ > 0)
+   {
+      columns_ = texture_.size_.x / iconWidth_;
+      rows_    = texture_.size_.y / iconHeight_;
+   }
+   else
+   {
+      columns_ = 1u;
+      rows_    = 1u;
+
+      iconWidth_  = static_cast(texture_.size_.x);
+      iconHeight_ = static_cast(texture_.size_.y);
+   }
+
+   if (hotX_ == -1 || hotY_ == -1)
+   {
+      hotX_ = static_cast(iconWidth_ / 2);
+      hotY_ = static_cast(iconHeight_ / 2);
+   }
+
+   numIcons_ = columns_ * rows_;
+
+   // Pixel size
+   float xFactor = 0.0f;
+   float yFactor = 0.0f;
+
+   if (texture_.size_.x > 0 && texture_.size_.y > 0)
+   {
+      xFactor = (texture_.sRight_ - texture_.sLeft_) / texture_.size_.x;
+      yFactor = (texture_.tBottom_ - texture_.tTop_) / texture_.size_.y;
+   }
+
+   scaledWidth_  = iconWidth_ * xFactor;
+   scaledHeight_ = iconHeight_ * yFactor;
+}
+
+void GeoIcons::SetVisible(bool visible)
+{
+   p->visible_ = visible;
+}
+
+void GeoIcons::StartIconSheets()
+{
+   // Clear the new buffer
+   p->newIconSheets_.clear();
+}
+
+void GeoIcons::AddIconSheet(const std::string& name,
+                            std::size_t        iconWidth,
+                            std::size_t        iconHeight,
+                            std::int32_t       hotX,
+                            std::int32_t       hotY)
+{
+   // Populate icon sheet map
+   p->newIconSheets_.emplace(std::piecewise_construct,
+                             std::tuple {name},
+                             std::forward_as_tuple(IconInfo {
+                                name, iconWidth, iconHeight, hotX, hotY}));
+}
+
+void GeoIcons::FinishIconSheets()
+{
+   // Update icon sheets
+   for (auto& iconSheet : p->newIconSheets_)
+   {
+      iconSheet.second.UpdateTextureInfo();
+   }
+
+   std::unique_lock lock {p->iconMutex_};
+
+   // Swap buffers
+   p->currentIconSheets_.swap(p->newIconSheets_);
+
+   // Clear the new buffers
+   p->newIconSheets_.clear();
+
+   // Mark the draw item dirty
+   p->dirty_ = true;
+}
+
+void GeoIcons::StartIcons()
+{
+   // Clear the new buffer
+   p->newIconList_.clear();
+   p->newValidIconList_.clear();
+   p->newIconBuffer_.clear();
+   p->newIntegerBuffer_.clear();
+   p->newHoverIcons_.clear();
+}
+
+std::shared_ptr GeoIcons::AddIcon()
+{
+   return p->newIconList_.emplace_back(std::make_shared());
+}
+
+void GeoIcons::SetIconTexture(const std::shared_ptr& di,
+                              const std::string&                      iconSheet,
+                              std::size_t                             iconIndex)
+{
+   di->iconSheet_ = iconSheet;
+   di->iconIndex_ = iconIndex;
+}
+
+void GeoIcons::SetIconLocation(const std::shared_ptr& di,
+                               units::angle::degrees           latitude,
+                               units::angle::degrees longitude,
+                               double                        xOffset,
+                               double                        yOffset)
+{
+   di->latitude_  = latitude.value();
+   di->longitude_ = longitude.value();
+   di->x_         = xOffset;
+   di->y_         = yOffset;
+}
+
+void GeoIcons::SetIconLocation(const std::shared_ptr& di,
+                               double                                  latitude,
+                               double longitude,
+                               double xOffset,
+                               double yOffset)
+{
+   di->latitude_  = latitude;
+   di->longitude_ = longitude;
+   di->x_         = xOffset;
+   di->y_         = yOffset;
+}
+
+void GeoIcons::SetIconAngle(const std::shared_ptr& di,
+                            units::angle::degrees           angle)
+{
+   di->angle_ = angle;
+}
+
+void GeoIcons::SetIconModulate(const std::shared_ptr& di,
+                               boost::gil::rgba8_pixel_t               modulate)
+{
+   di->modulate_ = modulate;
+}
+
+void GeoIcons::SetIconHoverText(const std::shared_ptr& di,
+                                const std::string&                      text)
+{
+   di->hoverText_ = text;
+}
+
+void GeoIcons::FinishIcons()
+{
+   // Update buffers
+   p->UpdateBuffers();
+
+   std::unique_lock lock {p->iconMutex_};
+
+   // Swap buffers
+   p->currentIconList_.swap(p->newValidIconList_);
+   p->currentIconBuffer_.swap(p->newIconBuffer_);
+   p->currentIntegerBuffer_.swap(p->newIntegerBuffer_);
+   p->currentHoverIcons_.swap(p->newHoverIcons_);
+
+   // Clear the new buffers, except the full icon list (used to update buffers
+   // without re-adding icons)
+   p->newValidIconList_.clear();
+   p->newIconBuffer_.clear();
+   p->newIntegerBuffer_.clear();
+   p->newHoverIcons_.clear();
+
+   // Mark the draw item dirty
+   p->dirty_ = true;
+}
+
+void GeoIcons::Impl::UpdateBuffers()
+{
+   newIconBuffer_.clear();
+   newIconBuffer_.reserve(newIconList_.size() * kIconBufferLength);
+   newIntegerBuffer_.clear();
+   newIntegerBuffer_.reserve(newIconList_.size() * kVerticesPerRectangle *
+                             kIntegersPerVertex_);
+   newValidIconList_.clear();
+   newHoverIcons_.clear();
+
+   for (auto& di : newIconList_)
+   {
+      auto it = currentIconSheets_.find(di->iconSheet_);
+      if (it == currentIconSheets_.cend())
+      {
+         // No icon sheet found
+         logger_->warn("Could not find icon sheet: {}", di->iconSheet_);
+         continue;
+      }
+
+      auto& icon = it->second;
+
+      // Validate icon
+      if (di->iconIndex_ >= icon.numIcons_)
+      {
+         // No icon found
+         logger_->warn("Invalid icon index: {}", di->iconIndex_);
+         continue;
+      }
+
+      // Icon is valid, add to valid icon list
+      newValidIconList_.push_back(di);
+
+      // Threshold value
+      units::length::nautical_miles threshold = di->threshold_;
+      GLint thresholdValue = static_cast(std::round(threshold.value()));
+
+      // Start and end time
+      GLint startTime =
+         static_cast(std::chrono::duration_cast(
+                               di->startTime_.time_since_epoch())
+                               .count());
+      GLint endTime =
+         static_cast(std::chrono::duration_cast(
+                               di->endTime_.time_since_epoch())
+                               .count());
+
+      // Latitude and longitude coordinates in degrees
+      const float lat = static_cast(di->latitude_);
+      const float lon = static_cast(di->longitude_);
+
+      // Base X/Y offsets in pixels
+      const float x = static_cast(di->x_);
+      const float y = static_cast(di->y_);
+
+      // Icon size
+      const float iw = static_cast(icon.iconWidth_);
+      const float ih = static_cast(icon.iconHeight_);
+
+      // Hot X/Y (zero-based icon center)
+      const float hx = static_cast(icon.hotX_);
+      const float hy = static_cast(icon.hotY_);
+
+      // Final X/Y offsets in pixels
+      const float lx = std::roundf(x - hx);
+      const float rx = std::roundf(lx + iw);
+      const float ty = std::roundf(y + hy);
+      const float by = std::roundf(ty - ih);
+
+      // Angle in degrees
+      units::angle::degrees angle = di->angle_;
+      const float                  a     = angle.value();
+
+      // Modulate color
+      const float mc0 = di->modulate_[0] / 255.0f;
+      const float mc1 = di->modulate_[1] / 255.0f;
+      const float mc2 = di->modulate_[2] / 255.0f;
+      const float mc3 = di->modulate_[3] / 255.0f;
+
+      newIconBuffer_.insert(newIconBuffer_.end(),
+                            {
+                               // Icon
+                               lat, lon, lx, by, mc0, mc1, mc2, mc3, a, // BL
+                               lat, lon, lx, ty, mc0, mc1, mc2, mc3, a, // TL
+                               lat, lon, rx, by, mc0, mc1, mc2, mc3, a, // BR
+                               lat, lon, rx, by, mc0, mc1, mc2, mc3, a, // BR
+                               lat, lon, rx, ty, mc0, mc1, mc2, mc3, a, // TR
+                               lat, lon, lx, ty, mc0, mc1, mc2, mc3, a  // TL
+                            });
+      newIntegerBuffer_.insert(newIntegerBuffer_.end(),
+                               {thresholdValue,
+                                startTime,
+                                endTime,
+                                thresholdValue,
+                                startTime,
+                                endTime,
+                                thresholdValue,
+                                startTime,
+                                endTime,
+                                thresholdValue,
+                                startTime,
+                                endTime,
+                                thresholdValue,
+                                startTime,
+                                endTime,
+                                thresholdValue,
+                                startTime,
+                                endTime});
+
+      if (!di->hoverText_.empty())
+      {
+         const units::angle::radians radians = angle;
+
+         const auto sc = util::maplibre::LatLongToScreenCoordinate({lat, lon});
+
+         const float cosAngle = cosf(static_cast(radians.value()));
+         const float sinAngle = sinf(static_cast(radians.value()));
+
+         const glm::mat2 rotate {cosAngle, -sinAngle, sinAngle, cosAngle};
+
+         const glm::vec2 otl = rotate * glm::vec2 {lx, ty};
+         const glm::vec2 otr = rotate * glm::vec2 {rx, ty};
+         const glm::vec2 obl = rotate * glm::vec2 {lx, by};
+         const glm::vec2 obr = rotate * glm::vec2 {rx, by};
+
+         newHoverIcons_.emplace_back(
+            IconHoverEntry {di, sc, otl, otr, obl, obr});
+      }
+   }
+}
+
+void GeoIcons::Impl::UpdateTextureBuffer()
+{
+   textureBuffer_.clear();
+   textureBuffer_.reserve(currentIconList_.size() * kTextureBufferLength);
+
+   for (auto& di : currentIconList_)
+   {
+      auto it = currentIconSheets_.find(di->iconSheet_);
+      if (it == currentIconSheets_.cend())
+      {
+         // No file found. Should not get here, but insert empty data to match
+         // up with data already buffered
+         logger_->error("Could not find icon sheet: {}", di->iconSheet_);
+
+         // clang-format off
+         textureBuffer_.insert(
+            textureBuffer_.end(),
+            {
+               // Icon
+               0.0f, 0.0f, 0.0f, // BL
+               0.0f, 0.0f, 0.0f, // TL
+               0.0f, 0.0f, 0.0f, // BR
+               0.0f, 0.0f, 0.0f, // BR
+               0.0f, 0.0f, 0.0f, // TR
+               0.0f, 0.0f, 0.0f  // TL
+            });
+         // clang-format on
+
+         continue;
+      }
+
+      auto& icon = it->second;
+
+      // Validate icon
+      if (di->iconIndex_ >= icon.numIcons_)
+      {
+         // No icon found
+         logger_->error("Invalid icon index: {}", di->iconIndex_);
+
+         // Will get here if a texture changes, and the texture shrunk such that
+         // the icon is no longer found
+
+         // clang-format off
+         textureBuffer_.insert(
+            textureBuffer_.end(),
+            {
+               // Icon
+               0.0f, 0.0f, 0.0f, // BL
+               0.0f, 0.0f, 0.0f, // TL
+               0.0f, 0.0f, 0.0f, // BR
+               0.0f, 0.0f, 0.0f, // BR
+               0.0f, 0.0f, 0.0f, // TR
+               0.0f, 0.0f, 0.0f  // TL
+            });
+         // clang-format on
+
+         continue;
+      }
+
+      // Texture coordinates
+      const std::size_t iconRow    = (di->iconIndex_) / icon.columns_;
+      const std::size_t iconColumn = (di->iconIndex_) % icon.columns_;
+
+      const float iconX = iconColumn * icon.scaledWidth_;
+      const float iconY = iconRow * icon.scaledHeight_;
+
+      const float ls = icon.texture_.sLeft_ + iconX;
+      const float rs = ls + icon.scaledWidth_;
+      const float tt = icon.texture_.tTop_ + iconY;
+      const float bt = tt + icon.scaledHeight_;
+      const float r  = static_cast(icon.texture_.layerId_);
+
+      // clang-format off
+      textureBuffer_.insert(
+         textureBuffer_.end(),
+         {
+            // Icon
+            ls, bt, r, // BL
+            ls, tt, r, // TL
+            rs, bt, r, // BR
+            rs, bt, r, // BR
+            rs, tt, r, // TR
+            ls, tt, r  // TL
+         });
+      // clang-format on
+   }
+}
+
+void GeoIcons::Impl::Update(bool textureAtlasChanged)
+{
+   gl::OpenGLFunctions& gl = context_->gl();
+
+   // If the texture atlas has changed
+   if (dirty_ || textureAtlasChanged || lastTextureAtlasChanged_)
+   {
+      // Update texture coordinates
+      for (auto& iconSheet : currentIconSheets_)
+      {
+         iconSheet.second.UpdateTextureInfo();
+      }
+
+      // Update OpenGL texture buffer data
+      UpdateTextureBuffer();
+
+      // Buffer texture data
+      gl.glBindBuffer(GL_ARRAY_BUFFER, vbo_[1]);
+      gl.glBufferData(GL_ARRAY_BUFFER,
+                      sizeof(float) * textureBuffer_.size(),
+                      textureBuffer_.data(),
+                      GL_DYNAMIC_DRAW);
+
+      lastTextureAtlasChanged_ = false;
+   }
+
+   // If buffers need updating
+   if (dirty_)
+   {
+      // Buffer vertex data
+      gl.glBindBuffer(GL_ARRAY_BUFFER, vbo_[0]);
+      gl.glBufferData(GL_ARRAY_BUFFER,
+                      sizeof(float) * currentIconBuffer_.size(),
+                      currentIconBuffer_.data(),
+                      GL_DYNAMIC_DRAW);
+
+      // Buffer threshold data
+      gl.glBindBuffer(GL_ARRAY_BUFFER, vbo_[2]);
+      gl.glBufferData(GL_ARRAY_BUFFER,
+                      sizeof(GLint) * currentIntegerBuffer_.size(),
+                      currentIntegerBuffer_.data(),
+                      GL_DYNAMIC_DRAW);
+
+      numVertices_ =
+         static_cast(currentIconBuffer_.size() / kPointsPerVertex);
+   }
+
+   dirty_ = false;
+}
+
+bool GeoIcons::RunMousePicking(
+   const QMapLibreGL::CustomLayerRenderParameters& params,
+   const QPointF& /* mouseLocalPos */,
+   const QPointF&   mouseGlobalPos,
+   const glm::vec2& mouseCoords)
+{
+   std::unique_lock lock {p->iconMutex_};
+
+   bool itemPicked = false;
+
+   // Calculate map scale, remove width and height from original calculation
+   glm::vec2 scale = util::maplibre::GetMapScale(params);
+   scale = 2.0f / glm::vec2 {scale.x * params.width, scale.y * params.height};
+
+   // Scale and rotate the identity matrix to create the map matrix
+   glm::mat4 mapMatrix {1.0f};
+   mapMatrix = glm::scale(mapMatrix, glm::vec3 {scale, 1.0f});
+   mapMatrix = glm::rotate(mapMatrix,
+                           glm::radians(params.bearing),
+                           glm::vec3(0.0f, 0.0f, 1.0f));
+
+   units::length::meters mapDistance =
+      (p->thresholded_) ? util::maplibre::GetMapDistance(params) :
+                          units::length::meters {0.0};
+
+   // If no time has been selected, use the current time
+   std::chrono::system_clock::time_point selectedTime =
+      (p->selectedTime_ == std::chrono::system_clock::time_point {}) ?
+         std::chrono::system_clock::now() :
+         p->selectedTime_;
+
+   // For each pickable icon
+   auto it = std::find_if(
+      std::execution::par_unseq,
+      p->currentHoverIcons_.crbegin(),
+      p->currentHoverIcons_.crend(),
+      [&mapDistance, &selectedTime, &mapMatrix, &mouseCoords](const auto& icon)
+      {
+         if ((
+                // Geo icon is thresholded
+                mapDistance > units::length::meters {0.0} &&
+
+                // Geo icon threshold is < 999 nmi
+                static_cast(std::round(
+                   units::length::nautical_miles {icon.di_->threshold_}
+                      .value())) < 999 &&
+
+                // Map distance is beyond the threshold
+                icon.di_->threshold_ < mapDistance) ||
+
+             (
+                // Geo icon has a start time
+                icon.di_->startTime_ !=
+                   std::chrono::system_clock::time_point {} &&
+
+                // The time range has not yet started
+                (selectedTime < icon.di_->startTime_ ||
+
+                 // The time range has ended
+                 icon.di_->endTime_ <= selectedTime)))
+         {
+            // Icon is not pickable
+            return false;
+         }
+
+         // Initialize vertices
+         glm::vec2 bl = icon.p_;
+         glm::vec2 br = bl;
+         glm::vec2 tl = br;
+         glm::vec2 tr = tl;
+
+         // Calculate offsets
+         // - Rotated offset is based on final X/Y offsets (pixels)
+         // - Multiply the offset by the scaled and rotated map matrix
+         const glm::vec2 otl = mapMatrix * glm::vec4 {icon.otl_, 0.0f, 1.0f};
+         const glm::vec2 obl = mapMatrix * glm::vec4 {icon.obl_, 0.0f, 1.0f};
+         const glm::vec2 obr = mapMatrix * glm::vec4 {icon.obr_, 0.0f, 1.0f};
+         const glm::vec2 otr = mapMatrix * glm::vec4 {icon.otr_, 0.0f, 1.0f};
+
+         // Offset vertices
+         tl += otl;
+         bl += obl;
+         br += obr;
+         tr += otr;
+
+         // Test point against polygon bounds
+         return util::maplibre::IsPointInPolygon({tl, bl, br, tr}, mouseCoords);
+      });
+
+   if (it != p->currentHoverIcons_.crend())
+   {
+      itemPicked = true;
+      util::tooltip::Show(it->di_->hoverText_, mouseGlobalPos);
+   }
+
+   return itemPicked;
+}
+
+} // namespace draw
+} // namespace gl
+} // namespace qt
+} // namespace scwx
diff --git a/scwx-qt/source/scwx/qt/gl/draw/geo_icons.hpp b/scwx-qt/source/scwx/qt/gl/draw/geo_icons.hpp
new file mode 100644
index 00000000..9d4bc9a7
--- /dev/null
+++ b/scwx-qt/source/scwx/qt/gl/draw/geo_icons.hpp
@@ -0,0 +1,176 @@
+#pragma once
+
+#include 
+#include 
+
+#include 
+#include 
+
+namespace scwx
+{
+namespace qt
+{
+namespace gl
+{
+namespace draw
+{
+
+struct GeoIconDrawItem;
+
+class GeoIcons : public DrawItem
+{
+public:
+   explicit GeoIcons(const std::shared_ptr& context);
+   ~GeoIcons();
+
+   GeoIcons(const GeoIcons&)            = delete;
+   GeoIcons& operator=(const GeoIcons&) = delete;
+
+   GeoIcons(GeoIcons&&) noexcept;
+   GeoIcons& operator=(GeoIcons&&) noexcept;
+
+   void set_selected_time(std::chrono::system_clock::time_point selectedTime);
+   void set_thresholded(bool thresholded);
+
+   void Initialize() override;
+   void Render(const QMapLibreGL::CustomLayerRenderParameters& params,
+               bool textureAtlasChanged) override;
+   void Deinitialize() override;
+
+   bool RunMousePicking(const QMapLibreGL::CustomLayerRenderParameters& params,
+                        const QPointF&   mouseLocalPos,
+                        const QPointF&   mouseGlobalPos,
+                        const glm::vec2& mouseCoords) override;
+
+   /**
+    * Sets the visibility of the geo icons.
+    *
+    * @param [in] visible Icon visibility
+    */
+   void SetVisible(bool visible);
+
+   /**
+    * Resets and prepares the draw item for adding a new set of icon sheets.
+    */
+   void StartIconSheets();
+
+   /**
+    * Adds an icon sheet for drawing the geo icons. The icon sheet must already
+    * exist in the texture atlas.
+    *
+    * @param [in] name The name of the icon sheet in the texture atlas
+    * @param [in] iconWidth The width of each icon in the icon sheet. Default is
+    * 0 for a single icon.
+    * @param [in] iconHeight The height of each icon in the icon sheet. Default
+    * is 0 for a single icon.
+    * @param [in] hotX The zero-based center of the each icon in the icon sheet.
+    * Default is -1 to center the icon.
+    * @param [in] hotY The zero-based center of the each icon in the icon sheet.
+    * Default is -1 to center the icon.
+    */
+   void AddIconSheet(const std::string& name,
+                     std::size_t        iconWidth  = 0,
+                     std::size_t        iconHeight = 0,
+                     std::int32_t       hotX       = -1,
+                     std::int32_t       hotY       = -1);
+
+   /**
+    * Resets and prepares the draw item for adding a new set of icon sheets.
+    */
+   void FinishIconSheets();
+
+   /**
+    * Resets and prepares the draw item for adding a new set of icons.
+    */
+   void StartIcons();
+
+   /**
+    * Adds a geo icon to the internal draw list.
+    *
+    * @return Geo icon draw item
+    */
+   std::shared_ptr AddIcon();
+
+   /**
+    * Sets the texture of a geo icon.
+    *
+    * @param [in] di Geo icon draw item
+    * @param [in] iconSheet The name of the icon sheet in the texture atlas
+    * @param [in] iconIndex The zero-based index of the icon in the icon sheet
+    */
+   static void SetIconTexture(const std::shared_ptr& di,
+                              const std::string&                      iconSheet,
+                              std::size_t iconIndex);
+
+   /**
+    * Sets the location of a geo icon.
+    *
+    * @param [in] di Geo icon draw item
+    * @param [in] latitude The latitude of the geo icon in degrees.
+    * @param [in] longitude The longitude of the geo icon in degrees.
+    * @param [in] xOffset The x-offset of the geo icon in pixels. Default is 0.
+    * @param [in] yOffset The y-offset of the geo icon in pixels. Default is 0.
+    */
+   static void SetIconLocation(const std::shared_ptr& di,
+                               units::angle::degrees           latitude,
+                               units::angle::degrees longitude,
+                               double                        xOffset = 0.0,
+                               double                        yOffset = 0.0);
+
+   /**
+    * Sets the location of a geo icon.
+    *
+    * @param [in] di Geo icon draw item
+    * @param [in] latitude The latitude of the geo icon in degrees.
+    * @param [in] longitude The longitude of the geo icon in degrees.
+    * @param [in] xOffset The x-offset of the geo icon in pixels. Default is 0.
+    * @param [in] yOffset The y-offset of the geo icon in pixels. Default is 0.
+    */
+   static void SetIconLocation(const std::shared_ptr& di,
+                               double                                  latitude,
+                               double longitude,
+                               double xOffset = 0.0,
+                               double yOffset = 0.0);
+
+   /**
+    * Sets the angle of a geo icon.
+    *
+    * @param [in] di Geo icon draw item
+    * @param [in] angle Angle in degrees
+    */
+   static void SetIconAngle(const std::shared_ptr& di,
+                            units::angle::degrees           angle);
+
+   /**
+    * Sets the modulate color of a geo icon.
+    *
+    * @param [in] di Geo icon draw item
+    * @param [in] modulate Modulate color
+    */
+   static void SetIconModulate(const std::shared_ptr& di,
+                               boost::gil::rgba8_pixel_t modulate);
+
+   /**
+    * Sets the hover text of a geo icon.
+    *
+    * @param [in] di Geo icon draw item
+    * @param [in] text Hover text
+    */
+   static void SetIconHoverText(const std::shared_ptr& di,
+                                const std::string&                      text);
+
+   /**
+    * Finalizes the draw item after adding new icons.
+    */
+   void FinishIcons();
+
+private:
+   class Impl;
+
+   std::unique_ptr p;
+};
+
+} // namespace draw
+} // namespace gl
+} // namespace qt
+} // namespace scwx
diff --git a/scwx-qt/source/scwx/qt/main/main_window.cpp b/scwx-qt/source/scwx/qt/main/main_window.cpp
index d07ca41c..3f4765b4 100644
--- a/scwx-qt/source/scwx/qt/main/main_window.cpp
+++ b/scwx-qt/source/scwx/qt/main/main_window.cpp
@@ -4,6 +4,7 @@
 #include 
 #include 
 #include 
+#include 
 #include 
 #include 
 #include 
@@ -82,6 +83,7 @@ public:
        settingsDialog_ {nullptr},
        updateDialog_ {nullptr},
        placefileManager_ {manager::PlacefileManager::Instance()},
+       positionManager_ {manager::PositionManager::Instance()},
        textEventManager_ {manager::TextEventManager::Instance()},
        timelineManager_ {manager::TimelineManager::Instance()},
        updateManager_ {manager::UpdateManager::Instance()},
@@ -117,6 +119,11 @@ public:
       settings_.setApiKey(QString {mapProviderApiKey.c_str()});
       settings_.setCacheDatabasePath(QString {cacheDbPath.c_str()});
       settings_.setCacheDatabaseMaximumSize(20 * 1024 * 1024);
+
+      if (settings::GeneralSettings::Instance().track_location().GetValue())
+      {
+         positionManager_->TrackLocation(true);
+      }
    }
    ~MainWindowImpl() { threadPool_.join(); }
 
@@ -172,6 +179,7 @@ public:
    ui::UpdateDialog*        updateDialog_;
 
    std::shared_ptr placefileManager_;
+   std::shared_ptr  positionManager_;
    std::shared_ptr textEventManager_;
    std::shared_ptr  timelineManager_;
    std::shared_ptr    updateManager_;
@@ -244,9 +252,13 @@ MainWindow::MainWindow(QWidget* parent) :
    p->mapSettingsGroup_ = new ui::CollapsibleGroup(tr("Map Settings"), this);
    p->mapSettingsGroup_->GetContentsLayout()->addWidget(ui->mapStyleLabel);
    p->mapSettingsGroup_->GetContentsLayout()->addWidget(ui->mapStyleComboBox);
+   p->mapSettingsGroup_->GetContentsLayout()->addWidget(
+      ui->trackLocationCheckBox);
    ui->radarToolboxScrollAreaContents->layout()->replaceWidget(
       ui->mapSettingsGroupBox, p->mapSettingsGroup_);
    ui->mapSettingsGroupBox->setVisible(false);
+   ui->trackLocationCheckBox->setChecked(
+      settings::GeneralSettings::Instance().track_location().GetValue());
 
    // Add Level 2 Products
    p->level2ProductsGroup_ =
@@ -846,6 +858,19 @@ void MainWindowImpl::ConnectOtherSignals()
                  }
               }
            });
+   connect(mainWindow_->ui->trackLocationCheckBox,
+           &QCheckBox::stateChanged,
+           mainWindow_,
+           [this](int state)
+           {
+              bool trackingEnabled = (state == Qt::CheckState::Checked);
+
+              settings::GeneralSettings::Instance().track_location().StageValue(
+                 trackingEnabled);
+
+              // Turn on location tracking
+              positionManager_->TrackLocation(trackingEnabled);
+           });
    connect(level2ProductsWidget_,
            &ui::Level2ProductsWidget::RadarProductSelected,
            mainWindow_,
diff --git a/scwx-qt/source/scwx/qt/main/main_window.ui b/scwx-qt/source/scwx/qt/main/main_window.ui
index 9cd627eb..56c659f3 100644
--- a/scwx-qt/source/scwx/qt/main/main_window.ui
+++ b/scwx-qt/source/scwx/qt/main/main_window.ui
@@ -39,7 +39,7 @@
      0
      0
      1024
-     21
+     22
     
    
    
          
diff --git a/scwx-qt/source/scwx/qt/manager/position_manager.cpp b/scwx-qt/source/scwx/qt/manager/position_manager.cpp
new file mode 100644
index 00000000..23357444
--- /dev/null
+++ b/scwx-qt/source/scwx/qt/manager/position_manager.cpp
@@ -0,0 +1,138 @@
+#include 
+#include 
+#include 
+
+#include 
+
+#include 
+#include 
+
+namespace scwx
+{
+namespace qt
+{
+namespace manager
+{
+
+static const std::string logPrefix_ = "scwx::qt::manager::position_manager";
+static const auto        logger_    = scwx::util::Logger::Create(logPrefix_);
+
+class PositionManager::Impl
+{
+public:
+   explicit Impl(PositionManager* self) :
+       self_ {self}, trackingUuid_ {boost::uuids::random_generator()()}
+   {
+      // TODO: macOS requires permission
+      geoPositionInfoSource_ =
+         QGeoPositionInfoSource::createDefaultSource(self);
+
+      if (geoPositionInfoSource_ != nullptr)
+      {
+         logger_->debug("Using position source: {}",
+                        geoPositionInfoSource_->sourceName().toStdString());
+
+         QObject::connect(geoPositionInfoSource_,
+                          &QGeoPositionInfoSource::positionUpdated,
+                          self_,
+                          [this](const QGeoPositionInfo& info)
+                          {
+                             auto coordinate = info.coordinate();
+
+                             if (coordinate != position_.coordinate())
+                             {
+                                logger_->debug("Position updated: {}, {}",
+                                               coordinate.latitude(),
+                                               coordinate.longitude());
+                             }
+
+                             position_ = info;
+
+                             Q_EMIT self_->PositionUpdated(info);
+                          });
+      }
+   }
+
+   ~Impl() {}
+
+   PositionManager* self_;
+
+   boost::uuids::uuid trackingUuid_;
+   bool               trackingEnabled_ {false};
+
+   std::set uuids_ {};
+
+   QGeoPositionInfoSource* geoPositionInfoSource_ {};
+   QGeoPositionInfo        position_ {};
+};
+
+PositionManager::PositionManager() : p(std::make_unique(this)) {}
+PositionManager::~PositionManager() = default;
+
+QGeoPositionInfo PositionManager::position() const
+{
+   return p->position_;
+}
+
+bool PositionManager::IsLocationTracked()
+{
+   return p->trackingEnabled_;
+}
+
+void PositionManager::EnablePositionUpdates(boost::uuids::uuid uuid,
+                                            bool               enabled)
+{
+   if (p->geoPositionInfoSource_ == nullptr)
+   {
+      return;
+   }
+
+   if (enabled)
+   {
+      if (p->uuids_.empty())
+      {
+         p->geoPositionInfoSource_->startUpdates();
+      }
+
+      p->uuids_.insert(uuid);
+   }
+   else
+   {
+      p->uuids_.erase(uuid);
+
+      if (p->uuids_.empty())
+      {
+         p->geoPositionInfoSource_->stopUpdates();
+      }
+   }
+}
+
+void PositionManager::TrackLocation(bool trackingEnabled)
+{
+   p->trackingEnabled_ = trackingEnabled;
+   EnablePositionUpdates(p->trackingUuid_, trackingEnabled);
+   Q_EMIT LocationTrackingChanged(trackingEnabled);
+}
+
+std::shared_ptr PositionManager::Instance()
+{
+   static std::weak_ptr positionManagerReference_ {};
+   static std::mutex                     instanceMutex_ {};
+
+   std::unique_lock lock(instanceMutex_);
+
+   std::shared_ptr positionManager =
+      positionManagerReference_.lock();
+
+   if (positionManager == nullptr)
+   {
+      positionManager           = std::make_shared();
+      positionManagerReference_ = positionManager;
+   }
+
+   return positionManager;
+}
+
+} // namespace manager
+} // namespace qt
+} // namespace scwx
diff --git a/scwx-qt/source/scwx/qt/manager/position_manager.hpp b/scwx-qt/source/scwx/qt/manager/position_manager.hpp
new file mode 100644
index 00000000..367114c5
--- /dev/null
+++ b/scwx-qt/source/scwx/qt/manager/position_manager.hpp
@@ -0,0 +1,46 @@
+#pragma once
+
+#include 
+
+#include 
+#include 
+
+class QGeoPositionInfo;
+
+namespace scwx
+{
+namespace qt
+{
+namespace manager
+{
+
+class PositionManager : public QObject
+{
+   Q_OBJECT
+   Q_DISABLE_COPY_MOVE(PositionManager)
+
+public:
+   explicit PositionManager();
+   ~PositionManager();
+
+   QGeoPositionInfo position() const;
+
+   bool IsLocationTracked();
+
+   void EnablePositionUpdates(boost::uuids::uuid uuid, bool enabled);
+   void TrackLocation(bool trackingEnabled);
+
+   static std::shared_ptr Instance();
+
+signals:
+   void LocationTrackingChanged(bool trackingEnabled);
+   void PositionUpdated(const QGeoPositionInfo& info);
+
+private:
+   class Impl;
+   std::unique_ptr p;
+};
+
+} // namespace manager
+} // namespace qt
+} // namespace scwx
diff --git a/scwx-qt/source/scwx/qt/manager/resource_manager.cpp b/scwx-qt/source/scwx/qt/manager/resource_manager.cpp
index 3048fc6c..df4b4f77 100644
--- a/scwx-qt/source/scwx/qt/manager/resource_manager.cpp
+++ b/scwx-qt/source/scwx/qt/manager/resource_manager.cpp
@@ -2,6 +2,7 @@
 #include 
 #include 
 #include 
+#include 
 #include 
 #include 
 
@@ -91,10 +92,19 @@ static void LoadFonts()
 static void LoadTextures()
 {
    util::TextureAtlas& textureAtlas = util::TextureAtlas::Instance();
-   textureAtlas.RegisterTexture("lines/default-1x7",
-                                ":/res/textures/lines/default-1x7.png");
-   textureAtlas.RegisterTexture("lines/test-pattern",
-                                ":/res/textures/lines/test-pattern.png");
+
+   for (auto imageTexture : types::ImageTextureIterator())
+   {
+      textureAtlas.RegisterTexture(GetTextureName(imageTexture),
+                                   GetTexturePath(imageTexture));
+   }
+
+   for (auto lineTexture : types::LineTextureIterator())
+   {
+      textureAtlas.RegisterTexture(GetTextureName(lineTexture),
+                                   GetTexturePath(lineTexture));
+   }
+
    textureAtlas.BuildAtlas(2048, 2048);
 }
 
diff --git a/scwx-qt/source/scwx/qt/map/generic_layer.hpp b/scwx-qt/source/scwx/qt/map/generic_layer.hpp
index 239d271b..355680b8 100644
--- a/scwx-qt/source/scwx/qt/map/generic_layer.hpp
+++ b/scwx-qt/source/scwx/qt/map/generic_layer.hpp
@@ -45,6 +45,9 @@ public:
                    const QPointF&   mouseGlobalPos,
                    const glm::vec2& mouseCoords);
 
+signals:
+   void NeedsRendering();
+
 protected:
    std::shared_ptr context() const;
 
diff --git a/scwx-qt/source/scwx/qt/map/map_widget.cpp b/scwx-qt/source/scwx/qt/map/map_widget.cpp
index 7c543bdd..a897748b 100644
--- a/scwx-qt/source/scwx/qt/map/map_widget.cpp
+++ b/scwx-qt/source/scwx/qt/map/map_widget.cpp
@@ -969,6 +969,11 @@ void MapWidgetImpl::AddLayer(const std::string&            id,
 
       layerList_.push_back(id);
       genericLayers_.push_back(layer);
+
+      connect(layer.get(),
+              &GenericLayer::NeedsRendering,
+              widget_,
+              [this]() { widget_->update(); });
    }
    catch (const std::exception&)
    {
diff --git a/scwx-qt/source/scwx/qt/map/overlay_layer.cpp b/scwx-qt/source/scwx/qt/map/overlay_layer.cpp
index c0577063..fdf17463 100644
--- a/scwx-qt/source/scwx/qt/map/overlay_layer.cpp
+++ b/scwx-qt/source/scwx/qt/map/overlay_layer.cpp
@@ -1,25 +1,19 @@
 #include 
+#include 
 #include 
-#include 
+#include 
+#include 
 #include 
 #include 
 
 #include 
-#include 
 
 #if defined(_MSC_VER)
 #   pragma warning(push, 0)
 #endif
 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
 #include 
-#include 
+#include 
 
 #if !defined(_MSC_VER)
 #   include 
@@ -44,13 +38,24 @@ class OverlayLayerImpl
 public:
    explicit OverlayLayerImpl(std::shared_ptr context) :
        activeBoxOuter_ {std::make_shared(context)},
-       activeBoxInner_ {std::make_shared(context)}
+       activeBoxInner_ {std::make_shared(context)},
+       icons_ {std::make_shared(context)},
+       locationIconName_ {
+          types::GetTextureName(types::ImageTexture::Crosshairs24)}
    {
    }
    ~OverlayLayerImpl() = default;
 
+   std::shared_ptr positionManager_ {
+      manager::PositionManager::Instance()};
+   QGeoPositionInfo currentPosition_ {};
+
    std::shared_ptr activeBoxOuter_;
    std::shared_ptr activeBoxInner_;
+   std::shared_ptr  icons_;
+
+   const std::string&                         locationIconName_;
+   std::shared_ptr locationIcon_ {};
 
    std::string sweepTimeString_ {};
    bool        sweepTimeNeedsUpdate_ {true};
@@ -62,6 +67,7 @@ OverlayLayer::OverlayLayer(std::shared_ptr context) :
 {
    AddDrawItem(p->activeBoxOuter_);
    AddDrawItem(p->activeBoxInner_);
+   AddDrawItem(p->icons_);
 
    p->activeBoxOuter_->SetPosition(0.0f, 0.0f);
 }
@@ -83,6 +89,45 @@ void OverlayLayer::Initialize()
               this,
               &OverlayLayer::UpdateSweepTimeNextFrame);
    }
+
+   p->currentPosition_ = p->positionManager_->position();
+   auto coordinate     = p->currentPosition_.coordinate();
+
+   p->icons_->StartIconSheets();
+   p->icons_->AddIconSheet(p->locationIconName_);
+   p->icons_->FinishIconSheets();
+
+   p->icons_->StartIcons();
+   p->locationIcon_ = p->icons_->AddIcon();
+   gl::draw::GeoIcons::SetIconTexture(
+      p->locationIcon_, p->locationIconName_, 0);
+   gl::draw::GeoIcons::SetIconAngle(p->locationIcon_,
+                                    units::angle::degrees {45.0});
+   gl::draw::GeoIcons::SetIconLocation(
+      p->locationIcon_, coordinate.latitude(), coordinate.longitude());
+   p->icons_->FinishIcons();
+
+   connect(p->positionManager_.get(),
+           &manager::PositionManager::LocationTrackingChanged,
+           this,
+           [this]() { Q_EMIT NeedsRendering(); });
+   connect(p->positionManager_.get(),
+           &manager::PositionManager::PositionUpdated,
+           this,
+           [this](const QGeoPositionInfo& position)
+           {
+              auto coordinate = position.coordinate();
+              if (position.isValid() &&
+                  p->currentPosition_.coordinate() != coordinate)
+              {
+                 gl::draw::GeoIcons::SetIconLocation(p->locationIcon_,
+                                                     coordinate.latitude(),
+                                                     coordinate.longitude());
+                 p->icons_->FinishIcons();
+                 Q_EMIT NeedsRendering();
+              }
+              p->currentPosition_ = position;
+           });
 }
 
 void OverlayLayer::Render(
@@ -95,6 +140,9 @@ void OverlayLayer::Render(
 
    context()->set_render_parameters(params);
 
+   // Set OpenGL blend mode for transparency
+   gl.glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
+
    p->sweepTimePicked_ = false;
 
    if (p->sweepTimeNeedsUpdate_ && radarProductView != nullptr)
@@ -127,6 +175,10 @@ void OverlayLayer::Render(
       p->activeBoxInner_->SetBorder(1.0f * pixelRatio, {255, 255, 255, 255});
    }
 
+   // Location Icon
+   p->icons_->SetVisible(p->currentPosition_.isValid() &&
+                         p->positionManager_->IsLocationTracked());
+
    DrawLayer::Render(params);
 
    if (radarProductView != nullptr)
@@ -208,6 +260,17 @@ void OverlayLayer::Deinitialize()
                  this,
                  &OverlayLayer::UpdateSweepTimeNextFrame);
    }
+
+   disconnect(p->positionManager_.get(),
+              &manager::PositionManager::LocationTrackingChanged,
+              this,
+              nullptr);
+   disconnect(p->positionManager_.get(),
+              &manager::PositionManager::PositionUpdated,
+              this,
+              nullptr);
+
+   p->locationIcon_ = nullptr;
 }
 
 bool OverlayLayer::RunMousePicking(
diff --git a/scwx-qt/source/scwx/qt/settings/general_settings.cpp b/scwx-qt/source/scwx/qt/settings/general_settings.cpp
index 997d4e06..0eab43e9 100644
--- a/scwx-qt/source/scwx/qt/settings/general_settings.cpp
+++ b/scwx-qt/source/scwx/qt/settings/general_settings.cpp
@@ -42,6 +42,7 @@ public:
       mapProvider_.SetDefault(defaultMapProviderValue);
       mapboxApiKey_.SetDefault("?");
       maptilerApiKey_.SetDefault("?");
+      trackLocation_.SetDefault(false);
       updateNotificationsEnabled_.SetDefault(true);
 
       fontSizes_.SetElementMinimum(1);
@@ -118,6 +119,7 @@ public:
    SettingsVariable                mapProvider_ {"map_provider"};
    SettingsVariable mapboxApiKey_ {"mapbox_api_key"};
    SettingsVariable maptilerApiKey_ {"maptiler_api_key"};
+   SettingsVariable        trackLocation_ {"track_location"};
    SettingsVariable updateNotificationsEnabled_ {"update_notifications"};
 };
 
@@ -137,6 +139,7 @@ GeneralSettings::GeneralSettings() :
                       &p->mapProvider_,
                       &p->mapboxApiKey_,
                       &p->maptilerApiKey_,
+                      &p->trackLocation_,
                       &p->updateNotificationsEnabled_});
    SetDefaults();
 }
@@ -212,6 +215,11 @@ SettingsVariable& GeneralSettings::maptiler_api_key() const
    return p->maptilerApiKey_;
 }
 
+SettingsVariable& GeneralSettings::track_location() const
+{
+   return p->trackLocation_;
+}
+
 SettingsVariable& GeneralSettings::update_notifications_enabled() const
 {
    return p->updateNotificationsEnabled_;
@@ -225,6 +233,7 @@ bool GeneralSettings::Shutdown()
    dataChanged |= p->loopDelay_.Commit();
    dataChanged |= p->loopSpeed_.Commit();
    dataChanged |= p->loopTime_.Commit();
+   dataChanged |= p->trackLocation_.Commit();
 
    return dataChanged;
 }
@@ -250,6 +259,7 @@ bool operator==(const GeneralSettings& lhs, const GeneralSettings& rhs)
            lhs.p->mapProvider_ == rhs.p->mapProvider_ &&
            lhs.p->mapboxApiKey_ == rhs.p->mapboxApiKey_ &&
            lhs.p->maptilerApiKey_ == rhs.p->maptilerApiKey_ &&
+           lhs.p->trackLocation_ == rhs.p->trackLocation_ &&
            lhs.p->updateNotificationsEnabled_ ==
               rhs.p->updateNotificationsEnabled_);
 }
diff --git a/scwx-qt/source/scwx/qt/settings/general_settings.hpp b/scwx-qt/source/scwx/qt/settings/general_settings.hpp
index 57045ac6..9c6256b7 100644
--- a/scwx-qt/source/scwx/qt/settings/general_settings.hpp
+++ b/scwx-qt/source/scwx/qt/settings/general_settings.hpp
@@ -38,6 +38,7 @@ public:
    SettingsVariable&                map_provider() const;
    SettingsVariable&                mapbox_api_key() const;
    SettingsVariable&                maptiler_api_key() const;
+   SettingsVariable&                       track_location() const;
    SettingsVariable& update_notifications_enabled() const;
 
    static GeneralSettings& Instance();
diff --git a/scwx-qt/source/scwx/qt/types/texture_types.cpp b/scwx-qt/source/scwx/qt/types/texture_types.cpp
new file mode 100644
index 00000000..5f2123ad
--- /dev/null
+++ b/scwx-qt/source/scwx/qt/types/texture_types.cpp
@@ -0,0 +1,50 @@
+#include 
+
+#include 
+
+namespace scwx
+{
+namespace qt
+{
+namespace types
+{
+
+struct TextureInfo
+{
+   std::string name_ {};
+   std::string path_ {};
+};
+
+static const std::unordered_map imageTextureInfo_ {
+   {ImageTexture::Crosshairs24,
+    {"images/crosshairs-24", ":/res/textures/images/crosshairs-24.png"}}};
+
+static const std::unordered_map lineTextureInfo_ {
+   {LineTexture::Default1x7,
+    {"lines/default-1x7", ":/res/textures/lines/default-1x7.png"}},
+   {LineTexture::TestPattern,
+    {"lines/test-pattern", ":/res/textures/lines/test-pattern.png"}}};
+
+const std::string& GetTextureName(ImageTexture imageTexture)
+{
+   return imageTextureInfo_.at(imageTexture).name_;
+}
+
+const std::string& GetTextureName(LineTexture lineTexture)
+{
+   return lineTextureInfo_.at(lineTexture).name_;
+}
+
+const std::string& GetTexturePath(ImageTexture imageTexture)
+{
+   return imageTextureInfo_.at(imageTexture).path_;
+}
+
+const std::string& GetTexturePath(LineTexture lineTexture)
+{
+   return lineTextureInfo_.at(lineTexture).path_;
+}
+
+} // namespace types
+} // namespace qt
+} // namespace scwx
diff --git a/scwx-qt/source/scwx/qt/types/texture_types.hpp b/scwx-qt/source/scwx/qt/types/texture_types.hpp
new file mode 100644
index 00000000..c40f7141
--- /dev/null
+++ b/scwx-qt/source/scwx/qt/types/texture_types.hpp
@@ -0,0 +1,40 @@
+#pragma once
+
+#include 
+
+#include 
+
+namespace scwx
+{
+namespace qt
+{
+namespace types
+{
+
+enum class ImageTexture
+{
+   Crosshairs24,
+   Crosshairs32
+};
+typedef scwx::util::Iterator
+   ImageTextureIterator;
+
+enum class LineTexture
+{
+   Default1x7,
+   TestPattern
+};
+typedef scwx::util::
+   Iterator
+      LineTextureIterator;
+
+const std::string& GetTextureName(ImageTexture imageTexture);
+const std::string& GetTextureName(LineTexture lineTexture);
+const std::string& GetTexturePath(ImageTexture imageTexture);
+const std::string& GetTexturePath(LineTexture lineTexture);
+
+} // namespace types
+} // namespace qt
+} // namespace scwx
diff --git a/test/data b/test/data
index fc428fa1..49a5bf59 160000
--- a/test/data
+++ b/test/data
@@ -1 +1 @@
-Subproject commit fc428fa1460b3d5ce04646727e379e2f4f90f5ec
+Subproject commit 49a5bf59c30822b585b5726d7262b8e4ed4f10a7