#include "tools/jotpluggler/app.h"
#include "tools/jotpluggler/common.h"
#include "tools/jotpluggler/map.h"

#include <GLFW/glfw3.h>

extern "C" {
#include <zstd.h>
}

#include <array>
#include <algorithm>
#include <cmath>
#include <condition_variable>
#include <cstdint>
#include <cstring>
#include <filesystem>
#include <limits>
#include <mutex>
#include <optional>
#include <string_view>
#include <thread>
#include <unordered_set>
#include <utility>
#include <vector>

#include "common/util.h"
#include "json11/json11.hpp"

namespace fs = std::filesystem;

namespace {

constexpr int MAP_MIN_ZOOM = 1;
constexpr int MAP_MAX_ZOOM = 18;
constexpr int MAP_SINGLE_POINT_MIN_ZOOM = 14;
constexpr float MAP_WHEEL_ZOOM_STEP = 0.25f;
constexpr double MAP_TRACE_PAD_FRAC = 0.45;
constexpr double MAP_TRACE_MIN_LAT_PAD = 0.01;
constexpr double MAP_BOUNDS_GRID = 0.005;
constexpr double MAP_CORRIDOR_LAT_PAD = 0.010;
constexpr double MAP_CORRIDOR_MIN_STEP_S = 1.5;
constexpr size_t MAP_CORRIDOR_MAX_BOXES = 36;
constexpr float MAP_INITIAL_FIT_FILL = 0.88f;
constexpr float MAP_MIN_ZOOM_FILL = 0.98f;
constexpr float MAP_EDGE_FADE_FRAC = 0.28f;
constexpr const char *MAP_QUERY_ENDPOINTS[] = {
  "https://overpass-api.de/api/interpreter",
  "https://overpass.private.coffee/api/interpreter",
};
struct GeoPoint {
  double lat = 0.0;
  double lon = 0.0;
};

struct ProjectedPoint {
  float x = 0.0f;
  float y = 0.0f;
};

struct ProjectedBounds {
  float min_x = 0.0f;
  float min_y = 0.0f;
  float max_x = 0.0f;
  float max_y = 0.0f;

  bool valid() const {
    return max_x >= min_x && max_y >= min_y;
  }
};

enum class RoadClass : uint8_t {
  Motorway,
  Primary,
  Secondary,
  Local,
};

struct RoadFeature {
  RoadClass road_class = RoadClass::Local;
  ProjectedBounds bounds;
  std::vector<ProjectedPoint> points;
};

struct WaterLineFeature {
  ProjectedBounds bounds;
  std::vector<ProjectedPoint> points;
};

struct WaterPolygonFeature {
  ProjectedBounds bounds;
  std::vector<ProjectedPoint> ring;
};

}  // namespace

struct RouteBasemap {
  std::string key;
  GeoBounds bounds;
  ProjectedBounds projected_bounds;
  std::vector<RoadFeature> roads;
  std::vector<WaterLineFeature> water_lines;
  std::vector<WaterPolygonFeature> water_polygons;
};

struct MapRequestSpec {
  std::string key;
  GeoBounds bounds;
  std::string query;
};

namespace {

double lon_to_world_x(double lon, double zoom) {
  return (lon + 180.0) / 360.0 * 256.0 * std::exp2(zoom);
}

double lat_to_world_y(double lat, double zoom) {
  const double lat_rad = lat * M_PI / 180.0;
  return (1.0 - std::log(std::tan(lat_rad) + 1.0 / std::cos(lat_rad)) / M_PI) / 2.0 * 256.0 * std::exp2(zoom);
}

double world_x_to_lon(double x, double zoom) {
  return x / std::exp2(zoom) / 256.0 * 360.0 - 180.0;
}

double world_y_to_lat(double y, double zoom) {
  const double n = M_PI - (2.0 * M_PI * (y / std::exp2(zoom))) / 256.0;
  return 180.0 / M_PI * std::atan(std::sinh(n));
}

double map_trace_center_lat(const GpsTrace &trace) {
  return (trace.min_lat + trace.max_lat) * 0.5;
}

double map_trace_center_lon(const GpsTrace &trace) {
  return (trace.min_lon + trace.max_lon) * 0.5;
}

double clamp_lat(double lat) {
  return std::clamp(lat, -85.0, 85.0);
}

double clamp_lon(double lon) {
  return std::clamp(lon, -179.999, 179.999);
}

float project_lon0(double lon) {
  return static_cast<float>((lon + 180.0) / 360.0 * 256.0);
}

float project_lat0(double lat) {
  const double lat_rad = lat * M_PI / 180.0;
  return static_cast<float>((1.0 - std::log(std::tan(lat_rad) + 1.0 / std::cos(lat_rad)) / M_PI) / 2.0 * 256.0);
}

double cos_lat_scale(double lat) {
  return std::max(0.2, std::cos(lat * M_PI / 180.0));
}

double quantize_down(double value, double step) {
  return std::floor(value / step) * step;
}

double quantize_up(double value, double step) {
  return std::ceil(value / step) * step;
}

ProjectedBounds compute_projected_bounds(const std::vector<ProjectedPoint> &points) {
  ProjectedBounds bounds;
  if (points.empty()) {
    return bounds;
  }
  bounds.min_x = bounds.max_x = points.front().x;
  bounds.min_y = bounds.max_y = points.front().y;
  for (const ProjectedPoint &point : points) {
    bounds.min_x = std::min(bounds.min_x, point.x);
    bounds.max_x = std::max(bounds.max_x, point.x);
    bounds.min_y = std::min(bounds.min_y, point.y);
    bounds.max_y = std::max(bounds.max_y, point.y);
  }
  return bounds;
}

ProjectedBounds project_bounds0(const GeoBounds &bounds) {
  if (!bounds.valid()) {
    return {};
  }
  return ProjectedBounds{
    .min_x = project_lon0(bounds.west),
    .min_y = project_lat0(bounds.north),
    .max_x = project_lon0(bounds.east),
    .max_y = project_lat0(bounds.south),
  };
}

bool feature_intersects_view(const ProjectedBounds &feature, const ProjectedBounds &view, float zoom_scale) {
  const float min_x = feature.min_x * zoom_scale;
  const float max_x = feature.max_x * zoom_scale;
  const float min_y = feature.min_y * zoom_scale;
  const float max_y = feature.max_y * zoom_scale;
  return !(max_x < view.min_x || min_x > view.max_x
        || max_y < view.min_y || min_y > view.max_y);
}

GeoBounds requested_bounds_for_trace(const GpsTrace &trace) {
  if (trace.points.empty()) {
    return {};
  }
  const double center_lat = map_trace_center_lat(trace);
  const double lat_span = std::max(trace.max_lat - trace.min_lat, 0.002);
  const double lon_span = std::max(trace.max_lon - trace.min_lon, 0.002 / cos_lat_scale(center_lat));
  const double lat_pad = std::max(lat_span * MAP_TRACE_PAD_FRAC, MAP_TRACE_MIN_LAT_PAD);
  const double lon_pad = std::max(lon_span * MAP_TRACE_PAD_FRAC, MAP_TRACE_MIN_LAT_PAD / cos_lat_scale(center_lat));

  GeoBounds bounds;
  bounds.south = clamp_lat(quantize_down(trace.min_lat - lat_pad, MAP_BOUNDS_GRID));
  bounds.north = clamp_lat(quantize_up(trace.max_lat + lat_pad, MAP_BOUNDS_GRID));
  bounds.west = clamp_lon(quantize_down(trace.min_lon - lon_pad, MAP_BOUNDS_GRID));
  bounds.east = clamp_lon(quantize_up(trace.max_lon + lon_pad, MAP_BOUNDS_GRID));
  return bounds;
}

GeoBounds merge_bounds(const GeoBounds &a, const GeoBounds &b) {
  if (!a.valid()) return b;
  if (!b.valid()) return a;
  return GeoBounds{
    .south = std::min(a.south, b.south),
    .west = std::min(a.west, b.west),
    .north = std::max(a.north, b.north),
    .east = std::max(a.east, b.east),
  };
}

bool bounds_overlap_or_touch(const GeoBounds &a, const GeoBounds &b) {
  return !(a.east < b.west || b.east < a.west || a.north < b.south || b.north < a.south);
}

std::vector<GeoBounds> corridor_boxes_for_trace(const GpsTrace &trace) {
  std::vector<GeoBounds> boxes;
  if (trace.points.empty()) {
    return boxes;
  }

  const double center_lat = map_trace_center_lat(trace);
  const double lon_pad = MAP_CORRIDOR_LAT_PAD / cos_lat_scale(center_lat);
  const double total_time = trace.points.back().time - trace.points.front().time;
  const double target_boxes = std::min<double>(MAP_CORRIDOR_MAX_BOXES, std::max<double>(8.0, total_time / MAP_CORRIDOR_MIN_STEP_S));
  const size_t stride = std::max<size_t>(1, static_cast<size_t>(std::ceil(trace.points.size() / target_boxes)));

  auto add_box = [&](double lat, double lon) {
    GeoBounds box{
      .south = clamp_lat(quantize_down(lat - MAP_CORRIDOR_LAT_PAD, MAP_BOUNDS_GRID)),
      .west = clamp_lon(quantize_down(lon - lon_pad, MAP_BOUNDS_GRID)),
      .north = clamp_lat(quantize_up(lat + MAP_CORRIDOR_LAT_PAD, MAP_BOUNDS_GRID)),
      .east = clamp_lon(quantize_up(lon + lon_pad, MAP_BOUNDS_GRID)),
    };
    if (!box.valid()) {
      return;
    }
    for (GeoBounds &existing : boxes) {
      if (bounds_overlap_or_touch(existing, box)) {
        existing = merge_bounds(existing, box);
        return;
      }
    }
    boxes.push_back(box);
  };

  add_box(trace.points.front().lat, trace.points.front().lon);
  for (size_t i = stride; i < trace.points.size(); i += stride) {
    add_box(trace.points[i].lat, trace.points[i].lon);
  }
  add_box(trace.points.back().lat, trace.points.back().lon);

  bool merged = true;
  while (merged) {
    merged = false;
    for (size_t i = 0; i < boxes.size() && !merged; ++i) {
      for (size_t j = i + 1; j < boxes.size(); ++j) {
        if (bounds_overlap_or_touch(boxes[i], boxes[j])) {
          boxes[i] = merge_bounds(boxes[i], boxes[j]);
          boxes.erase(boxes.begin() + static_cast<std::ptrdiff_t>(j));
          merged = true;
          break;
        }
      }
    }
  }
  return boxes;
}

ProjectedBounds view_bounds(double top_left_x, double top_left_y, float width, float height) {
  return ProjectedBounds{
    .min_x = static_cast<float>(top_left_x),
    .min_y = static_cast<float>(top_left_y),
    .max_x = static_cast<float>(top_left_x + width),
    .max_y = static_cast<float>(top_left_y + height),
  };
}

int fit_map_zoom_for_bounds(const GeoBounds &bounds, float width, float height, float fill_fraction) {
  if (!bounds.valid()) {
    return MAP_MIN_ZOOM;
  }
  const double max_width = std::max(1.0f, width * fill_fraction);
  const double max_height = std::max(1.0f, height * fill_fraction);
  for (int z = MAP_MAX_ZOOM; z >= MAP_MIN_ZOOM; --z) {
    const double pixel_width = std::abs(lon_to_world_x(bounds.east, z) - lon_to_world_x(bounds.west, z));
    const double pixel_height = std::abs(lat_to_world_y(bounds.south, z) - lat_to_world_y(bounds.north, z));
    if (pixel_width <= max_width && pixel_height <= max_height) {
      return z;
    }
  }
  return MAP_MIN_ZOOM;
}

int fit_map_zoom_for_trace(const GpsTrace &trace, float width, float height) {
  return fit_map_zoom_for_bounds(requested_bounds_for_trace(trace), width, height, MAP_INITIAL_FIT_FILL);
}

int minimum_allowed_map_zoom(const GeoBounds &bounds, const GpsTrace &trace, ImVec2 size) {
  if (trace.points.size() <= 1) {
    return MAP_SINGLE_POINT_MIN_ZOOM;
  }
  const int fit_zoom = fit_map_zoom_for_bounds(bounds.valid() ? bounds : requested_bounds_for_trace(trace),
                                               size.x, size.y, MAP_MIN_ZOOM_FILL);
  return std::clamp(fit_zoom, MAP_MIN_ZOOM, MAP_MAX_ZOOM);
}

std::optional<GpsPoint> interpolate_gps(const GpsTrace &trace, double time_value) {
  if (trace.points.empty()) {
    return std::nullopt;
  }
  if (time_value <= trace.points.front().time) {
    return trace.points.front();
  }
  if (time_value >= trace.points.back().time) {
    return trace.points.back();
  }
  auto upper = std::lower_bound(trace.points.begin(), trace.points.end(), time_value,
                                [](const GpsPoint &point, double target) {
                                  return point.time < target;
                                });
  if (upper == trace.points.begin()) {
    return trace.points.front();
  }
  const GpsPoint &p1 = *upper;
  const GpsPoint &p0 = *(upper - 1);
  const double dt = p1.time - p0.time;
  if (dt <= 1.0e-9) {
    return p0;
  }
  const double alpha = (time_value - p0.time) / dt;
  GpsPoint out;
  out.time = time_value;
  out.lat = p0.lat + (p1.lat - p0.lat) * alpha;
  out.lon = p0.lon + (p1.lon - p0.lon) * alpha;
  out.bearing = static_cast<float>(p0.bearing + (p1.bearing - p0.bearing) * alpha);
  out.type = alpha < 0.5 ? p0.type : p1.type;
  return out;
}

ImU32 map_timeline_color(TimelineEntry::Type type, float alpha = 1.0f) {
  return timeline_entry_color(type, alpha, {140, 150, 165});
}

ImVec2 gps_to_screen(double lat, double lon, double zoom, double top_left_x, double top_left_y, const ImVec2 &rect_min) {
  return ImVec2(rect_min.x + static_cast<float>(lon_to_world_x(lon, zoom) - top_left_x),
                rect_min.y + static_cast<float>(lat_to_world_y(lat, zoom) - top_left_y));
}

bool point_in_rect_with_margin(const ImVec2 &point, const ImVec2 &rect_min, const ImVec2 &rect_max,
                               float margin_fraction) {
  const float width = rect_max.x - rect_min.x;
  const float height = rect_max.y - rect_min.y;
  const float margin_x = width * margin_fraction;
  const float margin_y = height * margin_fraction;
  return point.x >= rect_min.x + margin_x && point.x <= rect_max.x - margin_x
      && point.y >= rect_min.y + margin_y && point.y <= rect_max.y - margin_y;
}

void draw_car_marker(ImDrawList *draw_list, ImVec2 center, float bearing_deg, ImU32 color, float size) {
  const float rad = bearing_deg * static_cast<float>(M_PI / 180.0);
  const ImVec2 forward(std::sin(rad), -std::cos(rad));
  const ImVec2 perp(-forward.y, forward.x);
  const ImVec2 tip(center.x + forward.x * size, center.y + forward.y * size);
  const ImVec2 base(center.x - forward.x * size * 0.45f, center.y - forward.y * size * 0.45f);
  const ImVec2 left(base.x + perp.x * size * 0.6f, base.y + perp.y * size * 0.6f);
  const ImVec2 right(base.x - perp.x * size * 0.6f, base.y - perp.y * size * 0.6f);
  draw_list->AddTriangleFilled(tip, left, right, color);
  draw_list->AddTriangle(tip, left, right, IM_COL32(255, 255, 255, 210), 2.0f);
}

bool is_convex_ring(const std::vector<ImVec2> &points) {
  if (points.size() < 4) {
    return false;
  }
  float sign = 0.0f;
  const size_t n = points.size();
  for (size_t i = 0; i < n; ++i) {
    const ImVec2 &a = points[i];
    const ImVec2 &b = points[(i + 1) % n];
    const ImVec2 &c = points[(i + 2) % n];
    const float cross = (b.x - a.x) * (c.y - b.y) - (b.y - a.y) * (c.x - b.x);
    if (std::abs(cross) < 1.0e-3f) {
      continue;
    }
    if (sign == 0.0f) {
      sign = cross;
    } else if ((cross > 0.0f) != (sign > 0.0f)) {
      return false;
    }
  }
  return sign != 0.0f;
}

uint64_t fnv1a64(std::string_view text) {
  uint64_t value = 1469598103934665603ULL;
  for (unsigned char c : text) {
    value ^= static_cast<uint64_t>(c);
    value *= 1099511628211ULL;
  }
  return value;
}

fs::path basemap_cache_root() {
  const char *home = std::getenv("HOME");
  fs::path root = home != nullptr ? fs::path(home) / ".comma" : fs::temp_directory_path();
  root /= "jotpluggler_vector_map";
  fs::create_directories(root);
  return root;
}

std::string bounds_key(const GeoBounds &bounds) {
  return util::string_format("v2_%.5f_%.5f_%.5f_%.5f",
                             bounds.south, bounds.west, bounds.north, bounds.east);
}

fs::path basemap_cache_path(const std::string &key) {
  const uint64_t hash = fnv1a64(key);
  return basemap_cache_root() / util::string_format("%016llx.bin.zst", static_cast<unsigned long long>(hash));
}

uint64_t cache_directory_size_bytes() {
  uint64_t total = 0;
  const fs::path root = basemap_cache_root();
  if (!fs::exists(root)) {
    return 0;
  }
  for (const fs::directory_entry &entry : fs::directory_iterator(root)) {
    if (entry.is_regular_file()) {
      total += static_cast<uint64_t>(entry.file_size());
    }
  }
  return total;
}

size_t cache_directory_file_count() {
  size_t count = 0;
  const fs::path root = basemap_cache_root();
  if (!fs::exists(root)) {
    return 0;
  }
  for (const fs::directory_entry &entry : fs::directory_iterator(root)) {
    if (entry.is_regular_file()) {
      ++count;
    }
  }
  return count;
}

void clear_cache_directory() {
  const fs::path root = basemap_cache_root();
  if (!fs::exists(root)) {
    return;
  }
  for (const fs::directory_entry &entry : fs::directory_iterator(root)) {
    if (entry.is_regular_file()) {
      std::error_code ec;
      fs::remove(entry.path(), ec);
    }
  }
}

std::string percent_encode(std::string_view text) {
  std::string out;
  out.reserve(text.size() * 3);
  for (unsigned char c : text) {
    if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9')
        || c == '-' || c == '_' || c == '.' || c == '~') {
      out.push_back(static_cast<char>(c));
    } else {
      out += util::string_format("%%%02X", static_cast<unsigned int>(c));
    }
  }
  return out;
}

std::string bbox_string(const GeoBounds &bounds) {
  return util::string_format("%.6f,%.6f,%.6f,%.6f",
                             bounds.south, bounds.west, bounds.north, bounds.east);
}

MapRequestSpec build_request_for_trace(const GpsTrace &trace) {
  const std::vector<GeoBounds> boxes = corridor_boxes_for_trace(trace);
  GeoBounds union_bounds;
  std::string query = "[out:json][timeout:25];(";
  for (const GeoBounds &box : boxes) {
    union_bounds = merge_bounds(union_bounds, box);
    const std::string bbox = bbox_string(box);
    query += "way[\"highway\"][\"area\"!=\"yes\"](" + bbox + ");";
    query += "way[\"natural\"=\"water\"](" + bbox + ");";
    query += "way[\"waterway\"=\"riverbank\"](" + bbox + ");";
    query += "way[\"waterway\"~\"river|stream|canal\"](" + bbox + ");";
  }
  query += ");out tags geom;";

  std::string key = bounds_key(union_bounds);
  key += ":";
  key += std::to_string(boxes.size());
  for (const GeoBounds &box : boxes) {
    key += ":";
    key += bbox_string(box);
  }
  return MapRequestSpec{
    .key = std::move(key),
    .bounds = union_bounds,
    .query = std::move(query),
  };
}

bool fetch_overpass_json(std::string_view query, std::string *out) {
  const std::string body = std::string("data=") + percent_encode(query);
  for (const char *endpoint : MAP_QUERY_ENDPOINTS) {
    const std::string command = "curl -fsSL --compressed --connect-timeout 8 --max-time 30 "
                              "-A 'jotpluggler-vector-map/1.0' "
                              "-H 'Content-Type: application/x-www-form-urlencoded; charset=UTF-8' "
                              "--data-raw " + shell_quote(body) + " "
                              + shell_quote(endpoint);
    const std::string response = util::check_output(command);
    if (!response.empty() && response.front() == '{') {
      *out = response;
      return true;
    }
  }
  return false;
}

std::string load_overpass_json(std::string_view query) {
  std::string response;
  if (!fetch_overpass_json(query, &response)) {
    return {};
  }
  return response;
}

template <typename T>
void append_pod(std::string *out, const T &value) {
  const size_t start = out->size();
  out->resize(start + sizeof(T));
  std::memcpy(out->data() + start, &value, sizeof(T));
}

template <typename T>
bool read_pod(std::string_view data, size_t *offset, T *value) {
  if (*offset + sizeof(T) > data.size()) {
    return false;
  }
  std::memcpy(value, data.data() + *offset, sizeof(T));
  *offset += sizeof(T);
  return true;
}

void append_points(std::string *out, const std::vector<ProjectedPoint> &points) {
  const uint32_t count = static_cast<uint32_t>(points.size());
  append_pod(out, count);
  for (const ProjectedPoint &point : points) {
    append_pod(out, point.x);
    append_pod(out, point.y);
  }
}

bool read_points(std::string_view data, size_t *offset, std::vector<ProjectedPoint> *points) {
  uint32_t count = 0;
  if (!read_pod(data, offset, &count)) {
    return false;
  }
  points->clear();
  points->reserve(count);
  for (uint32_t i = 0; i < count; ++i) {
    ProjectedPoint point;
    if (!read_pod(data, offset, &point.x) || !read_pod(data, offset, &point.y)) {
      return false;
    }
    points->push_back(point);
  }
  return true;
}

std::string serialize_basemap_payload(const RouteBasemap &basemap) {
  std::string raw;
  raw.reserve(1024 + basemap.roads.size() * 48);
  raw.append("JBM2", 4);
  append_pod(&raw, basemap.bounds.south);
  append_pod(&raw, basemap.bounds.west);
  append_pod(&raw, basemap.bounds.north);
  append_pod(&raw, basemap.bounds.east);

  const uint32_t road_count = static_cast<uint32_t>(basemap.roads.size());
  const uint32_t water_line_count = static_cast<uint32_t>(basemap.water_lines.size());
  const uint32_t water_polygon_count = static_cast<uint32_t>(basemap.water_polygons.size());
  append_pod(&raw, road_count);
  append_pod(&raw, water_line_count);
  append_pod(&raw, water_polygon_count);

  for (const RoadFeature &road : basemap.roads) {
    const uint8_t kind = static_cast<uint8_t>(road.road_class);
    append_pod(&raw, kind);
    append_points(&raw, road.points);
  }
  for (const WaterLineFeature &water : basemap.water_lines) {
    append_points(&raw, water.points);
  }
  for (const WaterPolygonFeature &water : basemap.water_polygons) {
    append_points(&raw, water.ring);
  }
  return raw;
}

std::optional<RouteBasemap> deserialize_basemap_payload(std::string_view raw, const std::string &key) {
  if (!util::starts_with(std::string(raw), "JBM2")) {
    return std::nullopt;
  }
  size_t offset = 4;
  RouteBasemap basemap;
  basemap.key = key;
  if (!read_pod(raw, &offset, &basemap.bounds.south)
      || !read_pod(raw, &offset, &basemap.bounds.west)
      || !read_pod(raw, &offset, &basemap.bounds.north)
      || !read_pod(raw, &offset, &basemap.bounds.east)) {
    return std::nullopt;
  }
  basemap.projected_bounds = project_bounds0(basemap.bounds);

  uint32_t road_count = 0;
  uint32_t water_line_count = 0;
  uint32_t water_polygon_count = 0;
  if (!read_pod(raw, &offset, &road_count)
      || !read_pod(raw, &offset, &water_line_count)
      || !read_pod(raw, &offset, &water_polygon_count)) {
    return std::nullopt;
  }

  basemap.roads.reserve(road_count);
  for (uint32_t i = 0; i < road_count; ++i) {
    uint8_t kind = 0;
    std::vector<ProjectedPoint> points;
    if (!read_pod(raw, &offset, &kind) || !read_points(raw, &offset, &points)) {
      return std::nullopt;
    }
    basemap.roads.push_back(RoadFeature{
      .road_class = static_cast<RoadClass>(kind),
      .bounds = compute_projected_bounds(points),
      .points = std::move(points),
    });
  }

  basemap.water_lines.reserve(water_line_count);
  for (uint32_t i = 0; i < water_line_count; ++i) {
    std::vector<ProjectedPoint> points;
    if (!read_points(raw, &offset, &points)) {
      return std::nullopt;
    }
    basemap.water_lines.push_back(WaterLineFeature{
      .bounds = compute_projected_bounds(points),
      .points = std::move(points),
    });
  }

  basemap.water_polygons.reserve(water_polygon_count);
  for (uint32_t i = 0; i < water_polygon_count; ++i) {
    std::vector<ProjectedPoint> ring;
    if (!read_points(raw, &offset, &ring)) {
      return std::nullopt;
    }
    basemap.water_polygons.push_back(WaterPolygonFeature{
      .bounds = compute_projected_bounds(ring),
      .ring = std::move(ring),
    });
  }
  return basemap;
}

bool save_compressed_basemap(const fs::path &path, const RouteBasemap &basemap) {
  const std::string raw = serialize_basemap_payload(basemap);
  const size_t bound = ZSTD_compressBound(raw.size());
  std::string compressed(bound, '\0');
  const size_t size = ZSTD_compress(compressed.data(), compressed.size(), raw.data(), raw.size(), 5);
  if (ZSTD_isError(size)) {
    return false;
  }
  compressed.resize(size);
  ensure_parent_dir(path);
  const std::string path_string = path.string();
  return util::write_file(path_string.c_str(), compressed.data(), compressed.size(), O_WRONLY | O_CREAT | O_TRUNC) == 0;
}

std::optional<RouteBasemap> load_compressed_basemap(const fs::path &path, const std::string &key) {
  const std::string compressed = util::read_file(path.string());
  if (compressed.empty()) {
    return std::nullopt;
  }
  const unsigned long long raw_size = ZSTD_getFrameContentSize(compressed.data(), compressed.size());
  if (raw_size == ZSTD_CONTENTSIZE_ERROR || raw_size == ZSTD_CONTENTSIZE_UNKNOWN || raw_size > (1ULL << 31)) {
    return std::nullopt;
  }
  std::string raw(static_cast<size_t>(raw_size), '\0');
  const size_t actual = ZSTD_decompress(raw.data(), raw.size(), compressed.data(), compressed.size());
  if (ZSTD_isError(actual)) {
    return std::nullopt;
  }
  raw.resize(actual);
  return deserialize_basemap_payload(raw, key);
}

std::vector<ProjectedPoint> geometry_points(const json11::Json &geometry_json) {
  std::vector<ProjectedPoint> points;
  const auto items = geometry_json.array_items();
  points.reserve(items.size());
  for (const json11::Json &point : items) {
    if (!point["lat"].is_number() || !point["lon"].is_number()) {
      continue;
    }
    points.push_back(ProjectedPoint{
      .x = project_lon0(point["lon"].number_value()),
      .y = project_lat0(point["lat"].number_value()),
    });
  }
  return points;
}

std::optional<RoadClass> classify_road(std::string_view highway) {
  if (highway == "motorway" || highway == "motorway_link" || highway == "trunk" || highway == "trunk_link") {
    return RoadClass::Motorway;
  }
  if (highway == "primary" || highway == "primary_link") {
    return RoadClass::Primary;
  }
  if (highway == "secondary" || highway == "secondary_link" || highway == "tertiary" || highway == "tertiary_link") {
    return RoadClass::Secondary;
  }
  if (highway == "residential" || highway == "unclassified" || highway == "living_street" || highway == "road") {
    return RoadClass::Local;
  }
  return std::nullopt;
}

std::optional<RouteBasemap> parse_basemap_json(const std::string &raw, const GeoBounds &bounds, const std::string &key) {
  std::string parse_error;
  const json11::Json root = json11::Json::parse(raw, parse_error);
  if (!parse_error.empty() || !root.is_object()) {
    return std::nullopt;
  }

  RouteBasemap basemap;
  basemap.key = key;
  basemap.bounds = bounds;
  basemap.projected_bounds = project_bounds0(bounds);

  for (const json11::Json &element : root["elements"].array_items()) {
    if (element["type"].string_value() != "way") {
      continue;
    }
    const json11::Json &tags = element["tags"];
    const std::vector<ProjectedPoint> points = geometry_points(element["geometry"]);
    if (points.size() < 2) {
      continue;
    }

    const std::string highway = tags["highway"].string_value();
    if (!highway.empty()) {
      const std::optional<RoadClass> road_class = classify_road(highway);
      if (!road_class.has_value()) {
        continue;
      }
      basemap.roads.push_back(RoadFeature{
        .road_class = *road_class,
        .bounds = compute_projected_bounds(points),
        .points = points,
      });
      continue;
    }

    const std::string natural = tags["natural"].string_value();
    const std::string waterway = tags["waterway"].string_value();
    const bool closed = points.size() >= 4
                     && std::abs(points.front().x - points.back().x) < 1.0e-6f
                     && std::abs(points.front().y - points.back().y) < 1.0e-6f;
    if ((natural == "water" || waterway == "riverbank") && closed) {
      basemap.water_polygons.push_back(WaterPolygonFeature{
        .bounds = compute_projected_bounds(points),
        .ring = points,
      });
      continue;
    }
    if (waterway == "river" || waterway == "stream" || waterway == "canal") {
      basemap.water_lines.push_back(WaterLineFeature{
        .bounds = compute_projected_bounds(points),
        .points = points,
      });
    }
  }

  return basemap;
}

struct RoadPaint {
  ImU32 casing = 0;
  ImU32 fill = 0;
  float casing_width = 1.0f;
  float fill_width = 1.0f;
};

constexpr ImU32 MAP_BG_COLOR = IM_COL32(244, 243, 238, 255);
constexpr ImU32 MAP_WATER_FILL = IM_COL32(193, 216, 235, 185);
constexpr ImU32 MAP_WATER_OUTLINE = IM_COL32(143, 173, 201, 220);
constexpr ImU32 MAP_WATER_LINE = IM_COL32(156, 186, 214, 205);
constexpr ImU32 MAP_ROUTE_HALO = IM_COL32(31, 40, 50, 92);

RoadPaint road_paint(RoadClass road_class, float zoom) {
  const float scale = std::clamp(0.88f + 0.12f * (zoom - 12.0f), 0.76f, 1.95f);
  switch (road_class) {
    case RoadClass::Motorway:
      return {
        .casing = IM_COL32(163, 157, 149, 235),
        .fill = IM_COL32(245, 235, 215, 255),
        .casing_width = 5.6f * scale,
        .fill_width = 3.7f * scale,
      };
    case RoadClass::Primary:
      return {
        .casing = IM_COL32(171, 171, 168, 220),
        .fill = IM_COL32(249, 246, 237, 248),
        .casing_width = 4.6f * scale,
        .fill_width = 2.95f * scale,
      };
    case RoadClass::Secondary:
      return {
        .casing = IM_COL32(183, 186, 189, 210),
        .fill = IM_COL32(252, 251, 247, 240),
        .casing_width = 3.5f * scale,
        .fill_width = 2.15f * scale,
      };
    case RoadClass::Local:
    default:
      return {
        .casing = IM_COL32(200, 202, 205, 195),
        .fill = IM_COL32(255, 255, 254, 230),
        .casing_width = 2.5f * scale,
        .fill_width = 1.5f * scale,
      };
  }
}

void clamp_map_center(TabUiState::MapPaneState *map_state, const GeoBounds &bounds, const ImVec2 &size) {
  if (!bounds.valid() || size.x <= 1.0f || size.y <= 1.0f) {
    return;
  }
  const double zoom = map_state->zoom;
  const double min_x = lon_to_world_x(bounds.west, zoom);
  const double max_x = lon_to_world_x(bounds.east, zoom);
  const double min_y = lat_to_world_y(bounds.north, zoom);
  const double max_y = lat_to_world_y(bounds.south, zoom);
  const double half_w = size.x * 0.5;
  const double half_h = size.y * 0.5;
  double center_x = lon_to_world_x(map_state->center_lon, zoom);
  double center_y = lat_to_world_y(map_state->center_lat, zoom);
  if (max_x - min_x <= size.x) {
    center_x = (min_x + max_x) * 0.5;
  } else {
    center_x = std::clamp(center_x, min_x + half_w, max_x - half_w);
  }
  if (max_y - min_y <= size.y) {
    center_y = (min_y + max_y) * 0.5;
  } else {
    center_y = std::clamp(center_y, min_y + half_h, max_y - half_h);
  }
  map_state->center_lon = world_x_to_lon(center_x, zoom);
  map_state->center_lat = world_y_to_lat(center_y, zoom);
}

void initialize_map_pane_state(TabUiState::MapPaneState *map_state,
                               const GpsTrace &trace,
                               const GeoBounds &bounds,
                               ImVec2 size,
                               SessionDataMode mode,
                               std::optional<GpsPoint> cursor_point) {
  if (trace.points.empty()) {
    return;
  }
  map_state->initialized = true;
  map_state->follow = mode == SessionDataMode::Stream;
  const int min_zoom = minimum_allowed_map_zoom(bounds, trace, size);
  if (mode == SessionDataMode::Stream && cursor_point.has_value()) {
    map_state->zoom = std::max(16.0f, static_cast<float>(min_zoom));
    map_state->center_lat = cursor_point->lat;
    map_state->center_lon = cursor_point->lon;
  } else {
    map_state->zoom = std::max(static_cast<float>(fit_map_zoom_for_trace(trace, size.x, size.y)),
                               static_cast<float>(min_zoom));
    map_state->center_lat = map_trace_center_lat(trace);
    map_state->center_lon = map_trace_center_lon(trace);
  }
  clamp_map_center(map_state, bounds, size);
}

void draw_feature_polyline(ImDrawList *draw_list,
                           const std::vector<ProjectedPoint> &points,
                           float zoom_scale,
                           double top_left_x,
                           double top_left_y,
                           const ImVec2 &rect_min,
                           ImU32 color,
                           float thickness,
                           bool closed = false) {
  if (points.size() < 2) {
    return;
  }
  std::vector<ImVec2> screen;
  screen.reserve(points.size());
  for (const ProjectedPoint &point : points) {
    screen.push_back(ImVec2(rect_min.x + point.x * zoom_scale - static_cast<float>(top_left_x),
                            rect_min.y + point.y * zoom_scale - static_cast<float>(top_left_y)));
  }
  draw_list->AddPolyline(screen.data(), static_cast<int>(screen.size()), color,
                         closed ? ImDrawFlags_Closed : ImDrawFlags_None, thickness);
}

void draw_water_polygon(ImDrawList *draw_list,
                        const WaterPolygonFeature &feature,
                        float zoom_scale,
                        double top_left_x,
                        double top_left_y,
                        const ImVec2 &rect_min) {
  if (feature.ring.size() < 3) {
    return;
  }
  std::vector<ImVec2> screen;
  screen.reserve(feature.ring.size());
  for (const ProjectedPoint &point : feature.ring) {
    screen.push_back(ImVec2(rect_min.x + point.x * zoom_scale - static_cast<float>(top_left_x),
                            rect_min.y + point.y * zoom_scale - static_cast<float>(top_left_y)));
  }
  if (screen.size() >= 3 && is_convex_ring(screen)) {
    draw_list->AddConvexPolyFilled(screen.data(), static_cast<int>(screen.size()), MAP_WATER_FILL);
  }
  draw_list->AddPolyline(screen.data(), static_cast<int>(screen.size()), MAP_WATER_OUTLINE,
                         ImDrawFlags_Closed, 1.8f);
}

void draw_edge_fade(ImDrawList *draw_list,
                    const GeoBounds &bounds,
                    double zoom,
                    double top_left_x,
                    double top_left_y,
                    const ImVec2 &rect_min,
                    const ImVec2 &rect_max) {
  if (!bounds.valid()) {
    return;
  }

  const float west_x = rect_min.x + static_cast<float>(lon_to_world_x(bounds.west, zoom) - top_left_x);
  const float east_x = rect_min.x + static_cast<float>(lon_to_world_x(bounds.east, zoom) - top_left_x);
  const float north_y = rect_min.y + static_cast<float>(lat_to_world_y(bounds.north, zoom) - top_left_y);
  const float south_y = rect_min.y + static_cast<float>(lat_to_world_y(bounds.south, zoom) - top_left_y);

  const float fade_x = std::max(28.0f, (rect_max.x - rect_min.x) * MAP_EDGE_FADE_FRAC);
  const float fade_y = std::max(28.0f, (rect_max.y - rect_min.y) * MAP_EDGE_FADE_FRAC);
  const ImU32 solid = MAP_BG_COLOR;
  const ImU32 clear = IM_COL32(244, 243, 238, 6);

  if (west_x > rect_min.x) {
    const float x0 = rect_min.x;
    const float x1 = std::min(rect_max.x, west_x);
    const float xfade = std::max(x0, x1 - fade_x);
    draw_list->AddRectFilledMultiColor(ImVec2(x0, rect_min.y), ImVec2(xfade, rect_max.y), solid, solid, solid, solid);
    draw_list->AddRectFilledMultiColor(ImVec2(xfade, rect_min.y), ImVec2(x1, rect_max.y), solid, clear, clear, solid);
  }
  if (east_x < rect_max.x) {
    const float x0 = std::max(rect_min.x, east_x);
    const float x1 = rect_max.x;
    const float xfade = std::min(x1, x0 + fade_x);
    draw_list->AddRectFilledMultiColor(ImVec2(x0, rect_min.y), ImVec2(xfade, rect_max.y), clear, solid, solid, clear);
    draw_list->AddRectFilledMultiColor(ImVec2(xfade, rect_min.y), ImVec2(x1, rect_max.y), solid, solid, solid, solid);
  }
  if (north_y > rect_min.y) {
    const float y0 = rect_min.y;
    const float y1 = std::min(rect_max.y, north_y);
    const float yfade = std::max(y0, y1 - fade_y);
    draw_list->AddRectFilledMultiColor(ImVec2(rect_min.x, y0), ImVec2(rect_max.x, yfade), solid, solid, solid, solid);
    draw_list->AddRectFilledMultiColor(ImVec2(rect_min.x, yfade), ImVec2(rect_max.x, y1), solid, solid, clear, clear);
  }
  if (south_y < rect_max.y) {
    const float y0 = std::max(rect_min.y, south_y);
    const float y1 = rect_max.y;
    const float yfade = std::min(y1, y0 + fade_y);
    draw_list->AddRectFilledMultiColor(ImVec2(rect_min.x, y0), ImVec2(rect_max.x, yfade), clear, clear, solid, solid);
    draw_list->AddRectFilledMultiColor(ImVec2(rect_min.x, yfade), ImVec2(rect_max.x, y1), solid, solid, solid, solid);
  }
}

}  // namespace

MapDataManager::MapDataManager() : worker_([this]() { run(); }) {}

MapDataManager::~MapDataManager() {
  {
    std::lock_guard<std::mutex> lock(mutex_);
    stopping_ = true;
  }
  cv_.notify_all();
  if (worker_.joinable()) {
    worker_.join();
  }
}

void MapDataManager::pump() {
  std::unique_ptr<RouteBasemap> ready;
  {
    std::lock_guard<std::mutex> lock(mutex_);
    ready = std::move(completed_);
  }
  if (ready) {
    current_ = std::move(ready);
  }
}

void MapDataManager::ensureTrace(const GpsTrace &trace) {
  if (trace.points.empty()) {
    return;
  }
  const MapRequestSpec wanted = build_request_for_trace(trace);
  if (!wanted.bounds.valid()) {
    return;
  }

  std::lock_guard<std::mutex> lock(mutex_);
  if ((current_ && current_->key == wanted.key) || (pending_ && pending_->key == wanted.key)) {
    return;
  }

  if (const auto cached = load_compressed_basemap(basemap_cache_path(wanted.key), wanted.key)) {
    current_ = std::make_unique<RouteBasemap>(std::move(*cached));
    completed_.reset();
    pending_.reset();
    active_.reset();
    return;
  }

  pending_ = std::make_unique<Request>(Request{
    .key = wanted.key,
    .bounds = wanted.bounds,
    .query = wanted.query,
  });
  cv_.notify_one();
}

bool MapDataManager::loading() const {
  std::lock_guard<std::mutex> lock(mutex_);
  return active_ || pending_;
}

const RouteBasemap *MapDataManager::current() const {
  return current_.get();
}

void MapDataManager::clearCache() {
  std::lock_guard<std::mutex> lock(mutex_);
  clear_cache_directory();
}

MapCacheStats MapDataManager::cacheStats() const {
  return MapCacheStats{
    .bytes = cache_directory_size_bytes(),
    .files = cache_directory_file_count(),
  };
}

void MapDataManager::run() {
  while (true) {
    Request request;
    {
      std::unique_lock<std::mutex> lock(mutex_);
      cv_.wait(lock, [&]() { return stopping_ || pending_ != nullptr; });
      if (stopping_) {
        return;
      }
      request = *pending_;
      active_ = std::move(pending_);
    }

    std::unique_ptr<RouteBasemap> parsed;
    const std::string raw = load_overpass_json(request.query);
    if (!raw.empty()) {
      if (auto basemap = parse_basemap_json(raw, request.bounds, request.key)) {
        save_compressed_basemap(basemap_cache_path(request.key), *basemap);
        parsed = std::make_unique<RouteBasemap>(std::move(*basemap));
      }
    }

    {
      std::lock_guard<std::mutex> lock(mutex_);
      if (active_ && active_->key == request.key) {
        completed_ = std::move(parsed);
        active_.reset();
      }
    }
  }
}

void draw_map_pane(AppSession *session, UiState *state, Pane *, int pane_index) {
  TabUiState *tab_state = app_active_tab_state(state);
  if (tab_state == nullptr || pane_index < 0 || pane_index >= static_cast<int>(tab_state->map_panes.size())) {
    ImGui::TextUnformatted("Map unavailable");
    return;
  }
  if (!session->map_data) {
    ImGui::TextUnformatted("Map unavailable");
    return;
  }

  session->map_data->ensureTrace(session->route_data.gps_trace);
  session->map_data->pump();

  TabUiState::MapPaneState &map_state = tab_state->map_panes[static_cast<size_t>(pane_index)];
  const GpsTrace &trace = session->route_data.gps_trace;
  const RouteBasemap *basemap = session->map_data->current();
  const GeoBounds map_bounds = basemap != nullptr ? basemap->bounds : requested_bounds_for_trace(trace);

  const ImVec2 rect_min = ImGui::GetCursorScreenPos();
  const ImVec2 size = ImGui::GetContentRegionAvail();
  const ImVec2 input_size(std::max(1.0f, size.x - 22.0f), std::max(1.0f, size.y));
  ImGui::SetNextItemAllowOverlap();
  ImGui::InvisibleButton("##map_canvas", input_size);
  const ImVec2 rect_max(rect_min.x + size.x, rect_min.y + size.y);
  const float rect_width = rect_max.x - rect_min.x;
  const float rect_height = rect_max.y - rect_min.y;
  ImDrawList *draw_list = ImGui::GetWindowDrawList();

  draw_list->PushClipRect(rect_min, rect_max, true);
  draw_list->AddRectFilled(rect_min, rect_max, MAP_BG_COLOR);

  if (trace.points.empty()) {
    const char *label = session->async_route_loading ? "Loading map..." : "No GPS trace";
    const ImVec2 text = ImGui::CalcTextSize(label);
    draw_list->AddText(ImVec2(rect_min.x + (rect_width - text.x) * 0.5f,
                              rect_min.y + (rect_height - text.y) * 0.5f),
                       IM_COL32(110, 118, 128, 255), label);
    draw_list->PopClipRect();
    return;
  }

  const std::optional<GpsPoint> cursor_point = state->has_tracker_time
    ? interpolate_gps(trace, state->tracker_time)
    : std::optional<GpsPoint>{};
  if (!map_state.initialized) {
    initialize_map_pane_state(&map_state, trace, map_bounds, size, session->data_mode, cursor_point);
  }

  const int min_zoom = minimum_allowed_map_zoom(map_bounds, trace, size);
  if (map_state.follow && cursor_point.has_value()) {
    const float follow_zoom = std::clamp(map_state.zoom, static_cast<float>(min_zoom), static_cast<float>(MAP_MAX_ZOOM));
    const double center_x = lon_to_world_x(map_state.center_lon, follow_zoom);
    const double center_y = lat_to_world_y(map_state.center_lat, follow_zoom);
    const double top_left_x = center_x - rect_width * 0.5;
    const double top_left_y = center_y - rect_height * 0.5;
    const ImVec2 car_screen = gps_to_screen(cursor_point->lat, cursor_point->lon, follow_zoom, top_left_x, top_left_y, rect_min);
    if (!point_in_rect_with_margin(car_screen, rect_min, rect_max, 0.22f)) {
      map_state.center_lat = cursor_point->lat;
      map_state.center_lon = cursor_point->lon;
    }
  }

  map_state.zoom = std::clamp(map_state.zoom, static_cast<float>(min_zoom), static_cast<float>(MAP_MAX_ZOOM));
  clamp_map_center(&map_state, map_bounds, size);

  const double zoom = map_state.zoom;
  const float zoom_scale = static_cast<float>(std::exp2(zoom));
  const double center_x = lon_to_world_x(map_state.center_lon, zoom);
  const double center_y = lat_to_world_y(map_state.center_lat, zoom);
  const double top_left_x = center_x - rect_width * 0.5;
  const double top_left_y = center_y - rect_height * 0.5;
  const ProjectedBounds current_view = view_bounds(top_left_x, top_left_y, rect_width, rect_height);

  if (basemap != nullptr) {
    for (const WaterPolygonFeature &water : basemap->water_polygons) {
      if (feature_intersects_view(water.bounds, current_view, zoom_scale)) {
        draw_water_polygon(draw_list, water, zoom_scale, top_left_x, top_left_y, rect_min);
      }
    }
    for (const WaterLineFeature &water : basemap->water_lines) {
      if (feature_intersects_view(water.bounds, current_view, zoom_scale)) {
        draw_feature_polyline(draw_list, water.points, zoom_scale, top_left_x, top_left_y, rect_min,
                              MAP_WATER_LINE, 2.4f);
      }
    }

    std::array<RoadClass, 4> order = {
      RoadClass::Local,
      RoadClass::Secondary,
      RoadClass::Primary,
      RoadClass::Motorway,
    };
    for (RoadClass road_class : order) {
      const RoadPaint paint = road_paint(road_class, static_cast<float>(zoom));
      for (const RoadFeature &road : basemap->roads) {
        if (road.road_class != road_class || !feature_intersects_view(road.bounds, current_view, zoom_scale)) {
          continue;
        }
        draw_feature_polyline(draw_list, road.points, zoom_scale, top_left_x, top_left_y, rect_min,
                              paint.casing, paint.casing_width);
        draw_feature_polyline(draw_list, road.points, zoom_scale, top_left_x, top_left_y, rect_min,
                              paint.fill, paint.fill_width);
      }
    }
  }

  if (basemap != nullptr) {
    draw_edge_fade(draw_list, basemap->bounds, zoom, top_left_x, top_left_y, rect_min, rect_max);
  }

  for (size_t i = 1; i < trace.points.size(); ++i) {
    const GpsPoint &p0 = trace.points[i - 1];
    const GpsPoint &p1 = trace.points[i];
    const ImVec2 s0 = gps_to_screen(p0.lat, p0.lon, zoom, top_left_x, top_left_y, rect_min);
    const ImVec2 s1 = gps_to_screen(p1.lat, p1.lon, zoom, top_left_x, top_left_y, rect_min);
    draw_list->AddLine(s0, s1, MAP_ROUTE_HALO, 5.8f);
    draw_list->AddLine(s0, s1, map_timeline_color(p1.type, 1.0f), 3.25f);
  }

  if (cursor_point.has_value()) {
    const ImVec2 marker = gps_to_screen(cursor_point->lat, cursor_point->lon, zoom, top_left_x, top_left_y, rect_min);
    const float marker_size = std::clamp(9.0f + 1.0f * static_cast<float>(zoom - min_zoom), 9.0f, 20.0f);
    draw_car_marker(draw_list, marker, cursor_point->bearing, map_timeline_color(cursor_point->type, 1.0f), marker_size);
  }

  if (session->map_data->loading()) {
    const char *label = basemap != nullptr ? "Refreshing roads..." : "Loading roads...";
    const ImVec2 text = ImGui::CalcTextSize(label);
    const ImVec2 pos(rect_min.x + 12.0f, rect_max.y - text.y - 12.0f);
    draw_list->AddRectFilled(ImVec2(pos.x - 6.0f, pos.y - 4.0f),
                             ImVec2(pos.x + text.x + 6.0f, pos.y + text.y + 4.0f),
                             IM_COL32(255, 255, 255, 180), 4.0f);
    draw_list->AddText(pos, IM_COL32(84, 93, 105, 255), label);
  }
  draw_list->PopClipRect();

  const bool canvas_hovered = ImGui::IsItemHovered();
  const bool double_clicked = canvas_hovered && ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left);
  bool overlay_hovered = false;
  if (const std::string google_maps_url = route_google_maps_url(trace); !google_maps_url.empty()) {
    std::string label = std::string("Google Maps ") + icon::BOX_ARROW_UP_RIGHT;
    const ImVec2 text_size = ImGui::CalcTextSize(label.c_str());
    const ImVec2 button_size(text_size.x + 20.0f, text_size.y + 10.0f);
    const ImVec2 button_pos(rect_max.x - button_size.x - 28.0f, rect_min.y + 10.0f);
    ImGui::SetCursorScreenPos(button_pos);
    ImGui::SetNextItemAllowOverlap();
    if (ImGui::Button("##open_google_maps", button_size)) {
      open_external_url(google_maps_url);
      state->status_text = "Opened Google Maps";
    }
    overlay_hovered = ImGui::IsItemHovered();
    draw_list->AddText(ImVec2(button_pos.x + 10.0f, button_pos.y + (button_size.y - text_size.y) * 0.5f),
                       ImGui::GetColorU32(ImGuiCol_Text), label.c_str());
  }
  const bool hovered = canvas_hovered && !overlay_hovered;
  if (hovered && ImGui::GetIO().MouseWheel != 0.0f) {
    const float next_zoom = std::clamp(static_cast<float>(zoom) + ImGui::GetIO().MouseWheel * MAP_WHEEL_ZOOM_STEP,
                                       static_cast<float>(min_zoom), static_cast<float>(MAP_MAX_ZOOM));
    if (std::abs(next_zoom - zoom) > 1.0e-4f) {
      const ImVec2 mouse = ImGui::GetIO().MousePos;
      const double mouse_world_x = top_left_x + (mouse.x - rect_min.x);
      const double mouse_world_y = top_left_y + (mouse.y - rect_min.y);
      const double mouse_lon = world_x_to_lon(mouse_world_x, zoom);
      const double mouse_lat = world_y_to_lat(mouse_world_y, zoom);
      const double next_center_x = lon_to_world_x(mouse_lon, next_zoom) - (mouse.x - rect_min.x) + rect_width * 0.5;
      const double next_center_y = lat_to_world_y(mouse_lat, next_zoom) - (mouse.y - rect_min.y) + rect_height * 0.5;
      map_state.zoom = next_zoom;
      map_state.center_lon = world_x_to_lon(next_center_x, next_zoom);
      map_state.center_lat = world_y_to_lat(next_center_y, next_zoom);
      map_state.follow = false;
      clamp_map_center(&map_state, map_bounds, size);
    }
  }
  if (hovered && ImGui::IsMouseDragging(ImGuiMouseButton_Left, 2.0f)) {
    const ImVec2 delta = ImGui::GetIO().MouseDelta;
    const double next_center_x = center_x - delta.x;
    const double next_center_y = center_y - delta.y;
    map_state.center_lon = world_x_to_lon(next_center_x, zoom);
    map_state.center_lat = world_y_to_lat(next_center_y, zoom);
    map_state.follow = false;
    clamp_map_center(&map_state, map_bounds, size);
  } else if (hovered && ImGui::IsMouseReleased(ImGuiMouseButton_Left)) {
    const ImVec2 drag_delta = ImGui::GetMouseDragDelta(ImGuiMouseButton_Left);
    if (drag_delta.x * drag_delta.x + drag_delta.y * drag_delta.y < 16.0f) {
      const ImVec2 mouse = ImGui::GetIO().MousePos;
      double best_dist = std::numeric_limits<double>::max();
      double best_time = state->tracker_time;
      for (const GpsPoint &point : trace.points) {
        const ImVec2 screen = gps_to_screen(point.lat, point.lon, zoom, top_left_x, top_left_y, rect_min);
        const double dx = static_cast<double>(screen.x - mouse.x);
        const double dy = static_cast<double>(screen.y - mouse.y);
        const double dist = dx * dx + dy * dy;
        if (dist < best_dist) {
          best_dist = dist;
          best_time = point.time;
        }
      }
      state->tracker_time = best_time;
      state->has_tracker_time = true;
    }
    ImGui::ResetMouseDragDelta(ImGuiMouseButton_Left);
  }
  if (double_clicked) {
    map_state.initialized = false;
  }
}
