#include "NodeListRenderer.h" #include "CompassRenderer.h" #include "NodeDB.h" #include "UIRenderer.h" #include "configuration.h" #include "gps/GeoCoord.h" #include "gps/RTC.h" // for getTime() function #include "graphics/ScreenFonts.h" #include "graphics/SharedUIDisplay.h" #include "graphics/images.h" #include // Forward declarations for functions defined in Screen.cpp namespace graphics { extern bool haveGlyphs(const char *str); } // namespace graphics // Global screen instance extern graphics::Screen *screen; namespace graphics { namespace NodeListRenderer { // Function moved from Screen.cpp to NodeListRenderer.cpp since it's primarily used here void drawScaledXBitmap16x16(int x, int y, int width, int height, const uint8_t *bitmapXBM, OLEDDisplay *display) { for (int row = 0; row < height; row++) { uint8_t rowMask = (1 << row); for (int col = 0; col < width; col++) { uint8_t colData = pgm_read_byte(&bitmapXBM[col]); if (colData & rowMask) { // Note: rows become X, columns become Y after transpose display->fillRect(x + row * 2, y + col * 2, 2, 2); } } } } // Static variables for dynamic cycling static NodeListMode currentMode = MODE_LAST_HEARD; static int scrollIndex = 0; // ============================= // Utility Functions // ============================= String getSafeNodeName(meshtastic_NodeInfoLite *node) { String nodeName = "?"; if (node->has_user && strlen(node->user.short_name) > 0) { bool valid = true; const char *name = node->user.short_name; for (size_t i = 0; i < strlen(name); i++) { uint8_t c = (uint8_t)name[i]; if (c < 32 || c > 126) { valid = false; break; } } if (valid) { nodeName = name; } else { char idStr[6]; snprintf(idStr, sizeof(idStr), "%04X", (uint16_t)(node->num & 0xFFFF)); nodeName = String(idStr); } } return nodeName; } uint32_t sinceLastSeen(meshtastic_NodeInfoLite *node) { uint32_t now = getTime(); uint32_t last_seen = node->last_heard; if (last_seen == 0 || now < last_seen) { return UINT32_MAX; } return now - last_seen; } const char *getCurrentModeTitle(int screenWidth) { switch (currentMode) { case MODE_LAST_HEARD: return "Node List"; case MODE_HOP_SIGNAL: return (screenWidth > 128) ? "Hops/Signal" : "Hops/Sig"; case MODE_DISTANCE: return "Distance"; default: return "Nodes"; } } // Use dynamic timing based on mode unsigned long getModeCycleIntervalMs() { return 3000; } // Calculate bearing between two lat/lon points float calculateBearing(double lat1, double lon1, double lat2, double lon2) { double dLon = (lon2 - lon1) * DEG_TO_RAD; double y = sin(dLon) * cos(lat2 * DEG_TO_RAD); double x = cos(lat1 * DEG_TO_RAD) * sin(lat2 * DEG_TO_RAD) - sin(lat1 * DEG_TO_RAD) * cos(lat2 * DEG_TO_RAD) * cos(dLon); double bearing = atan2(y, x) * RAD_TO_DEG; return fmod(bearing + 360.0, 360.0); } int calculateMaxScroll(int totalEntries, int visibleRows) { return std::max(0, (totalEntries - 1) / (visibleRows * 2)); } void retrieveAndSortNodes(std::vector &nodeList) { size_t numNodes = nodeDB->getNumMeshNodes(); for (size_t i = 0; i < numNodes; i++) { meshtastic_NodeInfoLite *node = nodeDB->getMeshNodeByIndex(i); if (!node || node->num == nodeDB->getNodeNum()) continue; NodeEntry entry; entry.node = node; entry.sortValue = sinceLastSeen(node); nodeList.push_back(entry); } // Sort nodes: favorites first, then by last heard (most recent first) std::sort(nodeList.begin(), nodeList.end(), [](const NodeEntry &a, const NodeEntry &b) { bool aFav = a.node->is_favorite; bool bFav = b.node->is_favorite; if (aFav != bFav) return aFav > bFav; if (a.sortValue == 0 || a.sortValue == UINT32_MAX) return false; if (b.sortValue == 0 || b.sortValue == UINT32_MAX) return true; return a.sortValue < b.sortValue; }); } void drawColumnSeparator(OLEDDisplay *display, int16_t x, int16_t yStart, int16_t yEnd) { int columnWidth = display->getWidth() / 2; int separatorX = x + columnWidth - 1; for (int y = yStart; y <= yEnd; y += 2) { display->setPixel(separatorX, y); } } void drawScrollbar(OLEDDisplay *display, int visibleNodeRows, int totalEntries, int scrollIndex, int columns, int scrollStartY) { if (totalEntries <= visibleNodeRows * columns) return; int scrollbarX = display->getWidth() - 2; int scrollbarHeight = display->getHeight() - scrollStartY - 10; int thumbHeight = std::max(4, (scrollbarHeight * visibleNodeRows * columns) / totalEntries); int maxScroll = calculateMaxScroll(totalEntries, visibleNodeRows); int thumbY = scrollStartY + (scrollIndex * (scrollbarHeight - thumbHeight)) / std::max(1, maxScroll); for (int i = 0; i < thumbHeight; i++) { display->setPixel(scrollbarX, thumbY + i); } } // ============================= // Entry Renderers // ============================= void drawEntryLastHeard(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth) { bool isLeftCol = (x < SCREEN_WIDTH / 2); int timeOffset = (SCREEN_WIDTH > 128) ? (isLeftCol ? 7 : 10) : (isLeftCol ? 3 : 7); String nodeName = getSafeNodeName(node); char timeStr[10]; uint32_t seconds = sinceLastSeen(node); if (seconds == 0 || seconds == UINT32_MAX) { snprintf(timeStr, sizeof(timeStr), "?"); } else { uint32_t minutes = seconds / 60, hours = minutes / 60, days = hours / 24; snprintf(timeStr, sizeof(timeStr), (days > 365 ? "?" : "%d%c"), (days ? days : hours ? hours : minutes), (days ? 'd' : hours ? 'h' : 'm')); } display->setTextAlignment(TEXT_ALIGN_LEFT); display->setFont(FONT_SMALL); display->drawString(x + ((SCREEN_WIDTH > 128) ? 6 : 3), y, nodeName); if (node->is_favorite) { if (SCREEN_WIDTH > 128) { drawScaledXBitmap16x16(x, y + 6, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint, display); } else { display->drawXbm(x, y + 5, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint); } } int rightEdge = x + columnWidth - timeOffset; int textWidth = display->getStringWidth(timeStr); display->drawString(rightEdge - textWidth, y, timeStr); } void drawEntryHopSignal(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth) { bool isLeftCol = (x < SCREEN_WIDTH / 2); int nameMaxWidth = columnWidth - 25; int barsOffset = (SCREEN_WIDTH > 128) ? (isLeftCol ? 16 : 20) : (isLeftCol ? 15 : 19); int hopOffset = (SCREEN_WIDTH > 128) ? (isLeftCol ? 17 : 25) : (isLeftCol ? 13 : 17); int barsXOffset = columnWidth - barsOffset; String nodeName = getSafeNodeName(node); display->setTextAlignment(TEXT_ALIGN_LEFT); display->setFont(FONT_SMALL); display->drawStringMaxWidth(x + ((SCREEN_WIDTH > 128) ? 6 : 3), y, nameMaxWidth, nodeName); if (node->is_favorite) { if (SCREEN_WIDTH > 128) { drawScaledXBitmap16x16(x, y + 6, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint, display); } else { display->drawXbm(x, y + 5, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint); } } // Draw signal strength bars int bars = (node->snr > 5) ? 4 : (node->snr > 0) ? 3 : (node->snr > -5) ? 2 : (node->snr > -10) ? 1 : 0; int barWidth = 2; int barStartX = x + barsXOffset; int barStartY = y + 1 + (FONT_HEIGHT_SMALL / 2) + 2; for (int b = 0; b < 4; b++) { if (b < bars) { int height = (b * 2); display->fillRect(barStartX + (b * (barWidth + 1)), barStartY - height, barWidth, height); } } // Draw hop count char hopStr[6] = ""; if (node->has_hops_away && node->hops_away > 0) snprintf(hopStr, sizeof(hopStr), "[%d]", node->hops_away); if (hopStr[0] != '\0') { int rightEdge = x + columnWidth - hopOffset; int textWidth = display->getStringWidth(hopStr); display->drawString(rightEdge - textWidth, y, hopStr); } } void drawNodeDistance(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth) { bool isLeftCol = (x < SCREEN_WIDTH / 2); int nameMaxWidth = columnWidth - (SCREEN_WIDTH > 128 ? (isLeftCol ? 25 : 28) : (isLeftCol ? 20 : 22)); String nodeName = getSafeNodeName(node); char distStr[10] = ""; meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum()); if (nodeDB->hasValidPosition(ourNode) && nodeDB->hasValidPosition(node)) { double lat1 = ourNode->position.latitude_i * 1e-7; double lon1 = ourNode->position.longitude_i * 1e-7; double lat2 = node->position.latitude_i * 1e-7; double lon2 = node->position.longitude_i * 1e-7; double earthRadiusKm = 6371.0; double dLat = (lat2 - lat1) * DEG_TO_RAD; double dLon = (lon2 - lon1) * DEG_TO_RAD; double a = sin(dLat / 2) * sin(dLat / 2) + cos(lat1 * DEG_TO_RAD) * cos(lat2 * DEG_TO_RAD) * sin(dLon / 2) * sin(dLon / 2); double c = 2 * atan2(sqrt(a), sqrt(1 - a)); double distanceKm = earthRadiusKm * c; if (distanceKm < 1.0) { int meters = (int)(distanceKm * 1000); if (meters < 1000) { snprintf(distStr, sizeof(distStr), "%dm", meters); } else { snprintf(distStr, sizeof(distStr), "1km"); } } else { int km = (int)(distanceKm + 0.5); if (km < 1000) { snprintf(distStr, sizeof(distStr), "%dkm", km); } else { snprintf(distStr, sizeof(distStr), "999"); } } } display->setTextAlignment(TEXT_ALIGN_LEFT); display->setFont(FONT_SMALL); display->drawStringMaxWidth(x + ((SCREEN_WIDTH > 128) ? 6 : 3), y, nameMaxWidth, nodeName); if (node->is_favorite) { if (SCREEN_WIDTH > 128) { drawScaledXBitmap16x16(x, y + 6, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint, display); } else { display->drawXbm(x, y + 5, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint); } } if (strlen(distStr) > 0) { int offset = (SCREEN_WIDTH > 128) ? (isLeftCol ? 7 : 10) : (isLeftCol ? 5 : 8); int rightEdge = x + columnWidth - offset; int textWidth = display->getStringWidth(distStr); display->drawString(rightEdge - textWidth, y, distStr); } } void drawEntryDynamic(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth) { switch (currentMode) { case MODE_LAST_HEARD: drawEntryLastHeard(display, node, x, y, columnWidth); break; case MODE_HOP_SIGNAL: drawEntryHopSignal(display, node, x, y, columnWidth); break; case MODE_DISTANCE: drawNodeDistance(display, node, x, y, columnWidth); break; default: break; } } void drawEntryCompass(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth) { bool isLeftCol = (x < SCREEN_WIDTH / 2); int nameMaxWidth = columnWidth - (SCREEN_WIDTH > 128 ? (isLeftCol ? 25 : 28) : (isLeftCol ? 20 : 22)); String nodeName = getSafeNodeName(node); display->setTextAlignment(TEXT_ALIGN_LEFT); display->setFont(FONT_SMALL); display->drawStringMaxWidth(x + ((SCREEN_WIDTH > 128) ? 6 : 3), y, nameMaxWidth, nodeName); if (node->is_favorite) { if (SCREEN_WIDTH > 128) { drawScaledXBitmap16x16(x, y + 6, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint, display); } else { display->drawXbm(x, y + 5, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint); } } } void drawCompassArrow(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth, float myHeading, double userLat, double userLon) { if (!nodeDB->hasValidPosition(node)) return; double nodeLat = node->position.latitude_i * 1e-7; double nodeLon = node->position.longitude_i * 1e-7; float bearing = calculateBearing(userLat, userLon, nodeLat, nodeLon); if (!config.display.compass_north_top) bearing -= myHeading; bool isLeftCol = (x < SCREEN_WIDTH / 2); int arrowSize = 6; int arrowX = x + columnWidth - (isLeftCol ? 12 : 16); int arrowY = y + FONT_HEIGHT_SMALL / 2; CompassRenderer::drawArrowToNode(display, arrowX, arrowY, arrowSize, bearing); } // ============================= // Main Screen Functions // ============================= void drawNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y, const char *title, EntryRenderer renderer, NodeExtrasRenderer extras, float heading, double lat, double lon) { const int COMMON_HEADER_HEIGHT = FONT_HEIGHT_SMALL - 1; const int rowYOffset = FONT_HEIGHT_SMALL - 3; int columnWidth = display->getWidth() / 2; display->clear(); // Draw the battery/time header graphics::drawCommonHeader(display, x, y); // Draw the centered title within the header const int highlightHeight = COMMON_HEADER_HEIGHT; const int textY = y + 1 + (highlightHeight - FONT_HEIGHT_SMALL) / 2; const int centerX = x + SCREEN_WIDTH / 2; display->setFont(FONT_SMALL); display->setTextAlignment(TEXT_ALIGN_CENTER); if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_INVERTED) display->setColor(BLACK); display->drawString(centerX, textY, title); if (config.display.heading_bold) display->drawString(centerX + 1, textY, title); display->setColor(WHITE); display->setTextAlignment(TEXT_ALIGN_LEFT); // Space below header y += COMMON_HEADER_HEIGHT; // Fetch and display sorted node list std::vector nodeList; retrieveAndSortNodes(nodeList); int totalEntries = nodeList.size(); int totalRowsAvailable = (display->getHeight() - y) / rowYOffset; #ifdef USE_EINK totalRowsAvailable -= 1; #endif int visibleNodeRows = totalRowsAvailable; int totalColumns = 2; int startIndex = scrollIndex * visibleNodeRows * totalColumns; int endIndex = std::min(startIndex + visibleNodeRows * totalColumns, totalEntries); int yOffset = 0; int col = 0; int lastNodeY = y; int shownCount = 0; int rowCount = 0; for (int i = startIndex; i < endIndex; ++i) { int xPos = x + (col * columnWidth); int yPos = y + yOffset; renderer(display, nodeList[i].node, xPos, yPos, columnWidth); if (extras) { extras(display, nodeList[i].node, xPos, yPos, columnWidth, heading, lat, lon); } lastNodeY = std::max(lastNodeY, yPos + FONT_HEIGHT_SMALL); yOffset += rowYOffset; shownCount++; rowCount++; if (rowCount >= totalRowsAvailable) { yOffset = 0; rowCount = 0; col++; if (col > (totalColumns - 1)) break; } } // Draw column separator if (shownCount > 0) { const int firstNodeY = y + 3; drawColumnSeparator(display, x, firstNodeY, lastNodeY); } const int scrollStartY = y + 3; drawScrollbar(display, visibleNodeRows, totalEntries, scrollIndex, 2, scrollStartY); } // ============================= // Screen Frame Functions // ============================= #ifndef USE_EINK void drawDynamicNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { // Static variables to track mode and duration static NodeListMode lastRenderedMode = MODE_COUNT; static unsigned long modeStartTime = 0; unsigned long now = millis(); // On very first call (on boot or state enter) if (lastRenderedMode == MODE_COUNT) { currentMode = MODE_LAST_HEARD; modeStartTime = now; } // Time to switch to next mode? if (now - modeStartTime >= getModeCycleIntervalMs()) { currentMode = static_cast((currentMode + 1) % MODE_COUNT); modeStartTime = now; } // Render screen based on currentMode const char *title = getCurrentModeTitle(display->getWidth()); drawNodeListScreen(display, state, x, y, title, drawEntryDynamic); // Track the last mode to avoid reinitializing modeStartTime lastRenderedMode = currentMode; } #endif #ifdef USE_EINK void drawLastHeardScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { const char *title = "Node List"; drawNodeListScreen(display, state, x, y, title, drawEntryLastHeard); } void drawHopSignalScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { const char *title = "Hops/Signal"; drawNodeListScreen(display, state, x, y, title, drawEntryHopSignal); } void drawDistanceScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { const char *title = "Distance"; drawNodeListScreen(display, state, x, y, title, drawNodeDistance); } #endif void drawNodeListWithCompasses(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { float heading = 0; bool validHeading = false; double lat = 0; double lon = 0; #if HAS_GPS if (screen->hasHeading()) { heading = screen->getHeading(); // degrees validHeading = true; } else { heading = screen->estimatedHeading(lat, lon); validHeading = !isnan(heading); } #endif if (!validHeading) return; drawNodeListScreen(display, state, x, y, "Bearings", drawEntryCompass, drawCompassArrow, heading, lat, lon); } void drawNodeInfo(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { // Cache favorite nodes for the current frame only, to save computation static std::vector favoritedNodes; static int prevFrame = -1; // Only rebuild favorites list if we're on a new frame if (state->currentFrame != prevFrame) { prevFrame = state->currentFrame; favoritedNodes.clear(); size_t total = nodeDB->getNumMeshNodes(); for (size_t i = 0; i < total; i++) { meshtastic_NodeInfoLite *n = nodeDB->getMeshNodeByIndex(i); // Skip nulls and ourself if (!n || n->num == nodeDB->getNodeNum()) continue; if (n->is_favorite) favoritedNodes.push_back(n); } // Keep a stable, consistent display order std::sort(favoritedNodes.begin(), favoritedNodes.end(), [](meshtastic_NodeInfoLite *a, meshtastic_NodeInfoLite *b) { return a->num < b->num; }); } if (favoritedNodes.empty()) return; // Only display if index is valid int nodeIndex = state->currentFrame - (screen->frameCount - favoritedNodes.size()); if (nodeIndex < 0 || nodeIndex >= (int)favoritedNodes.size()) return; meshtastic_NodeInfoLite *node = favoritedNodes[nodeIndex]; if (!node || node->num == nodeDB->getNodeNum() || !node->is_favorite) return; display->clear(); // Draw battery/time/mail header (common across screens) graphics::drawCommonHeader(display, x, y); // Draw the short node name centered at the top, with bold shadow if set const int highlightHeight = FONT_HEIGHT_SMALL - 1; const int textY = y + 1 + (highlightHeight - FONT_HEIGHT_SMALL) / 2; const int centerX = x + SCREEN_WIDTH / 2; const char *shortName = (node->has_user && haveGlyphs(node->user.short_name)) ? node->user.short_name : "Node"; if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_INVERTED) display->setColor(BLACK); display->setTextAlignment(TEXT_ALIGN_CENTER); display->setFont(FONT_SMALL); display->drawString(centerX, textY, shortName); if (config.display.heading_bold) display->drawString(centerX + 1, textY, shortName); display->setColor(WHITE); display->setTextAlignment(TEXT_ALIGN_LEFT); display->setFont(FONT_SMALL); // Dynamic row stacking with predefined Y positions const int yPositions[5] = {moreCompactFirstLine, moreCompactSecondLine, moreCompactThirdLine, moreCompactFourthLine, moreCompactFifthLine}; int line = 0; // 1. Long Name (always try to show first) const char *username = (node->has_user && node->user.long_name[0]) ? node->user.long_name : nullptr; if (username && line < 5) { display->drawString(x, yPositions[line++], username); } // 2. Signal and Hops (combined on one line, if available) char signalHopsStr[32] = ""; bool haveSignal = false; int percentSignal = clamp((int)((node->snr + 10) * 5), 0, 100); const char *signalLabel = " Sig"; // If SNR looks reasonable, show signal if ((int)((node->snr + 10) * 5) >= 0 && node->snr > -100) { snprintf(signalHopsStr, sizeof(signalHopsStr), "%s: %d%%", signalLabel, percentSignal); haveSignal = true; } // If hops is valid (>0), show right after signal if (node->hops_away > 0) { size_t len = strlen(signalHopsStr); if (haveSignal) { snprintf(signalHopsStr + len, sizeof(signalHopsStr) - len, " [%d %s]", node->hops_away, (node->hops_away == 1 ? "Hop" : "Hops")); } else { snprintf(signalHopsStr, sizeof(signalHopsStr), "[%d %s]", node->hops_away, (node->hops_away == 1 ? "Hop" : "Hops")); } } if (signalHopsStr[0] && line < 5) { display->drawString(x, yPositions[line++], signalHopsStr); } // 3. Heard (last seen, skip if node never seen) char seenStr[20] = ""; uint32_t seconds = sinceLastSeen(node); if (seconds != 0 && seconds != UINT32_MAX) { uint32_t minutes = seconds / 60, hours = minutes / 60, days = hours / 24; snprintf(seenStr, sizeof(seenStr), (days > 365 ? " Heard: ?" : " Heard: %d%c ago"), (days ? days : hours ? hours : minutes), (days ? 'd' : hours ? 'h' : 'm')); } if (seenStr[0] && line < 5) { display->drawString(x, yPositions[line++], seenStr); } // 4. Uptime (only show if metric is present) char uptimeStr[32] = ""; if (node->has_device_metrics && node->device_metrics.has_uptime_seconds) { uint32_t uptime = node->device_metrics.uptime_seconds; uint32_t days = uptime / 86400; uint32_t hours = (uptime % 86400) / 3600; uint32_t mins = (uptime % 3600) / 60; if (days > 0) { snprintf(uptimeStr, sizeof(uptimeStr), " Uptime: %dd %dh", days, hours); } else if (hours > 0) { snprintf(uptimeStr, sizeof(uptimeStr), " Uptime: %dh %dm", hours, mins); } else { snprintf(uptimeStr, sizeof(uptimeStr), " Uptime: %dm", mins); } } if (uptimeStr[0] && line < 5) { display->drawString(x, yPositions[line++], uptimeStr); } // 5. Distance (only if both nodes have GPS position) meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum()); char distStr[24] = ""; bool haveDistance = false; if (nodeDB->hasValidPosition(ourNode) && nodeDB->hasValidPosition(node)) { double lat1 = ourNode->position.latitude_i * 1e-7; double lon1 = ourNode->position.longitude_i * 1e-7; double lat2 = node->position.latitude_i * 1e-7; double lon2 = node->position.longitude_i * 1e-7; double earthRadiusKm = 6371.0; double dLat = (lat2 - lat1) * DEG_TO_RAD; double dLon = (lon2 - lon1) * DEG_TO_RAD; double a = sin(dLat / 2) * sin(dLat / 2) + cos(lat1 * DEG_TO_RAD) * cos(lat2 * DEG_TO_RAD) * sin(dLon / 2) * sin(dLon / 2); double c = 2 * atan2(sqrt(a), sqrt(1 - a)); double distanceKm = earthRadiusKm * c; // Format distance appropriately if (distanceKm < 1.0) { double miles = distanceKm * 0.621371; if (miles < 0.1) { int feet = (int)(miles * 5280); if (feet > 0 && feet < 1000) { snprintf(distStr, sizeof(distStr), " Distance: %dft", feet); haveDistance = true; } else if (feet >= 1000) { snprintf(distStr, sizeof(distStr), " Distance: ¼mi"); haveDistance = true; } } else { int roundedMiles = (int)(miles + 0.5); if (roundedMiles > 0 && roundedMiles < 1000) { snprintf(distStr, sizeof(distStr), " Distance: %dmi", roundedMiles); haveDistance = true; } } } else { int km = (int)(distanceKm + 0.5); if (km > 0 && km < 1000) { snprintf(distStr, sizeof(distStr), " Distance: %dkm", km); haveDistance = true; } } } // Only display if we actually have a value! if (haveDistance && distStr[0] && line < 5) { display->drawString(x, yPositions[line++], distStr); } // Compass rendering for different screen orientations if (SCREEN_WIDTH > SCREEN_HEIGHT) { // Landscape: side-aligned compass bool showCompass = false; if (ourNode && (nodeDB->hasValidPosition(ourNode) || screen->hasHeading()) && nodeDB->hasValidPosition(node)) { showCompass = true; } if (showCompass) { const int16_t topY = compactFirstLine; const int16_t bottomY = SCREEN_HEIGHT - (FONT_HEIGHT_SMALL - 1); const int16_t usableHeight = bottomY - topY - 5; int16_t compassRadius = usableHeight / 2; if (compassRadius < 8) compassRadius = 8; const int16_t compassDiam = compassRadius * 2; const int16_t compassX = x + SCREEN_WIDTH - compassRadius - 8; const int16_t compassY = topY + (usableHeight / 2) + ((FONT_HEIGHT_SMALL - 1) / 2) + 2; const auto &op = ourNode->position; float myHeading = screen->hasHeading() ? screen->getHeading() * PI / 180 : screen->estimatedHeading(DegD(op.latitude_i), DegD(op.longitude_i)); CompassRenderer::drawCompassNorth(display, compassX, compassY, myHeading); const auto &p = node->position; float bearing = GeoCoord::bearing(DegD(op.latitude_i), DegD(op.longitude_i), DegD(p.latitude_i), DegD(p.longitude_i)); if (!config.display.compass_north_top) bearing -= myHeading; CompassRenderer::drawNodeHeading(display, compassX, compassY, compassDiam, bearing); display->drawCircle(compassX, compassY, compassRadius); } } else { // Portrait: bottom-centered compass bool showCompass = false; if (ourNode && (nodeDB->hasValidPosition(ourNode) || screen->hasHeading()) && nodeDB->hasValidPosition(node)) { showCompass = true; } if (showCompass) { int yBelowContent = (line > 0 && line <= 5) ? (yPositions[line - 1] + FONT_HEIGHT_SMALL + 2) : moreCompactFirstLine; const int margin = 4; #if defined(USE_EINK) const int iconSize = (SCREEN_WIDTH > 128) ? 16 : 8; const int navBarHeight = iconSize + 6; #else const int navBarHeight = 0; #endif int availableHeight = SCREEN_HEIGHT - yBelowContent - navBarHeight - margin; if (availableHeight < FONT_HEIGHT_SMALL * 2) return; int compassRadius = availableHeight / 2; if (compassRadius < 8) compassRadius = 8; if (compassRadius * 2 > SCREEN_WIDTH - 16) compassRadius = (SCREEN_WIDTH - 16) / 2; int compassX = x + SCREEN_WIDTH / 2; int compassY = yBelowContent + availableHeight / 2; const auto &op = ourNode->position; float myHeading = screen->hasHeading() ? screen->getHeading() * PI / 180 : screen->estimatedHeading(DegD(op.latitude_i), DegD(op.longitude_i)); CompassRenderer::drawCompassNorth(display, compassX, compassY, myHeading); const auto &p = node->position; float bearing = GeoCoord::bearing(DegD(op.latitude_i), DegD(op.longitude_i), DegD(p.latitude_i), DegD(p.longitude_i)); if (!config.display.compass_north_top) bearing -= myHeading; CompassRenderer::drawNodeHeading(display, compassX, compassY, compassRadius * 2, bearing); display->drawCircle(compassX, compassY, compassRadius); } } } } // namespace NodeListRenderer } // namespace graphics