diff --git a/.gitmodules b/.gitmodules
index 81c8ed32..2bba2042 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -13,3 +13,6 @@
 [submodule "external/freetype-gl"]
 	path = external/freetype-gl
 	url = https://github.com/rougier/freetype-gl.git
+[submodule "external/stb"]
+	path = external/stb
+	url = https://github.com/nothings/stb.git
diff --git a/external/CMakeLists.txt b/external/CMakeLists.txt
index 37fbfe4c..221957b0 100644
--- a/external/CMakeLists.txt
+++ b/external/CMakeLists.txt
@@ -6,8 +6,10 @@ set_property(DIRECTORY
              PROPERTY CMAKE_CONFIGURE_DEPENDS
              freetype-gl.cmake
              hsluv-c.cmake
-             mapbox-gl-native.cmake)
+             mapbox-gl-native.cmake
+             stb.cmake)
 
 include(freetype-gl.cmake)
 include(hsluv-c.cmake)
 include(mapbox-gl-native.cmake)
+include(stb.cmake)
diff --git a/external/stb b/external/stb
new file mode 160000
index 00000000..8b5f1f37
--- /dev/null
+++ b/external/stb
@@ -0,0 +1 @@
+Subproject commit 8b5f1f37b5b75829fc72d38e7b5d4bcbf8a26d55
diff --git a/external/stb.cmake b/external/stb.cmake
new file mode 100644
index 00000000..52bbf5f2
--- /dev/null
+++ b/external/stb.cmake
@@ -0,0 +1,4 @@
+cmake_minimum_required(VERSION 3.19)
+set(PROJECT_NAME scwx-stb)
+
+set(STB_INCLUDE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/stb PARENT_SCOPE)
diff --git a/scwx-qt/gl/geo_line.vert b/scwx-qt/gl/geo_line.vert
new file mode 100644
index 00000000..ea7d6351
--- /dev/null
+++ b/scwx-qt/gl/geo_line.vert
@@ -0,0 +1,41 @@
+#version 330 core
+
+#define DEGREES_MAX   360.0f
+#define LATITUDE_MAX  85.051128779806604f
+#define LONGITUDE_MAX 180.0f
+#define PI            3.1415926535897932384626433f
+#define RAD2DEG       57.295779513082320876798156332941f
+
+layout (location = 0) in vec2 aLatLong;
+layout (location = 1) in vec2 aXYOffset;
+layout (location = 2) in vec2 aTexCoord;
+layout (location = 3) in vec4 aModulate;
+
+uniform mat4 uMVPMatrix;
+uniform mat4 uMapMatrix;
+uniform vec2 uMapScreenCoord;
+
+smooth out vec2 texCoord;
+flat   out vec4 modulate;
+
+vec2 latLngToScreenCoordinate(in vec2 latLng)
+{
+   vec2 p;
+   latLng.x = clamp(latLng.x, -LATITUDE_MAX, LATITUDE_MAX);
+   p.xy     = vec2(LONGITUDE_MAX + latLng.y,
+                   -(LONGITUDE_MAX - RAD2DEG * log(tan(PI / 4 + latLng.x * PI / DEGREES_MAX))));
+   return p;
+}
+
+void main()
+{
+   // Pass the texture coordinate and color modulate to the fragment shader
+   texCoord = aTexCoord;
+   modulate = aModulate;
+
+   vec2 p = latLngToScreenCoordinate(aLatLong) - uMapScreenCoord;
+
+   // Transform the position to screen coordinates
+   gl_Position = uMapMatrix * vec4(p, 0.0f, 1.0f) -
+                 uMVPMatrix * vec4(aXYOffset, 0.0f, 0.0f);
+}
diff --git a/scwx-qt/gl/texture2d.frag b/scwx-qt/gl/texture2d.frag
new file mode 100644
index 00000000..16ab8960
--- /dev/null
+++ b/scwx-qt/gl/texture2d.frag
@@ -0,0 +1,16 @@
+#version 330 core
+
+// Lower the default precision to medium
+precision mediump float;
+
+uniform sampler2D uTexture;
+
+smooth in vec2 texCoord;
+flat   in vec4 modulate;
+
+layout (location = 0) out vec4 fragColor;
+
+void main()
+{
+   fragColor = texture(uTexture, texCoord) * modulate;
+}
diff --git a/scwx-qt/res/textures/lines/default-1x7.png b/scwx-qt/res/textures/lines/default-1x7.png
new file mode 100644
index 00000000..0b72d67c
Binary files /dev/null and b/scwx-qt/res/textures/lines/default-1x7.png differ
diff --git a/scwx-qt/res/textures/lines/test-pattern.png b/scwx-qt/res/textures/lines/test-pattern.png
new file mode 100644
index 00000000..b9a0224e
Binary files /dev/null and b/scwx-qt/res/textures/lines/test-pattern.png differ
diff --git a/scwx-qt/scwx-qt.cmake b/scwx-qt/scwx-qt.cmake
index 00abbaf4..51ba42cf 100644
--- a/scwx-qt/scwx-qt.cmake
+++ b/scwx-qt/scwx-qt.cmake
@@ -40,14 +40,19 @@ set(SRC_MAIN source/scwx/qt/main/main_window.cpp)
 set(UI_MAIN  source/scwx/qt/main/main_window.ui)
 set(HDR_CONFIG source/scwx/qt/config/radar_site.hpp)
 set(SRC_CONFIG source/scwx/qt/config/radar_site.cpp)
+set(SRC_EXTERNAL source/scwx/qt/external/stb_rect_pack.cpp)
 set(HDR_GL source/scwx/qt/gl/gl.hpp
+           source/scwx/qt/gl/gl_context.hpp
            source/scwx/qt/gl/shader_program.hpp
            source/scwx/qt/gl/text_shader.hpp)
-set(SRC_GL source/scwx/qt/gl/shader_program.cpp
+set(SRC_GL source/scwx/qt/gl/gl_context.cpp
+           source/scwx/qt/gl/shader_program.cpp
            source/scwx/qt/gl/text_shader.cpp)
 set(HDR_GL_DRAW source/scwx/qt/gl/draw/draw_item.hpp
+                source/scwx/qt/gl/draw/geo_line.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_line.cpp
                 source/scwx/qt/gl/draw/rectangle.cpp)
 set(HDR_MANAGER source/scwx/qt/manager/radar_product_manager.hpp
                 source/scwx/qt/manager/radar_product_manager_notifier.hpp
@@ -71,6 +76,7 @@ set(SRC_MAP source/scwx/qt/map/color_table_layer.cpp
             source/scwx/qt/map/draw_layer.cpp
             source/scwx/qt/map/generic_layer.cpp
             source/scwx/qt/map/layer_wrapper.cpp
+            source/scwx/qt/map/map_context.cpp
             source/scwx/qt/map/map_widget.cpp
             source/scwx/qt/map/overlay_layer.cpp
             source/scwx/qt/map/radar_product_layer.cpp
@@ -101,10 +107,13 @@ set(SRC_UI source/scwx/qt/ui/flow_layout.cpp
            source/scwx/qt/ui/level3_products_widget.cpp)
 set(HDR_UTIL source/scwx/qt/util/font.hpp
              source/scwx/qt/util/font_buffer.hpp
-             source/scwx/qt/util/json.hpp)
+             source/scwx/qt/util/json.hpp
+             source/scwx/qt/util/streams.hpp
+             source/scwx/qt/util/texture_atlas.hpp)
 set(SRC_UTIL source/scwx/qt/util/font.cpp
              source/scwx/qt/util/font_buffer.cpp
-             source/scwx/qt/util/json.cpp)
+             source/scwx/qt/util/json.cpp
+             source/scwx/qt/util/texture_atlas.cpp)
 set(HDR_VIEW source/scwx/qt/view/level2_product_view.hpp
              source/scwx/qt/view/level3_product_view.hpp
              source/scwx/qt/view/level3_radial_view.hpp
@@ -122,12 +131,14 @@ set(RESOURCE_FILES scwx-qt.qrc)
 
 set(SHADER_FILES gl/color.frag
                  gl/color.vert
+                 gl/geo_line.vert
                  gl/radar.frag
                  gl/radar.vert
                  gl/text.frag
                  gl/text.vert
                  gl/texture1d.frag
-                 gl/texture1d.vert)
+                 gl/texture1d.vert
+                 gl/texture2d.frag)
 
 set(CMAKE_FILES scwx-qt.cmake)
 
@@ -139,6 +150,7 @@ set(PROJECT_SOURCES ${HDR_MAIN}
                     ${SRC_MAIN}
                     ${HDR_CONFIG}
                     ${SRC_CONFIG}
+                    ${SRC_EXTERNAL}
                     ${HDR_GL}
                     ${SRC_GL}
                     ${HDR_GL_DRAW}
@@ -173,6 +185,7 @@ source_group("Header Files\\main"     FILES ${HDR_MAIN})
 source_group("Source Files\\main"     FILES ${SRC_MAIN})
 source_group("Header Files\\config"   FILES ${HDR_CONFIG})
 source_group("Source Files\\config"   FILES ${SRC_CONFIG})
+source_group("Source Files\\external" FILES ${SRC_EXTERNAL})
 source_group("Header Files\\gl"       FILES ${HDR_GL})
 source_group("Source Files\\gl"       FILES ${SRC_GL})
 source_group("Header Files\\gl\\draw" FILES ${HDR_GL_DRAW})
@@ -223,7 +236,8 @@ endif()
 
 target_include_directories(scwx-qt PUBLIC ${scwx-qt_SOURCE_DIR}/source
                                           ${FTGL_INCLUDE_DIR}
-                                          ${MBGL_INCLUDE_DIR})
+                                          ${MBGL_INCLUDE_DIR}
+                                          ${STB_INCLUDE_DIR})
 
 target_include_directories(supercell-wx PUBLIC ${scwx-qt_SOURCE_DIR}/source)
 
diff --git a/scwx-qt/scwx-qt.qrc b/scwx-qt/scwx-qt.qrc
index d3386855..cf31a039 100644
--- a/scwx-qt/scwx-qt.qrc
+++ b/scwx-qt/scwx-qt.qrc
@@ -2,16 +2,20 @@
     
         gl/color.frag
         gl/color.vert
+        gl/geo_line.vert
         gl/radar.frag
         gl/radar.vert
         gl/text.frag
         gl/text.vert
         gl/texture1d.frag
         gl/texture1d.vert
+        gl/texture2d.frag
         res/config/radar_sites.json
         res/fonts/din1451alt.ttf
         res/fonts/din1451alt_g.ttf
         res/icons/font-awesome-6/square-minus-regular.svg
         res/icons/font-awesome-6/square-plus-regular.svg
+        res/textures/lines/default-1x7.png
+        res/textures/lines/test-pattern.png
     
 
diff --git a/scwx-qt/source/scwx/qt/external/stb_rect_pack.cpp b/scwx-qt/source/scwx/qt/external/stb_rect_pack.cpp
new file mode 100644
index 00000000..32d48134
--- /dev/null
+++ b/scwx-qt/source/scwx/qt/external/stb_rect_pack.cpp
@@ -0,0 +1,2 @@
+#define STB_RECT_PACK_IMPLEMENTATION
+#include 
diff --git a/scwx-qt/source/scwx/qt/gl/draw/draw_item.cpp b/scwx-qt/source/scwx/qt/gl/draw/draw_item.cpp
index ca4533b6..23ddbc8a 100644
--- a/scwx-qt/source/scwx/qt/gl/draw/draw_item.cpp
+++ b/scwx-qt/source/scwx/qt/gl/draw/draw_item.cpp
@@ -2,6 +2,13 @@
 
 #include 
 
+#pragma warning(push, 0)
+#include 
+#include 
+#include 
+#include 
+#pragma warning(pop)
+
 namespace scwx
 {
 namespace qt
@@ -13,20 +20,76 @@ namespace draw
 
 static const std::string logPrefix_ = "scwx::qt::gl::draw::draw_item";
 
-class DrawItemImpl
+class DrawItem::Impl
 {
 public:
-   explicit DrawItemImpl() {}
+   explicit Impl(OpenGLFunctions& gl) : gl_ {gl} {}
+   ~Impl() {}
 
-   ~DrawItemImpl() {}
+   OpenGLFunctions& gl_;
 };
 
-DrawItem::DrawItem() : p(std::make_unique()) {}
+DrawItem::DrawItem(OpenGLFunctions& gl) : p(std::make_unique(gl)) {}
 DrawItem::~DrawItem() = default;
 
-DrawItem::DrawItem(DrawItem&&) noexcept = default;
+DrawItem::DrawItem(DrawItem&&) noexcept            = default;
 DrawItem& DrawItem::operator=(DrawItem&&) noexcept = default;
 
+void DrawItem::UseDefaultProjection(
+   const QMapbox::CustomLayerRenderParameters& params, GLint uMVPMatrixLocation)
+{
+   glm::mat4 projection = glm::ortho(0.0f,
+                                     static_cast(params.width),
+                                     0.0f,
+                                     static_cast(params.height));
+
+   p->gl_.glUniformMatrix4fv(
+      uMVPMatrixLocation, 1, GL_FALSE, glm::value_ptr(projection));
+}
+
+// TODO: Refactor to utility class
+static glm::vec2
+LatLongToScreenCoordinate(const QMapbox::Coordinate& coordinate)
+{
+   double latitude = std::clamp(
+      coordinate.first, -mbgl::util::LATITUDE_MAX, mbgl::util::LATITUDE_MAX);
+   glm::vec2 screen {
+      mbgl::util::LONGITUDE_MAX + coordinate.second,
+      -(mbgl::util::LONGITUDE_MAX -
+        mbgl::util::RAD2DEG *
+           std::log(std::tan(M_PI / 4.0 +
+                             latitude * M_PI / mbgl::util::DEGREES_MAX)))};
+   return screen;
+}
+
+void DrawItem::UseMapProjection(
+   const QMapbox::CustomLayerRenderParameters& params,
+   GLint                                       uMVPMatrixLocation,
+   GLint                                       uMapScreenCoordLocation)
+{
+   OpenGLFunctions& gl = p->gl_;
+
+   // TODO: Refactor to utility class
+   const float scale = std::pow(2.0, params.zoom) * 2.0f *
+                       mbgl::util::tileSize / mbgl::util::DEGREES_MAX;
+   const float xScale = scale / params.width;
+   const float yScale = scale / params.height;
+
+   glm::mat4 uMVPMatrix(1.0f);
+   uMVPMatrix = glm::scale(uMVPMatrix, glm::vec3(xScale, yScale, 1.0f));
+   uMVPMatrix = glm::rotate(uMVPMatrix,
+                            glm::radians(params.bearing),
+                            glm::vec3(0.0f, 0.0f, 1.0f));
+
+   gl.glUniform2fv(uMapScreenCoordLocation,
+                   1,
+                   glm::value_ptr(LatLongToScreenCoordinate(
+                      {params.latitude, params.longitude})));
+
+   gl.glUniformMatrix4fv(
+      uMVPMatrixLocation, 1, GL_FALSE, glm::value_ptr(uMVPMatrix));
+}
+
 } // namespace draw
 } // namespace gl
 } // namespace qt
diff --git a/scwx-qt/source/scwx/qt/gl/draw/draw_item.hpp b/scwx-qt/source/scwx/qt/gl/draw/draw_item.hpp
index 09e98092..02086c45 100644
--- a/scwx-qt/source/scwx/qt/gl/draw/draw_item.hpp
+++ b/scwx-qt/source/scwx/qt/gl/draw/draw_item.hpp
@@ -1,7 +1,11 @@
 #pragma once
 
+#include 
+
 #include 
 
+#include 
+
 namespace scwx
 {
 namespace qt
@@ -11,26 +15,33 @@ namespace gl
 namespace draw
 {
 
-class DrawItemImpl;
-
 class DrawItem
 {
 public:
-   explicit DrawItem();
+   explicit DrawItem(OpenGLFunctions& gl);
    ~DrawItem();
 
-   DrawItem(const DrawItem&) = delete;
+   DrawItem(const DrawItem&)            = delete;
    DrawItem& operator=(const DrawItem&) = delete;
 
    DrawItem(DrawItem&&) noexcept;
    DrawItem& operator=(DrawItem&&) noexcept;
 
-   virtual void Initialize()   = 0;
-   virtual void Render()       = 0;
-   virtual void Deinitialize() = 0;
+   virtual void Initialize()                                               = 0;
+   virtual void Render(const QMapbox::CustomLayerRenderParameters& params) = 0;
+   virtual void Deinitialize()                                             = 0;
+
+protected:
+   void UseDefaultProjection(const QMapbox::CustomLayerRenderParameters& params,
+                             GLint uMVPMatrixLocation);
+   void UseMapProjection(const QMapbox::CustomLayerRenderParameters& params,
+                         GLint uMVPMatrixLocation,
+                         GLint uMapScreenCoordLocation);
 
 private:
-   std::unique_ptr p;
+   class Impl;
+
+   std::unique_ptr p;
 };
 
 } // namespace draw
diff --git a/scwx-qt/source/scwx/qt/gl/draw/geo_line.cpp b/scwx-qt/source/scwx/qt/gl/draw/geo_line.cpp
new file mode 100644
index 00000000..b44463ff
--- /dev/null
+++ b/scwx-qt/source/scwx/qt/gl/draw/geo_line.cpp
@@ -0,0 +1,292 @@
+#include 
+#include 
+#include 
+#include 
+
+#include 
+
+namespace scwx
+{
+namespace qt
+{
+namespace gl
+{
+namespace draw
+{
+
+static const std::string logPrefix_ = "scwx::qt::gl::draw::geo_line";
+static const auto        logger_    = scwx::util::Logger::Create(logPrefix_);
+
+static constexpr size_t kNumRectangles        = 1;
+static constexpr size_t kNumTriangles         = kNumRectangles * 2;
+static constexpr size_t kVerticesPerTriangle  = 3;
+static constexpr size_t kVerticesPerRectangle = kVerticesPerTriangle * 2;
+static constexpr size_t kPointsPerVertex      = 10;
+static constexpr size_t kBufferLength =
+   kNumTriangles * kVerticesPerTriangle * kPointsPerVertex;
+
+class GeoLine::Impl
+{
+public:
+   explicit Impl(std::shared_ptr context) :
+       context_ {context},
+       dirty_ {false},
+       visible_ {true},
+       points_ {},
+       width_ {7.0f},
+       modulateColor_ {std::nullopt},
+       shaderProgram_ {nullptr},
+       uMVPMatrixLocation_(GL_INVALID_INDEX),
+       uMapMatrixLocation_(GL_INVALID_INDEX),
+       uMapScreenCoordLocation_(GL_INVALID_INDEX),
+       texture_ {},
+       vao_ {GL_INVALID_INDEX},
+       vbo_ {GL_INVALID_INDEX}
+   {
+   }
+
+   ~Impl() {}
+
+   std::shared_ptr context_;
+
+   bool dirty_;
+
+   bool                              visible_;
+   std::array points_;
+   float                             width_;
+
+   std::optional modulateColor_;
+
+   std::shared_ptr shaderProgram_;
+   GLint                          uMVPMatrixLocation_;
+   GLint                          uMapMatrixLocation_;
+   GLint                          uMapScreenCoordLocation_;
+
+   util::TextureAttributes texture_;
+
+   GLuint vao_;
+   GLuint vbo_;
+
+   void Update();
+};
+
+GeoLine::GeoLine(std::shared_ptr context) :
+    DrawItem(context->gl()), p(std::make_unique(context))
+{
+}
+GeoLine::~GeoLine() = default;
+
+GeoLine::GeoLine(GeoLine&&) noexcept            = default;
+GeoLine& GeoLine::operator=(GeoLine&&) noexcept = default;
+
+void GeoLine::Initialize()
+{
+   gl::OpenGLFunctions& gl = p->context_->gl();
+
+   p->shaderProgram_ = p->context_->GetShaderProgram(":/gl/geo_line.vert",
+                                                     ":/gl/texture2d.frag");
+
+   p->uMVPMatrixLocation_ =
+      gl.glGetUniformLocation(p->shaderProgram_->id(), "uMVPMatrix");
+   if (p->uMVPMatrixLocation_ == -1)
+   {
+      logger_->warn("Could not find uMVPMatrix");
+   }
+
+   p->uMapMatrixLocation_ =
+      gl.glGetUniformLocation(p->shaderProgram_->id(), "uMapMatrix");
+   if (p->uMapMatrixLocation_ == -1)
+   {
+      logger_->warn("Could not find uMapMatrix");
+   }
+
+   p->uMapScreenCoordLocation_ =
+      gl.glGetUniformLocation(p->shaderProgram_->id(), "uMapScreenCoord");
+   if (p->uMapScreenCoordLocation_ == -1)
+   {
+      logger_->warn("Could not find uMapScreenCoord");
+   }
+
+   p->texture_ =
+      util::TextureAtlas::Instance().GetTextureAttributes("lines/default-1x7");
+
+   gl.glGenVertexArrays(1, &p->vao_);
+   gl.glGenBuffers(1, &p->vbo_);
+
+   gl.glBindVertexArray(p->vao_);
+   gl.glBindBuffer(GL_ARRAY_BUFFER, p->vbo_);
+   gl.glBufferData(
+      GL_ARRAY_BUFFER, sizeof(float) * kBufferLength, 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);
+
+   // aTexCoord
+   gl.glVertexAttribPointer(2,
+                            2,
+                            GL_FLOAT,
+                            GL_FALSE,
+                            kPointsPerVertex * sizeof(float),
+                            reinterpret_cast(4 * sizeof(float)));
+   gl.glEnableVertexAttribArray(2);
+
+   // aModulate
+   gl.glVertexAttribPointer(3,
+                            4,
+                            GL_FLOAT,
+                            GL_FALSE,
+                            kPointsPerVertex * sizeof(float),
+                            reinterpret_cast(6 * sizeof(float)));
+   gl.glEnableVertexAttribArray(3);
+
+   p->dirty_ = true;
+}
+
+void GeoLine::Render(const QMapbox::CustomLayerRenderParameters& params)
+{
+   if (p->visible_)
+   {
+      gl::OpenGLFunctions& gl = p->context_->gl();
+
+      gl.glBindVertexArray(p->vao_);
+      gl.glBindBuffer(GL_ARRAY_BUFFER, p->vbo_);
+
+      p->Update();
+      p->shaderProgram_->Use();
+      UseDefaultProjection(params, p->uMVPMatrixLocation_);
+      UseMapProjection(
+         params, p->uMapMatrixLocation_, p->uMapScreenCoordLocation_);
+
+      // Draw line
+      gl.glDrawArrays(GL_TRIANGLES, 0, 6);
+   }
+}
+
+void GeoLine::Deinitialize()
+{
+   gl::OpenGLFunctions& gl = p->context_->gl();
+
+   gl.glDeleteVertexArrays(1, &p->vao_);
+   gl.glDeleteBuffers(1, &p->vbo_);
+}
+
+void GeoLine::SetPoints(float latitude1,
+                        float longitude1,
+                        float latitude2,
+                        float longitude2)
+{
+   if (p->points_[0].latitude_ != latitude1 ||
+       p->points_[0].longitude_ != longitude1 ||
+       p->points_[1].latitude_ != latitude2 ||
+       p->points_[1].longitude_ != longitude2)
+   {
+      p->points_[0] = {latitude1, longitude1};
+      p->points_[1] = {latitude2, longitude2};
+      p->dirty_     = true;
+   }
+}
+
+void GeoLine::SetModulateColor(boost::gil::rgba8_pixel_t color)
+{
+   if (p->modulateColor_ != color)
+   {
+      p->modulateColor_ = color;
+      p->dirty_         = true;
+   }
+}
+
+void GeoLine::SetWidth(float width)
+{
+   if (p->width_ != width)
+   {
+      p->width_ = width;
+      p->dirty_ = true;
+   }
+}
+
+void GeoLine::SetVisible(bool visible)
+{
+   p->visible_ = visible;
+}
+
+void GeoLine::Impl::Update()
+{
+   if (dirty_)
+   {
+      gl::OpenGLFunctions& gl = context_->gl();
+
+      // Latitude and longitude coordinates in degrees
+      const float lx = points_[0].latitude_;
+      const float rx = points_[1].latitude_;
+      const float by = points_[0].longitude_;
+      const float ty = points_[1].longitude_;
+
+      // Offset x/y in pixels
+      const double i     = points_[1].longitude_ - points_[0].longitude_;
+      const double j     = points_[1].latitude_ - points_[0].latitude_;
+      const double angle = std::atan2(i, j) * 180.0 / M_PI;
+      const float  ox    = width_ * 0.5f * std::cosf(angle);
+      const float  oy    = width_ * 0.5f * std::sinf(angle);
+
+      // Texture coordinates
+      const float ls = texture_.sLeft_;
+      const float rs = texture_.sRight_;
+      const float tt = texture_.tTop_;
+      const float bt = texture_.tBottom_;
+
+      float mc0 = 1.0f;
+      float mc1 = 1.0f;
+      float mc2 = 1.0f;
+      float mc3 = 1.0f;
+
+      if (modulateColor_.has_value())
+      {
+         boost::gil::rgba8_pixel_t& mc = modulateColor_.value();
+
+         mc0 = mc[0] / 255.0f;
+         mc1 = mc[1] / 255.0f;
+         mc2 = mc[2] / 255.0f;
+         mc3 = mc[3] / 255.0f;
+      }
+
+      const float buffer[kNumRectangles][kVerticesPerRectangle]
+                        [kPointsPerVertex] = //
+         {                                   //
+          // Line
+          {
+             {lx, by, -ox, -oy, ls, bt, mc0, mc1, mc2, mc3}, // BL
+             {lx, by, +ox, +oy, ls, tt, mc0, mc1, mc2, mc3}, // TL
+             {rx, ty, -ox, -oy, rs, bt, mc0, mc1, mc2, mc3}, // BR
+             {rx, ty, -ox, -oy, rs, bt, mc0, mc1, mc2, mc3}, // BR
+             {rx, ty, +ox, +oy, rs, tt, mc0, mc1, mc2, mc3}, // TR
+             {lx, by, +ox, +oy, ls, tt, mc0, mc1, mc2, mc3}  // TL
+          }};
+
+      gl.glBufferData(GL_ARRAY_BUFFER,
+                      sizeof(float) * kBufferLength,
+                      buffer,
+                      GL_DYNAMIC_DRAW);
+
+      dirty_ = false;
+   }
+}
+
+} // namespace draw
+} // namespace gl
+} // namespace qt
+} // namespace scwx
diff --git a/scwx-qt/source/scwx/qt/gl/draw/geo_line.hpp b/scwx-qt/source/scwx/qt/gl/draw/geo_line.hpp
new file mode 100644
index 00000000..9ae14efb
--- /dev/null
+++ b/scwx-qt/source/scwx/qt/gl/draw/geo_line.hpp
@@ -0,0 +1,77 @@
+#pragma once
+
+#include 
+#include 
+
+#include 
+
+namespace scwx
+{
+namespace qt
+{
+namespace gl
+{
+namespace draw
+{
+
+class GeoLine : public DrawItem
+{
+public:
+   explicit GeoLine(std::shared_ptr context);
+   ~GeoLine();
+
+   GeoLine(const GeoLine&)            = delete;
+   GeoLine& operator=(const GeoLine&) = delete;
+
+   GeoLine(GeoLine&&) noexcept;
+   GeoLine& operator=(GeoLine&&) noexcept;
+
+   void Initialize() override;
+   void Render(const QMapbox::CustomLayerRenderParameters& params) override;
+   void Deinitialize() override;
+
+   /**
+    * Sets the geographic coordinate endpoints associated with the line.
+    *
+    * @param latitude1 Latitude of the first endpoint in degrees
+    * @param longitude1 Longitude of the first endpoint in degrees
+    * @param latitude2 Latitude of the second endpoint in degrees
+    * @param longitude2 Longitude of the second endpoint in degrees
+    */
+   void SetPoints(float latitude1,
+                  float longitude1,
+                  float latitude2,
+                  float longitude2);
+
+   /**
+    * Sets the modulate color of the line. If specified, the texture color will
+    * be multiplied by the modulate color to produce the result.
+    *
+    * @param color Modulate color (RGBA)
+    */
+   void SetModulateColor(boost::gil::rgba8_pixel_t color);
+
+   /**
+    * Sets the width of the line.
+    *
+    * @param width Width in pixels
+    */
+   void SetWidth(float width);
+
+   /**
+    * Sets the visibility of the line.
+    *
+    * @param visible
+    */
+   void SetVisible(bool visible);
+
+private:
+   class Impl;
+
+   std::unique_ptr p;
+};
+
+} // namespace draw
+} // namespace gl
+} // namespace qt
+} // namespace scwx
diff --git a/scwx-qt/source/scwx/qt/gl/draw/rectangle.cpp b/scwx-qt/source/scwx/qt/gl/draw/rectangle.cpp
index 10988613..f8b5756a 100644
--- a/scwx-qt/source/scwx/qt/gl/draw/rectangle.cpp
+++ b/scwx-qt/source/scwx/qt/gl/draw/rectangle.cpp
@@ -1,4 +1,5 @@
 #include 
+#include 
 
 #include 
 
@@ -12,6 +13,7 @@ namespace draw
 {
 
 static const std::string logPrefix_ = "scwx::qt::gl::draw::rectangle";
+static const auto        logger_    = scwx::util::Logger::Create(logPrefix_);
 
 static constexpr size_t NUM_RECTANGLES         = 5;
 static constexpr size_t NUM_TRIANGLES          = NUM_RECTANGLES * 2;
@@ -21,11 +23,11 @@ static constexpr size_t POINTS_PER_VERTEX      = 7;
 static constexpr size_t BUFFER_LENGTH =
    NUM_TRIANGLES * VERTICES_PER_TRIANGLE * POINTS_PER_VERTEX;
 
-class RectangleImpl
+class Rectangle::Impl
 {
 public:
-   explicit RectangleImpl(OpenGLFunctions& gl) :
-       gl_ {gl},
+   explicit Impl(std::shared_ptr context) :
+       context_ {context},
        dirty_ {false},
        visible_ {true},
        x_ {0.0f},
@@ -36,14 +38,16 @@ public:
        borderColor_ {0, 0, 0, 0},
        borderWidth_ {0.0f},
        fillColor_ {std::nullopt},
+       shaderProgram_ {nullptr},
+       uMVPMatrixLocation_(GL_INVALID_INDEX),
        vao_ {GL_INVALID_INDEX},
        vbo_ {GL_INVALID_INDEX}
    {
    }
 
-   ~RectangleImpl() {}
+   ~Impl() {}
 
-   OpenGLFunctions& gl_;
+   std::shared_ptr context_;
 
    bool dirty_;
 
@@ -59,25 +63,37 @@ public:
 
    std::optional fillColor_;
 
+   std::shared_ptr shaderProgram_;
+   GLint                          uMVPMatrixLocation_;
+
    GLuint vao_;
    GLuint vbo_;
 
    void Update();
 };
 
-// TODO: OpenGL context with shaders
-Rectangle::Rectangle(OpenGLFunctions& gl) :
-    DrawItem(), p(std::make_unique(gl))
+Rectangle::Rectangle(std::shared_ptr context) :
+    DrawItem(context->gl()), p(std::make_unique(context))
 {
 }
 Rectangle::~Rectangle() = default;
 
-Rectangle::Rectangle(Rectangle&&) noexcept = default;
+Rectangle::Rectangle(Rectangle&&) noexcept            = default;
 Rectangle& Rectangle::operator=(Rectangle&&) noexcept = default;
 
 void Rectangle::Initialize()
 {
-   gl::OpenGLFunctions& gl = p->gl_;
+   gl::OpenGLFunctions& gl = p->context_->gl();
+
+   p->shaderProgram_ =
+      p->context_->GetShaderProgram(":/gl/color.vert", ":/gl/color.frag");
+
+   p->uMVPMatrixLocation_ =
+      gl.glGetUniformLocation(p->shaderProgram_->id(), "uMVPMatrix");
+   if (p->uMVPMatrixLocation_ == -1)
+   {
+      logger_->warn("Could not find uMVPMatrix");
+   }
 
    gl.glGenVertexArrays(1, &p->vao_);
    gl.glGenBuffers(1, &p->vbo_);
@@ -106,16 +122,18 @@ void Rectangle::Initialize()
    p->dirty_ = true;
 }
 
-void Rectangle::Render()
+void Rectangle::Render(const QMapbox::CustomLayerRenderParameters& params)
 {
    if (p->visible_)
    {
-      gl::OpenGLFunctions& gl = p->gl_;
+      gl::OpenGLFunctions& gl = p->context_->gl();
 
       gl.glBindVertexArray(p->vao_);
       gl.glBindBuffer(GL_ARRAY_BUFFER, p->vbo_);
 
       p->Update();
+      p->shaderProgram_->Use();
+      UseDefaultProjection(params, p->uMVPMatrixLocation_);
 
       if (p->fillColor_.has_value())
       {
@@ -133,7 +151,7 @@ void Rectangle::Render()
 
 void Rectangle::Deinitialize()
 {
-   gl::OpenGLFunctions& gl = p->gl_;
+   gl::OpenGLFunctions& gl = p->context_->gl();
 
    gl.glDeleteVertexArrays(1, &p->vao_);
    gl.glDeleteBuffers(1, &p->vbo_);
@@ -184,11 +202,11 @@ void Rectangle::SetVisible(bool visible)
    p->visible_ = visible;
 }
 
-void RectangleImpl::Update()
+void Rectangle::Impl::Update()
 {
    if (dirty_)
    {
-      gl::OpenGLFunctions& gl = gl_;
+      gl::OpenGLFunctions& gl = context_->gl();
 
       const float lox = x_;
       const float rox = x_ + width_;
diff --git a/scwx-qt/source/scwx/qt/gl/draw/rectangle.hpp b/scwx-qt/source/scwx/qt/gl/draw/rectangle.hpp
index f1214309..481f7507 100644
--- a/scwx-qt/source/scwx/qt/gl/draw/rectangle.hpp
+++ b/scwx-qt/source/scwx/qt/gl/draw/rectangle.hpp
@@ -1,6 +1,6 @@
 #pragma once
 
-#include 
+#include 
 #include 
 
 #include 
@@ -14,22 +14,20 @@ namespace gl
 namespace draw
 {
 
-class RectangleImpl;
-
 class Rectangle : public DrawItem
 {
 public:
-   explicit Rectangle(OpenGLFunctions& gl);
+   explicit Rectangle(std::shared_ptr context);
    ~Rectangle();
 
-   Rectangle(const Rectangle&) = delete;
+   Rectangle(const Rectangle&)            = delete;
    Rectangle& operator=(const Rectangle&) = delete;
 
    Rectangle(Rectangle&&) noexcept;
    Rectangle& operator=(Rectangle&&) noexcept;
 
    void Initialize() override;
-   void Render() override;
+   void Render(const QMapbox::CustomLayerRenderParameters& params) override;
    void Deinitialize() override;
 
    void SetBorder(float width, boost::gil::rgba8_pixel_t color);
@@ -39,7 +37,9 @@ public:
    void SetVisible(bool visible);
 
 private:
-   std::unique_ptr p;
+   class Impl;
+
+   std::unique_ptr p;
 };
 
 } // namespace draw
diff --git a/scwx-qt/source/scwx/qt/gl/gl_context.cpp b/scwx-qt/source/scwx/qt/gl/gl_context.cpp
new file mode 100644
index 00000000..d0e0fd80
--- /dev/null
+++ b/scwx-qt/source/scwx/qt/gl/gl_context.cpp
@@ -0,0 +1,90 @@
+#include 
+#include 
+#include 
+#include 
+
+namespace scwx
+{
+namespace qt
+{
+namespace gl
+{
+
+static const std::string logPrefix_ = "scwx::qt::gl::gl_context";
+
+class GlContext::Impl
+{
+public:
+   explicit Impl() :
+       gl_ {},
+       shaderProgramMap_ {},
+       shaderProgramMutex_ {},
+       textureAtlas_ {GL_INVALID_INDEX},
+       textureMutex_ {}
+   {
+   }
+   ~Impl() {}
+
+   gl::OpenGLFunctions gl_;
+
+   std::unordered_map,
+                      std::shared_ptr,
+                      scwx::util::hash>>
+              shaderProgramMap_;
+   std::mutex shaderProgramMutex_;
+
+   GLuint     textureAtlas_;
+   std::mutex textureMutex_;
+};
+
+GlContext::GlContext() : p(std::make_unique()) {}
+GlContext::~GlContext() = default;
+
+GlContext::GlContext(GlContext&&) noexcept            = default;
+GlContext& GlContext::operator=(GlContext&&) noexcept = default;
+
+gl::OpenGLFunctions& GlContext::gl()
+{
+   return p->gl_;
+}
+
+std::shared_ptr
+GlContext::GetShaderProgram(const std::string& vertexPath,
+                            const std::string& fragmentPath)
+{
+   const std::pair key {vertexPath, fragmentPath};
+   std::shared_ptr        shaderProgram;
+
+   std::unique_lock lock(p->shaderProgramMutex_);
+
+   auto it = p->shaderProgramMap_.find(key);
+
+   if (it == p->shaderProgramMap_.end())
+   {
+      shaderProgram = std::make_shared(p->gl_);
+      shaderProgram->Load(vertexPath, fragmentPath);
+      p->shaderProgramMap_[key] = shaderProgram;
+   }
+   else
+   {
+      shaderProgram = it->second;
+   }
+
+   return shaderProgram;
+}
+
+GLuint GlContext::GetTextureAtlas()
+{
+   std::unique_lock lock(p->textureMutex_);
+
+   if (p->textureAtlas_ == GL_INVALID_INDEX)
+   {
+      p->textureAtlas_ = util::TextureAtlas::Instance().BufferAtlas(p->gl_);
+   }
+
+   return p->textureAtlas_;
+}
+
+} // namespace gl
+} // namespace qt
+} // namespace scwx
diff --git a/scwx-qt/source/scwx/qt/gl/gl_context.hpp b/scwx-qt/source/scwx/qt/gl/gl_context.hpp
new file mode 100644
index 00000000..623b5855
--- /dev/null
+++ b/scwx-qt/source/scwx/qt/gl/gl_context.hpp
@@ -0,0 +1,41 @@
+#pragma once
+
+#include 
+#include 
+
+namespace scwx
+{
+namespace qt
+{
+namespace gl
+{
+
+class GlContext
+{
+public:
+   explicit GlContext();
+   virtual ~GlContext();
+
+   GlContext(const GlContext&)            = delete;
+   GlContext& operator=(const GlContext&) = delete;
+
+   GlContext(GlContext&&) noexcept;
+   GlContext& operator=(GlContext&&) noexcept;
+
+   gl::OpenGLFunctions& gl();
+
+   std::shared_ptr
+   GetShaderProgram(const std::string& vertexPath,
+                    const std::string& fragmentPath);
+
+   GLuint GetTextureAtlas();
+
+private:
+   class Impl;
+
+   std::unique_ptr p;
+};
+
+} // namespace gl
+} // namespace qt
+} // namespace scwx
diff --git a/scwx-qt/source/scwx/qt/gl/shader_program.cpp b/scwx-qt/source/scwx/qt/gl/shader_program.cpp
index bbf0a73b..1d9e2143 100644
--- a/scwx-qt/source/scwx/qt/gl/shader_program.cpp
+++ b/scwx-qt/source/scwx/qt/gl/shader_program.cpp
@@ -13,19 +13,18 @@ namespace gl
 static const std::string logPrefix_ = "scwx::qt::gl::shader_program";
 static const auto        logger_    = scwx::util::Logger::Create(logPrefix_);
 
-static constexpr GLsizei INFO_LOG_BUF_SIZE = 512;
+static constexpr GLsizei kInfoLogBufSize = 512;
 
-class ShaderProgramImpl
+class ShaderProgram::Impl
 {
 public:
-   explicit ShaderProgramImpl(OpenGLFunctions& gl) :
-       gl_(gl), id_ {GL_INVALID_INDEX}
+   explicit Impl(OpenGLFunctions& gl) : gl_(gl), id_ {GL_INVALID_INDEX}
    {
       // Create shader program
       id_ = gl_.glCreateProgram();
    }
 
-   ~ShaderProgramImpl()
+   ~Impl()
    {
       // Delete shader program
       gl_.glDeleteProgram(id_);
@@ -37,12 +36,12 @@ public:
 };
 
 ShaderProgram::ShaderProgram(OpenGLFunctions& gl) :
-    p(std::make_unique(gl))
+    p(std::make_unique(gl))
 {
 }
 ShaderProgram::~ShaderProgram() = default;
 
-ShaderProgram::ShaderProgram(ShaderProgram&&) noexcept = default;
+ShaderProgram::ShaderProgram(ShaderProgram&&) noexcept            = default;
 ShaderProgram& ShaderProgram::operator=(ShaderProgram&&) noexcept = default;
 
 GLuint ShaderProgram::id() const
@@ -59,7 +58,7 @@ bool ShaderProgram::Load(const std::string& vertexPath,
 
    GLint   glSuccess;
    bool    success = true;
-   char    infoLog[INFO_LOG_BUF_SIZE];
+   char    infoLog[kInfoLogBufSize];
    GLsizei logLength;
 
    QFile vertexFile(vertexPath.c_str());
@@ -102,7 +101,7 @@ bool ShaderProgram::Load(const std::string& vertexPath,
 
    // Check for errors
    gl.glGetShaderiv(vertexShader, GL_COMPILE_STATUS, &glSuccess);
-   gl.glGetShaderInfoLog(vertexShader, INFO_LOG_BUF_SIZE, &logLength, infoLog);
+   gl.glGetShaderInfoLog(vertexShader, kInfoLogBufSize, &logLength, infoLog);
    if (!glSuccess)
    {
       logger_->error("Vertex shader compilation failed: {}", infoLog);
@@ -122,8 +121,7 @@ bool ShaderProgram::Load(const std::string& vertexPath,
 
    // Check for errors
    gl.glGetShaderiv(fragmentShader, GL_COMPILE_STATUS, &glSuccess);
-   gl.glGetShaderInfoLog(
-      fragmentShader, INFO_LOG_BUF_SIZE, &logLength, infoLog);
+   gl.glGetShaderInfoLog(fragmentShader, kInfoLogBufSize, &logLength, infoLog);
    if (!glSuccess)
    {
       logger_->error("Fragment shader compilation failed: {}", infoLog);
@@ -142,7 +140,7 @@ bool ShaderProgram::Load(const std::string& vertexPath,
 
       // Check for errors
       gl.glGetProgramiv(p->id_, GL_LINK_STATUS, &glSuccess);
-      gl.glGetProgramInfoLog(p->id_, INFO_LOG_BUF_SIZE, &logLength, infoLog);
+      gl.glGetProgramInfoLog(p->id_, kInfoLogBufSize, &logLength, infoLog);
       if (!glSuccess)
       {
          logger_->error("Shader program link failed: {}", infoLog);
diff --git a/scwx-qt/source/scwx/qt/gl/shader_program.hpp b/scwx-qt/source/scwx/qt/gl/shader_program.hpp
index 386cb627..9f84053e 100644
--- a/scwx-qt/source/scwx/qt/gl/shader_program.hpp
+++ b/scwx-qt/source/scwx/qt/gl/shader_program.hpp
@@ -16,15 +16,13 @@ namespace qt
 namespace gl
 {
 
-class ShaderProgramImpl;
-
 class ShaderProgram
 {
 public:
    explicit ShaderProgram(OpenGLFunctions& gl);
    virtual ~ShaderProgram();
 
-   ShaderProgram(const ShaderProgram&) = delete;
+   ShaderProgram(const ShaderProgram&)            = delete;
    ShaderProgram& operator=(const ShaderProgram&) = delete;
 
    ShaderProgram(ShaderProgram&&) noexcept;
@@ -37,7 +35,9 @@ public:
    void Use() const;
 
 private:
-   std::unique_ptr p;
+   class Impl;
+
+   std::unique_ptr p;
 };
 
 } // namespace gl
diff --git a/scwx-qt/source/scwx/qt/gl/text_shader.cpp b/scwx-qt/source/scwx/qt/gl/text_shader.cpp
index 2014da68..ec8cdcf1 100644
--- a/scwx-qt/source/scwx/qt/gl/text_shader.cpp
+++ b/scwx-qt/source/scwx/qt/gl/text_shader.cpp
@@ -1,4 +1,5 @@
 #include 
+#include 
 #include 
 
 #pragma warning(push, 0)
@@ -18,41 +19,46 @@ static const auto        logger_    = scwx::util::Logger::Create(logPrefix_);
 class TextShaderImpl
 {
 public:
-   explicit TextShaderImpl(OpenGLFunctions& gl) :
-       gl_ {gl}, projectionLocation_(GL_INVALID_INDEX)
+   explicit TextShaderImpl(std::shared_ptr context) :
+       context_ {context},
+       shaderProgram_ {nullptr},
+       projectionLocation_(GL_INVALID_INDEX)
    {
    }
 
    ~TextShaderImpl() {}
 
-   OpenGLFunctions& gl_;
+   std::shared_ptr     context_;
+   std::shared_ptr shaderProgram_;
 
    GLint projectionLocation_;
 };
 
-TextShader::TextShader(OpenGLFunctions& gl) :
-    ShaderProgram(gl), p(std::make_unique(gl))
+TextShader::TextShader(std::shared_ptr context) :
+    p(std::make_unique(context))
 {
 }
 TextShader::~TextShader() = default;
 
-TextShader::TextShader(TextShader&&) noexcept = default;
+TextShader::TextShader(TextShader&&) noexcept            = default;
 TextShader& TextShader::operator=(TextShader&&) noexcept = default;
 
 bool TextShader::Initialize()
 {
-   OpenGLFunctions& gl = p->gl_;
+   OpenGLFunctions& gl = p->context_->gl();
 
    // Load and configure shader
-   bool success = Load(":/gl/text.vert", ":/gl/text.frag");
+   p->shaderProgram_ =
+      p->context_->GetShaderProgram(":/gl/text.vert", ":/gl/text.frag");
 
-   p->projectionLocation_ = gl.glGetUniformLocation(id(), "projection");
+   p->projectionLocation_ =
+      gl.glGetUniformLocation(p->shaderProgram_->id(), "projection");
    if (p->projectionLocation_ == -1)
    {
       logger_->warn("Could not find projection");
    }
 
-   return success;
+   return true;
 }
 
 void TextShader::RenderText(const std::string&               text,
@@ -65,9 +71,9 @@ void TextShader::RenderText(const std::string&               text,
                             GLuint                           textureId,
                             TextAlign                        align)
 {
-   OpenGLFunctions& gl = p->gl_;
+   OpenGLFunctions& gl = p->context_->gl();
 
-   Use();
+   p->shaderProgram_->Use();
 
    gl.glEnable(GL_BLEND);
    gl.glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
@@ -103,7 +109,7 @@ void TextShader::RenderText(const std::string&               text,
 
 void TextShader::SetProjection(const glm::mat4& projection)
 {
-   p->gl_.glUniformMatrix4fv(
+   p->context_->gl().glUniformMatrix4fv(
       p->projectionLocation_, 1, GL_FALSE, glm::value_ptr(projection));
 }
 
diff --git a/scwx-qt/source/scwx/qt/gl/text_shader.hpp b/scwx-qt/source/scwx/qt/gl/text_shader.hpp
index a64f6d79..ed81907a 100644
--- a/scwx-qt/source/scwx/qt/gl/text_shader.hpp
+++ b/scwx-qt/source/scwx/qt/gl/text_shader.hpp
@@ -1,6 +1,6 @@
 #pragma once
 
-#include 
+#include 
 #include 
 
 #include 
@@ -24,13 +24,13 @@ enum class TextAlign
 
 class TextShaderImpl;
 
-class TextShader : public ShaderProgram
+class TextShader
 {
 public:
-   explicit TextShader(OpenGLFunctions& gl);
+   explicit TextShader(std::shared_ptr context);
    ~TextShader();
 
-   TextShader(const TextShader&) = delete;
+   TextShader(const TextShader&)            = delete;
    TextShader& operator=(const TextShader&) = delete;
 
    TextShader(TextShader&&) noexcept;
diff --git a/scwx-qt/source/scwx/qt/manager/resource_manager.cpp b/scwx-qt/source/scwx/qt/manager/resource_manager.cpp
index 12d041d9..748a1194 100644
--- a/scwx-qt/source/scwx/qt/manager/resource_manager.cpp
+++ b/scwx-qt/source/scwx/qt/manager/resource_manager.cpp
@@ -1,5 +1,6 @@
 #include 
 #include 
+#include 
 
 namespace scwx
 {
@@ -20,6 +21,13 @@ static void LoadFonts()
 {
    util::Font::Create(":/res/fonts/din1451alt.ttf");
    util::Font::Create(":/res/fonts/din1451alt_g.ttf");
+
+   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");
+   textureAtlas.BuildAtlas(8, 8);
 }
 
 } // namespace ResourceManager
diff --git a/scwx-qt/source/scwx/qt/map/color_table_layer.cpp b/scwx-qt/source/scwx/qt/map/color_table_layer.cpp
index c307c463..07512fd7 100644
--- a/scwx-qt/source/scwx/qt/map/color_table_layer.cpp
+++ b/scwx-qt/source/scwx/qt/map/color_table_layer.cpp
@@ -21,7 +21,7 @@ class ColorTableLayerImpl
 {
 public:
    explicit ColorTableLayerImpl(std::shared_ptr context) :
-       shaderProgram_(context->gl_),
+       shaderProgram_(nullptr),
        uMVPMatrixLocation_(GL_INVALID_INDEX),
        vbo_ {GL_INVALID_INDEX},
        vao_ {GL_INVALID_INDEX},
@@ -31,7 +31,8 @@ public:
    }
    ~ColorTableLayerImpl() = default;
 
-   gl::ShaderProgram     shaderProgram_;
+   std::shared_ptr shaderProgram_;
+
    GLint                 uMVPMatrixLocation_;
    std::array vbo_;
    GLuint                vao_;
@@ -52,13 +53,14 @@ void ColorTableLayer::Initialize()
 {
    logger_->debug("Initialize()");
 
-   gl::OpenGLFunctions& gl = context()->gl_;
+   gl::OpenGLFunctions& gl = context()->gl();
 
    // Load and configure overlay shader
-   p->shaderProgram_.Load(":/gl/texture1d.vert", ":/gl/texture1d.frag");
+   p->shaderProgram_ =
+      context()->GetShaderProgram(":/gl/texture1d.vert", ":/gl/texture1d.frag");
 
    p->uMVPMatrixLocation_ =
-      gl.glGetUniformLocation(p->shaderProgram_.id(), "uMVPMatrix");
+      gl.glGetUniformLocation(p->shaderProgram_->id(), "uMVPMatrix");
    if (p->uMVPMatrixLocation_ == -1)
    {
       logger_->warn("Could not find uMVPMatrix");
@@ -66,7 +68,7 @@ void ColorTableLayer::Initialize()
 
    gl.glGenTextures(1, &p->texture_);
 
-   p->shaderProgram_.Use();
+   p->shaderProgram_->Use();
 
    // Generate a vertex array object
    gl.glGenVertexArrays(1, &p->vao_);
@@ -99,7 +101,7 @@ void ColorTableLayer::Initialize()
    gl.glVertexAttribPointer(1, 1, GL_FLOAT, GL_FALSE, 0, static_cast(0));
    gl.glEnableVertexAttribArray(1);
 
-   connect(context()->radarProductView_.get(),
+   connect(context()->radar_product_view().get(),
            &view::RadarProductView::ColorTableUpdated,
            this,
            [=]() { p->colorTableNeedsUpdate_ = true; });
@@ -107,10 +109,11 @@ void ColorTableLayer::Initialize()
 
 void ColorTableLayer::Render(const QMapbox::CustomLayerRenderParameters& params)
 {
-   gl::OpenGLFunctions& gl = context()->gl_;
+   gl::OpenGLFunctions& gl               = context()->gl();
+   auto                 radarProductView = context()->radar_product_view();
 
-   if (context()->radarProductView_ == nullptr ||
-       !context()->radarProductView_->IsInitialized())
+   if (context()->radar_product_view() == nullptr ||
+       !context()->radar_product_view()->IsInitialized())
    {
       // Defer rendering until view is initialized
       return;
@@ -121,14 +124,14 @@ void ColorTableLayer::Render(const QMapbox::CustomLayerRenderParameters& params)
                                      0.0f,
                                      static_cast(params.height));
 
-   p->shaderProgram_.Use();
+   p->shaderProgram_->Use();
 
    gl.glUniformMatrix4fv(
       p->uMVPMatrixLocation_, 1, GL_FALSE, glm::value_ptr(projection));
 
    if (p->colorTableNeedsUpdate_)
    {
-      p->colorTable_ = context()->radarProductView_->color_table();
+      p->colorTable_ = radarProductView->color_table();
 
       gl.glActiveTexture(GL_TEXTURE0);
       gl.glBindTexture(GL_TEXTURE_1D, p->texture_);
@@ -145,9 +148,8 @@ void ColorTableLayer::Render(const QMapbox::CustomLayerRenderParameters& params)
       gl.glGenerateMipmap(GL_TEXTURE_1D);
    }
 
-   if (p->colorTable_.size() > 0 &&
-       context()->radarProductView_->sweep_time() !=
-          std::chrono::system_clock::time_point())
+   if (p->colorTable_.size() > 0 && radarProductView->sweep_time() !=
+                                       std::chrono::system_clock::time_point())
    {
       // Color table panel vertices
       const float vertexLX       = 0.0f;
@@ -176,7 +178,7 @@ void ColorTableLayer::Deinitialize()
 {
    logger_->debug("Deinitialize()");
 
-   gl::OpenGLFunctions& gl = context()->gl_;
+   gl::OpenGLFunctions& gl = context()->gl();
 
    gl.glDeleteVertexArrays(1, &p->vao_);
    gl.glDeleteBuffers(2, p->vbo_.data());
diff --git a/scwx-qt/source/scwx/qt/map/draw_layer.cpp b/scwx-qt/source/scwx/qt/map/draw_layer.cpp
index 16e1311d..47a3f6c4 100644
--- a/scwx-qt/source/scwx/qt/map/draw_layer.cpp
+++ b/scwx-qt/source/scwx/qt/map/draw_layer.cpp
@@ -2,12 +2,6 @@
 #include 
 #include 
 
-#pragma warning(push, 0)
-#include 
-#include 
-#include 
-#pragma warning(pop)
-
 namespace scwx
 {
 namespace qt
@@ -22,16 +16,14 @@ class DrawLayerImpl
 {
 public:
    explicit DrawLayerImpl(std::shared_ptr context) :
-       shaderProgram_ {context->gl_}, uMVPMatrixLocation_(GL_INVALID_INDEX)
+       context_ {context}, drawList_ {}, textureAtlas_ {GL_INVALID_INDEX}
    {
    }
-
    ~DrawLayerImpl() {}
 
-   gl::ShaderProgram shaderProgram_;
-   GLint             uMVPMatrixLocation_;
-
+   std::shared_ptr                      context_;
    std::vector> drawList_;
+   GLuint                                           textureAtlas_;
 };
 
 DrawLayer::DrawLayer(std::shared_ptr context) :
@@ -42,20 +34,9 @@ DrawLayer::~DrawLayer() = default;
 
 void DrawLayer::Initialize()
 {
-   gl::OpenGLFunctions& gl = context()->gl_;
+   p->textureAtlas_ = p->context_->GetTextureAtlas();
 
-   p->shaderProgram_.Load(":/gl/color.vert", ":/gl/color.frag");
-
-   p->uMVPMatrixLocation_ =
-      gl.glGetUniformLocation(p->shaderProgram_.id(), "uMVPMatrix");
-   if (p->uMVPMatrixLocation_ == -1)
-   {
-      logger_->warn("Could not find uMVPMatrix");
-   }
-
-   p->shaderProgram_.Use();
-
-   for (auto item : p->drawList_)
+   for (auto& item : p->drawList_)
    {
       item->Initialize();
    }
@@ -63,27 +44,22 @@ void DrawLayer::Initialize()
 
 void DrawLayer::Render(const QMapbox::CustomLayerRenderParameters& params)
 {
-   gl::OpenGLFunctions& gl = context()->gl_;
+   gl::OpenGLFunctions& gl = p->context_->gl();
 
-   p->shaderProgram_.Use();
+   gl.glActiveTexture(GL_TEXTURE0);
+   gl.glBindTexture(GL_TEXTURE_2D, p->textureAtlas_);
 
-   glm::mat4 projection = glm::ortho(0.0f,
-                                     static_cast(params.width),
-                                     0.0f,
-                                     static_cast(params.height));
-
-   gl.glUniformMatrix4fv(
-      p->uMVPMatrixLocation_, 1, GL_FALSE, glm::value_ptr(projection));
-
-   for (auto item : p->drawList_)
+   for (auto& item : p->drawList_)
    {
-      item->Render();
+      item->Render(params);
    }
 }
 
 void DrawLayer::Deinitialize()
 {
-   for (auto item : p->drawList_)
+   p->textureAtlas_ = GL_INVALID_INDEX;
+
+   for (auto& item : p->drawList_)
    {
       item->Deinitialize();
    }
diff --git a/scwx-qt/source/scwx/qt/map/map_context.cpp b/scwx-qt/source/scwx/qt/map/map_context.cpp
new file mode 100644
index 00000000..68b94e8e
--- /dev/null
+++ b/scwx-qt/source/scwx/qt/map/map_context.cpp
@@ -0,0 +1,90 @@
+#include 
+
+namespace scwx
+{
+namespace qt
+{
+namespace map
+{
+
+class MapContext::Impl
+{
+public:
+   explicit Impl(std::shared_ptr radarProductView) :
+       settings_ {},
+       radarProductView_ {radarProductView},
+       radarProductGroup_ {common::RadarProductGroup::Unknown},
+       radarProduct_ {"???"},
+       radarProductCode_ {0}
+   {
+   }
+
+   ~Impl() {}
+
+   MapSettings                             settings_;
+   std::shared_ptr radarProductView_;
+   common::RadarProductGroup               radarProductGroup_;
+   std::string                             radarProduct_;
+   int16_t                                 radarProductCode_;
+};
+
+MapContext::MapContext(
+   std::shared_ptr radarProductView) :
+    p(std::make_unique(radarProductView))
+{
+}
+MapContext::~MapContext() = default;
+
+MapContext::MapContext(MapContext&&) noexcept            = default;
+MapContext& MapContext::operator=(MapContext&&) noexcept = default;
+
+MapSettings& MapContext::settings()
+{
+   return p->settings_;
+}
+
+std::shared_ptr MapContext::radar_product_view() const
+{
+   return p->radarProductView_;
+}
+
+common::RadarProductGroup MapContext::radar_product_group() const
+{
+   return p->radarProductGroup_;
+}
+
+std::string MapContext::radar_product() const
+{
+   return p->radarProduct_;
+}
+
+int16_t MapContext::radar_product_code() const
+{
+   return p->radarProductCode_;
+}
+
+void MapContext::set_radar_product_view(
+   std::shared_ptr radarProductView)
+{
+   p->radarProductView_ = radarProductView;
+}
+
+void MapContext::set_radar_product_group(
+   common::RadarProductGroup radarProductGroup)
+{
+   p->radarProductGroup_ = radarProductGroup;
+}
+
+void MapContext::set_radar_product(const std::string& radarProduct)
+{
+   p->radarProduct_ = radarProduct;
+}
+
+void MapContext::set_radar_product_code(int16_t radarProductCode)
+{
+   p->radarProductCode_ = radarProductCode;
+}
+
+} // namespace map
+} // namespace qt
+} // namespace scwx
diff --git a/scwx-qt/source/scwx/qt/map/map_context.hpp b/scwx-qt/source/scwx/qt/map/map_context.hpp
index 7a249286..47f3cc11 100644
--- a/scwx-qt/source/scwx/qt/map/map_context.hpp
+++ b/scwx-qt/source/scwx/qt/map/map_context.hpp
@@ -1,6 +1,6 @@
 #pragma once
 
-#include 
+#include 
 #include 
 #include 
 
@@ -11,32 +11,35 @@ namespace qt
 namespace map
 {
 
-struct MapContext
+class MapContext : public gl::GlContext
 {
+public:
    explicit MapContext(
-      std::shared_ptr radarProductView = nullptr) :
-       gl_ {},
-       settings_ {},
-       radarProductView_ {radarProductView},
-       radarProductGroup_ {common::RadarProductGroup::Unknown},
-       radarProduct_ {"???"},
-       radarProductCode_ {0}
-   {
-   }
-   ~MapContext() = default;
+      std::shared_ptr radarProductView = nullptr);
+   ~MapContext();
 
-   MapContext(const MapContext&) = delete;
+   MapContext(const MapContext&)            = delete;
    MapContext& operator=(const MapContext&) = delete;
 
-   MapContext(MapContext&&) noexcept = default;
-   MapContext& operator=(MapContext&&) noexcept = default;
+   MapContext(MapContext&&) noexcept;
+   MapContext& operator=(MapContext&&) noexcept;
 
-   gl::OpenGLFunctions                     gl_;
-   MapSettings                             settings_;
-   std::shared_ptr radarProductView_;
-   common::RadarProductGroup               radarProductGroup_;
-   std::string                             radarProduct_;
-   int16_t                                 radarProductCode_;
+   MapSettings&                            settings();
+   std::shared_ptr radar_product_view() const;
+   common::RadarProductGroup               radar_product_group() const;
+   std::string                             radar_product() const;
+   int16_t                                 radar_product_code() const;
+
+   void set_radar_product_view(
+      std::shared_ptr radarProductView);
+   void set_radar_product_group(common::RadarProductGroup radarProductGroup);
+   void set_radar_product(const std::string& radarProduct);
+   void set_radar_product_code(int16_t radarProductCode);
+
+private:
+   class Impl;
+
+   std::unique_ptr p;
 };
 
 } // namespace map
diff --git a/scwx-qt/source/scwx/qt/map/map_widget.cpp b/scwx-qt/source/scwx/qt/map/map_widget.cpp
index 1b60c240..b6f348cf 100644
--- a/scwx-qt/source/scwx/qt/map/map_widget.cpp
+++ b/scwx-qt/source/scwx/qt/map/map_widget.cpp
@@ -153,9 +153,11 @@ common::Level3ProductCategoryMap MapWidget::GetAvailableLevel3Categories()
 
 float MapWidget::GetElevation() const
 {
-   if (p->context_->radarProductView_ != nullptr)
+   auto radarProductView = p->context_->radar_product_view();
+
+   if (radarProductView != nullptr)
    {
-      return p->context_->radarProductView_->elevation();
+      return radarProductView->elevation();
    }
    else
    {
@@ -165,9 +167,11 @@ float MapWidget::GetElevation() const
 
 std::vector MapWidget::GetElevationCuts() const
 {
-   if (p->context_->radarProductView_ != nullptr)
+   auto radarProductView = p->context_->radar_product_view();
+
+   if (radarProductView != nullptr)
    {
-      return p->context_->radarProductView_->GetElevationCuts();
+      return radarProductView->GetElevationCuts();
    }
    else
    {
@@ -182,10 +186,12 @@ MapWidgetImpl::GetLevel2ProductOrDefault(const std::string& productName) const
 
    if (level2Product == common::Level2Product::Unknown)
    {
-      if (context_->radarProductView_ != nullptr)
+      auto radarProductView = context_->radar_product_view();
+
+      if (radarProductView != nullptr)
       {
-         level2Product = common::GetLevel2Product(
-            context_->radarProductView_->GetRadarProductName());
+         level2Product =
+            common::GetLevel2Product(radarProductView->GetRadarProductName());
       }
    }
 
@@ -218,9 +224,11 @@ std::vector MapWidget::GetLevel3Products()
 
 common::RadarProductGroup MapWidget::GetRadarProductGroup() const
 {
-   if (p->context_->radarProductView_ != nullptr)
+   auto radarProductView = p->context_->radar_product_view();
+
+   if (radarProductView != nullptr)
    {
-      return p->context_->radarProductView_->GetRadarProductGroup();
+      return radarProductView->GetRadarProductGroup();
    }
    else
    {
@@ -230,10 +238,11 @@ common::RadarProductGroup MapWidget::GetRadarProductGroup() const
 
 std::string MapWidget::GetRadarProductName() const
 {
+   auto radarProductView = p->context_->radar_product_view();
 
-   if (p->context_->radarProductView_ != nullptr)
+   if (radarProductView != nullptr)
    {
-      return p->context_->radarProductView_->GetRadarProductName();
+      return radarProductView->GetRadarProductName();
    }
    else
    {
@@ -255,9 +264,11 @@ std::shared_ptr MapWidget::GetRadarSite() const
 
 uint16_t MapWidget::GetVcp() const
 {
-   if (p->context_->radarProductView_ != nullptr)
+   auto radarProductView = p->context_->radar_product_view();
+
+   if (radarProductView != nullptr)
    {
-      return p->context_->radarProductView_->vcp();
+      return radarProductView->vcp();
    }
    else
    {
@@ -267,10 +278,12 @@ uint16_t MapWidget::GetVcp() const
 
 void MapWidget::SelectElevation(float elevation)
 {
-   if (p->context_->radarProductView_ != nullptr)
+   auto radarProductView = p->context_->radar_product_view();
+
+   if (radarProductView != nullptr)
    {
-      p->context_->radarProductView_->SelectElevation(elevation);
-      p->context_->radarProductView_->Update();
+      radarProductView->SelectElevation(elevation);
+      radarProductView->Update();
    }
 }
 
@@ -280,8 +293,7 @@ void MapWidget::SelectRadarProduct(common::RadarProductGroup group,
 {
    bool radarProductViewCreated = false;
 
-   std::shared_ptr& radarProductView =
-      p->context_->radarProductView_;
+   auto radarProductView = p->context_->radar_product_view();
 
    std::string productName {product};
 
@@ -304,12 +316,13 @@ void MapWidget::SelectRadarProduct(common::RadarProductGroup group,
        (radarProductView->GetRadarProductGroup() ==
            common::RadarProductGroup::Level2 &&
         radarProductView->GetRadarProductName() != productName) ||
-       p->context_->radarProductCode_ != productCode)
+       p->context_->radar_product_code() != productCode)
    {
       p->RadarProductViewDisconnect();
 
       radarProductView = view::RadarProductViewFactory::Create(
          group, productName, productCode, p->radarProductManager_);
+      p->context_->set_radar_product_view(radarProductView);
 
       p->RadarProductViewConnect();
 
@@ -320,9 +333,9 @@ void MapWidget::SelectRadarProduct(common::RadarProductGroup group,
       radarProductView->SelectProduct(productName);
    }
 
-   p->context_->radarProductGroup_ = group;
-   p->context_->radarProduct_      = productName;
-   p->context_->radarProductCode_  = productCode;
+   p->context_->set_radar_product_group(group);
+   p->context_->set_radar_product(productName);
+   p->context_->set_radar_product_code(productCode);
 
    if (radarProductView != nullptr)
    {
@@ -372,7 +385,7 @@ void MapWidget::SelectRadarProduct(
 
 void MapWidget::SetActive(bool isActive)
 {
-   p->context_->settings_.isActive_ = isActive;
+   p->context_->settings().isActive_ = isActive;
    update();
 }
 
@@ -382,11 +395,13 @@ void MapWidget::SetAutoRefresh(bool enabled)
    {
       p->autoRefreshEnabled_ = enabled;
 
-      if (p->autoRefreshEnabled_ && p->context_->radarProductView_ != nullptr)
+      auto radarProductView = p->context_->radar_product_view();
+
+      if (p->autoRefreshEnabled_ && radarProductView != nullptr)
       {
          p->radarProductManager_->EnableRefresh(
-            p->context_->radarProductView_->GetRadarProductGroup(),
-            p->context_->radarProductView_->GetRadarProductName(),
+            radarProductView->GetRadarProductGroup(),
+            radarProductView->GetRadarProductName(),
             true);
       }
    }
@@ -430,7 +445,9 @@ void MapWidget::AddLayers()
    }
    p->layerList_.clear();
 
-   if (p->context_->radarProductView_ != nullptr)
+   auto radarProductView = p->context_->radar_product_view();
+
+   if (radarProductView != nullptr)
    {
       p->radarProductLayer_ = std::make_shared(p->context_);
       p->colorTableLayer_   = std::make_shared(p->context_);
@@ -453,7 +470,7 @@ void MapWidget::AddLayers()
 
       p->AddLayer("radar", p->radarProductLayer_, before);
       RadarRangeLayer::Add(p->map_,
-                           p->context_->radarProductView_->range(),
+                           radarProductView->range(),
                            {radarSite->latitude(), radarSite->longitude()});
       p->AddLayer("colorTable", p->colorTableLayer_);
    }
@@ -572,7 +589,7 @@ void MapWidget::initializeGL()
    logger_->debug("initializeGL()");
 
    makeCurrent();
-   p->context_->gl_.initializeOpenGLFunctions();
+   p->context_->gl().initializeOpenGLFunctions();
 
    p->map_.reset(new QMapboxGL(nullptr, p->settings_, size(), pixelRatio()));
    connect(p->map_.get(),
@@ -637,9 +654,10 @@ void MapWidgetImpl::RadarProductManagerConnect()
              const std::string&                    product,
              std::chrono::system_clock::time_point latestTime)
          {
-            if (autoRefreshEnabled_ && context_->radarProductGroup_ == group &&
+            if (autoRefreshEnabled_ &&
+                context_->radar_product_group() == group &&
                 (group == common::RadarProductGroup::Level2 ||
-                 context_->radarProduct_ == product))
+                 context_->radar_product() == product))
             {
                // Create file request
                std::shared_ptr request =
@@ -698,16 +716,18 @@ void MapWidgetImpl::InitializeNewRadarProductView(
    util::async(
       [=]()
       {
+         auto radarProductView = context_->radar_product_view();
+
          std::string colorTableFile =
             manager::SettingsManager::palette_settings()->palette(colorPalette);
          if (!colorTableFile.empty())
          {
             std::shared_ptr colorTable =
                common::ColorTable::Load(colorTableFile);
-            context_->radarProductView_->LoadColorTable(colorTable);
+            radarProductView->LoadColorTable(colorTable);
          }
 
-         context_->radarProductView_->Initialize();
+         radarProductView->Initialize();
       });
 
    if (map_ != nullptr)
@@ -718,26 +738,28 @@ void MapWidgetImpl::InitializeNewRadarProductView(
 
 void MapWidgetImpl::RadarProductViewConnect()
 {
-   if (context_->radarProductView_ != nullptr)
+   auto radarProductView = context_->radar_product_view();
+
+   if (radarProductView != nullptr)
    {
       connect(
-         context_->radarProductView_.get(),
+         radarProductView.get(),
          &view::RadarProductView::ColorTableUpdated,
          this,
          [&]() { widget_->update(); },
          Qt::QueuedConnection);
       connect(
-         context_->radarProductView_.get(),
+         radarProductView.get(),
          &view::RadarProductView::SweepComputed,
          this,
-         [&]()
+         [=]()
          {
             std::shared_ptr radarSite =
                radarProductManager_->radar_site();
 
             RadarRangeLayer::Update(
                map_,
-               context_->radarProductView_->range(),
+               radarProductView->range(),
                {radarSite->latitude(), radarSite->longitude()});
             widget_->update();
             emit widget_->RadarSweepUpdated();
@@ -748,13 +770,15 @@ void MapWidgetImpl::RadarProductViewConnect()
 
 void MapWidgetImpl::RadarProductViewDisconnect()
 {
-   if (context_->radarProductView_ != nullptr)
+   auto radarProductView = context_->radar_product_view();
+
+   if (radarProductView != nullptr)
    {
-      disconnect(context_->radarProductView_.get(),
+      disconnect(radarProductView.get(),
                  &view::RadarProductView::ColorTableUpdated,
                  this,
                  nullptr);
-      disconnect(context_->radarProductView_.get(),
+      disconnect(radarProductView.get(),
                  &view::RadarProductView::SweepComputed,
                  this,
                  nullptr);
diff --git a/scwx-qt/source/scwx/qt/map/overlay_layer.cpp b/scwx-qt/source/scwx/qt/map/overlay_layer.cpp
index 91ef54ef..a6e136fa 100644
--- a/scwx-qt/source/scwx/qt/map/overlay_layer.cpp
+++ b/scwx-qt/source/scwx/qt/map/overlay_layer.cpp
@@ -34,12 +34,12 @@ class OverlayLayerImpl
 {
 public:
    explicit OverlayLayerImpl(std::shared_ptr context) :
-       textShader_(context->gl_),
+       textShader_(context),
        font_(util::Font::Create(":/res/fonts/din1451alt.ttf")),
        texture_ {GL_INVALID_INDEX},
-       activeBoxOuter_ {std::make_shared(context->gl_)},
-       activeBoxInner_ {std::make_shared(context->gl_)},
-       timeBox_ {std::make_shared(context->gl_)},
+       activeBoxOuter_ {std::make_shared(context)},
+       activeBoxInner_ {std::make_shared(context)},
+       timeBox_ {std::make_shared(context)},
        sweepTimeString_ {},
        sweepTimeNeedsUpdate_ {true}
    {
@@ -81,7 +81,8 @@ void OverlayLayer::Initialize()
 
    DrawLayer::Initialize();
 
-   gl::OpenGLFunctions& gl = context()->gl_;
+   gl::OpenGLFunctions& gl               = context()->gl();
+   auto                 radarProductView = context()->radar_product_view();
 
    p->textShader_.Initialize();
 
@@ -90,9 +91,9 @@ void OverlayLayer::Initialize()
       p->texture_ = p->font_->GenerateTexture(gl);
    }
 
-   if (context()->radarProductView_ != nullptr)
+   if (radarProductView != nullptr)
    {
-      connect(context()->radarProductView_.get(),
+      connect(radarProductView.get(),
               &view::RadarProductView::SweepComputed,
               this,
               &OverlayLayer::UpdateSweepTimeNextFrame);
@@ -103,14 +104,14 @@ void OverlayLayer::Render(const QMapbox::CustomLayerRenderParameters& params)
 {
    constexpr float fontSize = 16.0f;
 
-   gl::OpenGLFunctions& gl = context()->gl_;
+   gl::OpenGLFunctions& gl               = context()->gl();
+   auto                 radarProductView = context()->radar_product_view();
+   auto&                settings         = context()->settings();
 
-   if (p->sweepTimeNeedsUpdate_ && context()->radarProductView_ != nullptr)
+   if (p->sweepTimeNeedsUpdate_ && radarProductView != nullptr)
    {
-      p->sweepTimeString_ =
-         scwx::util::TimeString(context()->radarProductView_->sweep_time(),
-                                std::chrono::current_zone(),
-                                false);
+      p->sweepTimeString_ = scwx::util::TimeString(
+         radarProductView->sweep_time(), std::chrono::current_zone(), false);
       p->sweepTimeNeedsUpdate_ = false;
    }
 
@@ -120,9 +121,9 @@ void OverlayLayer::Render(const QMapbox::CustomLayerRenderParameters& params)
                                      static_cast(params.height));
 
    // Active Box
-   p->activeBoxOuter_->SetVisible(context()->settings_.isActive_);
-   p->activeBoxInner_->SetVisible(context()->settings_.isActive_);
-   if (context()->settings_.isActive_)
+   p->activeBoxOuter_->SetVisible(settings.isActive_);
+   p->activeBoxInner_->SetVisible(settings.isActive_);
+   if (settings.isActive_)
    {
       p->activeBoxOuter_->SetSize(params.width, params.height);
       p->activeBoxInner_->SetSize(params.width - 2.0f, params.height - 2.0f);
@@ -164,15 +165,16 @@ void OverlayLayer::Deinitialize()
 
    DrawLayer::Deinitialize();
 
-   gl::OpenGLFunctions& gl = context()->gl_;
+   gl::OpenGLFunctions& gl               = context()->gl();
+   auto                 radarProductView = context()->radar_product_view();
 
    gl.glDeleteTextures(1, &p->texture_);
 
    p->texture_ = GL_INVALID_INDEX;
 
-   if (context()->radarProductView_ != nullptr)
+   if (radarProductView != nullptr)
    {
-      disconnect(context()->radarProductView_.get(),
+      disconnect(radarProductView.get(),
                  &view::RadarProductView::SweepComputed,
                  this,
                  &OverlayLayer::UpdateSweepTimeNextFrame);
diff --git a/scwx-qt/source/scwx/qt/map/radar_product_layer.cpp b/scwx-qt/source/scwx/qt/map/radar_product_layer.cpp
index 2f47b777..c7b501ac 100644
--- a/scwx-qt/source/scwx/qt/map/radar_product_layer.cpp
+++ b/scwx-qt/source/scwx/qt/map/radar_product_layer.cpp
@@ -33,7 +33,7 @@ class RadarProductLayerImpl
 {
 public:
    explicit RadarProductLayerImpl(std::shared_ptr context) :
-       shaderProgram_(context->gl_),
+       shaderProgram_(nullptr),
        uMVPMatrixLocation_(GL_INVALID_INDEX),
        uMapScreenCoordLocation_(GL_INVALID_INDEX),
        uDataMomentOffsetLocation_(GL_INVALID_INDEX),
@@ -50,7 +50,8 @@ public:
    }
    ~RadarProductLayerImpl() = default;
 
-   gl::ShaderProgram     shaderProgram_;
+   std::shared_ptr shaderProgram_;
+
    GLint                 uMVPMatrixLocation_;
    GLint                 uMapScreenCoordLocation_;
    GLint                 uDataMomentOffsetLocation_;
@@ -78,47 +79,48 @@ void RadarProductLayer::Initialize()
 {
    logger_->debug("Initialize()");
 
-   gl::OpenGLFunctions& gl = context()->gl_;
+   gl::OpenGLFunctions& gl = context()->gl();
 
    // Load and configure radar shader
-   p->shaderProgram_.Load(":/gl/radar.vert", ":/gl/radar.frag");
+   p->shaderProgram_ =
+      context()->GetShaderProgram(":/gl/radar.vert", ":/gl/radar.frag");
 
    p->uMVPMatrixLocation_ =
-      gl.glGetUniformLocation(p->shaderProgram_.id(), "uMVPMatrix");
+      gl.glGetUniformLocation(p->shaderProgram_->id(), "uMVPMatrix");
    if (p->uMVPMatrixLocation_ == -1)
    {
       logger_->warn("Could not find uMVPMatrix");
    }
 
    p->uMapScreenCoordLocation_ =
-      gl.glGetUniformLocation(p->shaderProgram_.id(), "uMapScreenCoord");
+      gl.glGetUniformLocation(p->shaderProgram_->id(), "uMapScreenCoord");
    if (p->uMapScreenCoordLocation_ == -1)
    {
       logger_->warn("Could not find uMapScreenCoord");
    }
 
    p->uDataMomentOffsetLocation_ =
-      gl.glGetUniformLocation(p->shaderProgram_.id(), "uDataMomentOffset");
+      gl.glGetUniformLocation(p->shaderProgram_->id(), "uDataMomentOffset");
    if (p->uDataMomentOffsetLocation_ == -1)
    {
       logger_->warn("Could not find uDataMomentOffset");
    }
 
    p->uDataMomentScaleLocation_ =
-      gl.glGetUniformLocation(p->shaderProgram_.id(), "uDataMomentScale");
+      gl.glGetUniformLocation(p->shaderProgram_->id(), "uDataMomentScale");
    if (p->uDataMomentScaleLocation_ == -1)
    {
       logger_->warn("Could not find uDataMomentScale");
    }
 
    p->uCFPEnabledLocation_ =
-      gl.glGetUniformLocation(p->shaderProgram_.id(), "uCFPEnabled");
+      gl.glGetUniformLocation(p->shaderProgram_->id(), "uCFPEnabled");
    if (p->uCFPEnabledLocation_ == -1)
    {
       logger_->warn("Could not find uCFPEnabled");
    }
 
-   p->shaderProgram_.Use();
+   p->shaderProgram_->Use();
 
    // Generate a vertex array object
    gl.glGenVertexArrays(1, &p->vao_);
@@ -138,11 +140,12 @@ void RadarProductLayer::Initialize()
    gl.glTexParameteri(GL_TEXTURE_1D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
    gl.glTexParameteri(GL_TEXTURE_1D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
 
-   connect(context()->radarProductView_.get(),
+   auto radarProductView = context()->radar_product_view();
+   connect(radarProductView.get(),
            &view::RadarProductView::ColorTableUpdated,
            this,
            [=]() { p->colorTableNeedsUpdate_ = true; });
-   connect(context()->radarProductView_.get(),
+   connect(radarProductView.get(),
            &view::RadarProductView::SweepComputed,
            this,
            [=]() { p->sweepNeedsUpdate_ = true; });
@@ -152,12 +155,12 @@ void RadarProductLayer::UpdateSweep()
 {
    logger_->debug("UpdateSweep()");
 
-   gl::OpenGLFunctions& gl = context()->gl_;
+   gl::OpenGLFunctions& gl = context()->gl();
 
    boost::timer::cpu_timer timer;
 
    std::shared_ptr radarProductView =
-      context()->radarProductView_;
+      context()->radar_product_view();
 
    std::unique_lock sweepLock(radarProductView->sweep_mutex(),
                               std::try_to_lock);
@@ -253,9 +256,9 @@ void RadarProductLayer::UpdateSweep()
 void RadarProductLayer::Render(
    const QMapbox::CustomLayerRenderParameters& params)
 {
-   gl::OpenGLFunctions& gl = context()->gl_;
+   gl::OpenGLFunctions& gl = context()->gl();
 
-   p->shaderProgram_.Use();
+   p->shaderProgram_->Use();
 
    if (p->colorTableNeedsUpdate_)
    {
@@ -300,7 +303,7 @@ void RadarProductLayer::Deinitialize()
 {
    logger_->debug("Deinitialize()");
 
-   gl::OpenGLFunctions& gl = context()->gl_;
+   gl::OpenGLFunctions& gl = context()->gl();
 
    gl.glDeleteVertexArrays(1, &p->vao_);
    gl.glDeleteBuffers(3, p->vbo_.data());
@@ -321,9 +324,9 @@ void RadarProductLayer::UpdateColorTable()
 
    p->colorTableNeedsUpdate_ = false;
 
-   gl::OpenGLFunctions&                    gl = context()->gl_;
+   gl::OpenGLFunctions&                    gl = context()->gl();
    std::shared_ptr radarProductView =
-      context()->radarProductView_;
+      context()->radar_product_view();
 
    const std::vector& colorTable =
       radarProductView->color_table();
diff --git a/scwx-qt/source/scwx/qt/util/streams.hpp b/scwx-qt/source/scwx/qt/util/streams.hpp
new file mode 100644
index 00000000..ca3a3d9c
--- /dev/null
+++ b/scwx-qt/source/scwx/qt/util/streams.hpp
@@ -0,0 +1,33 @@
+#pragma once
+
+#include 
+#include 
+
+namespace scwx
+{
+namespace qt
+{
+namespace util
+{
+
+class IoDeviceSource
+{
+public:
+   typedef char                         char_type;
+   typedef boost::iostreams::source_tag category;
+
+   IoDeviceSource(QIODevice& source) : source_ {source} {}
+   ~IoDeviceSource() {}
+
+   std::streamsize read(char* buffer, std::streamsize n)
+   {
+      return source_.read(buffer, n);
+   }
+
+private:
+   QIODevice& source_;
+};
+
+} // namespace util
+} // namespace qt
+} // namespace scwx
diff --git a/scwx-qt/source/scwx/qt/util/texture_atlas.cpp b/scwx-qt/source/scwx/qt/util/texture_atlas.cpp
new file mode 100644
index 00000000..22a7f0f7
--- /dev/null
+++ b/scwx-qt/source/scwx/qt/util/texture_atlas.cpp
@@ -0,0 +1,292 @@
+#include 
+#include 
+#include 
+
+#include 
+#include 
+
+#pragma warning(push, 0)
+#pragma warning(disable : 4714)
+#include 
+#include 
+#include 
+#include 
+#pragma warning(pop)
+
+namespace scwx
+{
+namespace qt
+{
+namespace util
+{
+
+static const std::string logPrefix_ = "scwx::qt::util::texture_atlas";
+static const auto        logger_    = scwx::util::Logger::Create(logPrefix_);
+
+class TextureAtlas::Impl
+{
+public:
+   explicit Impl() :
+       texturePathMap_ {},
+       texturePathMutex_ {},
+       atlas_ {},
+       atlasMap_ {},
+       atlasMutex_ {}
+   {
+   }
+   ~Impl() {}
+
+   static boost::gil::rgba8_image_t LoadImage(const std::string& imagePath);
+
+   std::unordered_map texturePathMap_;
+   std::shared_mutex                            texturePathMutex_;
+
+   boost::gil::rgba8_image_t                          atlas_;
+   std::unordered_map atlasMap_;
+   std::shared_mutex                                  atlasMutex_;
+};
+
+TextureAtlas::TextureAtlas() : p(std::make_unique()) {}
+TextureAtlas::~TextureAtlas() = default;
+
+TextureAtlas::TextureAtlas(TextureAtlas&&) noexcept            = default;
+TextureAtlas& TextureAtlas::operator=(TextureAtlas&&) noexcept = default;
+
+void TextureAtlas::RegisterTexture(const std::string& name,
+                                   const std::string& path)
+{
+   std::unique_lock lock(p->texturePathMutex_);
+   p->texturePathMap_.insert_or_assign(name, path);
+}
+
+void TextureAtlas::BuildAtlas(size_t width, size_t height)
+{
+   logger_->debug("Building {}x{} texture atlas", width, height);
+
+   if (width > INT_MAX || height > INT_MAX)
+   {
+      logger_->error("Cannot build texture atlas of size {}x{}", width, height);
+      return;
+   }
+
+   std::vector> images;
+   std::vector                                        stbrpRects;
+
+   // Load images
+   {
+      // Take a read lock on the texture path map
+      std::shared_lock lock(p->texturePathMutex_);
+
+      // For each registered texture
+      std::for_each(p->texturePathMap_.cbegin(),
+                    p->texturePathMap_.cend(),
+                    [&](const auto& pair)
+                    {
+                       // Load texture image
+                       boost::gil::rgba8_image_t image =
+                          Impl::LoadImage(pair.second);
+
+                       if (image.width() > 0u && image.height() > 0u)
+                       {
+                          // Store STB rectangle pack data in a vector
+                          stbrpRects.push_back(stbrp_rect {
+                             0,
+                             static_cast(image.width()),
+                             static_cast(image.height()),
+                             0,
+                             0,
+                             0});
+
+                          // Store image data in a vector
+                          images.emplace_back(pair.first, std::move(image));
+                       }
+                    });
+   }
+
+   // Pack images
+   {
+      logger_->trace("Packing {} images", images.size());
+
+      // Optimal number of nodes = width
+      stbrp_context           stbrpContext;
+      std::vector stbrpNodes(width);
+
+      stbrp_init_target(&stbrpContext,
+                        static_cast(width),
+                        static_cast(height),
+                        stbrpNodes.data(),
+                        static_cast(stbrpNodes.size()));
+
+      // Pack loaded textures
+      stbrp_pack_rects(
+         &stbrpContext, stbrpRects.data(), static_cast(stbrpRects.size()));
+   }
+
+   // Lock atlas
+   std::unique_lock lock(p->atlasMutex_);
+
+   // Clear index
+   p->atlasMap_.clear();
+
+   // Clear atlas
+   p->atlas_.recreate(width, height);
+   boost::gil::rgba8_view_t atlasView = boost::gil::view(p->atlas_);
+   boost::gil::fill_pixels(atlasView,
+                           boost::gil::rgba8_pixel_t {255, 0, 255, 255});
+
+   // Populate atlas
+   logger_->trace("Populating atlas");
+
+   const float xStep = 1.0f / width;
+   const float yStep = 1.0f / height;
+   const float xMin  = xStep * 0.5f;
+   const float yMin  = yStep * 0.5f;
+
+   for (size_t i = 0; i < images.size(); i++)
+   {
+      // If the image was packed successfully
+      if (stbrpRects[i].was_packed != 0)
+      {
+         // Populate the atlas
+         boost::gil::rgba8c_view_t imageView =
+            boost::gil::const_view(images[i].second);
+         boost::gil::rgba8_view_t atlasSubView =
+            boost::gil::subimage_view(atlasView,
+                                      stbrpRects[i].x,
+                                      stbrpRects[i].y,
+                                      imageView.width(),
+                                      imageView.height());
+
+         boost::gil::copy_pixels(imageView, atlasSubView);
+
+         // Add texture image to the index
+         const stbrp_coord x = stbrpRects[i].x;
+         const stbrp_coord y = stbrpRects[i].y;
+
+         const float sLeft = x * xStep + xMin;
+         const float sRight =
+            sLeft + static_cast(imageView.width() - 1) / width;
+         const float tTop = y * yStep + yMin;
+         const float tBottom =
+            tTop + static_cast(imageView.height() - 1) / height;
+
+         p->atlasMap_.emplace(
+            std::piecewise_construct,
+            std::forward_as_tuple(images[i].first),
+            std::forward_as_tuple(
+               boost::gil::point_t {x, y},
+               boost::gil::point_t {imageView.width(), imageView.height()},
+               sLeft,
+               sRight,
+               tTop,
+               tBottom));
+      }
+      else
+      {
+         logger_->warn("Unable to pack texture: {}", images[i].first);
+      }
+   }
+}
+
+GLuint TextureAtlas::BufferAtlas(gl::OpenGLFunctions& gl)
+{
+   GLuint texture = GL_INVALID_INDEX;
+
+   std::shared_lock lock(p->atlasMutex_);
+
+   if (p->atlas_.width() > 0u && p->atlas_.height() > 0u)
+   {
+      boost::gil::rgba8_view_t               view = boost::gil::view(p->atlas_);
+      std::vector pixelData(view.width() *
+                                                       view.height());
+
+      boost::gil::copy_pixels(
+         view,
+         boost::gil::interleaved_view(view.width(),
+                                      view.height(),
+                                      pixelData.data(),
+                                      view.width() *
+                                         sizeof(boost::gil::rgba8_pixel_t)));
+
+      lock.unlock();
+
+      gl.glGenTextures(1, &texture);
+      gl.glBindTexture(GL_TEXTURE_2D, texture);
+
+      gl.glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
+      gl.glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
+      gl.glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
+      gl.glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
+
+      gl.glTexImage2D(GL_TEXTURE_2D,
+                      0,
+                      GL_RGBA,
+                      view.width(),
+                      view.height(),
+                      0,
+                      GL_RGBA,
+                      GL_UNSIGNED_BYTE,
+                      pixelData.data());
+   }
+
+   return texture;
+}
+
+TextureAttributes TextureAtlas::GetTextureAttributes(const std::string& name)
+{
+   TextureAttributes attr {};
+   std::shared_lock  lock(p->atlasMutex_);
+
+   const auto& it = p->atlasMap_.find(name);
+   if (it != p->atlasMap_.cend())
+   {
+      attr = it->second;
+   }
+
+   return attr;
+}
+
+boost::gil::rgba8_image_t
+TextureAtlas::Impl::LoadImage(const std::string& imagePath)
+{
+   logger_->debug("Loading image: {}", imagePath);
+
+   boost::gil::rgba8_image_t image;
+
+   QFile imageFile(imagePath.c_str());
+
+   imageFile.open(QIODevice::ReadOnly);
+
+   if (!imageFile.isOpen())
+   {
+      logger_->error("Could not open image: {}", imagePath);
+      return std::move(image);
+   }
+
+   boost::iostreams::stream dataStream(imageFile);
+
+   boost::gil::image x;
+
+   try
+   {
+      boost::gil::read_and_convert_image(
+         dataStream, image, boost::gil::png_tag());
+   }
+   catch (const std::exception& ex)
+   {
+      logger_->error("Error reading image: {}", ex.what());
+      return std::move(image);
+   }
+
+   return std::move(image);
+}
+
+TextureAtlas& TextureAtlas::Instance()
+{
+   static TextureAtlas instance_ {};
+   return instance_;
+}
+
+} // namespace util
+} // namespace qt
+} // namespace scwx
diff --git a/scwx-qt/source/scwx/qt/util/texture_atlas.hpp b/scwx-qt/source/scwx/qt/util/texture_atlas.hpp
new file mode 100644
index 00000000..bf904e6c
--- /dev/null
+++ b/scwx-qt/source/scwx/qt/util/texture_atlas.hpp
@@ -0,0 +1,83 @@
+#pragma once
+
+#include 
+
+#include 
+#include 
+
+#include 
+
+namespace scwx
+{
+namespace qt
+{
+namespace util
+{
+
+struct TextureAttributes
+{
+   TextureAttributes() :
+       valid_ {false},
+       position_ {},
+       size_ {},
+       sLeft_ {},
+       sRight_ {},
+       tTop_ {},
+       tBottom_ {}
+   {
+   }
+
+   TextureAttributes(boost::gil::point_t position,
+                     boost::gil::point_t size,
+                     float               sLeft,
+                     float               sRight,
+                     float               tTop,
+                     float               tBottom) :
+       valid_ {true},
+       position_ {position},
+       size_ {size},
+       sLeft_ {sLeft},
+       sRight_ {sRight},
+       tTop_ {tTop},
+       tBottom_ {tBottom}
+   {
+   }
+
+   bool                valid_;
+   boost::gil::point_t position_;
+   boost::gil::point_t size_;
+   float               sLeft_;
+   float               sRight_;
+   float               tTop_;
+   float               tBottom_;
+};
+
+class TextureAtlas
+{
+public:
+   explicit TextureAtlas();
+   ~TextureAtlas();
+
+   TextureAtlas(const TextureAtlas&)            = delete;
+   TextureAtlas& operator=(const TextureAtlas&) = delete;
+
+   TextureAtlas(TextureAtlas&&) noexcept;
+   TextureAtlas& operator=(TextureAtlas&&) noexcept;
+
+   static TextureAtlas& Instance();
+
+   void   RegisterTexture(const std::string& name, const std::string& path);
+   void   BuildAtlas(size_t width, size_t height);
+   GLuint BufferAtlas(gl::OpenGLFunctions& gl);
+
+   TextureAttributes GetTextureAttributes(const std::string& name);
+
+private:
+   class Impl;
+
+   std::unique_ptr p;
+};
+
+} // namespace util
+} // namespace qt
+} // namespace scwx
diff --git a/wxdata/include/scwx/common/geographic.hpp b/wxdata/include/scwx/common/geographic.hpp
index 639114f0..66a9233f 100644
--- a/wxdata/include/scwx/common/geographic.hpp
+++ b/wxdata/include/scwx/common/geographic.hpp
@@ -13,6 +13,8 @@ struct Coordinate
    double latitude_;  ///< Latitude in degrees
    double longitude_; ///< Longitude in degrees
 
+   Coordinate() : Coordinate(0.0, 0.0) {}
+
    Coordinate(double latitude, double longitude) :
        latitude_ {latitude}, longitude_ {longitude}
    {
diff --git a/wxdata/include/scwx/util/hash.hpp b/wxdata/include/scwx/util/hash.hpp
new file mode 100644
index 00000000..6559b6a4
--- /dev/null
+++ b/wxdata/include/scwx/util/hash.hpp
@@ -0,0 +1,21 @@
+#pragma once
+
+#include 
+#include 
+
+namespace scwx
+{
+namespace util
+{
+
+template
+struct hash;
+
+template<>
+struct hash>
+{
+   size_t operator()(const std::pair& x) const;
+};
+
+} // namespace util
+} // namespace scwx
diff --git a/wxdata/source/scwx/util/hash.cpp b/wxdata/source/scwx/util/hash.cpp
new file mode 100644
index 00000000..31d928a4
--- /dev/null
+++ b/wxdata/source/scwx/util/hash.cpp
@@ -0,0 +1,20 @@
+#include 
+
+#include 
+
+namespace scwx
+{
+namespace util
+{
+
+size_t hash>::operator()(
+   const std::pair& x) const
+{
+   size_t seed = 0;
+   boost::hash_combine(seed, x.first);
+   boost::hash_combine(seed, x.second);
+   return seed;
+}
+
+} // namespace util
+} // namespace scwx
diff --git a/wxdata/wxdata.cmake b/wxdata/wxdata.cmake
index 4adbc60d..de57a39b 100644
--- a/wxdata/wxdata.cmake
+++ b/wxdata/wxdata.cmake
@@ -46,6 +46,7 @@ set(SRC_PROVIDER source/scwx/provider/aws_level2_data_provider.cpp
                  source/scwx/provider/nexrad_data_provider_factory.cpp)
 set(HDR_UTIL include/scwx/util/environment.hpp
              include/scwx/util/float.hpp
+             include/scwx/util/hash.hpp
              include/scwx/util/iterator.hpp
              include/scwx/util/logger.hpp
              include/scwx/util/map.hpp
@@ -56,6 +57,7 @@ set(HDR_UTIL include/scwx/util/environment.hpp
              include/scwx/util/vectorbuf.hpp)
 set(SRC_UTIL source/scwx/util/environment.cpp
              source/scwx/util/float.cpp
+             source/scwx/util/hash.cpp
              source/scwx/util/logger.cpp
              source/scwx/util/rangebuf.cpp
              source/scwx/util/streams.cpp