Machine-Learning basierte Überwachung von Fischbeständen des Rheins

Mithilfe moderner Machine-Learning-Methoden werden Fische im Rhein automatisch erkannt und gezählt. Anschließend fließen die erfassten Daten in ein interaktives Visualisierungstool ein, um Veränderungen im Bestand frühzeitig zu erkennen und das ökologische Gleichgewicht langfristig zu bewahren. Weiter unten auf dieser Seite wird der gesamte Prozess – von der Datenerfassung bis zur finalen Darstellung – detailliert aufgezeigt.

Motivation & Hintergrund

Die persönliche Begeisterung für das Angeln und der Wunsch, die Artenvielfalt langfristig zu bewahren, bilden den Ausgangspunkt dieses Projekts. Gerade weil das Angeln eine große Faszination ausübt, ist es unerlässlich, für ein ökologisches Gleichgewicht in unseren Gewässern zu sorgen. Nur so bleibt das Erlebnis des nachhaltigen Angelns auch in Zukunft möglich.

Die derzeitige Praxis zur Erfassung von Fischbeständen – beispielsweise das Betäuben der Fische mit elektrischem Strom – sehe ich als kritisch an, da sie mit einem gewissen Stress für die Tiere verbunden ist. Um einen schonenderen und gleichzeitig effizienteren Ansatz zu verfolgen, wird in diesem Projekt auf moderne Machine-Learning-Methoden gesetzt. So können Fischbestände überwacht werden, ohne die Tiere direkt zu beeinträchtigen, und ein wertvoller Beitrag zum Erhalt der Artenvielfalt wird geleistet.

Technische Umsetzung

1. Machine Learning & Modelltraining

Die Auswahl der geeigneten Bilddaten erfolgten über die Videoaufnahmen, die WFBW online gestellt hat.

Nach der Auswahl der Daten erfolgt die Annotationmit CVAT. Dabei werden Begrenzungsrahmen (Bounding Boxes) um die Fische gezogen und deren Art angegeben, sodass das Machine-Learning-Tool erkennt, um welche Fischart es sich handelt.

Das Training des Modells erfolgt mit YOLO11, einem von Ultralytics entwickelten Machine-Learning-Modell, das sich besonders für die Objekterkennung in Videos eignet. Seine hohe Geschwindigkeit und Genauigkeit ermöglichen eine effiziente und zuverlässige Identifizierung von Fischen auch in Echtzeit.

2. Modell-Implementierung

Im aktuellen Setup läuft der Code in Python und verwendet verschiedene Bibliotheken, um eine lückenlose Datenverarbeitung zu gewährleisten. Für die Bildverarbeitung und das Frame-by-Frame-Auslesen der Live-Kamera von der WFBW kommt die OpenCV-Bibliothek (import cv2) zum Einsatz. Das Modul datetime fügt jedem erkannten Fisch einen Zeitstempel hinzu, und mithilfe von defaultdict sowie deque (aus collections) werden die Daten effizient zwischengespeichert.

Das Machine-Learning-Modell YOLO (eingebunden über from ultralytics import YOLO) führt die eigentliche Fischdetektion durch. Sobald ein Fisch erkannt wird, werden das Datum, die Uhrzeit und die ermittelte Fischart mithilfe der Google-API (from googleapiclient.discovery import build und from google.oauth2 import service_account) automatisch in ein Google Spreadsheet geschrieben.

Derzeit läuft das Modell lokal, könnte jedoch zukünftig auch in einer Cloud-Umgebung betrieben werden. Dies würde eine noch flexiblere und skalierbarere Auswertung ermöglichen und den Einsatz des Systems an verschiedenen Standorten vereinfachen.

				
					#Detection to Spreadsheet

import cv2
import datetime
from collections import defaultdict, deque
from ultralytics import YOLO
from googleapiclient.discovery import build
from google.oauth2 import service_account

# YOLO Setup
model = YOLO('best.pt')  # Pfad zum custom YOLO-Modell
tracked_objects = set()  # Set für eindeutige Kombinationen von Klassenname und ID
confidence_buffer = defaultdict(deque)  # Puffer für die Confidence-Werte für jede Klasse pro ID

# Google Sheets Setup
SERVICE_ACCOUNT_FILE = 'credentials.json'  # Pfad zur heruntergeladenen credentials.json-Datei
SCOPES = ['https://www.googleapis.com/auth/spreadsheets']
SPREADSHEET_ID = '1i-0HYoiLWieAK4XNKwA-khVLoJhGEkG61E0NbuTsqSo'  # Google Spreadsheet ID einfügen
SHEET_NAME = 'Sheet1'  # Name des Sheets (Standard ist "Sheet1")

# Google Sheets API-Service einrichten
credentials = service_account.Credentials.from_service_account_file(
    SERVICE_ACCOUNT_FILE, scopes=SCOPES)
service = build('sheets', 'v4', credentials=credentials)
sheet = service.spreadsheets()

# Funktion zum Schreiben in Google Spreadsheet
def write_to_google_sheets(data):
    body = {
        'values': [data]
    }
    sheet.values().append(
        spreadsheetId=SPREADSHEET_ID,
        range=f"{SHEET_NAME}!A:D",
        valueInputOption='RAW',
        body=body
    ).execute()

# IoU-Berechnung
def calculate_iou(box1, box2):
    x1 = max(box1[0], box2[0])
    y1 = max(box1[1], box2[1])
    x2 = min(box1[2], box2[2])
    y2 = min(box1[3], box2[3])

    intersection = max(0, x2 - x1) * max(0, y2 - y1)
    area1 = (box1[2] - box1[0]) * (box1[3] - box1[1])
    area2 = (box2[2] - box2[0]) * (box2[3] - box2[1])
    union = area1 + area2 - intersection

    return intersection / union if union > 0 else 0

# IoU- und Confidence-Parameter
iou_threshold = 0.8  # IoU-Schwelle für Stabilität
min_confidence_to_confirm = 0.8  # Mindestdurchschnittliche Confidence

# Stream-URL
stream_url = 'https://s55.ipcamlive.com/streams/37ocbcgexlfkjnxir/stream.m3u8'  # Stream-URL eingeben

# Video-Capture initialisieren
cap = cv2.VideoCapture(stream_url)

# Größe des Buffers für die gespeicherten Frames
frame_buffer_size = 45

while cap.isOpened():
    ret, frame = cap.read()
    if not ret:
        print("Stream beendet oder Fehler beim Lesen.")
        break

    # YOLO Inferenz und Tracking
    results = model.track(source=frame, persist=True, conf=0.7, iou=0.7, stream=True, stream_buffer=2000)
    for result in results:
        if result.boxes:
            for box in result.boxes:
                # Überprüfen, ob die Box-ID existiert
                if box.id is None:
                    print("Box ID ist None, überspringe diese Box.")
                    continue

                obj_id = int(box.id)
                class_name = model.names[int(box.cls)]
                object_identifier = f"{class_name}_{obj_id}"
                current_box = box.xyxy[0].tolist()  # Aktuelle Box-Koordinaten
                confidence = float(box.conf)

                # Aktuellen Frame und Confidence in den Puffer einfügen
                confidence_buffer[object_identifier].append(confidence)
                if len(confidence_buffer[object_identifier]) > frame_buffer_size:
                    confidence_buffer[object_identifier].popleft()  # Ältesten Frame entfernen

                # Durchschnittliche Confidence berechnen
                avg_confidence = sum(confidence_buffer[object_identifier]) / len(confidence_buffer[object_identifier])

                # Wenn die durchschnittliche Confidence ausreichend ist
                if avg_confidence >= min_confidence_to_confirm:
                    # Objekt bestätigen und speichern
                    if object_identifier not in tracked_objects:
                        tracked_objects.add(object_identifier)

                        # Datum, Zeit und Klasse erfassen
                        current_time = datetime.datetime.now()
                        date = current_time.strftime('%Y-%m-%d')
                        time = current_time.strftime('%H:%M:%S')
                        row = [date, time, obj_id, class_name]

                        # In Google Spreadsheet schreiben
                        write_to_google_sheets(row)
                        print(f"Daten geschrieben: {row}")

     #Frame anzeigen (optional)
    #cv2.imshow('Stream', frame)
    #if cv2.waitKey(1) & 0xFF == ord('q'):
       #break

cap.release()
cv2.destroyAllWindows()

				
			

Datentool & Visualisierung

1. Konzept und Design

Diese Anwendung basiert auf einer einfachen HTML-Struktur, in der Google Charts zum Erstellen verschiedener Diagramme eingebunden ist. Die benötigten Bibliotheken – darunter das Google-Charts-Skript und die Material-Icons – werden direkt im Head-Bereich geladen, während das zugehörige CSS für das Grundlayout sorgt. Im Body sind eine Sidebar mit Navigationspunkten und ein Main-Container angelegt, der die einzelnen Diagrammsektionen (stundenweise, täglich, Populationstrends und Jahresgesamt) darstellt.

Der integrierte JavaScript-Code greift auf Google Sheets zu, um Daten per Query auszulesen und sie anschließend für die Diagramme aufzubereiten. Über einen Datepicker lassen sich zeitbezogene Filter setzen, und die „Chips“ ermöglichen das Aktivieren und Deaktivieren verschiedener Fischarten. Außerdem werden responsive Funktionen bereitgestellt: Bei einem Seiten-Resize oder Scrollen aktualisieren sich die Diagramme und das Erscheinungsbild der Oberfläche automatisch.

Derzeit ist der Code so ausgelegt, dass alle Berechnungen lokal ausgeführt werden. Sollte das System jedoch wachsen oder an mehreren Standorten eingesetzt werden, lässt sich die Anwendung durch eine geeignete Cloud-Lösung leicht skalieren. Damit ist eine effiziente und zukunftssichere Datenauswertung gewährleistet.

2. Technische Basis

Diese Anwendung basiert auf einer einfachen HTML-Struktur, in der Google Charts zum Erstellen verschiedener Diagramme eingebunden ist. Die benötigten Bibliotheken – darunter das Google-Charts-Skript und die Material-Icons – werden direkt im Head-Bereich geladen, während das zugehörige CSS für das Grundlayout sorgt. Im Body sind eine Sidebar mit Navigationspunkten und ein Main-Container angelegt, der die einzelnen Diagrammsektionen (stundenweise, täglich, Populationstrends und Jahresgesamt) darstellt.

Der integrierte JavaScript-Code greift auf Google Sheets zu, um Daten per Query auszulesen und sie anschließend für die Diagramme aufzubereiten. Über einen Datepicker lassen sich zeitbezogene Filter setzen, und die „Chips“ ermöglichen das Aktivieren und Deaktivieren verschiedener Fischarten. Außerdem werden responsive Funktionen bereitgestellt: Bei einem Seiten-Resize oder Scrollen aktualisieren sich die Diagramme und das Erscheinungsbild der Oberfläche automatisch.

Derzeit ist der Code so ausgelegt, dass alle Berechnungen lokal ausgeführt werden. Sollte das System jedoch wachsen oder an mehreren Standorten eingesetzt werden, lässt sich die Anwendung durch eine geeignete Cloud-Lösung leicht skalieren. Damit ist eine effiziente und zukunftssichere Datenauswertung gewährleistet.

HTML

				
					<!DOCTYPE html>
<html lang="de">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Main Page</title>
    <script type="text/javascript" src="https://www.gstatic.com/charts/loader.js"></script>
    <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined" />
    <link rel="stylesheet" href="style.css">
</head>

<body>

    <div id="tooltipOverlay">
        <div class="tooltip-wrap">
            <div class="tooltip-section">
                <div class="datepicker-toggle2">
                    <input type="date" id="datePicker2" class="datestyle2" disabled>
                    <span class="material-symbols-outlined">event</span>
                </div>
                <h3>Datepicker</h3>
                <p>Verwenden Sie den Datepicker, um ein Datum auszuwählen und die Diagramme entsprechend zu aktualisieren.</p>
            </div>
            <div class="tooltip-section">
                <div class="chips2">
                    <div class="chip2"
                        style="background-color: #474747; color: rgb(255, 255, 255); border: 2px solid #474747;">Aal
                    </div>
                    <div class="chip2"
                        style="background-color: rgb(255, 177, 78); color: rgb(0, 0, 0); border: 2px solid rgb(255, 177, 78);">
                        Meerneunauge</div>
                    <div class="chip2"
                        style="background-color: #474747; color: rgb(255, 255, 255); border: 2px solid #474747;">
                        Maifisch</div>
                    <div class="chip2"
                        style="background-color: rgb(234, 95, 148); color: rgb(0, 0, 0); border: 2px solid rgb(234, 95, 148);">
                        Brachse</div>
                </div>
                <h3>Chips</h3>
                <p>Klicken Sie auf die Chips, um Fische zu aktivieren oder zu deaktivieren und die Daten in den Diagrammen
                    anzupassen.</p>
            </div>
        </div>
        <button onclick="closeTooltip()"
            style="margin-top: 20px; padding: 10px 20px; background-color: white; color: black; border: none; border-radius: 4px; cursor: pointer;">
            Schließen
        </button>
    </div>

    <div class="sidebar">
        <button class="home-button" onclick="window.scrollTo({ top: 0, behavior: 'smooth' })">
            <span style="font-size: 20px;" class="material-symbols-outlined">arrow_back</span>
            <span>Home</span>
        </button>
        <ul id="sidebar-list">
            <div id="indicator" class="indicator"></div>
            <li>
                <button onclick="scrollToSection('section1')" class="active">
                    <span class="material-symbols-outlined">bar_chart</span>
                    <span>Stündlich</span>
                </button>
            </li>
            <li>
                <button onclick="scrollToSection('section2')">
                    <span class="material-symbols-outlined">stacked_bar_chart</span>
                    <span>Täglich</span>
                </button>
            </li>
            <li>
                <button onclick="scrollToSection('section3')">
                    <span class="material-symbols-outlined">stacked_line_chart</span>
                    <span>Populationstrends</span>
                </button>
            </li>
            <li>
                <button onclick="scrollToSection('section4')">
                    <span class="material-symbols-outlined">pie_chart</span>
                    <span>Jahres Gesamt</span>
                </button>
            </li>
        </ul>
    </div>

    <div class="main">
        <div class="sticky">
            <div style="width: 100%; display: flex; justify-content:space-between;">
                <div class="datepicker-toggle">
                    <input type="date" id="datePicker" class="datestyle" onchange="updateSelectedDate()">
                    <span class="material-symbols-outlined" onclick="openDatePicker()">event</span>
                </div>
                <div class="material-symbols-outlined" id="question">info</div>
            </div>
            <div class="chips" id="chipsContainer"></div>
        </div>

        <!-- 1) Stündliches Chart -->
        <div id="section1" class="section">
            <h2>Stündliche Zählungen</h2>
            <div id="chart_div" style="width: 100%; height: 70vh;"></div>
        </div>

        <!-- 2) Tägliche Zählungen (Monat-Bar-Chart) -->
        <div id="section2" class="section">
            <h2>Tägliche Zählungen</h2>
            <div id="chart_div_month" style="width: 100%; height:70vh;"></div>
        </div>

        <!-- 3) Populationstrends (Monat-Line-Chart) -->
        <div id="section3" class="section">
            <h2>Populationstrends</h2>
            <div id="chart_div_month_line" style="width: 100%; height: 70vh;"></div>
        </div>

        <!-- 4) Jahres Gesamt (Pie-Chart) -->
        <div id="section4" class="section">
            <h2 id="jahresgesamt"></h2>
            <div id="chart_div_pie" style="width: 100%; height: 70vh;"></div>
        </div>
    </div>
    </body>

</html>

				
			

Java Script

				
					window.addEventListener("resize", () => {
    updateCharts();
});
        // -- Tooltips-Overlay
        document.getElementById("question").addEventListener("click", () => {
            const tooltipOverlay = document.getElementById("tooltipOverlay");
            tooltipOverlay.style.display = "flex";
        });
        function closeTooltip() {
            const tooltipOverlay = document.getElementById("tooltipOverlay");
            tooltipOverlay.style.display = "none";
        }

        // -- Sidebar-Indikator
        document.addEventListener("DOMContentLoaded", () => {
            const sections = [
                { id: "section1", label: "Stündlich", offset: 100 },
                { id: "section2", label: "Täglich", offset: 130 },
                { id: "section3", label: "Populationstrends", offset: 100 },
                { id: "section4", label: "Jahres Gesamt", offset: 100 },
            ];
            const indicator = document.getElementById("indicator");
            const buttons = Array.from(document.querySelectorAll("#sidebar-list li button"));

            function setActiveSection(sectionId) {
                const activeButton = buttons.find((button) =>
                    button.textContent.trim().includes(
                        sections.find((s) => s.id === sectionId).label
                    )
                );

                if (activeButton) {
                    buttons.forEach((button) => button.classList.remove("active"));
                    activeButton.classList.add("active");
                    // Indicator-Position aktualisieren
                    indicator.style.top = `${activeButton.parentElement.offsetTop}px`;
                    indicator.style.height = `${activeButton.offsetHeight}px`;
                }
            }

            function updateActiveSectionOnScroll() {
                const scrollPosition = window.scrollY + window.innerHeight / 2;
                sections.forEach((section) => {
                    const element = document.getElementById(section.id);
                    const offset = section.offset;
                    if (
                        element.offsetTop - offset <= scrollPosition &&
                        element.offsetTop + element.offsetHeight - offset > scrollPosition
                    ) {
                        setActiveSection(section.id);
                    }
                });
            }

            // Initiales Setzen der aktiven Sektion
            setActiveSection(sections[0].id);
            window.addEventListener("scroll", updateActiveSectionOnScroll);

            // Smooth-Scrolling bei Klick
            buttons.forEach((button, index) => {
                button.addEventListener("click", () => {
                    const targetSection = document.getElementById(sections[index].id);
                    const targetOffset = sections[index].offset;
                    const targetPosition = targetSection.offsetTop - targetOffset;
                    window.scrollTo({ top: targetPosition, behavior: "smooth" });
                    setActiveSection(sections[index].id);
                });
            });
        });

        function openDatePicker() {
            const dateInput = document.getElementById("datePicker");
            dateInput.showPicker();
        }

        // -- GOOGLE CHARTS LADEN
        google.charts.load('current', { packages: ['corechart'] });

        // -- HELFSFUNKTION: ISO -> dd.mm.yyyy
        function formatDateToGerman(isoDateString) {
            const [year, month, day] = isoDateString.split("-");
            return `${day}.${month}.${year}`;
        }

        // -- Ausgewählte Daten
        let selectedDateIso = "";     // wird z.B. "2024-02-03"
        let selectedDateGerman = "";  // wird z.B. "03.02.2024"

        // -- Beim Laden der Seite: Default = Heute
        document.addEventListener('DOMContentLoaded', () => {
            const datePicker = document.getElementById("datePicker");

            // Heute in ISO
            const today = new Date();
            const year = today.getFullYear();
            const month = String(today.getMonth() + 1).padStart(2, '0');
            const day = String(today.getDate()).padStart(2, '0');
            const isoToday = `${year}-${month}-${day}`;

            // DatePicker-Wert setzen
            datePicker.value = isoToday;
            // Globale Variablen belegen
            selectedDateIso = isoToday;
            selectedDateGerman = formatDateToGerman(isoToday);

            // Diagramme laden
            updateCharts();
        });

        // -- Chips & Fische
        const chipLabels = [
            "Aal", "Meerneunauge", "Maifisch", "Brachse", "Zobel", "Güster", "Rapfen", "Aland",
            "Ukelei", "Wels", "Nase", "Barbe", "Lachs", "Forelle", "Döbel", "Karpfen"
        ];

        const fishColorMap = {
            "Aal": "#ffd700",
            "Meerneunauge": "#ffb14e",
            "Maifisch": "#fa8775",
            "Brachse": "#ea5f94",
            "Zobel": "#cd34b5",
            "Güster": "#f669f4",
            "Rapfen": "#9775ff",
            "Aland": "#00ff00",
            "Ukelei": "#00ffff",
            "Wels": "#00bfff",
            "Nase": "#abfb70",
            "Barbe": "#ff6f61",
            "Lachs": "#ffb3c1",
            "Forelle": "#00bda0",
            "Döbel": "#ff00ff",
            "Karpfen": "#fcff18"
        };

        // Zuordnung "Daily" => Spalten in Google Sheet
        const dailyFishToColumnMap = {
            "Aal": "H", "Meerneunauge": "I", "Maifisch": "J", "Brachse": "K",
            "Zobel": "L", "Güster": "M", "Rapfen": "N", "Aland": "O",
            "Ukelei": "P", "Wels": "Q", "Nase": "R", "Barbe": "S",
            "Lachs": "T", "Forelle": "U", "Döbel": "V", "Karpfen": "W"
        };

        // Zuordnung "Month/Year" => Spalten in Google Sheet
        const fishToColumnMap = {
            "Aal": "AA", "Meerneunauge": "AB", "Maifisch": "AC", "Brachse": "AD",
            "Zobel": "AE", "Güster": "AF", "Rapfen": "AG", "Aland": "AH",
            "Ukelei": "AI", "Wels": "AJ", "Nase": "AK", "Barbe": "AL",
            "Lachs": "AM", "Forelle": "AN", "Döbel": "AO", "Karpfen": "AP"
        };

        // Array mit deutschen Monatsnamen
        const monthNames = [
            "Januar", "Februar", "März", "April", "Mai", "Juni",
            "Juli", "August", "September", "Oktober", "November", "Dezember"
        ];

        let selectedFish = [...chipLabels];

        // -- CHIPS ERSTELLEN
        const chipsContainer = document.getElementById('chipsContainer');
        chipLabels.forEach((label) => {
            const chip = document.createElement('div');
            chip.textContent = label;
            chip.className = 'chip';
            chip.style.backgroundColor = fishColorMap[label];
            chip.style.color = '#000';
            chip.style.border = `2px solid ${fishColorMap[label]}`;
            chip.dataset.active = "true";

            chip.addEventListener('click', () => {
                if (chip.dataset.active === "true") {
                    chip.dataset.active = "false";
                    chip.style.backgroundColor = '#474747';
                    chip.style.color = '#fff';
                    chip.style.border = '2px solid #474747';
                } else {
                    chip.dataset.active = "true";
                    chip.style.backgroundColor = fishColorMap[label];
                    chip.style.color = '#000';
                    chip.style.border = `2px solid ${fishColorMap[label]}`;
                }
                updateSelectedFish();
            });

            chipsContainer.appendChild(chip);
        });

        function updateSelectedFish() {
            selectedFish = Array.from(document.querySelectorAll('.chip'))
                .filter(chip => chip.dataset.active === "true")
                .map(chip => chip.textContent);

            if (selectedFish.length === 0) {
                console.warn("No fish selected. Clearing charts.");
                clearCharts();
                return;
            }
            updateCharts();
        }

        // -- DATE SELECTION
        function updateSelectedDate() {
            const datePicker = document.getElementById("datePicker");
            selectedDateIso = datePicker.value;                 // z.B. "2024-02-03"
            selectedDateGerman = formatDateToGerman(selectedDateIso);  // z.B. "03.02.2024"
            updateCharts();
        }

        // -- CHARTS NEU LADEN
        function updateCharts() {
            if (selectedFish.length > 0 && selectedDateIso) {
                drawDailyChart();
                drawMonthlyChart();
                drawMonthlyLineChart();
                drawYearlyPieChart();
            } else {
                clearCharts();
            }
        }

        // -- CHARTS LEEREN
        function clearCharts() {
            document.getElementById("chart_div").innerHTML = "";
            document.getElementById("chart_div_month").innerHTML = "";
            document.getElementById("chart_div_month_line").innerHTML = "";
            document.getElementById("chart_div_pie").innerHTML = "";
        }

        /* -----------------------------------------
         * 1) STÜNDLICHES DIAGRAMM
         * ----------------------------------------- */
        function drawDailyChart() {
            if (!selectedDateIso || selectedFish.length === 0) return;

            // Überschrift => dd.mm.yyyy
            const dailyHeading = document.querySelector('#section1 h2');
            dailyHeading.textContent = `Stündliche Zählungen am ${selectedDateGerman}`;

            // Abfrage an Google Sheets (ISO-Format für Filter!)
            const fishColumns = selectedFish
                .map(fish => `SUM(${dailyFishToColumnMap[fish]})`)
                .join(", ");

            const query = new google.visualization.Query(
                "https://docs.google.com/spreadsheets/d/1i-0HYoiLWieAK4XNKwA-khVLoJhGEkG61E0NbuTsqSo/edit?usp=sharingheaders=1"
            );
            const dateFilter = `WHERE F = '${selectedDateIso}'`;
            query.setQuery(`SELECT G, ${fishColumns} ${dateFilter} GROUP BY G ORDER BY G`);

            query.send(response => {
                if (response.isError()) {
                    console.error("Error in query:", response.getMessage());
                    return;
                }

                const rawData = response.getDataTable();
                if (!rawData) {
                    console.error("No data returned for daily chart.");
                    return;
                }

                // Daten aufbereiten
                const data = new google.visualization.DataTable();
                data.addColumn("string", "Zeit");
                selectedFish.forEach(fish => {
                    data.addColumn("number", fish);
                    data.addColumn({ type: "string", role: "tooltip" });
                });

                for (let i = 0; i < rawData.getNumberOfRows(); i++) {
                    const row = [rawData.getValue(i, 0)]; // "G" = Zeit z. B. "00:00"
                    selectedFish.forEach((fish, index) => {
                        const count = rawData.getValue(i, index + 1);
                        row.push(count);
                        row.push(`${fish}: ${count}`);
                    });
                    data.addRow(row);
                }

                const options = {
                    backgroundColor: '#0e0e0e',
                    titleTextStyle: { color: 'white' },
                    isStacked: true,
                    hAxis: {
                        title: 'Zeit',
                        titleTextStyle: { color: 'white' },
                        textStyle: { color: 'white' }
                    },
                    vAxis: {
                        title: 'Anzahl',
                        format: '0',
                        min: 0,
                        titleTextStyle: { color: 'white' },
                        textStyle: { color: 'white' }
                    },
                    chartArea: { width: '70%', height: '80%' },
                    legend: 'none',
                    colors: selectedFish.map(fish => fishColorMap[fish]),
                };

                const chart = new google.visualization.ColumnChart(document.getElementById('chart_div'));
                chart.draw(data, options);
            });
        }

        /* -----------------------------------------
         * 2) TÄGLICHE ZAHLEN (MONATLICHES BALKEN-DIAGRAMM)
         * ----------------------------------------- */
        function drawMonthlyChart() {
            if (!selectedDateIso || selectedFish.length === 0) return;

            const [year, month] = selectedDateIso.split('-');
            const startOfMonth = `${year}-${month}-01`;
            const endOfMonth = new Date(year, month, 0).toISOString().split('T')[0];

            // Monatsnamen + Jahr in die Überschrift
            const spelledOutMonth = monthNames[parseInt(month, 10) - 1];
            document.querySelector('#section2 h2').textContent =
                `Tägliche Zählungen im ${spelledOutMonth} ${year}`;

            // Abfrage
            const fishColumns = selectedFish
                .map(fish => `SUM(${fishToColumnMap[fish]})`)
                .join(", ");

            const query = new google.visualization.Query(
                "https://docs.google.com/spreadsheets/d/1i-0HYoiLWieAK4XNKwA-khVLoJhGEkG61E0NbuTsqSo/edit?usp=sharingheaders=1"
            );
            const monthFilter = `WHERE F >= '${startOfMonth}' AND F <= '${endOfMonth}'`;
            query.setQuery(`SELECT Z, ${fishColumns} ${monthFilter} GROUP BY Z ORDER BY Z`);

            query.send(response => {
                if (response.isError()) {
                    console.error("Error in query:", response.getMessage());
                    return;
                }

                const rawData = response.getDataTable();
                if (!rawData || rawData.getNumberOfRows() === 0) {
                    const container = document.getElementById("chart_div_month");
                    container.innerHTML = "<p style='color: white; text-align: center;'>Keine Daten für den Monat verfügbar</p>";
                    return;
                }

                const data = new google.visualization.DataTable();
                data.addColumn("string", "Datum");
                selectedFish.forEach(fish => {
                    data.addColumn("number", fish);
                    data.addColumn({ type: "string", role: "tooltip" });
                });

                for (let i = 0; i < rawData.getNumberOfRows(); i++) {
                    // isoDate, z.B. "2024-02-07"
                    const isoDate = rawData.getValue(i, 0);
                    // In dd.mm.yyyy umwandeln
                    const germanDate = formatDateToGerman(isoDate);

                    const row = [germanDate];
                    selectedFish.forEach((fish, index) => {
                        const count = rawData.getValue(i, index + 1);
                        row.push(count);
                        row.push(`${fish}: ${count}`);
                    });
                    data.addRow(row);
                }

                const options = {
                    backgroundColor: "#0e0e0e",
                    titleTextStyle: { color: "white" },
                    hAxis: {
                        title: "Datum",
                        titleTextStyle: { color: "white" },
                        textStyle: { color: "white" },
                    },
                    vAxis: {
                        title: "Anzahl",
                        format: "0",
                        minValue: 0,
                        titleTextStyle: { color: "white" },
                        textStyle: { color: "white" },
                    },
                    chartArea: { width: "70%", height: "80%" },
                    isStacked: true,
                    legend: "none",
                    colors: selectedFish.map(fish => fishColorMap[fish]),
                };

                const chart = new google.visualization.ColumnChart(document.getElementById("chart_div_month"));
                chart.draw(data, options);
            });
        }

        /* -----------------------------------------
         * 3) POPULATIONSTRENDS (MONATLICHES LINIEN-DIAGRAMM)
         * ----------------------------------------- */
        function drawMonthlyLineChart() {
            if (!selectedDateIso || selectedFish.length === 0) return;

            const [year, month] = selectedDateIso.split('-');
            const startOfMonth = `${year}-${month}-01`;
            const endOfMonth = new Date(year, month, 0).toISOString().split('T')[0];

            // Überschrift anpassen
            const spelledOutMonth = monthNames[parseInt(month, 10) - 1];
            document.querySelector('#section3 h2').textContent =
                `Populationstrends im ${spelledOutMonth} ${year}`;

            const fishColumns = selectedFish
                .map(fish => `SUM(${fishToColumnMap[fish]})`)
                .join(", ");

            const query = new google.visualization.Query(
                "https://docs.google.com/spreadsheets/d/1i-0HYoiLWieAK4XNKwA-khVLoJhGEkG61E0NbuTsqSo/edit?usp=sharingheaders=1"
            );
            const monthFilter = `WHERE F >= '${startOfMonth}' AND F <= '${endOfMonth}'`;
            query.setQuery(`SELECT Z, ${fishColumns} ${monthFilter} GROUP BY Z ORDER BY Z`);

            query.send(response => {
                if (response.isError()) {
                    console.error("Error in query:", response.getMessage());
                    return;
                }

                const rawData = response.getDataTable();
                if (!rawData || rawData.getNumberOfRows() === 0) {
                    const container = document.getElementById("chart_div_month_line");
                    container.innerHTML = "<p style='color: white; text-align: center;'>Keine Daten für den Monat verfügbar</p>";
                    return;
                }

                const data = new google.visualization.DataTable();
                data.addColumn("string", "Datum");
                selectedFish.forEach(fish => {
                    data.addColumn("number", fish);
                    data.addColumn({ type: "string", role: "tooltip" });
                });

                for (let i = 0; i < rawData.getNumberOfRows(); i++) {
                    const isoDate = rawData.getValue(i, 0);
                    const germanDate = formatDateToGerman(isoDate);

                    const row = [germanDate];
                    selectedFish.forEach((fish, index) => {
                        const count = rawData.getValue(i, index + 1);
                        row.push(count);
                        row.push(`${fish}: ${count}`);
                    });
                    data.addRow(row);
                }

                 const options = {
            backgroundColor: "#0e0e0e",
            titleTextStyle: { color: "white" },
            hAxis: {
                title: "Datum",
                titleTextStyle: { color: "white" },
                textStyle: { color: "white" },
            },
            vAxis: {
                title: "Anzahl",
                format: "0",
                minValue: 0,
                titleTextStyle: { color: "white" },
                textStyle: { color: "white" },
            },
            chartArea: { width: "70%", height: "80%" },
            legend: "none",
            colors: selectedFish.map(fish => fishColorMap[fish]),
            // --- Neue Optionen für dickere Linien und sichtbare Datenpunkte ---
            lineWidth: 4,          // Linie etwas dicker
            pointSize: 8,          // Größe der Punkte
            pointsVisible: true,   // Datenpunkte immer anzeigen
        };

      

                const chart = new google.visualization.LineChart(document.getElementById("chart_div_month_line"));
                chart.draw(data, options);
            });
        }

        /* -----------------------------------------
         * 4) JAHRES GESAMT (PIE-CHART)
         * ----------------------------------------- */
        function drawYearlyPieChart() {
            if (!selectedDateIso || selectedFish.length === 0) return;

            const year = selectedDateIso.split('-')[0];
            const startOfYear = `${year}-01-01`;
            const endOfYear = `${year}-12-31`;

            // Überschrift
            const jahresGesamt = document.getElementById("jahresgesamt");
            jahresGesamt.textContent = `Jahres Gesamt ${year}`;

            const fishColumns = selectedFish
                .map(fish => `SUM(${fishToColumnMap[fish]})`)
                .join(", ");

            const query = new google.visualization.Query(
                "https://docs.google.com/spreadsheets/d/1i-0HYoiLWieAK4XNKwA-khVLoJhGEkG61E0NbuTsqSo/edit?usp=sharingheaders=1"
            );
            const yearFilter = `WHERE F >= '${startOfYear}' AND F <= '${endOfYear}'`;
            query.setQuery(`SELECT ${fishColumns} ${yearFilter}`);

            query.send(response => {
                if (response.isError()) {
                    console.error("Error in query:", response.getMessage());
                    return;
                }

                const rawData = response.getDataTable();
                if (!rawData || rawData.getNumberOfColumns() === 0) {
                    const container = document.getElementById("chart_div_pie");
                    container.innerHTML = "<p style='color: white; text-align: center;'>Keine Daten für das Jahr verfügbar</p>";
                    return;
                }

                const data = new google.visualization.DataTable();
                data.addColumn("string", "Fischart");
                data.addColumn("number", "Anzahl");

                selectedFish.forEach((fish, index) => {
                    const value = rawData.getValue(0, index);
                    if (value !== null) {
                        data.addRow([fish, value]);
                    }
                });

                const options = {
                    
                    pieHole: 0.4,
                    pieSliceText: "percentage",
                    pieSliceBorderColor: "#fff",
                    backgroundColor: "#0e0e0e",
                    titleTextStyle: { color: "white" },
                    pieSliceTextStyle: { color: "#0e0e0e" },
                    chartArea: { width: "80%", height: "80%" },
                    legend: "none",
                    colors: selectedFish.map(fish => fishColorMap[fish]),
                };

                const chart = new google.visualization.PieChart(document.getElementById("chart_div_pie"));
                chart.draw(data, options);
            });
        }

        // -- Beim Laden von Google Charts einmal aufrufen
        google.charts.setOnLoadCallback(() => {
            updateCharts();
        });

        // -- Dynamisches Styling bei Scrollen
        window.addEventListener("scroll", () => {
            const stickyElement = document.querySelector(".chips");
            const datepickerToggle = document.querySelector(".datepicker-toggle");
            const datepickerToggleSpan = document.querySelector(".datepicker-toggle span");
            const datestyle = document.querySelector(".datestyle");
            const chipstyle = document.querySelectorAll(".chip");
            const infobutton = document.querySelector("#question");

            if (window.scrollY > 0) {
                chipstyle.forEach(chip => {
                    chip.style.padding = "4px 8px";
                    chip.style.fontSize = "12px";
                });
            } else {
                chipstyle.forEach(chip => {
                    chip.style.padding = "8px 12px";
                    chip.style.fontSize = "14px";
                });
            }

            if (window.scrollY > 0) {
                stickyElement.style.marginTop = "10px";
                datepickerToggle.style.borderWidth = "1px";
                datepickerToggle.style.paddingTop = "0px";
                datepickerToggle.style.paddingBottom = "0px";
                datepickerToggle.style.paddingLeft = "3px";
                datepickerToggle.style.paddingRight = "3px";
                datepickerToggle.style.borderRadius = "2px";
                datepickerToggleSpan.style.fontSize = "18px";
                datestyle.style.fontSize = "10px";
                infobutton.style.fontSize = "20px";
            } else {
                stickyElement.style.marginTop = "20px";
                datepickerToggle.style.borderWidth = "2px";
                datepickerToggle.style.paddingTop = "8px";
                datepickerToggle.style.paddingBottom = "8px";
                datepickerToggle.style.paddingLeft = "12px";
                datepickerToggle.style.paddingRight = "12px";
                datepickerToggle.style.borderRadius = "7px";
                datepickerToggleSpan.style.fontSize = "24px";
                datestyle.style.fontSize = "15px";
                infobutton.style.fontSize = "28px";
            }
        });
				
			

3. Anwendungsfälle

Das Datentool ist vorrangig für den Natur- und Artenschutz konzipiert und bietet zugleich Fischereivereinen eine wertvolle Unterstützung: Durch die präzise Überwachung der Bestände können Schonzeiten bei Bedarf kurzfristig angepasst werden. Darüber hinaus profitieren Organisationen wie die IKSR, die „Internationale Kommission zum Schutz des Rheins“, von der automatisierten und kontinuierlichen Erfassung.

Da die IKSR unter anderem für den Schutz und die nachhaltige Nutzung des Rheins verantwortlich ist, lassen sich mithilfe dieser Lösung Bestandszahlen im Jahresverlauf detailliert nachvollziehen, Trends erkennen und potenziell Personalkosten einsparen.

Ausblick & Weiterentwicklung

In Zukunft wird das System konsequent weiterentwickelt und um zusätzliche Funktionen erweitert, um Fischpopulationen noch umfassender analysieren und vergleichen zu können. Kurzfristig liegt der Fokus dabei vor allem auf Verbesserungen des Modells selbst sowie der Aufnahme weiterer Fischarten. Als technische Optimierung bietet sich zudem eine Cloud-Umgebung an, die eine höhere Flexibilität und Skalierbarkeit ermöglicht.

Dadurch kann das System ohne großen Aufwand auch in anderen Regionen eingesetzt werden. Langfristig trägt dieser Ansatz zu einem verbesserten Schutz von Fischbeständen bei und liefert wichtige Erkenntnisse für eine nachhaltige Bewirtschaftung unserer Gewässer.

Kontakt