From bc301c5fdbcc6b62f893d617f532b00a9f1074c3 Mon Sep 17 00:00:00 2001 From: Mark Thompson <129641948+NotherNgineer@users.noreply.github.com> Date: Tue, 16 Jan 2024 02:30:31 -0600 Subject: [PATCH] Geomap enhancements (#1767) * Increase Geomap zoom-in ability * Use floating point for current position * Show grid squares when zoomed in too much for map * Zoom in fast after exceeding map resolution & clean up redundant code * Revert order of functions to make it easier to review * Changed grid color for better contrast with markers * Optimizations * Set x_pos/x_pos to center pixel versus upper left corner * Show more distant planes when zoomed out * Correct pixel offset when zooming in * Fix oops in x_pos/y_pos centering change * Wrapping support for lat/lon fields * Wrapping support (for Geomap lat/lon fields) * Handle wrapping for negative lat/lon --- firmware/application/ui/ui_geomap.cpp | 350 ++++++++++++++++---------- firmware/application/ui/ui_geomap.hpp | 67 +++-- firmware/common/ui_widget.cpp | 10 +- firmware/common/ui_widget.hpp | 1 + 4 files changed, 269 insertions(+), 159 deletions(-) diff --git a/firmware/application/ui/ui_geomap.cpp b/firmware/application/ui/ui_geomap.cpp index b277e44b..c1535730 100644 --- a/firmware/application/ui/ui_geomap.cpp +++ b/firmware/application/ui/ui_geomap.cpp @@ -65,6 +65,7 @@ GeoPos::GeoPos( set_lon(0); const auto changed_fn = [this](int32_t) { + // Convert degrees/minutes/seconds fields to decimal (floating point) lat/lon degree float lat_value = lat(); float lon_value = lon(); @@ -84,6 +85,27 @@ GeoPos::GeoPos( field_lon_minutes.on_change = changed_fn; field_lon_seconds.on_change = changed_fn; + const auto wrapped_lat_seconds = [this](int32_t v) { + field_lat_minutes.on_encoder(v); + }; + + const auto wrapped_lat_minutes = [this](int32_t v) { + field_lat_degrees.on_encoder((field_lat_degrees.value() >= 0) ? v : -v); + }; + + const auto wrapped_lon_seconds = [this](int32_t v) { + field_lon_minutes.on_encoder(v); + }; + + const auto wrapped_lon_minutes = [this](int32_t v) { + field_lon_degrees.on_encoder((field_lon_degrees.value() >= 0) ? v : -v); + }; + + field_lat_seconds.on_wrap = wrapped_lat_seconds; + field_lat_minutes.on_wrap = wrapped_lat_minutes; + field_lon_seconds.on_wrap = wrapped_lon_seconds; + field_lon_minutes.on_wrap = wrapped_lon_minutes; + text_alt_unit.set(altitude_unit_ ? "m" : "ft"); if (speed_unit_ == KMPH) text_speed_unit.set("kmph"); if (speed_unit_ == MPH) text_speed_unit.set("mph"); @@ -173,12 +195,8 @@ bool GeoMap::on_encoder(const EncoderEvent delta) { if (map_zoom == -2) { map_zoom = 1; } else { - map_zoom++; - - // When zooming in, ensure that MOD(240,map_zoom)==0 for the map_zoom_line() function - if ((map_zoom > 1) && (geomap_rect_width % map_zoom != 0)) { - map_zoom--; - } + // zoom in faster after exceeding the map resolution limit + map_zoom += (map_zoom >= MAP_ZOOM_RESOLUTION_LIMIT) ? map_zoom : 1; } } } else if (delta < 0) { @@ -186,13 +204,19 @@ bool GeoMap::on_encoder(const EncoderEvent delta) { if (map_zoom == 1) { map_zoom = -2; } else { - map_zoom--; + if (map_zoom > MAP_ZOOM_RESOLUTION_LIMIT) + map_zoom /= 2; + else + map_zoom--; } } } else { return false; } + map_visible = map_opened && (map_zoom <= MAP_ZOOM_RESOLUTION_LIMIT); + zoom_pixel_offset = (map_visible && (map_zoom > 1)) ? (float)map_zoom / 2 : 0.0f; + // Trigger map redraw markerListUpdated = true; set_dirty(); @@ -208,7 +232,9 @@ void GeoMap::map_read_line(ui::Color* buffer, uint16_t pixels) { // Zoom in: Expand each pixel to "map_zoom" number of pixels. // Future TODO: Add dithering to smooth out the pixelation. // As long as MOD(width,map_zoom)==0 then we don't need to check buffer overflow case when stretching last pixel; - // For 240 width, than means no check is needed for map_zoom values up to 6 + // For 240 width, than means no check is needed for map_zoom values up to 6. + // (Rectangle height must also divide evenly into map_zoom or we get black lines at end of screen) + // Note that zooming in results in a map offset of (1/map_zoom) pixels to the right & downward directions (see zoom_pixel_offset). for (int i = (geomap_rect_width / map_zoom) - 1; i >= 0; i--) { for (int j = 0; j < map_zoom; j++) { buffer[(i * map_zoom) + j] = buffer[i]; @@ -228,91 +254,145 @@ void GeoMap::map_read_line(ui::Color* buffer, uint16_t pixels) { } void GeoMap::draw_markers(Painter& painter) { + for (int i = 0; i < markerListLen; ++i) { + draw_marker_item(painter, markerList[i], Color::blue(), Color::blue(), Color::magenta()); + } +} + +void GeoMap::draw_marker_item(Painter& painter, GeoMarker& item, const Color color, const Color fontColor, const Color backColor) { + const auto r = screen_rect(); + const ui::Point itemPoint = item_rect_pixel(item); + + if ((itemPoint.x() >= 0) && (itemPoint.x() < r.width()) && + (itemPoint.y() > 10) && (itemPoint.y() < r.height())) // Dont draw within symbol size of top + { + draw_marker(painter, {itemPoint.x(), itemPoint.y() + r.top()}, item.angle, item.tag, color, fontColor, backColor); + } +} + +// Calculate screen position of item, adjusted for zoom factor. +ui::Point GeoMap::item_rect_pixel(GeoMarker& item) { + const auto r = screen_rect(); + const auto geomap_rect_half_width = r.width() / 2; + const auto geomap_rect_half_height = r.height() / 2; + + GeoPoint mapPoint = lat_lon_to_map_pixel(item.lat, item.lon); + float x = mapPoint.x - x_pos; + float y = mapPoint.y - y_pos; + + if (map_zoom > 1) { + x = x * map_zoom + zoom_pixel_offset; + y = y * map_zoom + zoom_pixel_offset; + } else if (map_zoom < 0) { + x = x / (-map_zoom); + y = y / (-map_zoom); + } + + x += geomap_rect_half_width; + y += geomap_rect_half_height; + + return {(int16_t)x, (int16_t)y}; +} + +// Converts latitude/longitude to pixel coordinates in map file. +// (Note that when map_zoom==1, one pixel in map file corresponds to 1 pixel on screen) +GeoPoint GeoMap::lat_lon_to_map_pixel(float lat, float lon) { + // Using WGS 84/Pseudo-Mercator projection + float x = (map_width * (lon + 180) / 360); + + // Latitude calculation based on https://stackoverflow.com/a/10401734/2278659 + double lat_rad = sin(lat * pi / 180); + float y = (map_height - ((map_world_lon / 2 * log((1 + lat_rad) / (1 - lat_rad))) - map_offset)); + + return {x, y}; +} + +// Draw grid in place of map (when zoom-in level is too high). +void GeoMap::draw_map_grid() { const auto r = screen_rect(); - for (int i = 0; i < markerListLen; ++i) { - GeoMarker& item = markerList[i]; - double lat_rad = sin(item.lat * pi / 180); - int x = (map_width * (item.lon + 180) / 360) - x_pos; - int y = (map_height - ((map_world_lon / 2 * log((1 + lat_rad) / (1 - lat_rad))) - map_offset)) - y_pos; // Offset added for the GUI + // Grid spacing is just based on zoom at the moment, and centered on screen. + // TODO: Maybe align with latitude/longitude seconds instead? + int grid_spacing = map_zoom * 2; + int x = (r.width() / 2) % grid_spacing; + int y = (r.height() / 2) % grid_spacing; - if (map_zoom > 1) { - x = ((x - (r.width() / 2)) * map_zoom) + (r.width() / 2); - y = ((y - (r.height() / 2)) * map_zoom) + (r.height() / 2); - } else if (map_zoom < 0) { - x = ((x - (r.width() / 2)) / (-map_zoom)) + (r.width() / 2); - y = ((y - (r.height() / 2)) / (-map_zoom)) + (r.height() / 2); - } + if (map_zoom <= MAP_ZOOM_RESOLUTION_LIMIT) + return; - if ((x >= 0) && (x < r.width()) && - (y > 10) && (y < r.height())) // Dont draw within symbol size of top - { - ui::Point itemPoint(x, y + r.top()); - if (y >= 32) { // Dont draw text if it would overlap top - // Text and symbol - draw_marker(painter, itemPoint, item.angle, item.tag, Color::blue(), Color::blue(), Color::magenta()); - } else { - // Only symbol - draw_bearing(itemPoint, item.angle, 10, Color::blue()); - } - } + display.fill_rectangle({{0, r.top()}, {r.width(), r.height()}}, Color::black()); + + for (uint16_t line = y; line < r.height(); line += grid_spacing) { + display.fill_rectangle({{0, r.top() + line}, {r.width(), 1}}, Color::darker_grey()); + } + for (uint16_t column = x; column < r.width(); column += grid_spacing) { + display.fill_rectangle({{column, r.top()}, {1, r.height()}}, Color::darker_grey()); } } void GeoMap::paint(Painter& painter) { const auto r = screen_rect(); - std::array map_line_buffer; + std::array map_line_buffer; + int16_t zoom_seek_x, zoom_seek_y; // Ony redraw map if it moved by at least 1 pixel // or the markers list was updated - int x_diff = abs(x_pos - prev_x_pos); - int y_diff = abs(y_pos - prev_y_pos); + int x_diff = abs(x_pos - prev_x_pos) * ((map_zoom > 1) ? map_zoom : 1); + int y_diff = abs(y_pos - prev_y_pos) * ((map_zoom > 1) ? map_zoom : 1); + int min_diff = 1; - if (markerListUpdated || (x_diff >= 3) || (y_diff >= 3)) { - int32_t zoom_seek_x = x_pos; - int32_t zoom_seek_y = y_pos; - - // Adjust starting corner position of map per zoom setting - if (map_zoom > 1) { - zoom_seek_x += (r.width() - (r.width() / map_zoom)) / 2; - zoom_seek_y += (r.height() - (r.height() / map_zoom)) / 2; - } else if (map_zoom < 0) { - zoom_seek_x += (r.width() - (r.width() * (-map_zoom))) / 2; - zoom_seek_y += (r.height() - (r.height() * (-map_zoom))) / 2; - } - - // Read from map file and display to zoomed scale - int duplicate_lines = (map_zoom < 0) ? 1 : map_zoom; - for (uint16_t line = 0; line < (r.height() / duplicate_lines); line++) { - uint16_t seek_line = zoom_seek_y + ((map_zoom >= 0) ? line : line * (-map_zoom)); - map_file.seek(4 + ((zoom_seek_x + (map_width * seek_line)) << 1)); - map_read_line(map_line_buffer.data(), r.width()); - - for (uint16_t j = 0; j < duplicate_lines; j++) { - display.draw_pixels({0, r.top() + (line * duplicate_lines) + j, r.width(), 1}, map_line_buffer); - } - } - - prev_x_pos = x_pos; + if (markerListUpdated || (x_diff >= min_diff) || (y_diff >= min_diff)) { + prev_x_pos = x_pos; // Note x_pos/y_pos pixel position in map file now correspond to screen rect CENTER pixel prev_y_pos = y_pos; + // Adjust starting corner position of map per zoom setting; + // When zooming in the map should technically by shifted left & up by another map_zoom/2 pixels but + // the map_read_line() function doesn't handle that yet so we're adjusting markers instead (see zoom_pixel_offset). + if (map_zoom > 1) { + zoom_seek_x = x_pos - (float)r.width() / (2 * map_zoom); + zoom_seek_y = y_pos - (float)r.height() / (2 * map_zoom); + } else if (map_zoom < 0) { + zoom_seek_x = x_pos - (r.width() * (-map_zoom)) / 2; + zoom_seek_y = y_pos - (r.height() * (-map_zoom)) / 2; + } else { + zoom_seek_x = x_pos - (r.width() / 2); + zoom_seek_y = y_pos - (r.height() / 2); + } + + if (map_visible) { + // Read from map file and display to zoomed scale + int duplicate_lines = (map_zoom < 0) ? 1 : map_zoom; + for (uint16_t line = 0; line < (r.height() / duplicate_lines); line++) { + uint16_t seek_line = zoom_seek_y + ((map_zoom >= 0) ? line : line * (-map_zoom)); + map_file.seek(4 + ((zoom_seek_x + (map_width * seek_line)) << 1)); + map_read_line(map_line_buffer.data(), r.width()); + + for (uint16_t j = 0; j < duplicate_lines; j++) { + display.draw_pixels({0, r.top() + (line * duplicate_lines) + j, r.width(), 1}, map_line_buffer); + } + } + } else { + // No map data or excessive zoom; just draw a grid + draw_map_grid(); + } + // Draw crosshairs in center in manual panning mode if (manual_panning_) { - display.fill_rectangle({r.center() - Point(16, 1), {32, 2}}, Color::red()); - display.fill_rectangle({r.center() - Point(1, 16), {2, 32}}, Color::red()); + display.fill_rectangle({r.center() - Point(16, 1) + Point(zoom_pixel_offset, zoom_pixel_offset), {32, 2}}, Color::red()); + display.fill_rectangle({r.center() - Point(1, 16) + Point(zoom_pixel_offset, zoom_pixel_offset), {2, 32}}, Color::red()); } // Draw the other markers draw_markers(painter); draw_scale(painter); - draw_mypos(); + draw_mypos(painter); markerListUpdated = false; set_clean(); } // Draw the marker in the center - if (!manual_panning_) { - draw_marker(painter, r.center(), angle_, tag_, Color::red(), Color::white(), Color::black()); + if (!manual_panning_ && !hide_center_marker_) { + draw_marker(painter, r.center() + Point(zoom_pixel_offset, zoom_pixel_offset), angle_, tag_, Color::red(), Color::white(), Color::black()); } } @@ -336,35 +416,41 @@ bool GeoMap::on_touch(const TouchEvent event) { } void GeoMap::move(const float lon, const float lat) { + const auto r = screen_rect(); + lon_ = lon; lat_ = lat; - // Using WGS 84/Pseudo-Mercator projection - x_pos = map_width * (lon_ + 180) / 360 - (geomap_rect_width / 2); - - // Latitude calculation based on https://stackoverflow.com/a/10401734/2278659 - double lat_rad = sin(lat * pi / 180); - y_pos = map_height - ((map_world_lon / 2 * log((1 + lat_rad) / (1 - lat_rad))) - map_offset) - 128; // Offset added for the GUI + // Calculate x_pos/y_pos in map file corresponding to CENTER pixel of screen rect + // (Note there is a 1:1 correspondence between map file pixels and screen pixels when map_zoom=1) + GeoPoint mapPoint = lat_lon_to_map_pixel(lat_, lon_); + x_pos = mapPoint.x; + y_pos = mapPoint.y; // Cap position - if (x_pos > (map_width - geomap_rect_width)) - x_pos = map_width - geomap_rect_width; - if (y_pos > (map_height + geomap_rect_height)) - y_pos = map_height - geomap_rect_height; + if (x_pos > (map_width - r.width() / 2)) + x_pos = map_width - r.width() / 2; + if (y_pos > (map_height + r.height() / 2)) + y_pos = map_height - r.height() / 2; // Scale calculation float km_per_deg_lon = cos(lat * pi / 180) * 111.321; // 111.321 km/deg longitude at equator, and 0 km at poles - pixels_per_km = (geomap_rect_width / 2) / km_per_deg_lon; + pixels_per_km = (r.width() / 2) / km_per_deg_lon; } bool GeoMap::init() { auto result = map_file.open("ADSB/world_map.bin"); - if (result.is_valid()) - return false; + map_opened = !result.is_valid(); - map_file.read(&map_width, 2); - map_file.read(&map_height, 2); + if (map_opened) { + map_file.read(&map_width, 2); + map_file.read(&map_height, 2); + } else { + map_width = 32768; + map_height = 32768; + } + map_visible = map_opened; map_center_x = map_width >> 1; map_center_y = map_height >> 1; @@ -375,7 +461,7 @@ bool GeoMap::init() { map_world_lon = map_width / (2 * pi); map_offset = (map_world_lon / 2 * log((1 + map_bottom) / (1 - map_bottom))); - return true; + return map_opened; } void GeoMap::set_mode(GeoMapMode mode) { @@ -391,19 +477,31 @@ bool GeoMap::manual_panning() { } void GeoMap::draw_scale(Painter& painter) { - uint16_t km = 800; - uint16_t scale_width = (map_zoom > 0) ? km * pixels_per_km * map_zoom : km * pixels_per_km / (-map_zoom); + uint32_t m = 800000; + uint32_t scale_width = (map_zoom > 0) ? m * map_zoom * pixels_per_km : m * pixels_per_km / (-map_zoom); + ui::Color scale_color = (map_visible) ? Color::black() : Color::white(); + std::string km_string; - while (scale_width > screen_width / 2) { + while (scale_width > screen_width * 1000 / 2) { scale_width /= 2; - km /= 2; + m /= 2; + } + scale_width /= 1000; + if (m < 1000) { + km_string = to_string_dec_uint(m) + "m"; + } else { + uint32_t km = m / 1000; + m -= km * 1000; + if (m == 0) { + km_string = to_string_dec_uint(km) + " km"; + } else { + km_string = to_string_dec_uint(km) + "." + to_string_dec_uint((m + 50) / 100, 1) + "km"; + } } - std::string km_string = to_string_dec_uint(km) + " km"; - - display.fill_rectangle({{screen_width - 5 - scale_width, screen_height - 4}, {scale_width, 2}}, Color::black()); - display.fill_rectangle({{screen_width - 5, screen_height - 8}, {2, 6}}, Color::black()); - display.fill_rectangle({{screen_width - 5 - scale_width, screen_height - 8}, {2, 6}}, Color::black()); + display.fill_rectangle({{screen_width - 5 - (uint16_t)scale_width, screen_height - 4}, {(uint16_t)scale_width, 2}}, scale_color); + display.fill_rectangle({{screen_width - 5, screen_height - 8}, {2, 6}}, scale_color); + display.fill_rectangle({{screen_width - 5 - (uint16_t)scale_width, screen_height - 8}, {2, 6}}, scale_color); painter.draw_string({(uint16_t)(screen_width - 25 - scale_width - km_string.length() * 5 / 2), screen_height - 10}, ui::font::fixed_5x8, Color::black(), Color::white(), km_string); } @@ -422,9 +520,13 @@ void GeoMap::draw_bearing(const Point origin, const uint16_t angle, uint32_t siz size--; } + + display.draw_pixel(origin, color); // 1 pixel indicating center pivot point of bearing symbol } void GeoMap::draw_marker(Painter& painter, const ui::Point itemPoint, const uint16_t itemAngle, const std::string itemTag, const Color color, const Color fontColor, const Color backColor) { + const auto r = screen_rect(); + int tagOffset = 10; if (mode_ == PROMPT) { // Cross @@ -442,45 +544,15 @@ void GeoMap::draw_marker(Painter& painter, const ui::Point itemPoint, const uint tagOffset = 8; } // center tag above point - if (itemTag.find_first_not_of(' ') != itemTag.npos) { // only draw tag if we have something other than spaces + if ((itemPoint.y() - r.top() >= 32) && (itemTag.find_first_not_of(' ') != itemTag.npos)) { // only draw tag if doesn't overlap top & not just spaces painter.draw_string(itemPoint - Point(((int)itemTag.length() * 8 / 2), 14 + tagOffset), style().font, fontColor, backColor, itemTag); } } -void GeoMap::draw_mypos() { - const auto r = screen_rect(); - if (my_lat >= 200 || my_lon >= 200) return; // invalid - int x = (map_width * (my_lon + 180) / 360) - x_pos; - double lat_rad = sin(my_lat * pi / 180); - int y = (map_height - ((map_world_lon / 2 * log((1 + lat_rad) / (1 - lat_rad))) - map_offset)) - y_pos; // Offset added for the GUI - auto color = Color::yellow(); - - if (map_zoom > 1) { - x = ((x - (r.width() / 2)) * map_zoom) + (r.width() / 2); - y = ((y - (r.height() / 2)) * map_zoom) + (r.height() / 2); - } else if (map_zoom < 0) { - x = ((x - (r.width() / 2)) / (-map_zoom)) + (r.width() / 2); - y = ((y - (r.height() / 2)) / (-map_zoom)) + (r.height() / 2); - } - - if ((x >= 0) && (x < r.width()) && - (y > 10) && (y < r.height())) // Dont draw within symbol size of top - { - ui::Point itemPoint(x, y + r.top()); - if (mode_ == PROMPT) { - // Cross - display.fill_rectangle({itemPoint - Point(16, 1), {32, 2}}, color); - display.fill_rectangle({itemPoint - Point(1, 16), {2, 32}}, color); - } else if (my_angle < 360) { - // if we have a valid angle draw bearing - draw_bearing(itemPoint, my_angle, 10, color); - } else { - // draw a small cross - display.fill_rectangle({itemPoint - Point(8, 1), {16, 2}}, color); - display.fill_rectangle({itemPoint - Point(1, 8), {2, 16}}, color); - } - } +void GeoMap::draw_mypos(Painter& painter) { + if ((my_pos.lat < INVALID_LAT_LON) && (my_pos.lon < INVALID_LAT_LON)) + draw_marker_item(painter, my_pos, Color::yellow()); } void GeoMap::clear_markers() { @@ -488,15 +560,17 @@ void GeoMap::clear_markers() { } MapMarkerStored GeoMap::store_marker(GeoMarker& marker) { + const auto r = screen_rect(); MapMarkerStored ret; // Check if it could be on screen - // Only checking one direction to reduce CPU - double lat_rad = sin(marker.lat * pi / 180); - int x = (map_width * (marker.lon + 180) / 360) - x_pos; - int y = (map_height - ((map_world_lon / 2 * log((1 + lat_rad) / (1 - lat_rad))) - map_offset)) - y_pos; // Offset added for the GUI - if (false == ((x >= 0) && (x < geomap_rect_width) && (y > 10) && (y < geomap_rect_height))) // Dont draw within symbol size of top - { + // (Shows more distant planes when zoomed out) + GeoPoint mapPoint = lat_lon_to_map_pixel(marker.lat, marker.lon); + int x_dist = abs((int)mapPoint.x - (int)x_pos); + int y_dist = abs((int)mapPoint.y - (int)y_pos); + int zoom_out = (map_zoom < 0) ? -map_zoom : 1; + + if ((x_dist >= (zoom_out * r.width() / 2)) || (y_dist >= (zoom_out * r.height() / 2))) { ret = MARKER_NOT_STORED; } else if (markerListLen < NumMarkerListElements) { markerList[markerListLen] = marker; @@ -510,14 +584,14 @@ MapMarkerStored GeoMap::store_marker(GeoMarker& marker) { } void GeoMap::update_my_position(float lat, float lon, int32_t altitude) { - my_lat = lat; - my_lon = lon; + my_pos.lat = lat; + my_pos.lon = lon; my_altitude = altitude; markerListUpdated = true; set_dirty(); } void GeoMap::update_my_orientation(uint16_t angle, bool refresh) { - my_angle = angle; + my_pos.angle = angle; if (refresh) { markerListUpdated = true; set_dirty(); @@ -527,7 +601,7 @@ void GeoMap::update_my_orientation(uint16_t angle, bool refresh) { void GeoMapView::focus() { geopos.focus(); - if (!map_opened) + if (!geomap.map_file_opened()) nav_.display_modal("No map", "No world_map.bin file in\n/ADSB/ directory", ABORT); } @@ -627,8 +701,7 @@ GeoMapView::GeoMapView( add_child(&geopos); - map_opened = geomap.init(); - if (!map_opened) return; + geomap.init(); setup(); @@ -660,8 +733,7 @@ GeoMapView::GeoMapView( add_child(&geopos); - map_opened = geomap.init(); - if (!map_opened) return; + geomap.init(); setup(); add_child(&button_ok); diff --git a/firmware/application/ui/ui_geomap.hpp b/firmware/application/ui/ui_geomap.hpp index 1e0245b3..2c2c1d1c 100644 --- a/firmware/application/ui/ui_geomap.hpp +++ b/firmware/application/ui/ui_geomap.hpp @@ -29,16 +29,30 @@ #include "portapack.hpp" -#define MAX_MAP_ZOOM_IN 5 -#define MAX_MAP_ZOOM_OUT 10 - namespace ui { +#define MAX_MAP_ZOOM_IN 4000 +#define MAX_MAP_ZOOM_OUT 12 +#define MAP_ZOOM_RESOLUTION_LIMIT 5 // Max zoom-in to show map; rect height & width must divide into this evenly + +#define INVALID_LAT_LON 200 +#define INVALID_ANGLE 400 + +#define GEOMAP_BANNER_HEIGHT (3 * 16) +#define GEOMAP_RECT_WIDTH 240 +#define GEOMAP_RECT_HEIGHT (320 - 16 - GEOMAP_BANNER_HEIGHT) + enum GeoMapMode { DISPLAY, PROMPT }; +struct GeoPoint { + public: + float x{0}; + float y{0}; +}; + struct GeoMarker { public: float lat{0}; @@ -133,13 +147,15 @@ class GeoPos : public View { 2, {0, 59}, 1, - ' '}; + ' ', + true}; NumberField field_lat_seconds{ {13 * 8, 1 * 16}, 2, {0, 59}, 1, - ' '}; + ' ', + true}; Text text_lat_decimal{ {17 * 8, 1 * 16, 13 * 8, 1 * 16}, ""}; @@ -155,13 +171,15 @@ class GeoPos : public View { 2, {0, 59}, 1, - ' '}; + ' ', + true}; NumberField field_lon_seconds{ {13 * 8, 2 * 16}, 2, {0, 59}, 1, - ' '}; + ' ', + true}; Text text_lon_decimal{ {17 * 8, 2 * 16, 13 * 8, 1 * 16}, ""}; @@ -201,26 +219,40 @@ class GeoMap : public Widget { angle_ = new_angle; } + bool map_file_opened() { return map_opened; } + + void set_hide_center_marker(bool hide) { + hide_center_marker_ = hide; + } + bool hide_center_marker() { return hide_center_marker_; } + static const int NumMarkerListElements = 30; void clear_markers(); MapMarkerStored store_marker(GeoMarker& marker); - static const Dim banner_height = 3 * 16; - static const Dim geomap_rect_width = 240; - static const Dim geomap_rect_height = 320 - 16 - banner_height; + static const Dim banner_height = GEOMAP_BANNER_HEIGHT; + static const Dim geomap_rect_width = GEOMAP_RECT_WIDTH; + static const Dim geomap_rect_height = GEOMAP_RECT_HEIGHT; private: void draw_scale(Painter& painter); - void draw_bearing(const Point origin, const uint16_t angle, uint32_t size, const Color color); + ui::Point item_rect_pixel(GeoMarker& item); + GeoPoint lat_lon_to_map_pixel(float lat, float lon); + void draw_marker_item(Painter& painter, GeoMarker& item, const Color color, const Color fontColor = Color::white(), const Color backColor = Color::black()); void draw_marker(Painter& painter, const ui::Point itemPoint, const uint16_t itemAngle, const std::string itemTag, const Color color = Color::red(), const Color fontColor = Color::white(), const Color backColor = Color::black()); void draw_markers(Painter& painter); - void draw_mypos(); + void draw_mypos(Painter& painter); + void draw_bearing(const Point origin, const uint16_t angle, uint32_t size, const Color color); + void draw_map_grid(); void map_read_line(ui::Color* buffer, uint16_t pixels); bool manual_panning_{false}; + bool hide_center_marker_{false}; GeoMapMode mode_{}; File map_file{}; + bool map_opened{}; + bool map_visible{}; uint16_t map_width{}, map_height{}; int32_t map_center_x{}, map_center_y{}; int16_t map_zoom{1}; @@ -229,19 +261,18 @@ class GeoMap : public Widget { double map_world_lon{}; double map_offset{}; - int32_t x_pos{}, y_pos{}; - int32_t prev_x_pos{0xFFFF}, prev_y_pos{0xFFFF}; + float x_pos{}, y_pos{}; + float prev_x_pos{32767.0f}, prev_y_pos{32767.0f}; float lat_{}; float lon_{}; + float zoom_pixel_offset{0.0f}; float pixels_per_km{}; uint16_t angle_{}; std::string tag_{}; // the portapack's position data ( for example injected from serial ) - float my_lat{200}; - float my_lon{200}; + GeoMarker my_pos{INVALID_LAT_LON, INVALID_LAT_LON, INVALID_ANGLE, ""}; // lat, lon, angle, tag int32_t my_altitude{0}; - uint16_t my_angle{400}; int markerListLen{0}; GeoMarker markerList[NumMarkerListElements]; @@ -303,8 +334,6 @@ class GeoMapView : public View { uint16_t angle_{}; std::function on_close_{nullptr}; - bool map_opened{}; - GeoPos geopos{ {0, 0}, altitude_unit_, diff --git a/firmware/common/ui_widget.cpp b/firmware/common/ui_widget.cpp index 2f654c05..7711e82f 100644 --- a/firmware/common/ui_widget.cpp +++ b/firmware/common/ui_widget.cpp @@ -31,7 +31,7 @@ #include "chprintf.h" #include "irq_controls.hpp" #include "string_format.hpp" -#include "usb_serial_device_to_host.h" +#include "usb_serial_io.h" using namespace portapack; @@ -2118,7 +2118,15 @@ bool NumberField::on_key(const KeyEvent key) { } bool NumberField::on_encoder(const EncoderEvent delta) { + int32_t old_value = value(); set_value(value() + (delta * step)); + + if (on_wrap) { + if ((delta > 0) && (value() < old_value)) + on_wrap(1); + else if ((delta < 0) && (value() > old_value)) + on_wrap(-1); + } return true; } diff --git a/firmware/common/ui_widget.hpp b/firmware/common/ui_widget.hpp index 0c989234..5b57cd2e 100644 --- a/firmware/common/ui_widget.hpp +++ b/firmware/common/ui_widget.hpp @@ -783,6 +783,7 @@ class NumberField : public Widget { public: std::function on_select{}; std::function on_change{}; + std::function on_wrap{}; using range_t = std::pair;