Page 1 of 1

Northern Lights Info

Posted: Tue Jan 20, 2026 3:05 pm
by dige
Did you also see the Northern Lights last night? They were visible as far south as 42° MLAT.
So you don’t miss the next ones, here’s a little info tool below…just for fun a little website grabber.
Maybe someone can make use of it.

Code: Select all

; To set your own location, change this line: cookie.s = "locationOption=4; country=Deutschland;[..]"

EnableExplicit

Structure PolarlichtRow
  datum.s
  sturm.s
  ab.s
  kp.f
  mlat.f
  dateValue.q
  isToday.b
  probable.b
  visible.b
EndStructure

Global NewList Rows.PolarlichtRow()

Enumeration
  #WinMain
EndEnumeration

Enumeration Gadgets
  #GadList
  #GadCanvas
EndEnumeration

Enumeration Fonts
  #FontUI
EndEnumeration


Global HoverMX.i, HoverMY.i
Global HoverActive.b
Global HoverIndex.i = -1

Declare DrawHoverOverlay(canvasW.i, canvasH.i, plotX.i, plotY.i, plotW.i, plotH.i, stepX.f, n.i, barW.i)

Procedure.s HtmlDecodeBasic(s.s)
  s = ReplaceString(s, " ", " ")
  s = ReplaceString(s, "&",  "&")
  s = ReplaceString(s, """, #DQUOTE$)
  s = ReplaceString(s, "&lt;",   "<")
  s = ReplaceString(s, "&gt;",   ">")
  s = ReplaceString(s, "&deg;",  "°")

  Protected reDec = CreateRegularExpression(#PB_Any, "&#(\d+);")
  If reDec
    While ExamineRegularExpression(reDec, s) And NextRegularExpressionMatch(reDec)
      Protected ent.s  = RegularExpressionMatchString(reDec)
      Protected code.i = Val(RegularExpressionGroup(reDec, 1))
      s = ReplaceString(s, ent, Chr(code))
    Wend
    FreeRegularExpression(reDec)
  EndIf

  Protected reHex = CreateRegularExpression(#PB_Any, "&#x([0-9A-Fa-f]+);")
  If reHex
    While ExamineRegularExpression(reHex, s) And NextRegularExpressionMatch(reHex)
      Protected ent2.s  = RegularExpressionMatchString(reHex)
      Protected code2.i = Val("$" + RegularExpressionGroup(reHex, 1))
      s = ReplaceString(s, ent2, Chr(code2))
    Wend
    FreeRegularExpression(reHex)
  EndIf

  ProcedureReturn s
EndProcedure

Procedure.s CellHtmlToText(cellHtml.s)
  Protected s.s = cellHtml

  ; Soft-Hyphen entfernen (kommt gern in "Polar­lichter" vor)
  s = ReplaceString(s, Chr($AD), "")
  s = ReplaceString(s, Chr($A0), " ")

  s = HtmlDecodeBasic(s)

  ; Tags raus
  Protected reTags = CreateRegularExpression(#PB_Any, "(?is)<[^>]+>")
  If reTags
    s = ReplaceRegularExpression(reTags, s, " ")
    FreeRegularExpression(reTags)
  EndIf

  ; Whitespace normalisieren (wichtig wegen Zeilenumbrüchen im <td>)
  Protected reWS = CreateRegularExpression(#PB_Any, "(?is)\s+")
  If reWS
    s = ReplaceRegularExpression(reWS, s, " ")
    FreeRegularExpression(reWS)
  EndIf

  ProcedureReturn Trim(s)
EndProcedure

Procedure.s ExtractTargetTableHtml(html.s)
  Protected reTable = CreateRegularExpression(#PB_Any, "(?is)<table\b[^>]*>.*?Stärke\s+des\s+magnetischen\s+Sturms.*?</table>")
  If reTable = 0
    ProcedureReturn ""
  EndIf

  Protected tableHtml.s = ""
  If ExamineRegularExpression(reTable, html) And NextRegularExpressionMatch(reTable)
    tableHtml = RegularExpressionMatchString(reTable)
  EndIf

  FreeRegularExpression(reTable)
  ProcedureReturn tableHtml
EndProcedure

Procedure ParseRowsFromTable(tableHtml.s)
  ClearList(Rows())

  Protected reTR = CreateRegularExpression(#PB_Any, "(?is)<tr\b[^>]*>.*?</tr>")
  Protected reTD = CreateRegularExpression(#PB_Any, "(?is)<td\b[^>]*>(.*?)</td>")

  If reTR = 0 Or reTD = 0
    If reTR : FreeRegularExpression(reTR) : EndIf
    If reTD : FreeRegularExpression(reTD) : EndIf
    ProcedureReturn
  EndIf

  If ExamineRegularExpression(reTR, tableHtml)
    While NextRegularExpressionMatch(reTR)
      Protected trHtml.s = RegularExpressionMatchString(reTR)

      ; aus diesem <tr> die ersten 3 <td> ziehen
      Protected tdCount.i = 0
      Protected c1.s, c2.s, c3.s

      If ExamineRegularExpression(reTD, trHtml)
        While NextRegularExpressionMatch(reTD)
          tdCount + 1
          Select tdCount
            Case 1 : c1 = CellHtmlToText(RegularExpressionGroup(reTD, 1))
            Case 2 : c2 = CellHtmlToText(RegularExpressionGroup(reTD, 1))
            Case 3 : c3 = CellHtmlToText(RegularExpressionGroup(reTD, 1))
          EndSelect
          If tdCount >= 3
            Break
          EndIf
        Wend
      EndIf

      If tdCount = 3
        If FindString(c1, "Datum", 1) = 0 And c1 <> ""
          AddElement(Rows())
          Rows()\datum = c1
          Rows()\sturm = c2
          Rows()\ab    = c3
        EndIf
      EndIf
    Wend
  EndIf

  FreeRegularExpression(reTR)
  FreeRegularExpression(reTD)
EndProcedure

Procedure.s CSVQuote(s.s)
  s = ReplaceString(s, #DQUOTE$, #DQUOTE$ + #DQUOTE$)
  ProcedureReturn #DQUOTE$ + s + #DQUOTE$
EndProcedure

Procedure.f ExtractFirstNumber(s.s)
  Protected re = CreateRegularExpression(#PB_Any, "(?i)(-?\d+(?:[\,\.]\d+)?)")
  If re = 0
    ProcedureReturn 0.0
  EndIf

  Protected v.f = 0.0
  If ExamineRegularExpression(re, s) And NextRegularExpressionMatch(re)
    Protected t.s = ReplaceString(RegularExpressionGroup(re, 1), ",", ".")
    v = ValF(t)
  EndIf
  FreeRegularExpression(re)

  ProcedureReturn v
EndProcedure

Procedure.f ExtractKPFromSturm(s.s)
  Protected kp.f = -1.0

  ; bevorzugt: "Kp 5" / "KP:7" / "Kp=6"
  Protected re = CreateRegularExpression(#PB_Any, "(?i)\bkp\b\s*[:=]?\s*([0-9](?:[\,\.][0-9])?)")
  If re
    If ExamineRegularExpression(re, s) And NextRegularExpressionMatch(re)
      kp = ValF(ReplaceString(RegularExpressionGroup(re, 1), ",", "."))
    EndIf
    FreeRegularExpression(re)
  EndIf

  If kp < 0
    kp = ExtractFirstNumber(s)
  EndIf

  If kp < 0 : kp = 0 : EndIf
  If kp > 9 : kp = 9 : EndIf

  ProcedureReturn kp
EndProcedure

Procedure.f ExtractMLATFromAb(s.s)
  Protected mlat.f = -1.0

  Protected re = CreateRegularExpression(#PB_Any, "(?i)\bmlat\b\s*[:=]?\s*(\d+(?:[\,\.]\d+)?)")
  If re
    If ExamineRegularExpression(re, s) And NextRegularExpressionMatch(re)
      mlat = ValF(ReplaceString(RegularExpressionGroup(re, 1), ",", "."))
    EndIf
    FreeRegularExpression(re)
  EndIf

  If mlat < 0
    mlat = ExtractFirstNumber(s)
  EndIf

  ; Plausibilisierung
  If mlat < 0 : mlat = 0 : EndIf
  If mlat > 90 : mlat = 90 : EndIf

  ProcedureReturn mlat
EndProcedure

Procedure.q ExtractDateValue(datum.s)
  Protected re = CreateRegularExpression(#PB_Any, "(\d{1,2})\.(\d{1,2})(?:\.(\d{2,4}))?")
  If re = 0
    ProcedureReturn 0
  EndIf

  Protected d.i, m.i, y.i
  If ExamineRegularExpression(re, datum) And NextRegularExpressionMatch(re)
    d = Val(RegularExpressionGroup(re, 1))
    m = Val(RegularExpressionGroup(re, 2))

    Protected yStr.s = RegularExpressionGroup(re, 3)
    If yStr <> ""
      y = Val(yStr)
      If Len(yStr) = 2
        ; 2-stellig -> 20xx/19xx
        If y < 70
          y + 2000
        Else
          y + 1900
        EndIf
      EndIf
    Else

      Protected now.q = Date()
      Protected curY.i = Year(now)
      Protected curM.i = Month(now)

      y = curY
      If m < curM - 6
        y = curY + 1
      ElseIf m > curM + 6
        y = curY - 1
      EndIf
    EndIf
  EndIf

  FreeRegularExpression(re)

  If d >= 1 And d <= 31 And m >= 1 And m <= 12 And y >= 1900
    ProcedureReturn Date(y, m, d, 0, 0, 0)
  EndIf

  ProcedureReturn 0
EndProcedure

Procedure PrepareComputedFields()
  Protected today0.q = Date(Year(Date()), Month(Date()), Day(Date()), 0, 0, 0)
  Protected todayShort.s = FormatDate("%dd.%mm.", Date())

  ForEach Rows()
    Rows()\kp        = ExtractKPFromSturm(Rows()\sturm)
    Rows()\mlat      = ExtractMLATFromAb(Rows()\ab)
    Rows()\dateValue = ExtractDateValue(Rows()\datum)

    Rows()\isToday   = #False
    If Rows()\dateValue <> 0 And Rows()\dateValue = today0
      Rows()\isToday = #True
    ElseIf FindString(Rows()\datum, todayShort, 1)
      Rows()\isToday = #True
    EndIf

    Rows()\probable  = Bool(Rows()\kp > 7.0)
    Rows()\visible   = Bool(Rows()\kp > 7.0 And Rows()\mlat > 0.0 And Rows()\mlat < 48.0)
  Next
EndProcedure

Procedure FillListGadget()
  ClearGadgetItems(#GadList)

  Protected i.i = 0
  ForEach Rows()
    Protected dateDisp.s = Rows()\datum
    If Rows()\isToday
      dateDisp = "★ " + dateDisp
    EndIf

    AddGadgetItem(#GadList, -1, dateDisp + #LF$ + StrF(Rows()\kp, 1) + #LF$ + StrF(Rows()\mlat, 1))

    ; Einfärbung
    Protected back.i = RGB(255, 255, 255)
    If Rows()\visible
      back = RGB(175, 255, 220)  ; sichtbar
    ElseIf Rows()\probable
      back = RGB(255, 235, 175)  ; wahrscheinlich
    EndIf

    ; aktuelles Datum zusätzlich markieren
    If Rows()\isToday
      If Rows()\visible
        back = RGB(140, 255, 215)
      ElseIf Rows()\probable
        back = RGB(255, 225, 160)
      Else
        back = RGB(210, 230, 255)
      EndIf
    EndIf

    SetGadgetItemColor(#GadList, i, #PB_Gadget_BackColor, back)
    i + 1
  Next
EndProcedure

Procedure DrawChart()
  If IsGadget(#GadCanvas) = 0
    ProcedureReturn
  EndIf

  Protected w.i = GadgetWidth(#GadCanvas)
  Protected h.i = GadgetHeight(#GadCanvas)
  If w < 50 Or h < 50
    ProcedureReturn
  EndIf

  Protected n.i = ListSize(Rows())

  StartDrawing(CanvasOutput(#GadCanvas))
  Box(0, 0, w, h, RGB(255, 255, 255))

  If n <= 0
    DrawingMode(#PB_2DDrawing_Transparent)
    DrawText(10, 10, "Keine Daten.")
    StopDrawing()
    ProcedureReturn
  EndIf

  ; Plot-Rahmen
  Protected mL.i = 55
  Protected mR.i = 55
  Protected mT.i = 28
  Protected mB.i = 55

  Protected plotX.i = mL
  Protected plotY.i = mT
  Protected plotW.i = w - mL - mR
  Protected plotH.i = h - mT - mB

  ; MLAT Min/Max aus Daten
  Protected mlatMin.f = 9999.0
  Protected mlatMax.f = -9999.0
  ForEach Rows()
    If Rows()\mlat > 0
      If Rows()\mlat < mlatMin : mlatMin = Rows()\mlat : EndIf
      If Rows()\mlat > mlatMax : mlatMax = Rows()\mlat : EndIf
    EndIf
  Next
  If mlatMin = 9999.0
    mlatMin = 40.0 : mlatMax = 60.0
  EndIf

  ; Padding
  If mlatMax - mlatMin < 5.0
    mlatMin - 2.5
    mlatMax + 2.5
  Else
    mlatMin - 1.5
    mlatMax + 1.5
  EndIf

  If mlatMin < 0 : mlatMin = 0 : EndIf
  If mlatMax > 90 : mlatMax = 90 : EndIf

  ; Grid + Achsen
  Protected axisColor.i = RGB(30, 30, 30)
  Protected gridColor.i = RGB(225, 225, 225)

  ; Hintergrund Plot
  Box(plotX, plotY, plotW, plotH, RGB(250, 250, 250))

  ; KP-Gitter (0..9)
  Protected k.i
  For k = 0 To 9
    Protected yy.i = plotY + plotH - Int(k / 9.0 * plotH)
    Line(plotX, yy, plotW, 1, gridColor)
  Next

  ; Achsen
  Line(plotX, plotY, 1, plotH, axisColor)
  Line(plotX, plotY + plotH, plotW, 1, axisColor)
  Line(plotX + plotW, plotY, 1, plotH, axisColor)

  ; Labels links (KP)
  DrawingMode(#PB_2DDrawing_Transparent)
  For k = 0 To 9
    yy = plotY + plotH - Int(k / 9.0 * plotH)
    DrawText(5, yy - 7, Str(k))
  Next

  ; Labels rechts (MLAT)
  Protected t.i
  For t = 0 To 4
    Protected v.f = mlatMin + (mlatMax - mlatMin) * (t / 4.0)
    yy = plotY + plotH - Int((v - mlatMin) / (mlatMax - mlatMin) * plotH)
    DrawText(plotX + plotW + 5, yy - 7, StrF(v, 0))
  Next

  ; Titel/Legende
  DrawingFont(FontID(#FontUI))
  DrawText(plotX, 4, "KP (Balken) / MLAT (Linie)")

  ; X-Skalierung
  Protected stepX.f
  If n <= 1
    stepX = 0
  Else
    stepX = plotW / (n - 1.0)
  EndIf

  Protected barW.i
  If n <= 1
    barW = 18
  Else
    barW = Int(stepX * 0.6)
    If barW < 4 : barW = 4 : EndIf
    If barW > 22 : barW = 22 : EndIf
  EndIf

  ; MLAT Linie zeichnen
  Protected i.i = 0
  Protected prevX.i = -1
  Protected prevY.i = -1

  ForEach Rows()
    Protected x.i
    If n <= 1
      x = plotX + plotW / 2
    Else
      x = plotX + Int(i * stepX)
    EndIf

    ; aktuelles Datum markieren (vertikale Linie)
    If Rows()\isToday
      Line(x, plotY, 1, plotH, RGB(200, 60, 60))
    EndIf

    ; KP Balken
    Protected kp.f = Rows()\kp
    If kp < 0 : kp = 0 : EndIf
    If kp > 9 : kp = 9 : EndIf

    Protected barH.i = Int(kp / 9.0 * plotH)
    Protected yBar.i = plotY + plotH - barH

    Protected colKP.i = RGB(80, 140, 210)
    If Rows()\visible
      colKP = RGB(40, 180, 120)
    ElseIf Rows()\probable
      colKP = RGB(230, 150, 60)
    EndIf

    Box(x - barW / 2, yBar, barW, barH, colKP)

    ; MLAT Punkt + Linie
    If Rows()\mlat > 0
      Protected yLine.i = plotY + plotH - Int((Rows()\mlat - mlatMin) / (mlatMax - mlatMin) * plotH)

      ; Verbindung
      If prevX >= 0
        LineXY(prevX, prevY, x, yLine, axisColor)
      EndIf

      ; Punkt
      Box(x - 2, yLine - 2, 5, 5, axisColor)

      prevX = x
      prevY = yLine
    EndIf

    i + 1
  Next

  ; X-Labels (ausdünnen)
  DrawingMode(#PB_2DDrawing_Transparent)
  Protected every.i = 1
  If n > 12
    every = Int(n / 12)
    If every < 1 : every = 1 : EndIf
  EndIf

  i = 0
  ForEach Rows()
    If (i % every) = 0 Or Rows()\isToday
      If n <= 1
        x = plotX + plotW / 2
      Else
        x = plotX + Int(i * stepX)
      EndIf

      ; kurze Label-Variante
      Protected lab.s = Rows()\datum
      ; oft reicht dd.mm.
      Protected reShort = CreateRegularExpression(#PB_Any, "(\d{1,2}\.\d{1,2}\.)")
      If reShort
        If ExamineRegularExpression(reShort, lab) And NextRegularExpressionMatch(reShort)
          lab = RegularExpressionGroup(reShort, 1)
        EndIf
        FreeRegularExpression(reShort)
      EndIf

      DrawText(x - 18, plotY + plotH + 6, lab)
    EndIf
    i + 1
  Next

  ; Schwelle MLAT=48 (optional als Linie)
  If 48.0 >= mlatMin And 48.0 <= mlatMax
    Protected y48.i = plotY + plotH - Int((48.0 - mlatMin) / (mlatMax - mlatMin) * plotH)
    Line(plotX, y48, plotW, 1, RGB(180, 180, 180))
    DrawText(plotX + plotW - 60, y48 - 16, "MLAT 48")
  EndIf

  ; Hover-Infobox (Tooltip)
  DrawHoverOverlay(w, h, plotX, plotY, plotW, plotH, stepX, n, barW)

  StopDrawing()
EndProcedure

Procedure DrawHoverOverlay(canvasW.i, canvasH.i, plotX.i, plotY.i, plotW.i, plotH.i, stepX.f, n.i, barW.i)
  If HoverActive = #False
    ProcedureReturn
  EndIf

  If n <= 0
    HoverIndex = -1
    ProcedureReturn
  EndIf

  If HoverMX < plotX Or HoverMX > plotX + plotW Or HoverMY < plotY Or HoverMY > plotY + plotH + 40
    HoverIndex = -1
    ProcedureReturn
  EndIf

  Protected idx.i
  If n <= 1 Or stepX <= 0.0
    idx = 0
  Else
    Protected pos.f = (HoverMX - plotX) / stepX
    idx = Round(pos, #PB_Round_Nearest)
    If idx < 0 : idx = 0 : EndIf
    If idx > n - 1 : idx = n - 1 : EndIf
  EndIf

  HoverIndex = idx

  If SelectElement(Rows(), idx) = 0
    ProcedureReturn
  EndIf

  ; X-Position fuer diese Spalte
  Protected x.i
  If n <= 1
    x = plotX + plotW / 2
  Else
    x = plotX + Int(idx * stepX)
  EndIf

  ; Marker-Linie
  DrawingMode(#PB_2DDrawing_Default)
  Line(x, plotY, 1, plotH, RGB(120, 120, 120))

  ; Balken-Outline (KP)
  Protected kp.f = Rows()\kp
  If kp < 0 : kp = 0 : EndIf
  If kp > 9 : kp = 9 : EndIf
  Protected barH.i = Int(kp / 9.0 * plotH)
  Protected yBar.i = plotY + plotH - barH

  DrawingMode(#PB_2DDrawing_Outlined)
  Box(x - barW / 2, yBar, barW, barH, RGB(30, 30, 30))

  ; Tooltip Text
  DrawingMode(#PB_2DDrawing_Transparent)
  Protected t1.s = "Datum: " + Rows()\datum
  Protected t2.s = "KP: " + StrF(Rows()\kp, 1)
  Protected t3.s = "MLAT: " + StrF(Rows()\mlat, 1)

  Protected pad.i = 7
  Protected lineH.i = TextHeight("Ay") + 2
  Protected tw.i = TextWidth(t1)
  Protected tmp.i = TextWidth(t2) : If tmp > tw : tw = tmp : EndIf
  tmp = TextWidth(t3) : If tmp > tw : tw = tmp : EndIf

  Protected bw.i = tw + pad * 2
  Protected bh.i = lineH * 3 + pad * 2

  Protected bx.i = HoverMX + 16
  Protected by.i = HoverMY + 16

  ; im Canvas halten
  If bx + bw > canvasW
    bx = HoverMX - bw - 16
  EndIf
  If by + bh > canvasH
    by = HoverMY - bh - 16
  EndIf
  If bx < 2 : bx = 2 : EndIf
  If by < 2 : by = 2 : EndIf

  ; Farbe: sichtbar / wahrscheinlich / normal (+ heute)
  Protected bg.i = RGBA(255, 255, 255, 240)
  If Rows()\visible
    bg = RGBA(140, 255, 215, 240)
  ElseIf Rows()\probable
    bg = RGBA(255, 225, 160, 240)
  ElseIf Rows()\isToday
    bg = RGBA(210, 230, 255, 240)
  EndIf

  ; Tooltip zeichnen
  DrawingMode(#PB_2DDrawing_AlphaBlend)
  Box(bx, by, bw, bh, bg)

  DrawingMode(#PB_2DDrawing_Outlined)
  Box(bx, by, bw, bh, RGB(70, 70, 70))

  DrawingMode(#PB_2DDrawing_Transparent)
  DrawText(bx + pad, by + pad + 0 * lineH, t1, RGB(0, 0, 0))
  DrawText(bx + pad, by + pad + 1 * lineH, t2, RGB(0, 0, 0))
  DrawText(bx + pad, by + pad + 2 * lineH, t3, RGB(0, 0, 0))
EndProcedure
Procedure ResizeUI()
  Protected ww.i = WindowWidth(#WinMain)
  Protected wh.i = WindowHeight(#WinMain)

  Protected listH.i = Int(wh * 0.33)
  If listH < 170 : listH = 170 : EndIf
  If listH > wh - 120 : listH = wh - 120 : EndIf

  ResizeGadget(#GadList,   10, 10, ww - 20, listH)
  ResizeGadget(#GadCanvas, 10, 20 + listH, ww - 20, wh - listH - 30)

  DrawChart()
EndProcedure

Define url.s = "https://www.heute-am-himmel.de/polarlichter"
Define cookie.s = "locationOption=4; country=Deutschland; city=Dresden; timeZone=Europe%2FBerlin; lat=51.00166; lon=13.64880"

NewMap H.s()
H("User-Agent")      = "Mozilla/5.0"
H("Accept-Language") = "de-DE,de;q=0.9"
H("Accept-Encoding") = "identity"
H("Cookie")          = cookie


 LoadFont(#FontUI, "Arial", 10)

Define req.i = HTTPRequest(#PB_HTTP_Get, url, "", 0, H())
If req = 0
  MessageRequester("Polarlicht", "HTTPRequest() fehlgeschlagen.")
  End
EndIf

Define status.s = HTTPInfo(req, #PB_HTTP_StatusCode)
Define html.s   = HTTPInfo(req, #PB_HTTP_Response)
FinishHTTP(req)

If status <> "200" Or html = ""
  MessageRequester("Polarlicht", "HTTP Status: " + status + #LF$ + "Keine Antwortdaten.")
  End
EndIf

Define tableHtml.s = ExtractTargetTableHtml(html)
If tableHtml = ""
  MessageRequester("Polarlicht", "Zieltabelle nicht gefunden (Text 'Stärke des magnetischen Sturms' fehlt?).")
  End
EndIf

ParseRowsFromTable(tableHtml)
If ListSize(Rows()) = 0
  MessageRequester("Polarlicht", "Keine Datenzeilen gefunden.")
  End
EndIf

PrepareComputedFields()

Define winTitle.s = "Polarlicht – KP/MLAT (" + FormatDate("%dd.%mm.%yyyy %hh:%ii", Date()) + ")"

If OpenWindow(#WinMain, 0, 0, 980, 720, winTitle, #PB_Window_SystemMenu | #PB_Window_SizeGadget | #PB_Window_ScreenCentered)

  ListIconGadget(#GadList, 10, 10, 960, 220, "Datum", 260, #PB_ListIcon_FullRowSelect | #PB_ListIcon_AlwaysShowSelection)
  AddGadgetColumn(#GadList, 1, "KP",   70)
  AddGadgetColumn(#GadList, 2, "MLAT", 80)

  CanvasGadget(#GadCanvas, 10, 240, 960, 470, #PB_Canvas_Keyboard)

  FillListGadget()
  ResizeUI()

  ; Event-Loop
  Repeat
    Select WaitWindowEvent()

      Case #PB_Event_Gadget
        Select EventGadget()
          Case #GadCanvas
            Select EventType()
              Case #PB_EventType_MouseMove
                HoverMX = GetGadgetAttribute(#GadCanvas, #PB_Canvas_MouseX)
                HoverMY = GetGadgetAttribute(#GadCanvas, #PB_Canvas_MouseY)
                HoverActive = #True
                DrawChart()

              Case #PB_EventType_MouseLeave
                HoverActive = #False
                HoverIndex = -1
                DrawChart()
            EndSelect
        EndSelect

      Case #PB_Event_CloseWindow
        Break

      Case #PB_Event_SizeWindow
        ResizeUI()

    EndSelect
  ForEver

EndIf


Re: Northern Lights Info

Posted: Tue Jan 20, 2026 4:28 pm
by benubi
I haven't seen them. I like the chart a lot, even though I don't really understand it. This looks very good IMO.

Re: Northern Lights Info

Posted: Tue Jan 20, 2026 9:57 pm
by idle
Nice thank you. Unfortunately I'm literally under a long white cloud at the moment which is the translation of the Māori name for New Zealand Aotearoa, so no southern lights to see but maybe we'll be lucky tonight if the clouds break, it's a big solar storm!

Re: Northern Lights Info

Posted: Wed Jan 21, 2026 2:06 am
by minimy
Thanks for share, very nice interface.
From Spain I can only see the lights of the neighbors who are partying all night :lol:

Re: Northern Lights Info

Posted: Wed Jan 21, 2026 9:58 am
by Michael Vogel
Nice one, thank you...

Didn't see any text on the graph - so I've added a color constant to the code and an arrow to indicate a selected item from the list.

Code: Select all


; Define

	; To set your own location, change this line: cookie.s = "locationOption=4; country=Deutschland;[..]"

	#colText=	#Gray
	#colArrow=	$40

	EnableExplicit

	Structure PolarlichtRow
		datum.s
		sturm.s
		ab.s
		kp.f
		mlat.f
		dateValue.q
		isToday.b
		probable.b
		visible.b
	EndStructure

	Global NewList Rows.PolarlichtRow()

	Enumeration
		#WinMain
	EndEnumeration

	Enumeration Gadgets
		#GadList
		#GadCanvas
	EndEnumeration

	Enumeration Fonts
		#FontUI
	EndEnumeration


	Global HoverMX.i, HoverMY.i
	Global HoverActive.b
	Global HoverIndex.i = -1

	Declare DrawHoverOverlay(canvasW.i, canvasH.i, plotX.i, plotY.i, plotW.i, plotH.i, stepX.f, n.i, barW.i)

; EndDefine

Procedure.s HtmlDecodeBasic(s.s)
	s = ReplaceString(s, "&nbsp;", " ")
	s = ReplaceString(s, "&amp;",  "&")
	s = ReplaceString(s, "&quot;", #DQUOTE$)
	s = ReplaceString(s, "&lt;",   "<")
	s = ReplaceString(s, "&gt;",   ">")
	s = ReplaceString(s, "&deg;",  "°")

	Protected reDec = CreateRegularExpression(#PB_Any, "&#(\d+);")
	If reDec
		While ExamineRegularExpression(reDec, s) And NextRegularExpressionMatch(reDec)
			Protected ent.s  = RegularExpressionMatchString(reDec)
			Protected code.i = Val(RegularExpressionGroup(reDec, 1))
			s = ReplaceString(s, ent, Chr(code))
		Wend
		FreeRegularExpression(reDec)
	EndIf

	Protected reHex = CreateRegularExpression(#PB_Any, "&#x([0-9A-Fa-f]+);")
	If reHex
		While ExamineRegularExpression(reHex, s) And NextRegularExpressionMatch(reHex)
			Protected ent2.s  = RegularExpressionMatchString(reHex)
			Protected code2.i = Val("$" + RegularExpressionGroup(reHex, 1))
			s = ReplaceString(s, ent2, Chr(code2))
		Wend
		FreeRegularExpression(reHex)
	EndIf

	ProcedureReturn s
EndProcedure
Procedure.s CellHtmlToText(cellHtml.s)
	Protected s.s = cellHtml

	; Soft-Hyphen entfernen (kommt gern in "Polar­lichter" vor)
	s = ReplaceString(s, Chr($AD), "")
	s = ReplaceString(s, Chr($A0), " ")

	s = HtmlDecodeBasic(s)

	; Tags raus
	Protected reTags = CreateRegularExpression(#PB_Any, "(?is)<[^>]+>")
	If reTags
		s = ReplaceRegularExpression(reTags, s, " ")
		FreeRegularExpression(reTags)
	EndIf

	; Whitespace normalisieren (wichtig wegen Zeilenumbrüchen im <td>)
	Protected reWS = CreateRegularExpression(#PB_Any, "(?is)\s+")
	If reWS
		s = ReplaceRegularExpression(reWS, s, " ")
		FreeRegularExpression(reWS)
	EndIf

	ProcedureReturn Trim(s)
EndProcedure
Procedure.s ExtractTargetTableHtml(html.s)
	Protected reTable = CreateRegularExpression(#PB_Any, "(?is)<table\b[^>]*>.*?Stärke\s+des\s+magnetischen\s+Sturms.*?</table>")
	If reTable = 0
		ProcedureReturn ""
	EndIf

	Protected tableHtml.s = ""
	If ExamineRegularExpression(reTable, html) And NextRegularExpressionMatch(reTable)
		tableHtml = RegularExpressionMatchString(reTable)
	EndIf

	FreeRegularExpression(reTable)
	ProcedureReturn tableHtml
EndProcedure
Procedure ParseRowsFromTable(tableHtml.s)
	ClearList(Rows())

	Protected reTR = CreateRegularExpression(#PB_Any, "(?is)<tr\b[^>]*>.*?</tr>")
	Protected reTD = CreateRegularExpression(#PB_Any, "(?is)<td\b[^>]*>(.*?)</td>")

	If reTR = 0 Or reTD = 0
		If reTR : FreeRegularExpression(reTR) : EndIf
		If reTD : FreeRegularExpression(reTD) : EndIf
		ProcedureReturn
	EndIf

	If ExamineRegularExpression(reTR, tableHtml)
		While NextRegularExpressionMatch(reTR)
			Protected trHtml.s = RegularExpressionMatchString(reTR)

			; aus diesem <tr> die ersten 3 <td> ziehen
			Protected tdCount.i = 0
			Protected c1.s, c2.s, c3.s

			If ExamineRegularExpression(reTD, trHtml)
				While NextRegularExpressionMatch(reTD)
					tdCount + 1
					Select tdCount
					Case 1 : c1 = CellHtmlToText(RegularExpressionGroup(reTD, 1))
					Case 2 : c2 = CellHtmlToText(RegularExpressionGroup(reTD, 1))
					Case 3 : c3 = CellHtmlToText(RegularExpressionGroup(reTD, 1))
					EndSelect
					If tdCount >= 3
						Break
					EndIf
				Wend
			EndIf

			If tdCount = 3
				If FindString(c1, "Datum", 1) = 0 And c1 <> ""
					AddElement(Rows())
					Rows()\datum = c1
					Rows()\sturm = c2
					Rows()\ab    = c3
				EndIf
			EndIf
		Wend
	EndIf

	FreeRegularExpression(reTR)
	FreeRegularExpression(reTD)
EndProcedure
Procedure.s CSVQuote(s.s)
	s = ReplaceString(s, #DQUOTE$, #DQUOTE$ + #DQUOTE$)
	ProcedureReturn #DQUOTE$ + s + #DQUOTE$
EndProcedure
Procedure.f ExtractFirstNumber(s.s)
	Protected re = CreateRegularExpression(#PB_Any, "(?i)(-?\d+(?:[\,\.]\d+)?)")
	If re = 0
		ProcedureReturn 0.0
	EndIf

	Protected v.f = 0.0
	If ExamineRegularExpression(re, s) And NextRegularExpressionMatch(re)
		Protected t.s = ReplaceString(RegularExpressionGroup(re, 1), ",", ".")
		v = ValF(t)
	EndIf
	FreeRegularExpression(re)

	ProcedureReturn v
EndProcedure
Procedure.f ExtractKPFromSturm(s.s)
	Protected kp.f = -1.0

	; bevorzugt: "Kp 5" / "KP:7" / "Kp=6"
	Protected re = CreateRegularExpression(#PB_Any, "(?i)\bkp\b\s*[:=]?\s*([0-9](?:[\,\.][0-9])?)")
	If re
		If ExamineRegularExpression(re, s) And NextRegularExpressionMatch(re)
			kp = ValF(ReplaceString(RegularExpressionGroup(re, 1), ",", "."))
		EndIf
		FreeRegularExpression(re)
	EndIf

	If kp < 0
		kp = ExtractFirstNumber(s)
	EndIf

	If kp < 0 : kp = 0 : EndIf
	If kp > 9 : kp = 9 : EndIf

	ProcedureReturn kp
EndProcedure
Procedure.f ExtractMLATFromAb(s.s)
	Protected mlat.f = -1.0

	Protected re = CreateRegularExpression(#PB_Any, "(?i)\bmlat\b\s*[:=]?\s*(\d+(?:[\,\.]\d+)?)")
	If re
		If ExamineRegularExpression(re, s) And NextRegularExpressionMatch(re)
			mlat = ValF(ReplaceString(RegularExpressionGroup(re, 1), ",", "."))
		EndIf
		FreeRegularExpression(re)
	EndIf

	If mlat < 0
		mlat = ExtractFirstNumber(s)
	EndIf

	; Plausibilisierung
	If mlat < 0 : mlat = 0 : EndIf
	If mlat > 90 : mlat = 90 : EndIf

	ProcedureReturn mlat
EndProcedure
Procedure.q ExtractDateValue(datum.s)
	Protected re = CreateRegularExpression(#PB_Any, "(\d{1,2})\.(\d{1,2})(?:\.(\d{2,4}))?")
	If re = 0
		ProcedureReturn 0
	EndIf

	Protected d.i, m.i, y.i
	If ExamineRegularExpression(re, datum) And NextRegularExpressionMatch(re)
		d = Val(RegularExpressionGroup(re, 1))
		m = Val(RegularExpressionGroup(re, 2))

		Protected yStr.s = RegularExpressionGroup(re, 3)
		If yStr <> ""
			y = Val(yStr)
			If Len(yStr) = 2
				; 2-stellig -> 20xx/19xx
				If y < 70
					y + 2000
				Else
					y + 1900
				EndIf
			EndIf
		Else

			Protected now.q = Date()
			Protected curY.i = Year(now)
			Protected curM.i = Month(now)

			y = curY
			If m < curM - 6
				y = curY + 1
			ElseIf m > curM + 6
				y = curY - 1
			EndIf
		EndIf
	EndIf

	FreeRegularExpression(re)

	If d >= 1 And d <= 31 And m >= 1 And m <= 12 And y >= 1900
		ProcedureReturn Date(y, m, d, 0, 0, 0)
	EndIf

	ProcedureReturn 0
EndProcedure
Procedure PrepareComputedFields()
	Protected today0.q = Date(Year(Date()), Month(Date()), Day(Date()), 0, 0, 0)
	Protected todayShort.s = FormatDate("%dd.%mm.", Date())

	ForEach Rows()
		Rows()\kp        = ExtractKPFromSturm(Rows()\sturm)
		Rows()\mlat      = ExtractMLATFromAb(Rows()\ab)
		Rows()\dateValue = ExtractDateValue(Rows()\datum)

		Rows()\isToday   = #False
		If Rows()\dateValue <> 0 And Rows()\dateValue = today0
			Rows()\isToday = #True
		ElseIf FindString(Rows()\datum, todayShort, 1)
			Rows()\isToday = #True
		EndIf

		Rows()\probable  = Bool(Rows()\kp > 7.0)
		Rows()\visible   = Bool(Rows()\kp > 7.0 And Rows()\mlat > 0.0 And Rows()\mlat < 48.0)
	Next
EndProcedure
Procedure FillListGadget()
	ClearGadgetItems(#GadList)

	Protected i.i = 0
	ForEach Rows()
		Protected dateDisp.s = Rows()\datum
		If Rows()\isToday
			dateDisp = "? " + dateDisp
		EndIf

		AddGadgetItem(#GadList, -1, dateDisp + #LF$ + StrF(Rows()\kp, 1) + #LF$ + StrF(Rows()\mlat, 1))

		; Einfärbung
		Protected back.i = RGB(255, 255, 255)
		If Rows()\visible
			back = RGB(175, 255, 220)  ; sichtbar
		ElseIf Rows()\probable
			back = RGB(255, 235, 175)  ; wahrscheinlich
		EndIf

		; aktuelles Datum zusätzlich markieren
		If Rows()\isToday
			If Rows()\visible
				back = RGB(140, 255, 215)
			ElseIf Rows()\probable
				back = RGB(255, 225, 160)
			Else
				back = RGB(210, 230, 255)
			EndIf
		EndIf

		SetGadgetItemColor(#GadList, i, #PB_Gadget_BackColor, back)
		i + 1
	Next
EndProcedure
Procedure DrawChart()

	If IsGadget(#GadCanvas) = 0
		ProcedureReturn
	EndIf

	Protected w.i = GadgetWidth(#GadCanvas)
	Protected h.i = GadgetHeight(#GadCanvas)
	If w < 50 Or h < 50
		ProcedureReturn
	EndIf

	Protected n.i = ListSize(Rows())

	StartDrawing(CanvasOutput(#GadCanvas))
	Box(0, 0, w, h, RGB(255, 255, 255))

	If n <= 0
		DrawingMode(#PB_2DDrawing_Transparent)
		DrawText(10, 10, "Keine Daten.",#colText)
		StopDrawing()
		ProcedureReturn
	EndIf

	; Plot-Rahmen
	Protected mL.i = 55
	Protected mR.i = 55
	Protected mT.i = 28
	Protected mB.i = 55

	Protected plotX.i = mL
	Protected plotY.i = mT
	Protected plotW.i = w - mL - mR
	Protected plotH.i = h - mT - mB

	; MLAT Min/Max aus Daten
	Protected mlatMin.f = 9999.0
	Protected mlatMax.f = -9999.0

	ForEach Rows()
		If Rows()\mlat > 0
			If Rows()\mlat < mlatMin : mlatMin = Rows()\mlat : EndIf
			If Rows()\mlat > mlatMax : mlatMax = Rows()\mlat : EndIf
		EndIf
	Next
	If mlatMin = 9999.0
		mlatMin = 40.0 : mlatMax = 60.0
	EndIf

	; Padding
	If mlatMax - mlatMin < 5.0
		mlatMin - 2.5
		mlatMax + 2.5
	Else
		mlatMin - 1.5
		mlatMax + 1.5
	EndIf

	If mlatMin < 0 : mlatMin = 0 : EndIf
	If mlatMax > 90 : mlatMax = 90 : EndIf

	; Grid + Achsen
	Protected axisColor.i = RGB(30, 30, 30)
	Protected gridColor.i = RGB(225, 225, 225)

	; Hintergrund Plot
	Box(plotX, plotY, plotW, plotH, RGB(250, 250, 250))

	; KP-Gitter (0..9)
	Protected k.i
	For k = 0 To 9
		Protected yy.i = plotY + plotH - Int(k / 9.0 * plotH)
		Line(plotX, yy, plotW, 1, gridColor)
	Next

	; Achsen
	Line(plotX, plotY, 1, plotH, axisColor)
	Line(plotX, plotY + plotH, plotW, 1, axisColor)
	Line(plotX + plotW, plotY, 1, plotH, axisColor)

	; Labels links (KP)
	DrawingMode(#PB_2DDrawing_Transparent)
	For k = 0 To 9
		yy = plotY + plotH - Int(k / 9.0 * plotH)
		DrawText(5, yy - 7, Str(k),#colText)
	Next

	; Labels rechts (MLAT)
	Protected t.i
	For t = 0 To 4
		Protected v.f = mlatMin + (mlatMax - mlatMin) * (t / 4.0)
		yy = plotY + plotH - Int((v - mlatMin) / (mlatMax - mlatMin) * plotH)
		DrawText(plotX + plotW + 5, yy - 7, StrF(v, 0),#colText)
	Next

	; Titel/Legende
	DrawingFont(FontID(#FontUI))
	DrawText(plotX, 4, "KP (Balken) / MLAT (Linie)",#colText)

	; X-Skalierung
	Protected stepX.f
	If n <= 1
		stepX = 0
	Else
		stepX = plotW / (n - 1.0)
	EndIf

	Protected barW.i
	If n <= 1
		barW = 18
	Else
		barW = Int(stepX * 0.6)
		If barW < 4 : barW = 4 : EndIf
		If barW > 22 : barW = 22 : EndIf
	EndIf

	; MLAT Linie zeichnen
	Protected i.i = 0
	Protected prevX.i = -1
	Protected prevY.i = -1

	ForEach Rows()
		Protected x.i
		If n <= 1
			x = plotX + plotW / 2
		Else
			x = plotX + Int(i * stepX)
		EndIf

		; aktuelles Datum markieren (vertikale Linie)
		If Rows()\isToday
			Line(x, plotY, 1, plotH, RGB(200, 60, 60))
		EndIf

		; KP Balken
		Protected kp.f = Rows()\kp
		If kp < 0 : kp = 0 : EndIf
		If kp > 9 : kp = 9 : EndIf

		Protected barH.i = Int(kp / 9.0 * plotH)
		Protected yBar.i = plotY + plotH - barH

		Protected colKP.i = RGB(80, 140, 210)
		If Rows()\visible
			colKP = RGB(40, 180, 120)
		ElseIf Rows()\probable
			colKP = RGB(230, 150, 60)
		EndIf

		Box(x - barW / 2, yBar, barW, barH, colKP)

		Protected arrW,a
		If GetGadgetItemState(#GadList,i)
			arrW=barW+5
			a=1
			While arrW
				Box(x-arrW/2-1,plotH+plotY-a,arrW+2,1,#White)
				Box(x-arrW/2,plotH+plotY-a,arrW,1,#colArrow)
				arrW-1
				a+1
			Wend
		EndIf


		; MLAT Punkt + Linie
		If Rows()\mlat > 0
			Protected yLine.i = plotY + plotH - Int((Rows()\mlat - mlatMin) / (mlatMax - mlatMin) * plotH)

			; Verbindung
			If prevX >= 0
				LineXY(prevX, prevY, x, yLine, axisColor)
			EndIf

			; Punkt
			Box(x - 2, yLine - 2, 5, 5, axisColor)

			prevX = x
			prevY = yLine
		EndIf

		i + 1
	Next

	; X-Labels (ausdünnen)
	DrawingMode(#PB_2DDrawing_Transparent)
	Protected every.i = 1
	If n > 12
		every = Int(n / 12)
		If every < 1 : every = 1 : EndIf
	EndIf

	i = 0
	ForEach Rows()
		If (i % every) = 0 Or Rows()\isToday
			If n <= 1
				x = plotX + plotW / 2
			Else
				x = plotX + Int(i * stepX)
			EndIf

			; kurze Label-Variante
			Protected lab.s = Rows()\datum
			; oft reicht dd.mm.
			Protected reShort = CreateRegularExpression(#PB_Any, "(\d{1,2}\.\d{1,2}\.)")
			If reShort
				If ExamineRegularExpression(reShort, lab) And NextRegularExpressionMatch(reShort)
					lab = RegularExpressionGroup(reShort, 1)
				EndIf
				FreeRegularExpression(reShort)
			EndIf

			DrawText(x - 18, plotY + plotH + 6, lab,#colText)
		EndIf
		i + 1
	Next

	; Schwelle MLAT=48 (optional als Linie)
	If 48.0 >= mlatMin And 48.0 <= mlatMax
		Protected y48.i = plotY + plotH - Int((48.0 - mlatMin) / (mlatMax - mlatMin) * plotH)
		Line(plotX, y48, plotW, 1, RGB(180, 180, 180))
		DrawText(plotX + plotW - 60, y48 - 16, "MLAT 48",#colText)
	EndIf

	; Hover-Infobox (Tooltip)
	DrawHoverOverlay(w, h, plotX, plotY, plotW, plotH, stepX, n, barW)

	StopDrawing()
EndProcedure
Procedure DrawHoverOverlay(canvasW.i, canvasH.i, plotX.i, plotY.i, plotW.i, plotH.i, stepX.f, n.i, barW.i)

	If HoverActive = #False
		ProcedureReturn
	EndIf

	If n <= 0
		HoverIndex = -1
		ProcedureReturn
	EndIf

	If HoverMX < plotX Or HoverMX > plotX + plotW Or HoverMY < plotY Or HoverMY > plotY + plotH + 40
		HoverIndex = -1
		ProcedureReturn
	EndIf

	Protected idx.i
	If n <= 1 Or stepX <= 0.0
		idx = 0
	Else
		Protected pos.f = (HoverMX - plotX) / stepX
		idx = Round(pos, #PB_Round_Nearest)
		If idx < 0 : idx = 0 : EndIf
		If idx > n - 1 : idx = n - 1 : EndIf
	EndIf

	HoverIndex = idx

	If SelectElement(Rows(), idx) = 0
		ProcedureReturn
	EndIf

	; X-Position fuer diese Spalte
	Protected x.i
	If n <= 1
		x = plotX + plotW / 2
	Else
		x = plotX + Int(idx * stepX)
	EndIf

	; Marker-Linie
	DrawingMode(#PB_2DDrawing_Default)
	Line(x, plotY, 1, plotH, RGB(120, 120, 120))

	; Balken-Outline (KP)
	Protected kp.f = Rows()\kp
	If kp < 0 : kp = 0 : EndIf
	If kp > 9 : kp = 9 : EndIf
	Protected barH.i = Int(kp / 9.0 * plotH)
	Protected yBar.i = plotY + plotH - barH

	DrawingMode(#PB_2DDrawing_Outlined)
	Box(x - barW / 2, yBar, barW, barH, RGB(30, 30, 30))

	; Tooltip Text
	DrawingMode(#PB_2DDrawing_Transparent)
	Protected t1.s = "Datum: " + Rows()\datum
	Protected t2.s = "KP: " + StrF(Rows()\kp, 1)
	Protected t3.s = "MLAT: " + StrF(Rows()\mlat, 1)

	Protected pad.i = 7
	Protected lineH.i = TextHeight("Ay") + 2
	Protected tw.i = TextWidth(t1)
	Protected tmp.i = TextWidth(t2)
	If tmp > tw : tw = tmp : EndIf
	tmp = TextWidth(t3)
	If tmp > tw : tw = tmp : EndIf

	Protected bw.i = tw + pad * 2
	Protected bh.i = lineH * 3 + pad * 2

	Protected bx.i = HoverMX + 16
	Protected by.i = HoverMY + 16

	; im Canvas halten
	If bx + bw > canvasW
		bx = HoverMX - bw - 16
	EndIf
	If by + bh > canvasH
		by = HoverMY - bh - 16
	EndIf
	If bx < 2 : bx = 2 : EndIf
	If by < 2 : by = 2 : EndIf

	; Farbe: sichtbar / wahrscheinlich / normal (+ heute)
	Protected bg.i = RGBA(255, 255, 255, 240)
	If Rows()\visible
		bg = RGBA(140, 255, 215, 240)
	ElseIf Rows()\probable
		bg = RGBA(255, 225, 160, 240)
	ElseIf Rows()\isToday
		bg = RGBA(210, 230, 255, 240)
	EndIf

	; Tooltip zeichnen
	DrawingMode(#PB_2DDrawing_AlphaBlend)
	Box(bx, by, bw, bh, bg)

	DrawingMode(#PB_2DDrawing_Outlined)
	Box(bx, by, bw, bh, RGB(70, 70, 70))

	DrawingMode(#PB_2DDrawing_Transparent)
	DrawText(bx + pad, by + pad + 0 * lineH, t1, RGB(0, 0, 0),#colText)
	DrawText(bx + pad, by + pad + 1 * lineH, t2, RGB(0, 0, 0),#colText)
	DrawText(bx + pad, by + pad + 2 * lineH, t3, RGB(0, 0, 0),#colText)
EndProcedure
Procedure ResizeUI()
	Protected ww.i = WindowWidth(#WinMain)
	Protected wh.i = WindowHeight(#WinMain)

	Protected listH.i = Int(wh * 0.33)
	If listH < 170 : listH = 170 : EndIf
	If listH > wh - 120 : listH = wh - 120 : EndIf

	ResizeGadget(#GadList,   10, 10, ww - 20, listH)
	ResizeGadget(#GadCanvas, 10, 20 + listH, ww - 20, wh - listH - 30)

	DrawChart()
EndProcedure

; Define Main

	Define i

	Define url.s = "https://www.heute-am-himmel.de/polarlichter"
	Define cookie.s = "locationOption=4; country=Deutschland; city=Dresden; timeZone=Europe%2FBerlin; lat=51.00166; lon=13.64880"
	
	NewMap H.s()
	H("User-Agent")      = "Mozilla/5.0"
	H("Accept-Language") = "de-DE,de;q=0.9"
	H("Accept-Encoding") = "identity"
	H("Cookie")          = cookie


	LoadFont(#FontUI, "Arial", 10)

	Define req.i = HTTPRequest(#PB_HTTP_Get, url, "", 0, H())
	If req = 0
		MessageRequester("Polarlicht", "HTTPRequest() fehlgeschlagen.")
		End
	EndIf

	Define status.s = HTTPInfo(req, #PB_HTTP_StatusCode)
	Define html.s   = HTTPInfo(req, #PB_HTTP_Response)
	FinishHTTP(req)

	If status <> "200" Or html = ""
		MessageRequester("Polarlicht", "HTTP Status: " + status + #LF$ + "Keine Antwortdaten.")
		End
	EndIf

	Define tableHtml.s = ExtractTargetTableHtml(html)
	If tableHtml = ""
		MessageRequester("Polarlicht", "Zieltabelle nicht gefunden (Text 'Stärke des magnetischen Sturms' fehlt?).")
		End
	EndIf

	ParseRowsFromTable(tableHtml)
	If ListSize(Rows()) = 0
		MessageRequester("Polarlicht", "Keine Datenzeilen gefunden.")
		End
	EndIf

	PrepareComputedFields()

	Define winTitle.s = "Polarlicht – KP/MLAT (" + FormatDate("%dd.%mm.%yyyy %hh:%ii", Date()) + ")"

	If OpenWindow(#WinMain, 0, 0, 980, 720, winTitle, #PB_Window_SystemMenu | #PB_Window_SizeGadget | #PB_Window_ScreenCentered)

		ListIconGadget(#GadList, 10, 10, 960, 220, "Datum", 260, #PB_ListIcon_FullRowSelect | #PB_ListIcon_AlwaysShowSelection | #PB_ListIcon_MultiSelect)
		AddGadgetColumn(#GadList, 1, "KP",   70)
		AddGadgetColumn(#GadList, 2, "MLAT", 80)

		CanvasGadget(#GadCanvas, 10, 240, 960, 470, #PB_Canvas_Keyboard)

		FillListGadget()
		ResizeUI()

		; Event-Loop
		Repeat
			Select WaitWindowEvent()

			Case #PB_Event_Gadget
				Select EventGadget()

				Case #GadList
					Select EventType()
					Case #PB_EventType_Change
						DrawChart()
					EndSelect

				Case #GadCanvas
					Select EventType()
					Case #PB_EventType_MouseMove
						HoverMX = GetGadgetAttribute(#GadCanvas, #PB_Canvas_MouseX)
						HoverMY = GetGadgetAttribute(#GadCanvas, #PB_Canvas_MouseY)
						HoverActive = #True
						DrawChart()

					Case #PB_EventType_MouseLeave
						HoverActive = #False
						HoverIndex = -1
						DrawChart()
					EndSelect
				EndSelect

			Case #PB_Event_CloseWindow
				Break

			Case #PB_Event_SizeWindow
				ResizeUI()

			EndSelect
		ForEver

	EndIf

; EndDefine

Re: Northern Lights Info

Posted: Wed Jan 21, 2026 3:07 pm
by dige
I asked ChatGPT to clean up the code and make the UI nicer 👇🏼 this is what I got. (This approach is where AI works pretty well.)

Code: Select all

EnableExplicit

; ============================
; Data model
; ============================

Structure PolarlichtRow
  datum.s
  sturm.s
  ab.s
  kp.f
  mlat.f
  dateValue.q
  isToday.b
  probable.b
  visible.b
EndStructure

Global NewList Rows.PolarlichtRow()

; ============================
; App / UI IDs
; ============================

Enumeration
  #WinMain
  #WinSettings
EndEnumeration

Enumeration #PB_Event_FirstCustomValue
  #Event_DataReady
EndEnumeration

Enumeration Gadgets
  #GadHeader
  #GadBtnRefresh
  #GadBtnExport
  #GadBtnSettings
  #GadChkDark
  #GadLblSummary
  #GadSplitter
  #GadList
  #GadCanvas
  #GadDetails
EndEnumeration

Enumeration Menus
  #MenuList
EndEnumeration

Enumeration MenuItems
  #MiCopyRow
EndEnumeration

Enumeration Fonts
  #FontUI
  #FontUIBold
  #FontMono
EndEnumeration

; ============================
; Theme
; ============================

Global gDark.b
Global T_BG.i, T_Panel.i, T_Text.i, T_Muted.i, T_Grid.i, T_Axis.i
Global T_Bar.i, T_Prob.i, T_Vis.i, T_Today.i, T_Accent.i
Global T_TooltipBg.i, T_TooltipBorder.i

Procedure ApplyTheme(dark.b)
  gDark = dark

  If gDark
    T_BG            = RGB(18, 18, 20)
    T_Panel         = RGB(28, 28, 32)
    T_Text          = RGB(235, 235, 245)
    T_Muted         = RGB(170, 170, 185)
    T_Grid          = RGB(55, 55, 62)
    T_Axis          = RGB(210, 210, 225)
    T_Bar           = RGB(90, 160, 255)
    T_Prob          = RGB(255, 170, 90)
    T_Vis           = RGB(60, 210, 150)
    T_Today         = RGB(255, 95, 95)
    T_Accent        = RGB(125, 190, 255)
    T_TooltipBg     = RGBA(34, 34, 40, 240)
    T_TooltipBorder = RGB(90, 90, 105)
  Else
    T_BG            = RGB(245, 246, 248)
    T_Panel         = RGB(255, 255, 255)
    T_Text          = RGB(30, 30, 35)
    T_Muted         = RGB(90, 95, 105)
    T_Grid          = RGB(225, 227, 232)
    T_Axis          = RGB(40, 40, 45)
    T_Bar           = RGB(80, 140, 210)
    T_Prob          = RGB(230, 150, 60)
    T_Vis           = RGB(40, 180, 120)
    T_Today         = RGB(200, 60, 60)
    T_Accent        = RGB(60, 120, 200)
    T_TooltipBg     = RGBA(255, 255, 255, 240)
    T_TooltipBorder = RGB(90, 90, 100)
  EndIf

  If IsGadget(#GadHeader)
    SetGadgetColor(#GadHeader, #PB_Gadget_BackColor, T_Panel)
    SetGadgetColor(#GadHeader, #PB_Gadget_FrontColor, T_Text)
  EndIf
  If IsGadget(#GadList)
    SetGadgetColor(#GadList, #PB_Gadget_BackColor, T_Panel)
    SetGadgetColor(#GadList, #PB_Gadget_FrontColor, T_Text)
  EndIf
  If IsGadget(#GadDetails)
    SetGadgetColor(#GadDetails, #PB_Gadget_BackColor, T_Panel)
    SetGadgetColor(#GadDetails, #PB_Gadget_FrontColor, T_Text)
  EndIf
EndProcedure

; ============================
; Hover/selection state
; ============================

Global HoverMX.i, HoverMY.i
Global HoverActive.b
Global HoverIndex.i = -1
Global SelectedIndex.i = -1

; ============================
; Config (INI)
; ============================

Global gUrl.s
Global gCookie.s
Global gConfigPath.s

Procedure LoadConfig()
  gConfigPath = GetCurrentDirectory() + "polarlicht.ini"

  ; defaults
  gUrl = "https://www.heute-am-himmel.de/polarlichter"
  gCookie = "locationOption=4; country=Deutschland; city=Freital; timeZone=Europe%2FBerlin; lat=51.00166; lon=13.64880; locationUpdateTimestamp=1768892714286"
  gDark = #False

  If OpenPreferences(gConfigPath)
    PreferenceGroup("app")
    gUrl = ReadPreferenceString("url", gUrl)
    gCookie = ReadPreferenceString("cookie", gCookie)
    gDark = Bool(ReadPreferenceLong("dark", 0))
    ClosePreferences()
  Else
    ; create file on first run
    If CreatePreferences(gConfigPath)
      PreferenceGroup("app")
      WritePreferenceString("url", gUrl)
      WritePreferenceString("cookie", gCookie)
      WritePreferenceLong("dark", 0)
      ClosePreferences()
    EndIf
  EndIf
EndProcedure

Procedure SaveConfig()
  If CreatePreferences(gConfigPath)
    PreferenceGroup("app")
    WritePreferenceString("url", gUrl)
    WritePreferenceString("cookie", gCookie)
    WritePreferenceLong("dark", gDark)
    ClosePreferences()
  EndIf
EndProcedure

; ============================
; Helpers: HTML decode + parsing
; ============================

Procedure.s HtmlDecodeBasic(s.s)
  s = ReplaceString(s, "&nbsp;", " ")
  s = ReplaceString(s, "&amp;",  "&")
  s = ReplaceString(s, "&quot;", #DQUOTE$)
  s = ReplaceString(s, "&lt;",   "<")
  s = ReplaceString(s, "&gt;",   ">")
  s = ReplaceString(s, "&deg;",  "°")
  s = ReplaceString(s, "&shy;",  "")
  s = ReplaceString(s, "&ndash;", "-")
  s = ReplaceString(s, "&mdash;", "-")
  s = ReplaceString(s, "&hellip;", "…")

  ; numerisch: &#176;
  Protected reDec = CreateRegularExpression(#PB_Any, "&#(\d+);")
  If reDec
    While ExamineRegularExpression(reDec, s) And NextRegularExpressionMatch(reDec)
      Protected ent.s  = RegularExpressionMatchString(reDec)
      Protected code.i = Val(RegularExpressionGroup(reDec, 1))
      s = ReplaceString(s, ent, Chr(code))
    Wend
    FreeRegularExpression(reDec)
  EndIf

  ; hex: &#xB0;
  Protected reHex = CreateRegularExpression(#PB_Any, "&#x([0-9A-Fa-f]+);")
  If reHex
    While ExamineRegularExpression(reHex, s) And NextRegularExpressionMatch(reHex)
      Protected ent2.s  = RegularExpressionMatchString(reHex)
      Protected code2.i = Val("$" + RegularExpressionGroup(reHex, 1))
      s = ReplaceString(s, ent2, Chr(code2))
    Wend
    FreeRegularExpression(reHex)
  EndIf

  ProcedureReturn s
EndProcedure

Procedure.s CellHtmlToText(cellHtml.s)
  Protected s.s = cellHtml

  ; Soft-Hyphen entfernen
  s = ReplaceString(s, Chr($AD), "")
  s = ReplaceString(s, Chr($A0), " ")

  ; HTML Entity Soft-Hyphen (falls als &shy; kommt)
  s = ReplaceString(s, "&shy;", "")

  s = HtmlDecodeBasic(s)

  ; Tags raus
  Protected reTags = CreateRegularExpression(#PB_Any, "(?is)<[^>]+>")
  If reTags
    s = ReplaceRegularExpression(reTags, s, " ")
    FreeRegularExpression(reTags)
  EndIf

  ; Whitespace normalisieren
  Protected reWS = CreateRegularExpression(#PB_Any, "(?is)\s+")
  If reWS
    s = ReplaceRegularExpression(reWS, s, " ")
    FreeRegularExpression(reWS)
  EndIf

  ProcedureReturn Trim(s)
EndProcedure

Procedure.s ExtractTargetTableHtml(html.s)
  ; Tabelle, die "Stärke des magnetischen Sturms" enthält (robust gegen Layout-Änderungen)
  Protected tableHtml.s = ""

  ; Primär: Text im Tabellenkopf
  Protected reTable = CreateRegularExpression(#PB_Any, "(?is)<table\b[^>]*>.*?Stärke\s+des\s+magnetischen\s+Sturms.*?</table>")
  If reTable
    If ExamineRegularExpression(reTable, html) And NextRegularExpressionMatch(reTable)
      tableHtml = RegularExpressionMatchString(reTable)
    EndIf
    FreeRegularExpression(reTable)
  EndIf

  ; Fallback: Anker-Link im Tabellenkopf
  If tableHtml = ""
    ; ✅ Variante C (Quotes elegant vermeiden, matcht ' oder " )
    Protected reTable2.i = CreateRegularExpression(#PB_Any, "(?is)<table\b[^>]*>.*?href\s*=\s*[" + #DQUOTE$ + "']#sturmstaerke-kp[" + #DQUOTE$ + "'].*?</table>")
    If reTable2
      If ExamineRegularExpression(reTable2, html) And NextRegularExpressionMatch(reTable2)
        tableHtml = RegularExpressionMatchString(reTable2)
      EndIf
      FreeRegularExpression(reTable2)
    EndIf
  EndIf

  ProcedureReturn tableHtml
EndProcedure


Procedure.s ParseRowsToDump(tableHtml.s)
  ; Ausgabe: pro Zeile "Datum\tSturm\tAb" (LF getrennt)
  Protected dump.s = ""
  Protected reTR = CreateRegularExpression(#PB_Any, "(?is)<tr\b[^>]*>.*?</tr>")
  Protected reTD = CreateRegularExpression(#PB_Any, "(?is)<td\b[^>]*>(.*?)</td\s*>")
  If reTR = 0 Or reTD = 0
    If reTR : FreeRegularExpression(reTR) : EndIf
    If reTD : FreeRegularExpression(reTD) : EndIf
    ProcedureReturn ""
  EndIf

  If ExamineRegularExpression(reTR, tableHtml)
    While NextRegularExpressionMatch(reTR)
      Protected trHtml.s = RegularExpressionMatchString(reTR)

      Protected tdCount.i = 0
      Protected c1.s, c2.s, c3.s

      If ExamineRegularExpression(reTD, trHtml)
        While NextRegularExpressionMatch(reTD)
          tdCount + 1
          Select tdCount
            Case 1 : c1 = CellHtmlToText(RegularExpressionGroup(reTD, 1))
            Case 2 : c2 = CellHtmlToText(RegularExpressionGroup(reTD, 1))
            Case 3 : c3 = CellHtmlToText(RegularExpressionGroup(reTD, 1))
          EndSelect
          If tdCount >= 3
            Break
          EndIf
        Wend
      EndIf

      If tdCount = 3
        If FindString(c1, "Datum", 1) = 0 And c1 <> ""
          If dump <> "" : dump + #LF$ : EndIf
          dump + c1 + #TAB$ + c2 + #TAB$ + c3
        EndIf
      EndIf
    Wend
  EndIf

  FreeRegularExpression(reTR)
  FreeRegularExpression(reTD)
  ProcedureReturn dump
EndProcedure

Procedure ParseDumpIntoRows(dump.s)
  ClearList(Rows())
  If dump = "" : ProcedureReturn : EndIf

  Protected i.i, line.s
  Protected count.i = CountString(dump, #LF$) + 1
  For i = 1 To count
    line = StringField(dump, i, #LF$)
    If line <> ""
      AddElement(Rows())
      Rows()\datum = StringField(line, 1, #TAB$)
      Rows()\sturm = StringField(line, 2, #TAB$)
      Rows()\ab    = StringField(line, 3, #TAB$)
    EndIf
  Next
EndProcedure

Procedure.s CSVQuote(s.s)
  s = ReplaceString(s, #DQUOTE$, #DQUOTE$ + #DQUOTE$)
  ProcedureReturn #DQUOTE$ + s + #DQUOTE$
EndProcedure

; ---------------- Helpers: KP / MLAT / Datum ----------------

Procedure.f ExtractFirstNumber(s.s)
  Protected re = CreateRegularExpression(#PB_Any, "(?i)(-?\d+(?:[\,\.]\d+)?)")
  If re = 0
    ProcedureReturn 0.0
  EndIf
  Protected v.f = 0.0
  If ExamineRegularExpression(re, s) And NextRegularExpressionMatch(re)
    Protected t.s = ReplaceString(RegularExpressionGroup(re, 1), ",", ".")
    v = ValF(t)
  EndIf
  FreeRegularExpression(re)
  ProcedureReturn v
EndProcedure

Procedure.f ExtractKPFromSturm(s.s)
  Protected kp.f = -1.0
  Protected re = CreateRegularExpression(#PB_Any, "(?i)\bkp\b\s*[:=]?\s*([0-9](?:[\,\.][0-9])?)")
  If re
    If ExamineRegularExpression(re, s) And NextRegularExpressionMatch(re)
      kp = ValF(ReplaceString(RegularExpressionGroup(re, 1), ",", "."))
    EndIf
    FreeRegularExpression(re)
  EndIf
  If kp < 0 : kp = ExtractFirstNumber(s) : EndIf
  If kp < 0 : kp = 0 : EndIf
  If kp > 9 : kp = 9 : EndIf
  ProcedureReturn kp
EndProcedure

Procedure.f ExtractMLATFromAb(s.s)
  Protected mlat.f = -1.0
  Protected re = CreateRegularExpression(#PB_Any, "(?i)\bmlat\b\s*[:=]?\s*(\d+(?:[\,\.]\d+)?)")
  If re
    If ExamineRegularExpression(re, s) And NextRegularExpressionMatch(re)
      mlat = ValF(ReplaceString(RegularExpressionGroup(re, 1), ",", "."))
    EndIf
    FreeRegularExpression(re)
  EndIf
  If mlat < 0 : mlat = ExtractFirstNumber(s) : EndIf
  If mlat < 0 : mlat = 0 : EndIf
  If mlat > 90 : mlat = 90 : EndIf
  ProcedureReturn mlat
EndProcedure

Procedure.q ExtractDateValue(datum.s)
  Protected re = CreateRegularExpression(#PB_Any, "(\d{1,2})\.(\d{1,2})(?:\.(\d{2,4}))?")
  If re = 0
    ProcedureReturn 0
  EndIf

  Protected d.i, m.i, y.i
  If ExamineRegularExpression(re, datum) And NextRegularExpressionMatch(re)
    d = Val(RegularExpressionGroup(re, 1))
    m = Val(RegularExpressionGroup(re, 2))
    Protected yStr.s = RegularExpressionGroup(re, 3)
    If yStr <> ""
      y = Val(yStr)
      If Len(yStr) = 2
        If y < 70 : y + 2000 : Else : y + 1900 : EndIf
      EndIf
    Else
      Protected now.q = Date()
      Protected curY.i = Year(now)
      Protected curM.i = Month(now)
      y = curY
      If m < curM - 6
        y = curY + 1
      ElseIf m > curM + 6
        y = curY - 1
      EndIf
    EndIf
  EndIf
  FreeRegularExpression(re)

  If d >= 1 And d <= 31 And m >= 1 And m <= 12 And y >= 1900
    ProcedureReturn Date(y, m, d, 0, 0, 0)
  EndIf
  ProcedureReturn 0
EndProcedure

Procedure PrepareComputedFields()
  Protected today0.q = Date(Year(Date()), Month(Date()), Day(Date()), 0, 0, 0)
  Protected todayShort.s = FormatDate("%dd.%mm.", Date())

  ForEach Rows()
    Rows()\kp        = ExtractKPFromSturm(Rows()\sturm)
    Rows()\mlat      = ExtractMLATFromAb(Rows()\ab)
    Rows()\dateValue = ExtractDateValue(Rows()\datum)

    Rows()\isToday = #False
    If Rows()\dateValue <> 0 And Rows()\dateValue = today0
      Rows()\isToday = #True
    ElseIf FindString(Rows()\datum, todayShort, 1)
      Rows()\isToday = #True
    EndIf

    Rows()\probable  = Bool(Rows()\kp > 7.0)
    Rows()\visible   = Bool(Rows()\kp > 7.0 And Rows()\mlat > 0.0 And Rows()\mlat < 48.0)
  Next
EndProcedure

; ============================
; UI helpers
; ============================

Global gLastUpdate.q
Global gLoading.b
Global gStatusMsg.s

Procedure UpdateSummary()
  Protected maxKP.f = -1
  Protected minMLAT.f = 999
  Protected todayKP.s = "–"
  Protected todayMLAT.s = "–"
  Protected todayTag.s = ""

  ForEach Rows()
    If Rows()\kp > maxKP : maxKP = Rows()\kp : EndIf
    If Rows()\mlat > 0 And Rows()\mlat < minMLAT : minMLAT = Rows()\mlat : EndIf
    If Rows()\isToday
      todayKP = StrF(Rows()\kp, 1)
      todayMLAT = StrF(Rows()\mlat, 1)
      If Rows()\visible
        todayTag = "  •  Sichtbar ✨"
      ElseIf Rows()\probable
        todayTag = "  •  Wahrscheinlich ⚡"
      Else
        todayTag = "  •  Ruhig"
      EndIf
    EndIf
  Next
  If maxKP < 0 : maxKP = 0 : EndIf
  If minMLAT = 999 : minMLAT = 0 : EndIf

  Protected upd.s
  If gLastUpdate
    upd = FormatDate("%dd.%mm.%yyyy %hh:%ii", gLastUpdate)
  Else
    upd = "–"
  EndIf

  Protected s.s
  s = "Heute: KP " + todayKP + "  •  MLAT " + todayMLAT + todayTag + #LF$
  s + "Max KP (Tabelle): " + StrF(maxKP, 1) + "  •  Min MLAT (Tabelle): " + StrF(minMLAT, 1) + "  •  Update: " + upd
  SetGadgetText(#GadLblSummary, s)
EndProcedure

Procedure FillListGadget()
  ClearGadgetItems(#GadList)
  Protected i.i = 0

  ForEach Rows()
    Protected dateDisp.s = Rows()\datum
    If Rows()\isToday
      dateDisp = "★ " + dateDisp
    EndIf
    AddGadgetItem(#GadList, -1, dateDisp + #LF$ + StrF(Rows()\kp, 1) + #LF$ + StrF(Rows()\mlat, 1))

    ; Einfärbung je nach Status
    Protected back.i
    Protected front.i = T_Text

    If gDark
      back = T_Panel
      If Rows()\visible
        back = RGB(20, 56, 44)
      ElseIf Rows()\probable
        back = RGB(66, 46, 18)
      EndIf
      If Rows()\isToday
        ; leichtes Highlight
        If Rows()\visible
          back = RGB(24, 70, 56)
        ElseIf Rows()\probable
          back = RGB(78, 54, 22)
        Else
          back = RGB(36, 40, 54)
        EndIf
      EndIf
    Else
      back = RGB(255, 255, 255)
      If Rows()\visible
        back = RGB(175, 255, 220)
      ElseIf Rows()\probable
        back = RGB(255, 235, 175)
      EndIf
      If Rows()\isToday
        If Rows()\visible
          back = RGB(140, 255, 215)
        ElseIf Rows()\probable
          back = RGB(255, 225, 160)
        Else
          back = RGB(210, 230, 255)
        EndIf
      EndIf
    EndIf

    SetGadgetItemColor(#GadList, i, #PB_Gadget_BackColor, back)
    SetGadgetItemColor(#GadList, i, #PB_Gadget_FrontColor, front)
    i + 1
  Next
EndProcedure

Procedure UpdateDetailsFromSelection()
  If SelectedIndex < 0
    SetGadgetText(#GadDetails, "")
    ProcedureReturn
  EndIf
  If SelectElement(Rows(), SelectedIndex) = 0
    SetGadgetText(#GadDetails, "")
    ProcedureReturn
  EndIf

  Protected s.s
  s = "Datum: " + Rows()\datum + #LF$
  s + "KP: " + StrF(Rows()\kp, 1) + #LF$
  s + "MLAT: " + StrF(Rows()\mlat, 1) + #LF$
  s + #LF$ + "Sturm: " + Rows()\sturm + #LF$
  s + "Ab: " + Rows()\ab + #LF$

  If Rows()\visible
    s + #LF$ + "Status: Sichtbar (KP > 7 & MLAT < 48)"
  ElseIf Rows()\probable
    s + #LF$ + "Status: Wahrscheinlich (KP > 7)"
  Else
    s + #LF$ + "Status: Ruhig"
  EndIf

  SetGadgetText(#GadDetails, s)
EndProcedure

; ============================
; Chart
; ============================

Declare DrawChart()
Declare DrawHoverOverlay(canvasW.i, canvasH.i, plotX.i, plotY.i, plotW.i, plotH.i, stepX.f, n.i, barW.i, mlatMin.f, mlatMax.f)

Procedure.i GetIndexFromCanvasX(plotX.i, plotW.i, n.i, mouseX.i)
  If n <= 0
    ProcedureReturn -1
  EndIf
  If n = 1
    ProcedureReturn 0
  EndIf

  Protected stepX.f = plotW / (n - 1.0)
  If stepX <= 0
    ProcedureReturn 0
  EndIf
  Protected pos.f = (mouseX - plotX) / stepX
  Protected idx.i = Round(pos, #PB_Round_Nearest)
  If idx < 0 : idx = 0 : EndIf
  If idx > n - 1 : idx = n - 1 : EndIf
  ProcedureReturn idx
EndProcedure

Procedure DrawLoadingOverlay(w.i, h.i)
  DrawingMode(#PB_2DDrawing_AlphaBlend)
  Box(0, 0, w, h, RGBA(Red(T_BG), Green(T_BG), Blue(T_BG), 190))
  DrawingMode(#PB_2DDrawing_Transparent)
  DrawingFont(FontID(#FontUIBold))
  Protected msg.s = "Lade Daten …"
  DrawText((w - TextWidth(msg)) / 2, (h - TextHeight(msg)) / 2 - 10, msg, T_Text)
  DrawingFont(FontID(#FontUI))
  msg = "Quelle: heute-am-himmel.de"
  DrawText((w - TextWidth(msg)) / 2, (h - TextHeight(msg)) / 2 + 16, msg, T_Muted)
EndProcedure

Procedure DrawChart()
  If IsGadget(#GadCanvas) = 0
    ProcedureReturn
  EndIf

  Protected w.i = GadgetWidth(#GadCanvas)
  Protected h.i = GadgetHeight(#GadCanvas)
  If w < 50 Or h < 50
    ProcedureReturn
  EndIf

  Protected n.i = ListSize(Rows())

  StartDrawing(CanvasOutput(#GadCanvas))
  Box(0, 0, w, h, T_BG)

  If n <= 0
    DrawingMode(#PB_2DDrawing_Transparent)
    DrawingFont(FontID(#FontUIBold))
    DrawText(14, 12, "Keine Daten.", T_Text)
    DrawingFont(FontID(#FontUI))
    DrawText(14, 36, "Tipp: In Einstellungen URL/Cookie prüfen.", T_Muted)
    If gLoading : DrawLoadingOverlay(w, h) : EndIf
    StopDrawing()
    ProcedureReturn
  EndIf

  ; Plot-Rahmen
  Protected mL.i = 58
  Protected mR.i = 58
  Protected mT.i = 38
  Protected mB.i = 62
  Protected plotX.i = mL
  Protected plotY.i = mT
  Protected plotW.i = w - mL - mR
  Protected plotH.i = h - mT - mB

  ; MLAT Min/Max
  Protected mlatMin.f = 9999.0
  Protected mlatMax.f = -9999.0
  ForEach Rows()
    If Rows()\mlat > 0
      If Rows()\mlat < mlatMin : mlatMin = Rows()\mlat : EndIf
      If Rows()\mlat > mlatMax : mlatMax = Rows()\mlat : EndIf
    EndIf
  Next
  If mlatMin = 9999.0
    mlatMin = 40.0 : mlatMax = 60.0
  EndIf
  If mlatMax - mlatMin < 5.0
    mlatMin - 2.5
    mlatMax + 2.5
  Else
    mlatMin - 1.5
    mlatMax + 1.5
  EndIf
  If mlatMin < 0 : mlatMin = 0 : EndIf
  If mlatMax > 90 : mlatMax = 90 : EndIf

  ; Hintergrund Plot
  Box(plotX, plotY, plotW, plotH, T_Panel)

  ; Grid (KP 0..9)
  Protected k.i, yy.i
  For k = 0 To 9
    yy = plotY + plotH - Int(k / 9.0 * plotH)
    Line(plotX, yy, plotW, 1, T_Grid)
  Next

  ; Schwellen
  Protected yKP7.i = plotY + plotH - Int(7.0 / 9.0 * plotH)
  Line(plotX, yKP7, plotW, 1, RGBA(Red(T_Prob), Green(T_Prob), Blue(T_Prob), 140))
  DrawingMode(#PB_2DDrawing_Transparent)
  DrawText(plotX + plotW - 58, yKP7 - 16, "KP 7", T_Muted)

  If 48.0 >= mlatMin And 48.0 <= mlatMax
    Protected y48.i = plotY + plotH - Int((48.0 - mlatMin) / (mlatMax - mlatMin) * plotH)
    Line(plotX, y48, plotW, 1, RGBA(Red(T_Axis), Green(T_Axis), Blue(T_Axis), 80))
    DrawText(plotX + plotW - 68, y48 - 16, "MLAT 48", T_Muted)
  EndIf

  ; Achsen
  Line(plotX, plotY, 1, plotH, T_Axis)
  Line(plotX, plotY + plotH, plotW, 1, T_Axis)
  Line(plotX + plotW, plotY, 1, plotH, T_Axis)

  ; Labels links (KP)
  DrawingMode(#PB_2DDrawing_Transparent)
  DrawingFont(FontID(#FontUI))
  For k = 0 To 9
    yy = plotY + plotH - Int(k / 9.0 * plotH)
    DrawText(10, yy - 7, Str(k), T_Muted)
  Next

  ; Labels rechts (MLAT)
  Protected t.i
  For t = 0 To 4
    Protected v.f = mlatMin + (mlatMax - mlatMin) * (t / 4.0)
    yy = plotY + plotH - Int((v - mlatMin) / (mlatMax - mlatMin) * plotH)
    DrawText(plotX + plotW + 8, yy - 7, StrF(v, 0), T_Muted)
  Next

  ; Titel/Legende
  DrawingFont(FontID(#FontUIBold))
  DrawText(plotX, 10, "KP (Balken) / MLAT (Linie)", T_Text)
  DrawingFont(FontID(#FontUI))
  DrawText(plotX + 220, 12, "• Grün: sichtbar  • Orange: wahrscheinlich", T_Muted)

  ; X-Skalierung
  Protected stepX.f
  If n <= 1
    stepX = 0
  Else
    stepX = plotW / (n - 1.0)
  EndIf
  Protected barW.i
  If n <= 1
    barW = 18
  Else
    barW = Int(stepX * 0.62)
    If barW < 4 : barW = 4 : EndIf
    If barW > 24 : barW = 24 : EndIf
  EndIf

  ; MLAT Linie
  Protected i.i = 0
  Protected prevX.i = -1
  Protected prevY.i = -1

  ForEach Rows()
    Protected x.i
    If n <= 1
      x = plotX + plotW / 2
    Else
      x = plotX + Int(i * stepX)
    EndIf

    ; Today marker
    If Rows()\isToday
      Line(x, plotY, 1, plotH, T_Today)
    EndIf

    ; Selected marker
    If i = SelectedIndex
      DrawingMode(#PB_2DDrawing_AlphaBlend)
      Box(x - barW, plotY, barW * 2, plotH, RGBA(Red(T_Accent), Green(T_Accent), Blue(T_Accent), 40))
    EndIf

    ; KP bar
    Protected kp.f = Rows()\kp
    If kp < 0 : kp = 0 : EndIf
    If kp > 9 : kp = 9 : EndIf
    Protected barH.i = Int(kp / 9.0 * plotH)
    Protected yBar.i = plotY + plotH - barH
    Protected colKP.i = T_Bar
    If Rows()\visible
      colKP = T_Vis
    ElseIf Rows()\probable
      colKP = T_Prob
    EndIf

    DrawingMode(#PB_2DDrawing_Default)
    Box(x - barW / 2, yBar, barW, barH, colKP)

    ; subtle outline
    DrawingMode(#PB_2DDrawing_AlphaBlend)
    Box(x - barW / 2, yBar, barW, barH, RGBA(255, 255, 255, 35))

    ; MLAT point + line
    If Rows()\mlat > 0
      Protected yLine.i = plotY + plotH - Int((Rows()\mlat - mlatMin) / (mlatMax - mlatMin) * plotH)
      DrawingMode(#PB_2DDrawing_Default)
      If prevX >= 0
        LineXY(prevX, prevY, x, yLine, T_Axis)
      EndIf
      Box(x - 2, yLine - 2, 5, 5, T_Axis)
      prevX = x
      prevY = yLine
    EndIf

    i + 1
  Next

  ; X Labels (ausdünnen)
  DrawingMode(#PB_2DDrawing_Transparent)
  Protected every.i = 1
  If n > 12
    every = Int(n / 12)
    If every < 1 : every = 1 : EndIf
  EndIf
  i = 0
  ForEach Rows()
    If (i % every) = 0 Or Rows()\isToday Or i = SelectedIndex
      If n <= 1
        x = plotX + plotW / 2
      Else
        x = plotX + Int(i * stepX)
      EndIf
      Protected lab.s = Rows()\datum
      Protected reShort = CreateRegularExpression(#PB_Any, "(\d{1,2}\.\d{1,2}\.)")
      If reShort
        If ExamineRegularExpression(reShort, lab) And NextRegularExpressionMatch(reShort)
          lab = RegularExpressionGroup(reShort, 1)
        EndIf
        FreeRegularExpression(reShort)
      EndIf
      Protected colLab.i = T_Muted
      If i = SelectedIndex
        colLab = T_Text
      EndIf
      DrawText(x - 18, plotY + plotH + 8, lab, colLab)
    EndIf
    i + 1
  Next

  ; Hover tooltip
  DrawHoverOverlay(w, h, plotX, plotY, plotW, plotH, stepX, n, barW, mlatMin, mlatMax)

  If gLoading
    DrawLoadingOverlay(w, h)
  EndIf

  StopDrawing()
EndProcedure

Procedure DrawHoverOverlay(canvasW.i, canvasH.i, plotX.i, plotY.i, plotW.i, plotH.i, stepX.f, n.i, barW.i, mlatMin.f, mlatMax.f)
  If HoverActive = #False
    ProcedureReturn
  EndIf
  If n <= 0
    HoverIndex = -1
    ProcedureReturn
  EndIf
  If HoverMX < plotX Or HoverMX > plotX + plotW Or HoverMY < plotY Or HoverMY > plotY + plotH + 44
    HoverIndex = -1
    ProcedureReturn
  EndIf

  Protected idx.i
  If n <= 1 Or stepX <= 0.0
    idx = 0
  Else
    Protected pos.f = (HoverMX - plotX) / stepX
    idx = Round(pos, #PB_Round_Nearest)
    If idx < 0 : idx = 0 : EndIf
    If idx > n - 1 : idx = n - 1 : EndIf
  EndIf
  HoverIndex = idx
  If SelectElement(Rows(), idx) = 0
    ProcedureReturn
  EndIf

  Protected x.i
  If n <= 1
    x = plotX + plotW / 2
  Else
    x = plotX + Int(idx * stepX)
  EndIf

  ; Marker line
  DrawingMode(#PB_2DDrawing_Default)
  Line(x, plotY, 1, plotH, RGBA(Red(T_Axis), Green(T_Axis), Blue(T_Axis), 130))

  ; Bar outline
  Protected kp.f = Rows()\kp
  If kp < 0 : kp = 0 : EndIf
  If kp > 9 : kp = 9 : EndIf
  Protected barH.i = Int(kp / 9.0 * plotH)
  Protected yBar.i = plotY + plotH - barH
  DrawingMode(#PB_2DDrawing_Outlined)
  Box(x - barW / 2, yBar, barW, barH, T_Axis)

  ; Tooltip content
  DrawingMode(#PB_2DDrawing_Transparent)
  DrawingFont(FontID(#FontUI))
  Protected t1.s = "Datum: " + Rows()\datum
  Protected t2.s = "KP: " + StrF(Rows()\kp, 1)
  Protected t3.s = "MLAT: " + StrF(Rows()\mlat, 1)
  Protected t4.s
  If Rows()\visible
    t4 = "Status: Sichtbar ✨"
  ElseIf Rows()\probable
    t4 = "Status: Wahrscheinlich ⚡"
  Else
    t4 = "Status: Ruhig"
  EndIf

  Protected pad.i = 8
  Protected lineH.i = TextHeight("Ay") + 2
  Protected tw.i = TextWidth(t1)
  Protected tmp.i = TextWidth(t2) : If tmp > tw : tw = tmp : EndIf
  tmp = TextWidth(t3) : If tmp > tw : tw = tmp : EndIf
  tmp = TextWidth(t4) : If tmp > tw : tw = tmp : EndIf

  Protected bw.i = tw + pad * 2
  Protected bh.i = lineH * 4 + pad * 2
  Protected bx.i = HoverMX + 16
  Protected by.i = HoverMY + 16
  If bx + bw > canvasW : bx = HoverMX - bw - 16 : EndIf
  If by + bh > canvasH : by = HoverMY - bh - 16 : EndIf
  If bx < 2 : bx = 2 : EndIf
  If by < 2 : by = 2 : EndIf

  ; Tooltip background (status-tinted)
  Protected bg.i = T_TooltipBg
  If Rows()\visible
    If gDark
      bg = RGBA(18, 70, 56, 240)
    Else
      bg = RGBA(140, 255, 215, 240)
    EndIf
  ElseIf Rows()\probable
    If gDark
      bg = RGBA(78, 54, 22, 240)
    Else
      bg = RGBA(255, 225, 160, 240)
    EndIf
  ElseIf Rows()\isToday
    If gDark
      bg = RGBA(36, 40, 54, 240)
    Else
      bg = RGBA(210, 230, 255, 240)
    EndIf
  EndIf

  DrawingMode(#PB_2DDrawing_AlphaBlend)
  Box(bx, by, bw, bh, bg)
  DrawingMode(#PB_2DDrawing_Outlined)
  Box(bx, by, bw, bh, T_TooltipBorder)

  DrawingMode(#PB_2DDrawing_Transparent)
  DrawText(bx + pad, by + pad + 0 * lineH, t1, T_Text)
  DrawText(bx + pad, by + pad + 1 * lineH, t2, T_Text)
  DrawText(bx + pad, by + pad + 2 * lineH, t3, T_Text)
  DrawText(bx + pad, by + pad + 3 * lineH, t4, T_Text)
EndProcedure

; ============================
; Export
; ============================

Procedure ExportCSVInteractive()
  If ListSize(Rows()) <= 0
    MessageRequester("Polarlicht", "Keine Daten zum Export.")
    ProcedureReturn
  EndIf

  Protected suggested.s = GetCurrentDirectory() + "polarlichter.csv"
  Protected outFile.s = SaveFileRequester("CSV speichern", suggested, "CSV (*.csv)|*.csv|Alle Dateien (*.*)|*.*", 0)
  If outFile = "" : ProcedureReturn : EndIf

  If CreateFile(0, outFile)
    WriteStringN(0, "Datum;KP;MLAT;Stärke des magnetischen Sturms;Polarlichter möglich ab")
    ForEach Rows()
      WriteStringN(0, CSVQuote(Rows()\datum) + ";" + CSVQuote(StrF(Rows()\kp, 1)) + ";" + CSVQuote(StrF(Rows()\mlat, 1)) + ";" + CSVQuote(Rows()\sturm) + ";" + CSVQuote(Rows()\ab))
    Next
    CloseFile(0)
    MessageRequester("Polarlicht", "Export OK:" + #LF$ + outFile)
  Else
    MessageRequester("Polarlicht", "Konnte Datei nicht schreiben:" + #LF$ + outFile)
  EndIf
EndProcedure

; ============================
; Fetch (Thread)
; ============================

Global gMutex.i
Global gRowsDump.s
Global gFetchError.s

Procedure.s FetchHtml(url.s, cookie.s)
  NewMap H.s()
  H("User-Agent")      = "Mozilla/5.0"
  H("Accept-Language") = "de-DE,de;q=0.9"
  H("Accept-Encoding") = "identity"
  If cookie <> ""
    H("Cookie") = cookie
  EndIf

  Protected req.i = HTTPRequest(#PB_HTTP_Get, url, "", 0, H())
  If req = 0
    ProcedureReturn ""
  EndIf
  Protected status.s = HTTPInfo(req, #PB_HTTP_StatusCode)
  Protected html.s   = HTTPInfo(req, #PB_HTTP_Response)
  FinishHTTP(req)

  If status <> "200" Or html = ""
    ProcedureReturn ""
  EndIf
  ProcedureReturn html
EndProcedure

Procedure FetchThread(*dummy)
  Protected html.s = FetchHtml(gUrl, gCookie)
  Protected dump.s = ""
  Protected err.s  = ""

  If html = ""
    err = "Keine HTTP Antwortdaten. (URL/Cookie prüfen)"
  Else
    Protected tableHtml.s = ExtractTargetTableHtml(html)
    If tableHtml = ""
      ; Fallback: nochmal ohne Cookie probieren
      Protected html2.s = FetchHtml(gUrl, "")
      If html2 <> ""
        tableHtml = ExtractTargetTableHtml(html2)
      EndIf
    EndIf

    If tableHtml = ""
      err = "Zieltabelle nicht gefunden (Text 'Stärke des magnetischen Sturms' fehlt?)."
    Else
      dump = ParseRowsToDump(tableHtml)
      If dump = ""
        err = "Keine Datenzeilen gefunden."
      EndIf
    EndIf
  EndIf

  LockMutex(gMutex)
  gRowsDump = dump
  gFetchError = err
  UnlockMutex(gMutex)

  PostEvent(#Event_DataReady, #WinMain, 0, 0)
EndProcedure

Procedure StartFetch()
  gLoading = #True
  gStatusMsg = "Lade…"
  DisableGadget(#GadBtnRefresh, #True)
  DisableGadget(#GadBtnExport, #True)
  DrawChart()
  CreateThread(@FetchThread(), 0)
EndProcedure

; ============================
; Settings dialog
; ============================

Procedure OpenSettingsDialog()
  If IsWindow(#WinSettings)
    SetActiveWindow(#WinSettings)
    ProcedureReturn
  EndIf

  Protected w.i = 760
  Protected h.i = 420
  If OpenWindow(#WinSettings, 0, 0, w, h, "Einstellungen", #PB_Window_SystemMenu | #PB_Window_ScreenCentered, WindowID(#WinMain))
    StickyWindow(#WinSettings, #True)
    SetWindowColor(#WinSettings, T_BG)

    Protected gadLblUrl.i = TextGadget(#PB_Any, 14, 16, 90, 20, "URL:")
    SetGadgetColor(gadLblUrl, #PB_Gadget_FrontColor, T_Text)
    SetGadgetColor(gadLblUrl, #PB_Gadget_BackColor, T_BG)

    Protected gadUrl.i = StringGadget(#PB_Any, 14, 38, w - 28, 26, gUrl)
    SetGadgetColor(gadUrl, #PB_Gadget_FrontColor, T_Text)
    SetGadgetColor(gadUrl, #PB_Gadget_BackColor, T_Panel)

    Protected gadLblCookie.i = TextGadget(#PB_Any, 14, 78, 180, 20, "Cookie (Location):")
    SetGadgetColor(gadLblCookie, #PB_Gadget_FrontColor, T_Text)
    SetGadgetColor(gadLblCookie, #PB_Gadget_BackColor, T_BG)

    Protected gadCookie.i = EditorGadget(#PB_Any, 14, 100, w - 28, 230)
    SetGadgetColor(gadCookie, #PB_Gadget_FrontColor, T_Text)
    SetGadgetColor(gadCookie, #PB_Gadget_BackColor, T_Panel)
    SetGadgetText(gadCookie, gCookie)

    Protected gadTip.i = TextGadget(#PB_Any, 14, 340, w - 28, 20, "Tipp: Wenn keine Daten kommen, Cookie/Location von der Website neu übernehmen.")
    SetGadgetColor(gadTip, #PB_Gadget_FrontColor, T_Muted)
    SetGadgetColor(gadTip, #PB_Gadget_BackColor, T_BG)

    Protected gadCancel.i = ButtonGadget(#PB_Any, w - 220, h - 50, 96, 30, "Abbrechen")
    Protected gadSave.i   = ButtonGadget(#PB_Any, w - 116, h - 50, 102, 30, "Speichern")

    Repeat
      Select WaitWindowEvent()
        Case #PB_Event_CloseWindow
          Break

        Case #PB_Event_Gadget
          Select EventGadget()
            Case gadCancel
              Break

            Case gadSave
              gUrl = GetGadgetText(gadUrl)
              gCookie = GetGadgetText(gadCookie)
              SaveConfig()
              Break
          EndSelect
      EndSelect
    ForEver

    CloseWindow(#WinSettings)
  EndIf
EndProcedure

; ============================
; Context menu
; ============================

Procedure InitContextMenu()
  If CreatePopupMenu(#MenuList)
    MenuItem(#MiCopyRow, "Zeile kopieren")
  EndIf
EndProcedure

Procedure CopySelectedRowToClipboard()
  If SelectedIndex < 0 : ProcedureReturn : EndIf
  If SelectElement(Rows(), SelectedIndex) = 0 : ProcedureReturn : EndIf
  Protected s.s
  s = Rows()\datum + #TAB$ + "KP " + StrF(Rows()\kp, 1) + #TAB$ + "MLAT " + StrF(Rows()\mlat, 1) + #TAB$ + Rows()\sturm + #TAB$ + Rows()\ab
  SetClipboardText(s)
EndProcedure

; ============================
; Layout
; ============================

Procedure ResizeUI()
  Protected ww.i = WindowWidth(#WinMain)
  Protected wh.i = WindowHeight(#WinMain)

  Protected headerW.i = ww - 20
  ResizeGadget(#GadHeader, 10, 10, headerW, 72)

  ; Header-Child-Gadgets: Koordinaten RELATIV zum Container
  Protected bx.i = headerW - 340
  ResizeGadget(#GadBtnRefresh, bx, 12, 118, 28)
  ResizeGadget(#GadBtnExport,  bx + 126, 12, 92, 28)
  ResizeGadget(#GadBtnSettings,bx + 226, 12, 114, 28)
  ResizeGadget(#GadChkDark,    bx, 42, 140, 22)
  ResizeGadget(#GadLblSummary, 12, 10, headerW - 380, 52)

  ResizeGadget(#GadSplitter, 10, 92, ww - 20, wh - 92 - 10)
EndProcedure

; ============================
; Main
; ============================

; Fonts
LoadFont(#FontUI,     "Segoe UI", 10)
LoadFont(#FontUIBold, "Segoe UI Semibold", 10)
LoadFont(#FontMono,   "Consolas", 10)

LoadConfig()
gMutex = CreateMutex()

If OpenWindow(#WinMain, 0, 0, 1100, 780, "Polarlicht Dashboard", #PB_Window_SystemMenu | #PB_Window_SizeGadget | #PB_Window_ScreenCentered)

  ; Header panel
  ContainerGadget(#GadHeader, 10, 10, 1080, 72, #PB_Container_Flat)
    TextGadget(#GadLblSummary, 12, 10, 720, 52, "")
    SetGadgetFont(#GadLblSummary, FontID(#FontUI))

    ButtonGadget(#GadBtnRefresh, 740, 12, 118, 28, "⟳ Aktualisieren")
    ButtonGadget(#GadBtnExport,  866, 12, 92, 28, "⇩ CSV")
    ButtonGadget(#GadBtnSettings,964, 12, 114, 28, "⚙ Setup")
    CheckBoxGadget(#GadChkDark,  740, 42, 140, 22, "Dark Mode")
  CloseGadgetList()

  ; Main split
  ListIconGadget(#GadList, 0, 0, 100, 100, "Datum", 270, #PB_ListIcon_FullRowSelect | #PB_ListIcon_AlwaysShowSelection)
  AddGadgetColumn(#GadList, 1, "KP",   70)
  AddGadgetColumn(#GadList, 2, "MLAT", 80)
  SetGadgetFont(#GadList, FontID(#FontUI))

  CanvasGadget(#GadCanvas, 0, 0, 100, 100, #PB_Canvas_Keyboard)
  EditorGadget(#GadDetails, 0, 0, 100, 100, #PB_Editor_ReadOnly)
  SetGadgetFont(#GadDetails, FontID(#FontMono))

  ; Splitter: top list / bottom chart+details via nested container
  ; We'll put chart+details into a vertical splitter for a "modern" inspector feel.
  Define splitBottom.i = SplitterGadget(#PB_Any, 0, 0, 100, 100, #GadCanvas, #GadDetails, #PB_Splitter_Vertical)
  SetGadgetState(splitBottom, 760) ; chart width

  SplitterGadget(#GadSplitter, 10, 92, 1080, 678, #GadList, splitBottom)
  SetGadgetState(#GadSplitter, 260) ; list height

  InitContextMenu()

  SetGadgetState(#GadChkDark, gDark)
  ApplyTheme(gDark)
  SetWindowColor(#WinMain, T_BG)

  ResizeUI()

  ; Kick off initial fetch
  StartFetch()

  Repeat
    Select WaitWindowEvent()

      Case #Event_DataReady
        LockMutex(gMutex)
        Define dump.s = gRowsDump
        Define  err.s  = gFetchError
        UnlockMutex(gMutex)

        gLoading = #False
        DisableGadget(#GadBtnRefresh, #False)
        DisableGadget(#GadBtnExport, #False)

        If err <> ""
          ClearList(Rows())
          SelectedIndex = -1
          SetGadgetText(#GadDetails, "")
          MessageRequester("Polarlicht", err)
        Else
          ParseDumpIntoRows(dump)
          PrepareComputedFields()
          gLastUpdate = Date()
          FillListGadget()
          UpdateSummary()
          ; auto-select today if found
          SelectedIndex = -1
          Define  idx.i = 0
          ForEach Rows()
            If Rows()\isToday
              SelectedIndex = idx
              Break
            EndIf
            idx + 1
          Next
          If SelectedIndex >= 0
            SetGadgetState(#GadList, SelectedIndex)
            UpdateDetailsFromSelection()
          EndIf
        EndIf
        DrawChart()

      Case #PB_Event_Gadget
        Select EventGadget()

          Case #GadBtnRefresh
            StartFetch()

          Case #GadBtnExport
            ExportCSVInteractive()

          Case #GadBtnSettings
            OpenSettingsDialog()

          Case #GadChkDark
            ApplyTheme(Bool(GetGadgetState(#GadChkDark)))
            SaveConfig()
            SetWindowColor(#WinMain, T_BG)
            FillListGadget()
            DrawChart()

          Case #GadList
            Select EventType()
              Case #PB_EventType_Change
                SelectedIndex = GetGadgetState(#GadList)
                UpdateDetailsFromSelection()
                DrawChart()

              Case #PB_EventType_RightClick
                DisplayPopupMenu(#MenuList, WindowID(#WinMain))
            EndSelect

          Case #GadCanvas
            Select EventType()
              Case #PB_EventType_MouseMove
                HoverMX = GetGadgetAttribute(#GadCanvas, #PB_Canvas_MouseX)
                HoverMY = GetGadgetAttribute(#GadCanvas, #PB_Canvas_MouseY)
                HoverActive = #True
                DrawChart()

              Case #PB_EventType_MouseLeave
                HoverActive = #False
                HoverIndex = -1
                DrawChart()

              Case #PB_EventType_LeftButtonDown
                ; Click selects closest index
                HoverMX = GetGadgetAttribute(#GadCanvas, #PB_Canvas_MouseX)
                HoverMY = GetGadgetAttribute(#GadCanvas, #PB_Canvas_MouseY)
                HoverActive = #True

                Define  wC.i = GadgetWidth(#GadCanvas)
                Define  nC.i = ListSize(Rows())
                If nC.i > 0
                  ; replicate plot margins
                  Define  mL2.i = 58, mR2.i = 58
                  Define  plotX2.i = mL2
                  Define  plotW2.i = wC - mL2 - mR2
                  Define  idxC.i = GetIndexFromCanvasX(plotX2, plotW2, nC.i, HoverMX)
                  If idxC >= 0
                    SelectedIndex = idxC
                    SetGadgetState(#GadList, SelectedIndex)
                    UpdateDetailsFromSelection()
                    DrawChart()
                  EndIf
                EndIf
            EndSelect
        EndSelect

      Case #PB_Event_Menu
        Select EventMenu()
          Case #MiCopyRow
            CopySelectedRowToClipboard()
        EndSelect

      Case #PB_Event_SizeWindow
        ResizeUI()
        DrawChart()

      Case #PB_Event_CloseWindow
        Break

    EndSelect
  ForEver

EndIf