projet crepp_git/crepp-projects/station-meteo/firmware/receiver/v1.0 · branche main
Widgets.h Voir sur GitLab
#pragma once
//Widgets.h — classe de base pour tous les widgets (colonne gauche et droite)
#include "Widget.h"
#include "WeatherData.h"
#include <math.h>

// ════════════════════════════════════════════════════════════════════════════
// WidgetMainTemp — colonne gauche : ville, température, stats
// ════════════════════════════════════════════════════════════════════════════
class WidgetMainTemp : public Widget {
public:
    WidgetMainTemp(int x, int y, int w, int h) : Widget(x, y, w, h) {}

    void draw(TFT_eSPI& tft) override {
        // Fond colonne gauche (pas de panel, fond transparent)
        tft.fillRect(x, y, w, h, C_BG);
        // Séparateur vertical droit
        tft.drawFastVLine(x + w, y, h, C_BORDER);

        const WeatherData& d = gWeather;

        // Ville + heure
        tft.setTextSize(1);
        tft.setTextColor(C_DIM, C_BG);
        tft.setCursor(x + MARGIN, y + 8);
        tft.print(d.city);
        tft.setCursor(x + MARGIN, y + 20);
        tft.print(d.time);

        // Icône météo principale centrée
        drawWeatherIcon(tft, x + w - 38, y + 32, d.uvIndex > 5 ? 0 : 3);

        // Température géante
        tft.setTextColor(C_WHITE, C_BG);
        tft.setTextSize(7);
        tft.setCursor(x + MARGIN - 2, y + 34);
        tft.print(d.temp);
        tft.setTextSize(2);
        tft.setTextColor(C_MUTED, C_BG);
        tft.setCursor(x + MARGIN + 50, y + 38);
        tft.print("oC");

        // Condition
        tft.setTextSize(1);
        tft.setTextColor(C_MUTED, C_BG);
        tft.setCursor(x + MARGIN, y + 98);
        tft.print(d.condition);

        // Min / Max / Ressenti
        tft.setTextColor(C_ACCENT_BLUE, C_BG);
        tft.setCursor(x + MARGIN, y + 110);
        tft.print("^"); tft.print(d.tempMax); tft.print("  ");
        tft.setTextColor(C_DIM, C_BG);
        tft.print("v"); tft.print(d.tempMin);
        tft.print("  R:"); tft.print(d.feelsLike);

        // ── 4 mini stats en grid 2x2 ──────────────────────────────────────
        int gx = x + MARGIN;
        int gy = y + 130;
        int sw = (w - MARGIN*2 - GAP) / 2;
        int sh = 46;

        drawStat(tft, gx,        gy,       sw, sh, "HUMIDITE",  d.humidity, "%");
        drawStat(tft, gx+sw+GAP, gy,       sw, sh, "VENT",      d.windSpeed, "km/h");
        drawStat(tft, gx,        gy+sh+GAP, sw, sh, "PRESSION", d.pressure, "hPa");
        drawStat(tft, gx+sw+GAP, gy+sh+GAP, sw, sh, "UV",       d.uvIndex, uvLabel(d.uvIndex), uvColor(d.uvIndex));
    }

private:
    void drawStat(TFT_eSPI& tft, int sx, int sy, int sw, int sh,
                  const char* label, int val, const char* unit,
                  uint16_t valColor = C_WHITE) {
        tft.fillRoundRect(sx, sy, sw, sh, RADIUS, C_PANEL);
        tft.drawRoundRect(sx, sy, sw, sh, RADIUS, C_BORDER);
        tft.setTextSize(1);
        tft.setTextColor(C_DIM, C_PANEL);
        tft.setCursor(sx + 5, sy + 5);
        tft.print(label);
        tft.setTextSize(2);
        tft.setTextColor(valColor, C_PANEL);
        tft.setCursor(sx + 5, sy + 17);
        tft.print(val);
        tft.setTextSize(1);
        tft.setTextColor(C_MUTED, C_PANEL);
        tft.print(" "); tft.print(unit);
    }

    const char* uvLabel(int uv) {
        if (uv <= 2)  return "Faible";
        if (uv <= 5)  return "Modere";
        if (uv <= 7)  return "Eleve";
        return "Danger";
    }

    uint16_t uvColor(int uv) {
        if (uv <= 2)  return C_ACCENT_GREEN;
        if (uv <= 5)  return C_ACCENT_GOLD;
        return C_ACCENT_RED;
    }

    void drawWeatherIcon(TFT_eSPI& tft, int cx, int cy, int type) {
        switch(type) {
            case 0: // Soleil
                tft.fillCircle(cx, cy, 9, C_ACCENT_GOLD);
                for(int i = 0; i < 8; i++) {
                    float a = i * PI / 4;
                    tft.drawLine(cx+12*cos(a), cy+12*sin(a),
                                 cx+17*cos(a), cy+17*sin(a), C_ACCENT_GOLD);
                }
                break;
            case 1: // Nuages
                tft.fillCircle(cx-5, cy+3, 8, C_MUTED);
                tft.fillCircle(cx+5, cy+3, 8, C_MUTED);
                tft.fillCircle(cx,   cy-2, 9, TFT_WHITE);
                tft.fillRect(cx-12, cy+3, 24, 8, C_MUTED);
                break;
            case 2: // Pluie
                tft.fillCircle(cx-4, cy,   7, 0x8C71);
                tft.fillCircle(cx+4, cy,   7, 0x8C71);
                tft.fillCircle(cx,   cy-4, 7, 0x8C71);
                tft.fillRect(cx-10, cy, 20, 7, 0x8C71);
                for(int i = 0; i < 4; i++)
                    tft.drawLine(cx-6+i*4, cy+11, cx-8+i*4, cy+18, C_ACCENT_BLUE);
                break;
            case 3: // Soleil + nuage
            default:
                tft.fillCircle(cx+6, cy-5, 7, C_ACCENT_GOLD);
                tft.fillCircle(cx-4, cy+2, 7, C_MUTED);
                tft.fillCircle(cx+4, cy+2, 7, C_MUTED);
                tft.fillCircle(cx,   cy-2, 7, TFT_WHITE);
                tft.fillRect(cx-9, cy+2, 18, 6, C_MUTED);
                break;
        }
    }
};


// ════════════════════════════════════════════════════════════════════════════
// WidgetForecast — prévisions 5 jours
// ════════════════════════════════════════════════════════════════════════════
class WidgetForecast : public Widget {
public:
    WidgetForecast(int x, int y, int w, int h) : Widget(x, y, w, h) {}

    void draw(TFT_eSPI& tft) override {
        drawBackground(tft);
        drawLabel(tft, 6, 5, "PREVISIONS 5 JOURS");

        int cellW = (w - 2) / 5;

        for (int i = 0; i < 5; i++) {
            int cx = x + 1 + i * cellW;
            bool highlight = (i == 1);

            if (highlight) {
                tft.fillRoundRect(cx+1, y+1, cellW-2, h-2, RADIUS, 0x0C64);
                tft.drawRoundRect(cx+1, y+1, cellW-2, h-2, RADIUS, C_ACCENT_BLUE);
            }

            uint16_t tc = highlight ? C_ACCENT_BLUE : C_MUTED;

            tft.setTextSize(1);
            tft.setTextColor(tc, highlight ? 0x0C64 : C_PANEL);
            tft.setCursor(cx + cellW/2 - 9, y + 16);
            tft.print(gForecast[i].label);

            drawMiniIcon(tft, cx + cellW/2, y + 36, gForecast[i].iconType);

            tft.setTextSize(1);
            uint16_t tempCol = (gForecast[i].temp >= 22) ? C_ACCENT_GOLD : C_WHITE;
            tft.setTextColor(tempCol, highlight ? 0x0C64 : C_PANEL);
            tft.setCursor(cx + cellW/2 - 8, y + 52);
            tft.print(gForecast[i].temp);
            tft.print("o");
        }
    }

private:
    void drawMiniIcon(TFT_eSPI& tft, int cx, int cy, int type) {
        switch(type) {
            case 0: tft.fillCircle(cx, cy, 6, C_ACCENT_GOLD); break;
            case 1:
                tft.fillCircle(cx-3, cy+2, 5, C_MUTED);
                tft.fillCircle(cx+3, cy+2, 5, C_MUTED);
                tft.fillCircle(cx,   cy-1, 5, TFT_WHITE);
                tft.fillRect(cx-7, cy+2, 14, 4, C_MUTED);
                break;
            case 2:
                tft.fillCircle(cx, cy-1, 5, 0x8C71);
                tft.fillRect(cx-6, cy-1, 12, 4, 0x8C71);
                tft.drawLine(cx-2, cy+7, cx-3, cy+12, C_ACCENT_BLUE);
                tft.drawLine(cx+2, cy+7, cx+1, cy+12, C_ACCENT_BLUE);
                break;
            case 3:
                tft.fillCircle(cx+4, cy-3, 5, C_ACCENT_GOLD);
                tft.fillCircle(cx-2, cy+2, 5, C_MUTED);
                tft.fillCircle(cx+3, cy+2, 5, C_MUTED);
                tft.fillRect(cx-6, cy+2, 12, 4, C_MUTED);
                break;
            case 4:
                tft.fillCircle(cx, cy, 5, 0xA514);
                tft.fillRect(cx-5, cy, 10, 4, 0xA514);
                tft.drawLine(cx, cy+8, cx-1, cy+13, C_ACCENT_BLUE);
                break;
        }
    }
};


// ════════════════════════════════════════════════════════════════════════════
// WidgetChart — courbe température 24h (8 points)
// ════════════════════════════════════════════════════════════════════════════
class WidgetChart : public Widget {
public:
    WidgetChart(int x, int y, int w, int h) : Widget(x, y, w, h) {}

    void draw(TFT_eSPI& tft) override {
        drawBackground(tft);
        drawLabel(tft, 6, 5, "TEMPERATURE 24H");

        int chartX = x + 6;
        int chartY = y + 16;
        int chartW = w - 12;
        int chartH = h - 24;

        // Trouver min/max
        int tMin = gHourly[0].temp, tMax = gHourly[0].temp;
        for (int i = 1; i < 8; i++) {
            tMin = min(tMin, gHourly[i].temp);
            tMax = max(tMax, gHourly[i].temp);
        }
        int range = max(tMax - tMin, 1);

        // Calculer les points
        int px[8], py[8];
        for (int i = 0; i < 8; i++) {
            px[i] = chartX + (i * chartW) / 7;
            py[i] = chartY + chartH - ((gHourly[i].temp - tMin) * (chartH - 14)) / range;
        }

        // Aire sous la courbe (remplie par lignes horizontales)
        for (int yy = chartY + chartH; yy > chartY; yy--) {
            float alpha = 1.0f - (float)(chartY + chartH - yy) / chartH;
            if (alpha < 0.05f) break;
            // Interpoler la courbe à cette hauteur Y
            for (int i = 0; i < 7; i++) {
                if (yy <= max(py[i], py[i+1])) continue;
                // Ligne de fill entre les deux points de la courbe
                tft.drawFastHLine(px[i], yy, px[i+1] - px[i],
                    alpha > 0.3f ? 0x0944 : 0x0822);
            }
        }

        // Courbe
        for (int i = 0; i < 7; i++)
            tft.drawLine(px[i], py[i], px[i+1], py[i+1], C_ACCENT_BLUE);

        // Points et labels
        for (int i = 0; i < 8; i++) {
            tft.fillCircle(px[i], py[i], 2, C_ACCENT_BLUE);
            // Label heure
            tft.setTextSize(1);
            tft.setTextColor(C_DIM, C_PANEL);
            tft.setCursor(px[i] - 6, chartY + chartH + 2);
            tft.print(gHourly[i].label);
        }

        // Point max mis en évidence
        int maxIdx = 0;
        for (int i = 1; i < 8; i++)
            if (gHourly[i].temp > gHourly[maxIdx].temp) maxIdx = i;
        tft.fillCircle(px[maxIdx], py[maxIdx], 3, C_ACCENT_GOLD);
        tft.setTextSize(1);
        tft.setTextColor(C_ACCENT_GOLD, C_PANEL);
        tft.setCursor(px[maxIdx] - 6, py[maxIdx] - 10);
        tft.print(gHourly[maxIdx].temp); tft.print("o");
    }
};


// ════════════════════════════════════════════════════════════════════════════
// WidgetSun — lever/coucher du soleil avec arc
// ════════════════════════════════════════════════════════════════════════════
class WidgetSun : public Widget {
public:
    WidgetSun(int x, int y, int w, int h) : Widget(x, y, w, h) {}

    void draw(TFT_eSPI& tft) override {
        drawBackground(tft);
        drawLabel(tft, 5, 5, "SOLEIL");

        int mx = x + w/2;
        int my = y + h - 8;
        int r  = min(w, h*2) / 2 - 6;

        // Arc du soleil (demi-cercle)
        for (float a = PI; a <= 2*PI; a += 0.05f) {
            int ax = mx + r * cos(a);
            int ay = my + r * sin(a);
            tft.drawPixel(ax, ay, C_MUTED);
        }

        // Position actuelle (exemple : 14h sur une journée 06h-22h)
        // Normalise entre 0..1 selon l'heure actuelle
        float pos = 0.52f; // 14h ≈ 52% de la journée
        float angle = PI + pos * PI;
        int sx = mx + r * cos(angle);
        int sy = my + r * sin(angle);
        tft.fillCircle(sx, sy, 4, C_ACCENT_GOLD);

        // Labels
        tft.setTextSize(1);
        tft.setTextColor(C_MUTED, C_PANEL);
        tft.setCursor(x + 4, y + h - 14);
        tft.print(gWeather.sunriseTime);
        tft.setCursor(x + w - 28, y + h - 14);
        tft.print(gWeather.sunsetTime);
    }
};


// ════════════════════════════════════════════════════════════════════════════
// WidgetAQI — qualité de l'air
// ════════════════════════════════════════════════════════════════════════════
class WidgetAQI : public Widget {
public:
    WidgetAQI(int x, int y, int w, int h) : Widget(x, y, w, h) {}

    void draw(TFT_eSPI& tft) override {
        drawBackground(tft);
        drawLabel(tft, 5, 5, "QUALITE AIR");

        int aqi = gWeather.aqi;
        uint16_t col = aqiColor(aqi);
        const char* lbl = aqiLabel(aqi);

        // Valeur AQI
        tft.setTextSize(3);
        tft.setTextColor(col, C_PANEL);
        tft.setCursor(x + 5, y + 16);
        tft.print(aqi);

        // Label
        tft.setTextSize(1);
        tft.setTextColor(col, C_PANEL);
        tft.setCursor(x + 5, y + 42);
        tft.print(lbl);

        // Barre
        drawBar(tft, 5, h - 8, w - 10, 4, aqi / 200.0f, col);
    }

private:
    uint16_t aqiColor(int aqi) {
        if (aqi <= 50)  return C_ACCENT_GREEN;
        if (aqi <= 100) return C_ACCENT_GOLD;
        return C_ACCENT_RED;
    }
    const char* aqiLabel(int aqi) {
        if (aqi <= 50)  return "Bon";
        if (aqi <= 100) return "Modere";
        return "Mauvais";
    }
};