diff --git a/firmware/application/apps/ais_app.cpp b/firmware/application/apps/ais_app.cpp index 07f2045cb..093d65716 100644 --- a/firmware/application/apps/ais_app.cpp +++ b/firmware/application/apps/ais_app.cpp @@ -456,7 +456,10 @@ void AISAppView::on_tick_second() { ++timer_seconds; if (timer_seconds % 10 == 0) { if (recent_entry_detail_view.hidden()) return; - recent_entry_detail_view.update_map_markers(recent); + if (got_new_packet) { + got_new_packet = false; + recent_entry_detail_view.update_map_markers(recent); + } } } @@ -475,7 +478,7 @@ void AISAppView::on_packet(const ais::Packet& packet) { if (logger) { logger->on_packet(packet); } - + got_new_packet = true; auto& entry = ::on_packet(recent, packet.source_id()); entry.update(packet); recent_entries_view.set_dirty(); diff --git a/firmware/application/apps/ais_app.hpp b/firmware/application/apps/ais_app.hpp index 20bb3a67c..094d1b37a 100644 --- a/firmware/application/apps/ais_app.hpp +++ b/firmware/application/apps/ais_app.hpp @@ -221,6 +221,7 @@ class AISAppView : public View { }; SignalToken signal_token_tick_second{}; uint8_t timer_seconds = 0; + bool got_new_packet{false}; // got any new packet since latest screen update? MessageHandlerRegistration message_handler_packet{ Message::ID::AISPacket, diff --git a/firmware/application/apps/ui_adsb_rx.cpp b/firmware/application/apps/ui_adsb_rx.cpp index 83a60eedc..c0f9fd4c1 100644 --- a/firmware/application/apps/ui_adsb_rx.cpp +++ b/firmware/application/apps/ui_adsb_rx.cpp @@ -635,14 +635,14 @@ void ADSBRxView::refresh_ui() { if (details_view->map_active()) { // Is it time to clear and refresh the map's markers? - if (ticks_since_marker_refresh >= MARKER_UPDATE_SECONDS) { + if (ticks_since_marker_refresh >= (details_view->get_map_type() == MAP_TYPE_OSM ? MARKER_UPDATE_SECONDS_OSM : MARKER_UPDATE_SECONDS_BIN)) { map_needs_update = true; ticks_since_marker_refresh = 0; details_view->clear_map_markers(); } } else { // Refresh map immediately once active. - ticks_since_marker_refresh = MARKER_UPDATE_SECONDS; + ticks_since_marker_refresh = MARKER_UPDATE_SECONDS_OSM; // this is the bigger, so set it to force } // Process the entries list. diff --git a/firmware/application/apps/ui_adsb_rx.hpp b/firmware/application/apps/ui_adsb_rx.hpp index 039135f5d..66f2cfbbc 100644 --- a/firmware/application/apps/ui_adsb_rx.hpp +++ b/firmware/application/apps/ui_adsb_rx.hpp @@ -64,8 +64,9 @@ namespace ui { #define VEL_AIR_SUBSONIC 3 #define VEL_AIR_SUPERSONIC 4 -#define O_E_FRAME_TIMEOUT 20 // timeout between odd and even frames -#define MARKER_UPDATE_SECONDS 5 // "other" map marker redraw interval +#define O_E_FRAME_TIMEOUT 20 // timeout between odd and even frames +#define MARKER_UPDATE_SECONDS_OSM 10 // "other" map marker redraw interval for osm +#define MARKER_UPDATE_SECONDS_BIN 5 // "other" map marker redraw interval for bin map /* Thresholds (in seconds) that define the transition between ages. */ struct ADSBAgeLimit { @@ -277,6 +278,13 @@ class ADSBRxDetailsView : public View { std::string title() const override { return "Details"; } + MapType get_map_type() { + if (geomap_view_) + return geomap_view_->get_map_type(); + else + return MapType::MAP_TYPE_BIN; // default + } + private: void refresh_ui(); void on_gps(const GPSPosDataMessage* msg); @@ -388,7 +396,7 @@ class ADSBRxView : public View { SignalToken signal_token_tick_second{}; uint8_t tick_count = 0; - uint16_t ticks_since_marker_refresh{MARKER_UPDATE_SECONDS}; + uint16_t ticks_since_marker_refresh{MARKER_UPDATE_SECONDS_OSM}; /* Max number of entries that can be updated in a single pass. * 16 is one screen of recent entries. */ diff --git a/firmware/application/external/calculator/ivt.hpp b/firmware/application/external/calculator/ivt.hpp index 8259c369a..bcf49f072 100644 --- a/firmware/application/external/calculator/ivt.hpp +++ b/firmware/application/external/calculator/ivt.hpp @@ -440,7 +440,7 @@ static void _sumlr(void); #undef __O #undef __P -#define PI 3.141592653589793 +#include "mathdef.hpp" static void _numinput(byte k); static double dpush(double d); diff --git a/firmware/application/external/doom/ui_doom.cpp b/firmware/application/external/doom/ui_doom.cpp index dc63b60a8..a49f413b4 100644 --- a/firmware/application/external/doom/ui_doom.cpp +++ b/firmware/application/external/doom/ui_doom.cpp @@ -31,7 +31,7 @@ int HALF_HEIGHT = 0; #define JOGGING_SPEED 1.2 #define MAX_ENTITY_DISTANCE 200 #define ITEM_COLLIDER_DIST 6 -#define PI 3.14159265358979323846 +#include "mathdef.hpp" #define GUN_WIDTH 30 #define GUN_HEIGHT 40 #define GUN_TARGET_POS 24 diff --git a/firmware/application/external/wardrivemap/ui_wardrivemap.cpp b/firmware/application/external/wardrivemap/ui_wardrivemap.cpp index 21f5d73a2..5001fec1a 100644 --- a/firmware/application/external/wardrivemap/ui_wardrivemap.cpp +++ b/firmware/application/external/wardrivemap/ui_wardrivemap.cpp @@ -158,9 +158,10 @@ WardriveMapView::WardriveMapView(NavigationView& nav) } // never move this before the first load() bc that will mess load up - geomap.on_move = [this](float lon, float lat) { + geomap.on_move = [this](float lon, float lat, bool absolute) { (void)lon; (void)lat; + (void)absolute; load_markers(); }; btn_back.on_select = [this, &nav](Button&) { diff --git a/firmware/application/file.cpp b/firmware/application/file.cpp index 6da93056e..df0f22849 100644 --- a/firmware/application/file.cpp +++ b/firmware/application/file.cpp @@ -113,7 +113,7 @@ File::Result File::eof() { File::Result File::seek(Offset new_position) { /* NOTE: Returns *old* position, not new position */ const auto old_position = tell(); - const auto result = f_lseek(&f, new_position); + const auto result = (old_position == new_position) ? FR_OK : f_lseek(&f, new_position); if (result != FR_OK) { return {static_cast(result)}; } diff --git a/firmware/application/ui/ui_geomap.cpp b/firmware/application/ui/ui_geomap.cpp index 83defae98..8e3ca5cd7 100644 --- a/firmware/application/ui/ui_geomap.cpp +++ b/firmware/application/ui/ui_geomap.cpp @@ -22,21 +22,17 @@ */ #include "ui_geomap.hpp" - #include "portapack.hpp" - #include #include - +#include using namespace portapack; - #include "string_format.hpp" #include "complex.hpp" #include "ui_font_fixed_5x8.hpp" #include "file_path.hpp" namespace ui { - GeoPos::GeoPos( const Point pos, const alt_unit altitude_unit, @@ -188,6 +184,7 @@ int32_t GeoPos::speed() { GeoMap::GeoMap( Rect parent_rect) : Widget{parent_rect}, markerListLen(0) { + has_osm = use_osm = find_osm_file_tile(); } bool GeoMap::on_encoder(const EncoderEvent delta) { @@ -201,6 +198,8 @@ bool GeoMap::on_encoder(const EncoderEvent delta) { map_zoom += (map_zoom >= MAP_ZOOM_RESOLUTION_LIMIT) ? map_zoom : 1; } } + map_osm_zoom++; + if (has_osm) set_osm_max_zoom(); } else if (delta < 0) { if (map_zoom > -MAX_MAP_ZOOM_OUT) { if (map_zoom == 1) { @@ -212,12 +211,18 @@ bool GeoMap::on_encoder(const EncoderEvent delta) { map_zoom--; } } + if (map_osm_zoom > 0) map_osm_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; + if (use_osm) { + map_visible = true; + zoom_pixel_offset = 0; + } else { + zoom_pixel_offset = (map_visible && (map_zoom > 1)) ? (float)map_zoom / 2 : 0.0f; + } // Trigger map redraw redraw_map = true; @@ -225,7 +230,7 @@ bool GeoMap::on_encoder(const EncoderEvent delta) { return true; } -void GeoMap::map_read_line(ui::Color* buffer, uint16_t pixels) { +void GeoMap::map_read_line_bin(ui::Color* buffer, uint16_t pixels) { if (map_zoom == 1) { map_file.read(buffer, pixels << 1); } else if (map_zoom > 1) { @@ -264,39 +269,86 @@ void GeoMap::draw_markers(Painter& painter) { 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); + draw_marker(painter, {itemPoint.x() + r.left(), 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); + if (!use_osm) { + 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}; } - - x += geomap_rect_half_width; - y += geomap_rect_half_height; - + // osm calculation + double y = lat_to_pixel_y_tile(item.lat, map_osm_zoom) - viewport_top_left_py; + double x = lon_to_pixel_x_tile(item.lon, map_osm_zoom) - viewport_top_left_px; return {(int16_t)x, (int16_t)y}; } -// Converts latitude/longitude to pixel coordinates in map file. +/** + * @brief Converts longitude to a map tile's X-coordinate. + * @param lon The longitude in degrees. + * @param zoom The zoom level. + * @return The X-coordinate of the tile. + */ +int GeoMap::lon2tile(double lon, int zoom) { + return (int)floor((lon + 180.0) / 360.0 * pow(2.0, zoom)); +} + +/** + * @brief Converts latitude to a map tile's Y-coordinate. + * @param lat The latitude in degrees. + * @param zoom The zoom level. + * @return The Y-coordinate of the tile. + */ +int GeoMap::lat2tile(double lat, int zoom) { + // Convert latitude from degrees to radians for trigonometric functions + double lat_rad = lat * M_PI / 180.0; + // Perform the Mercator projection calculation + return (int)floor((1.0 - log(tan(lat_rad) + 1.0 / cos(lat_rad)) / M_PI) / 2.0 * pow(2.0, zoom)); +} + +void GeoMap::set_osm_max_zoom() { + if (map_osm_zoom > 20) map_osm_zoom = 20; + for (uint8_t i = map_osm_zoom; i > 0; i--) { + int tile_x = lon2tile(lon_, i); + int tile_y = lat2tile(lat_, i); + std::string filename = "/OSM/" + to_string_dec_int(i) + "/" + to_string_dec_int(tile_x) + "/" + to_string_dec_int(tile_y) + ".bmp"; + std::filesystem::path file_path(filename); + if (file_exists(file_path)) { + map_osm_zoom = i; + return; + } + } + map_osm_zoom = 0; // should not happen +} + +// checks if the tile file presents or not. to determine if we got osm or not +uint8_t GeoMap::find_osm_file_tile() { + std::string filename = "/OSM/" + to_string_dec_int(0) + "/" + to_string_dec_int(0) + "/" + to_string_dec_int(0) + ".bmp"; + std::filesystem::path file_path(filename); + if (file_exists(file_path)) return 1; + return 0; // not found +} + +// Converts latitude/longitude to pixel coordinates in binary 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 @@ -310,9 +362,7 @@ GeoPoint GeoMap::lat_lon_to_map_pixel(float lat, float lon) { } // Draw grid in place of map (when zoom-in level is too high). -void GeoMap::draw_map_grid() { - const auto r = screen_rect(); - +void GeoMap::draw_map_grid(ui::Rect r) { // 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; @@ -332,60 +382,184 @@ void GeoMap::draw_map_grid() { } } +double GeoMap::tile_pixel_x_to_lon(int x, int zoom) { + double map_width = pow(2.0, zoom) * TILE_SIZE; + return (x / map_width * 360.0) - 180.0; +} + +double GeoMap::tile_pixel_y_to_lat(int y, int zoom) { + double map_height = pow(2.0, zoom) * TILE_SIZE; + double n = M_PI * (1.0 - 2.0 * y / map_height); + return atan(sinh(n)) * 180.0 / M_PI; +} + +double GeoMap::lon_to_pixel_x_tile(double lon, int zoom) { + return ((lon + 180.0) / 360.0) * pow(2.0, zoom) * TILE_SIZE; +} + +double GeoMap::lat_to_pixel_y_tile(double lat, int zoom) { + double lat_rad = lat * M_PI / 180.0; + double sin_lat = sin(lat_rad); + return ((1.0 - log((1.0 + sin_lat) / (1.0 - sin_lat)) / (2.0 * M_PI)) / 2.0) * pow(2.0, zoom) * TILE_SIZE; +} + +bool GeoMap::draw_osm_file(int zoom, int tile_x, int tile_y, int relative_x, int relative_y) { + const auto r = screen_rect(); + // Early exit if the tile is completely outside the viewport + if (relative_x >= r.width() || relative_y >= r.height() || + relative_x + TILE_SIZE <= 0 || relative_y + TILE_SIZE <= 0) { + return true; + } + + BMPFile bmp{}; + bmp.open("/OSM/" + to_string_dec_int(zoom) + "/" + to_string_dec_int(tile_x) + "/" + to_string_dec_int(tile_y) + ".bmp", true); + // 1. Define the source and destination areas, starting with the full tile. + int src_x = 0; + int src_y = 0; + int dest_x = relative_x; + int dest_y = relative_y; + int clip_w = TILE_SIZE; + int clip_h = TILE_SIZE; + // 2. Clip left edge + if (dest_x < 0) { + src_x = -dest_x; + clip_w += dest_x; + dest_x = 0; + } + // 3. Clip top edge + if (dest_y < 0) { + src_y = -dest_y; + clip_h += dest_y; + dest_y = 0; + } + // 4. Clip right edge + if (dest_x + clip_w > r.width()) { + clip_w = r.width() - dest_x; + } + // 5. Clip bottom edge + if (dest_y + clip_h > r.height()) { + clip_h = r.height() - dest_y; + } + // 6. If clipping resulted in no visible area, we're done. + if (clip_w <= 0 || clip_h <= 0) { + return true; + } + + if (!bmp.is_loaded()) { + // Draw an error rectangle using the calculated clipped dimensions + ui::Rect error_rect{{dest_x + r.left(), dest_y + r.top()}, {clip_w, clip_h}}; + display.fill_rectangle(error_rect, Theme::getInstance()->bg_darkest->background); + return false; + } + std::vector line(clip_w); + for (int y = 0; y < clip_h; ++y) { + int source_row = src_y + y; + int dest_row = dest_y + y; + bmp.seek(src_x, source_row); + for (int x = 0; x < clip_w; ++x) { + bmp.read_next_px(line[x], true); + } + display.draw_pixels({dest_x + r.left(), dest_row + r.top(), clip_w, 1}, line); + } + return true; +} + void GeoMap::paint(Painter& painter) { const auto r = screen_rect(); 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 - if (map_zoom <= 1) { - // Zooming out, or no zoom - const int min_diff = abs(map_zoom); - if ((int)abs(x_pos - prev_x_pos) >= min_diff) - redraw_map = true; - else if ((int)abs(y_pos - prev_y_pos) >= min_diff) - redraw_map = true; + if (!use_osm) { + // Ony redraw map if it moved by at least 1 pixel or the markers list was updated + if (map_zoom <= 1) { + // Zooming out, or no zoom + const int min_diff = abs(map_zoom); + if ((int)abs(x_pos - prev_x_pos) >= min_diff) + redraw_map = true; + else if ((int)abs(y_pos - prev_y_pos) >= min_diff) + redraw_map = true; + } else { + // Zooming in; magnify position differences (utilizing zoom_pixel_offset) + if ((int)(abs(x_pos - prev_x_pos) * map_zoom) >= 1) + redraw_map = true; + else if ((int)(abs(y_pos - prev_y_pos) * map_zoom) >= 1) + redraw_map = true; + } } else { - // Zooming in; magnify position differences (utilizing zoom_pixel_offset) - if ((int)(abs(x_pos - prev_x_pos) * map_zoom) >= 1) - redraw_map = true; - else if ((int)(abs(y_pos - prev_y_pos) * map_zoom) >= 1) - redraw_map = true; + // using osm; needs to be stricter with the redraws, it'll be checked on move } if (redraw_map) { - 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; redraw_map = false; - - // 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 { - zoom_seek_x = x_pos - (r.width() * abs(map_zoom)) / 2; - zoom_seek_y = y_pos - (r.height() * abs(map_zoom)) / 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()); + if (!use_osm) { + 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_bin() 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 { + zoom_seek_x = x_pos - (r.width() * abs(map_zoom)) / 2; + zoom_seek_y = y_pos - (r.height() * abs(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_bin(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); + } + } - 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 { + // display osm tiles + // Convert center GPS to a global pixel coordinate + double global_center_px = lon_to_pixel_x_tile(lon_, map_osm_zoom); + double global_center_py = lat_to_pixel_y_tile(lat_, map_osm_zoom); + + // Find the top-left corner of the screen (viewport) in global pixel coordinates + viewport_top_left_px = global_center_px - (r.width() / 2.0); + viewport_top_left_py = global_center_py - (r.height() / 2.0); + + // Find the tile ID that contains the top-left corner of the viewport + int start_tile_x = floor(viewport_top_left_px / TILE_SIZE); + int start_tile_y = floor(viewport_top_left_py / TILE_SIZE); + + // Calculate the crucial render offset. + // This determines how much the first tile is shifted to align the map correctly. + // This value will almost always be negative or zero. + double render_offset_x = -(viewport_top_left_px - (start_tile_x * TILE_SIZE)); + double render_offset_y = -(viewport_top_left_py - (start_tile_y * TILE_SIZE)); + + // Determine how many tiles we need to draw to fill the screen + int tiles_needed_x = (r.width() / TILE_SIZE) + 2; + int tiles_needed_y = (r.height() / TILE_SIZE) + 2; + + for (int y = 0; y < tiles_needed_y; ++y) { + for (int x = 0; x < tiles_needed_x; ++x) { + int current_tile_x = start_tile_x + x; + int current_tile_y = start_tile_y + y; + + // Calculate the final on-screen drawing position for this tile. + // For the first tile (x=0, y=0), this will be the negative offset. + int draw_pos_x = round(render_offset_x + x * TILE_SIZE); + int draw_pos_y = round(render_offset_y + y * TILE_SIZE); + if (!draw_osm_file(map_osm_zoom, current_tile_x, current_tile_y, draw_pos_x, draw_pos_y)) { + // already blanked it. + } + } } } + } else { // No map data or excessive zoom; just draw a grid - draw_map_grid(); + draw_map_grid(r); } - // Draw crosshairs in center in manual panning mode if (manual_panning_) { display.fill_rectangle({r.center() - Point(16, 1) + Point(zoom_pixel_offset, zoom_pixel_offset), {32, 2}}, Color::red()); @@ -394,8 +568,9 @@ void GeoMap::paint(Painter& painter) { // Draw the other markers draw_markers(painter); - draw_scale(painter); + if (!use_osm) draw_scale(painter); draw_mypos(painter); + if (has_osm) draw_switcher(painter); set_clean(); } @@ -405,6 +580,12 @@ void GeoMap::paint(Painter& painter) { } } +void GeoMap::draw_switcher(Painter& painter) { + painter.fill_rectangle({screen_rect().left(), screen_rect().top(), 3 * 20, 20}, Theme::getInstance()->bg_darker->background); + std::string_view txt = (use_osm) ? "B I N" : "O S M"; + painter.draw_string({screen_rect().left() + 5, screen_rect().top() + 2}, *Theme::getInstance()->fg_light, txt); +} + bool GeoMap::on_keyboard(KeyboardEvent key) { if (key == '+' || key == ' ') return on_encoder(1); if (key == '-') return on_encoder(-1); @@ -413,11 +594,26 @@ bool GeoMap::on_keyboard(KeyboardEvent key) { } bool GeoMap::on_touch(const TouchEvent event) { + if (has_osm && event.type == TouchEvent::Type::Start && event.point.x() < screen_rect().left() + 3 * 20 && event.point.y() < screen_rect().top() + 20) { + use_osm = !use_osm; + move(lon_, lat_); // to re calculate the center for each map type + if (use_osm) set_osm_max_zoom(); + redraw_map = true; + set_dirty(); + return false; // false, because with true this hits 2 times + } + if ((event.type == TouchEvent::Type::Start) && (mode_ == PROMPT)) { + Point p; set_highlighted(true); if (on_move) { - Point p = event.point - screen_rect().center(); - on_move(p.x() / 2.0 * lon_ratio, p.y() / 2.0 * lat_ratio); + if (!use_osm) { + p = event.point - screen_rect().center(); + on_move(p.x() / 2.0 * lon_ratio, p.y() / 2.0 * lat_ratio, false); + } else { + p = event.point - screen_rect().location(); + on_move(tile_pixel_x_to_lon(p.x() + viewport_top_left_px, map_osm_zoom), tile_pixel_y_to_lat(p.y() + viewport_top_left_py, map_osm_zoom), true); + } return true; } } @@ -426,25 +622,30 @@ bool GeoMap::on_touch(const TouchEvent event) { void GeoMap::move(const float lon, const float lat) { const auto r = screen_rect(); - + bool is_changed = (lon_ != lon || lat_ != lat); lon_ = lon; lat_ = lat; + if (!use_osm) { + // 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 - 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; - // 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 - 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 = (r.width() / 2) / km_per_deg_lon; + // 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 = (r.width() / 2) / km_per_deg_lon; + } else { + if (is_changed) { + set_osm_max_zoom(); + redraw_map = true; + } + } } bool GeoMap::init() { @@ -469,8 +670,7 @@ bool GeoMap::init() { map_bottom = sin(-85.05 * pi / 180); // Map bitmap only goes from about -85 to 85 lat map_world_lon = map_width / (2 * pi); map_offset = (map_world_lon / 2 * log((1 + map_bottom) / (1 - map_bottom))); - - return map_opened; + return map_opened || has_osm; } void GeoMap::set_mode(GeoMapMode mode) { @@ -595,24 +795,29 @@ MapMarkerStored GeoMap::store_marker(GeoMarker& marker) { } void GeoMap::update_my_position(float lat, float lon, int32_t altitude) { + bool is_changed = (my_pos.lat != lat) || (my_pos.lon != lon); my_pos.lat = lat; my_pos.lon = lon; my_altitude = altitude; - redraw_map = true; + redraw_map = is_changed; set_dirty(); } void GeoMap::update_my_orientation(uint16_t angle, bool refresh) { + bool is_changed = (my_pos.angle != angle); my_pos.angle = angle; - if (refresh) { + if (refresh && is_changed) { redraw_map = true; set_dirty(); } } +MapType GeoMap::get_map_type() { + return use_osm ? MAP_TYPE_OSM : MAP_TYPE_BIN; +} + void GeoMapView::focus() { geopos.focus(); - if (!geomap.map_file_opened()) nav_.display_modal("No map", "No world_map.bin file in\n/" + adsb_dir.string() + "/ directory", ABORT); } @@ -629,7 +834,7 @@ void GeoMapView::update_position(float lat, float lon, uint16_t angle, int32_t a geomap.set_dirty(); return; } - + bool is_changed = lat_ != lat || lon_ != lon || altitude_ != altitude || speed_ != speed || angle_ != angle; lat_ = lat; lon_ = lon; altitude_ = altitude; @@ -644,7 +849,7 @@ void GeoMapView::update_position(float lat, float lon, uint16_t angle, int32_t a geopos.set_report_change(true); geomap.set_angle(angle); - geomap.move(lon_, lat_); + if (is_changed) geomap.move(lon_, lat_); geomap.set_dirty(); } @@ -660,19 +865,25 @@ void GeoMapView::setup() { geopos.set_lon(lon_); geopos.on_change = [this](int32_t altitude, float lat, float lon, int32_t speed) { + bool is_changed = (altitude_ != altitude) || (lat_ != lat) || (lon_ != lon) || (speed_ != speed); altitude_ = altitude; lat_ = lat; lon_ = lon; speed_ = speed; geopos.hide_altandspeed(); geomap.set_manual_panning(true); - geomap.move(lon_, lat_); + if (is_changed) geomap.move(lon_, lat_); geomap.set_dirty(); }; - geomap.on_move = [this](float move_x, float move_y) { - lon_ += move_x; - lat_ += move_y; + geomap.on_move = [this](float move_x, float move_y, bool absolute) { + if (absolute) { + lon_ = move_x; + lat_ = move_y; + } else { + lon_ += move_x; + lat_ += move_y; + } // Stupid hack to avoid an event loop geopos.set_report_change(false); @@ -769,4 +980,8 @@ MapMarkerStored GeoMapView::store_marker(GeoMarker& marker) { return geomap.store_marker(marker); } +MapType GeoMapView::get_map_type() { + return geomap.get_map_type(); +} + } /* namespace ui */ diff --git a/firmware/application/ui/ui_geomap.hpp b/firmware/application/ui/ui_geomap.hpp index 621816192..fd8347fd7 100644 --- a/firmware/application/ui/ui_geomap.hpp +++ b/firmware/application/ui/ui_geomap.hpp @@ -27,6 +27,8 @@ #include "ui.hpp" #include "file.hpp" #include "ui_navigation.hpp" +#include "bmpfile.hpp" +#include "mathdef.hpp" #include "portapack.hpp" @@ -43,6 +45,8 @@ namespace ui { #define GEOMAP_RECT_WIDTH 240 #define GEOMAP_RECT_HEIGHT (320 - 16 - GEOMAP_BANNER_HEIGHT) +#define TILE_SIZE 256 + enum GeoMapMode { DISPLAY, PROMPT @@ -193,9 +197,14 @@ enum MapMarkerStored { MARKER_LIST_FULL }; +enum MapType { + MAP_TYPE_OSM, + MAP_TYPE_BIN +}; + class GeoMap : public Widget { public: - std::function on_move{}; + std::function on_move{}; GeoMap(Rect parent_rect); @@ -216,6 +225,7 @@ class GeoMap : public Widget { void set_tag(std::string new_tag) { tag_ = new_tag; } + MapType get_map_type(); void set_angle(uint16_t new_angle) { angle_ = new_angle; @@ -246,8 +256,22 @@ class GeoMap : public Widget { void draw_markers(Painter& painter); 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); + void draw_map_grid(ui::Rect r); + void draw_switcher(Painter& painter); + void map_read_line_bin(ui::Color* buffer, uint16_t pixels); + // open street map related + uint8_t find_osm_file_tile(); + void set_osm_max_zoom(); + bool draw_osm_file(int zoom, int tile_x, int tile_y, int relative_x, int relative_y); + int lon2tile(double lon, int zoom); + int lat2tile(double lat, int zoom); + double lon_to_pixel_x_tile(double lon, int zoom); + double lat_to_pixel_y_tile(double lat, int zoom); + double tile_pixel_x_to_lon(int x, int zoom); + double tile_pixel_y_to_lat(int y, int zoom); + uint8_t map_osm_zoom{3}; + double viewport_top_left_px = 0; + double viewport_top_left_py = 0; bool manual_panning_{false}; bool hide_center_marker_{false}; @@ -258,11 +282,11 @@ class GeoMap : public Widget { uint16_t map_width{}, map_height{}; int32_t map_center_x{}, map_center_y{}; int16_t map_zoom{1}; + float lon_ratio{}, lat_ratio{}; double map_bottom{}; double map_world_lon{}; double map_offset{}; - float x_pos{}, y_pos{}; float prev_x_pos{32767.0f}, prev_y_pos{32767.0f}; float lat_{}; @@ -278,7 +302,9 @@ class GeoMap : public Widget { int markerListLen{0}; GeoMarker markerList[NumMarkerListElements]; - bool redraw_map{false}; + bool redraw_map{true}; + bool use_osm{false}; + bool has_osm{false}; }; class GeoMapView : public View { @@ -313,6 +339,8 @@ class GeoMapView : public View { void update_my_position(float lat, float lon, int32_t altitude); void update_my_orientation(uint16_t angle, bool refresh = false); + MapType get_map_type(); + std::string title() const override { return "Map view"; }; void clear_markers(); diff --git a/firmware/baseband/proc_epirb.cpp b/firmware/baseband/proc_epirb.cpp index 358864e22..ad169a2f8 100644 --- a/firmware/baseband/proc_epirb.cpp +++ b/firmware/baseband/proc_epirb.cpp @@ -85,7 +85,8 @@ void EPIRBProcessor::payload_handler(const baseband::Packet& packet) { } } -void EPIRBProcessor::on_message(const Message*) { +void EPIRBProcessor::on_message(const Message* const message) { + (void)message; // Unused in this processor } int main() { diff --git a/firmware/baseband/proc_fsk_rx.cpp b/firmware/baseband/proc_fsk_rx.cpp index 930df3bf7..4aec1a32e 100644 --- a/firmware/baseband/proc_fsk_rx.cpp +++ b/firmware/baseband/proc_fsk_rx.cpp @@ -33,9 +33,7 @@ #include #include -#ifndef M_PI -#define M_PI 3.14159265358979323846 -#endif +#include "mathdef.hpp" float FSKRxProcessor::detect_peak_power(const buffer_c8_t& buffer, int N) { int32_t power = 0; diff --git a/firmware/baseband/proc_jammer.cpp b/firmware/baseband/proc_jammer.cpp index cce46029a..19c832e99 100644 --- a/firmware/baseband/proc_jammer.cpp +++ b/firmware/baseband/proc_jammer.cpp @@ -30,9 +30,7 @@ #include #include -#ifndef M_PI -#define M_PI 3.14159265358979323846f -#endif +#include "mathdef.hpp" void JammerProcessor::execute(const buffer_c8_t& buffer) { if (!configured) return; diff --git a/firmware/common/adsb.hpp b/firmware/common/adsb.hpp index ab4172925..68d9264b3 100644 --- a/firmware/common/adsb.hpp +++ b/firmware/common/adsb.hpp @@ -91,7 +91,7 @@ const float adsb_lat_lut[58] = { 83.07199445, 83.99173563, 84.89166191, 85.75541621, 86.53536998, 87.00000000}; -const float PI = 3.14159265358979323846; +#include "mathdef.hpp" const float NZ = 15.0; diff --git a/firmware/common/bmpfile.cpp b/firmware/common/bmpfile.cpp index d39c2fb6e..689984740 100644 --- a/firmware/common/bmpfile.cpp +++ b/firmware/common/bmpfile.cpp @@ -52,10 +52,10 @@ void BMPFile::close() { bmpimage.close(); } -// creates a new bmp file. for now, hardcoded to 3 byte colour depth +// creates a new bmp file. hardcoded to 3 byte (24-bit) colour depth bool BMPFile::create(const std::filesystem::path& file, uint32_t x, uint32_t y) { is_opened = false; - is_read_ony = true; + is_read_only = true; bmpimage.close(); // if already open, close before open a new if (file_exists(file)) { delete_file(file); // overwrite @@ -85,7 +85,7 @@ bool BMPFile::create(const std::filesystem::path& file, uint32_t x, uint32_t y) bmpimage.write(&bmp_header, sizeof(bmp_header_t)); file_pos = bmp_header.image_data; is_opened = true; - is_read_ony = false; + is_read_only = false; if (!expand_y(y)) return false; // will fill with 0, and update header data seek(0, 0); return true; @@ -94,30 +94,36 @@ bool BMPFile::create(const std::filesystem::path& file, uint32_t x, uint32_t y) // opens the file and parses header data. return true on success bool BMPFile::open(const std::filesystem::path& file, bool readonly) { is_opened = false; - is_read_ony = true; - bmpimage.close(); // if already open, close before open a new + is_read_only = true; + bmpimage.close(); auto result = bmpimage.open(file, readonly, false); if (!result.value().ok()) return false; file_pos = 0; bmpimage.seek(file_pos); - auto read_size = bmpimage.read(&bmp_header, sizeof(bmp_header_t)); - if (!((bmp_header.signature == 0x4D42) && // "BM" Signature - (bmp_header.planes == 1) && // Seems always to be 1 - (bmp_header.compression == 0 || bmp_header.compression == 3))) { // No compression + bmpimage.read(&bmp_header, sizeof(bmp_header_t)); + if (!((bmp_header.signature == 0x4D42) && + (bmp_header.planes == 1) && + (bmp_header.compression == 0 || bmp_header.compression == 3))) { return false; } - char buffer[257]; + switch (bmp_header.bpp) { + case 8: + type = 4; + byte_per_px = 1; + // bmpimage.seek(sizeof(bmp_header_t)); + // bmpimage.read(color_palette, 1024); + return false; // niy + break; + case 16: - file_pos = 0x36; - memset(buffer, 0, 16); - bmpimage.read(buffer, 16); byte_per_px = 2; - if (buffer[1] == 0x7C) - type = 3; // A1R5G5B5 - else - type = 0; // R5G6B5 + type = 5; + if (bmp_header.compression == 3) { + return false; + } // niy + break; case 24: type = 1; @@ -130,12 +136,11 @@ bool BMPFile::open(const std::filesystem::path& file, bool readonly) { default: // not supported return false; - break; } - byte_per_row = (bmp_header.width * byte_per_px % 4 == 0) ? bmp_header.width * byte_per_px : (bmp_header.width * byte_per_px + (4 - ((bmp_header.width * byte_per_px) % 4))); + byte_per_row = (bmp_header.width * byte_per_px + 3) & ~3; file_pos = bmp_header.image_data; is_opened = true; - is_read_ony = readonly; + is_read_only = readonly; currx = 0; curry = 0; return true; @@ -161,20 +166,24 @@ bool BMPFile::read_next_px(ui::Color& px, bool seek = true) { auto res = bmpimage.read(buffer, byte_per_px); if (res.is_error()) return false; switch (type) { - case 0: // R5G6B5 - case 3: // A1R5G5B5 - if (!type) - px = ui::Color((uint16_t)buffer[0] | ((uint16_t)buffer[1] << 8)); - else - px = ui::Color(((uint16_t)buffer[0] & 0x1F) | ((uint16_t)buffer[0] & 0xE0) << 1 | ((uint16_t)buffer[1] & 0x7F) << 9); + case 5: { + uint16_t color16 = (uint16_t)buffer[0] | ((uint16_t)buffer[1] << 8); + px = ui::Color(color16); // has glitches! break; + } + case 2: // 32 + px = ui::Color(buffer[2], buffer[1], buffer[0]); + break; + + case 4: { // 8-bit + // uint8_t index = buffer[0]; + // px = ui::Color(color_palette[index][2], color_palette[index][1], color_palette[index][0]); // Palette is BGR + // px = ui::Color(buffer[0]); // niy, since needs a lot of ram for the palette + break; + } case 1: // 24 default: px = ui::Color(buffer[2], buffer[1], buffer[0]); - - break; - case 2: // 32 - px = ui::Color(buffer[2], buffer[1], buffer[0]); break; } if (seek) advance_curr_px(); @@ -188,23 +197,23 @@ void BMPFile::set_bg_color(ui::Color background) { } // delete bg color. default. creation or expansion will be fast, but the file will contain random garbage. no problem if you write all pixels later. -void BMPFile::delete_db_color() { +void BMPFile::delete_bg_color() { use_bg = false; } // writes a color data to the current position, and advances 1 px. true on success, false on error bool BMPFile::write_next_px(ui::Color& px) { - if (!is_opened) return false; - if (is_read_ony) return false; + if (!is_opened || is_read_only) return false; uint8_t buffer[4]; switch (type) { + case 5: case 0: // R5G6B5 case 3: // A1R5G5B5 if (!type) { - buffer[0] = (px.r() << 3) | (px.g() >> 3); // todo test in future + buffer[0] = (px.r() << 3) | (px.g() >> 3); buffer[1] = (px.g() << 5) | px.b(); } else { - buffer[0] = (1 << 7) | (px.r() << 2) | (px.g() >> 3); // todo test in future + buffer[0] = (1 << 7) | (px.r() << 2) | (px.g() >> 3); buffer[1] = (px.g() << 5) | px.b(); } break; @@ -220,6 +229,8 @@ bool BMPFile::write_next_px(ui::Color& px) { buffer[0] = px.b(); buffer[3] = 255; break; + case 4: // 8-bit + return false; } auto res = bmpimage.write(buffer, byte_per_px); if (res.is_error()) return false; @@ -260,7 +271,7 @@ bool BMPFile::expand_y(uint32_t new_y) { if (!is_opened) return false; // not yet opened uint32_t old_height = get_real_height(); if (new_y < old_height) return true; // already bigger - if (is_read_ony) return false; // can't expand + if (is_read_only) return false; // can't expand uint32_t delta = (new_y - old_height) * byte_per_row; bmp_header.size += delta; bmp_header.data_size += delta; diff --git a/firmware/common/bmpfile.hpp b/firmware/common/bmpfile.hpp index efaf13818..f3709bd23 100644 --- a/firmware/common/bmpfile.hpp +++ b/firmware/common/bmpfile.hpp @@ -47,12 +47,12 @@ class BMPFile { uint32_t get_width(); bool is_bottomup(); void set_bg_color(ui::Color background); - void delete_db_color(); + void delete_bg_color(); private: bool advance_curr_px(uint32_t num); bool is_opened = false; - bool is_read_ony = true; + bool is_read_only = true; File bmpimage{}; size_t file_pos = 0; @@ -64,6 +64,7 @@ class BMPFile { uint32_t currx = 0; uint32_t curry = 0; ui::Color bg{}; + // uint8_t color_palette[256][4]; bool use_bg = false; }; diff --git a/firmware/common/complex.hpp b/firmware/common/complex.hpp index ac24b0ab5..b8f184e3a 100644 --- a/firmware/common/complex.hpp +++ b/firmware/common/complex.hpp @@ -26,7 +26,9 @@ #include #include -constexpr float pi{3.141592653589793238462643383279502884f}; +#include "mathdef.hpp" + +constexpr float pi{M_PI}; namespace std { diff --git a/firmware/common/mathdef.hpp b/firmware/common/mathdef.hpp new file mode 100644 index 000000000..52283bcac --- /dev/null +++ b/firmware/common/mathdef.hpp @@ -0,0 +1,4 @@ +#pragma once + +#define PI 3.1415926535897932384626433832795 +#define M_PI PI \ No newline at end of file diff --git a/firmware/common/sonde_packet.cpp b/firmware/common/sonde_packet.cpp index 9bda83d15..0878f51f9 100644 --- a/firmware/common/sonde_packet.cpp +++ b/firmware/common/sonde_packet.cpp @@ -47,7 +47,7 @@ static uint8_t calfrchk[51]; // so subframes are preserved while populate #define pos_GPSecefY 0x114 // 0x118 // 4 byte (not actually used since Y and Z are following X, and grabbed in that same loop) #define pos_GPSecefZ 0x118 // 0x11C // 4 byte (same as Y) -#define PI 3.1415926535897932384626433832795 // 3.1416 //(3.1415926535897932384626433832795) +#include "mathdef.hpp" Packet::Packet( const baseband::Packet& packet, diff --git a/sdcard/OSM/readme.md b/sdcard/OSM/readme.md new file mode 100644 index 000000000..050e6d2eb --- /dev/null +++ b/sdcard/OSM/readme.md @@ -0,0 +1,14 @@ +Download OSM Tile files to the root of the SD card to the folder: /OSM/ +There must be the standard directory structure of OSM ( /OSM/{zoom}/{x}/{y}.bmp). +The tile files must be BMP 24 bit color depth format. + + +To disable this function, simply delete the subfolders from this OSM folder. + + +There is a website, that can download tile files in the given format and structure: https://pposm.creativo.hu +You select the area you want to download, and the maximum zoom level. There will be a size estimation. +Then Click on start download button, and browse the sd cards /OSM folder. Then click select and enable (twice). +The download will begin. Depending on the area size and the max zoom level it can take a lot of time. +With this app you can download multiple regions in multiple depths, just run it again. +For the site to work, you must use up to date Chromium based browser.