{{ $stat['label'] }}
{{ $stat['slots'] }} Bloecke bis zum geladenen Planende
@extends('layouts.app') @section('title', 'Stundenplan Cockpit - Live Stundenplan mit Fach-Counter | kotsch.tech') @section('meta_description', 'Stundenplan Cockpit fuer DE23-1: Live-Wochenansicht, naechster Termin, Countdown und Counter, wie viele Termine pro Fach noch offen sind.') @section('meta_extra') @endsection @section('content') @include('tools._tool_base') @php $timezone = new DateTimeZone('Europe/Berlin'); $now = new DateTimeImmutable('now', $timezone); $sourceUrl = 'https://stundenplan.ba-glauchau.de/stundenplan/PlanServlet?Format=HTML&data=b4ead48f7d62ff07ed2e552ff3edcaa38c19110b06c6e6bf&LegendeFach&AnzahlPlaene=25'; if (!function_exists('ktScheduleText')) { function ktScheduleText(?string $value): string { $value = html_entity_decode((string) $value, ENT_QUOTES | ENT_HTML5, 'UTF-8'); $value = preg_replace('/\x{00a0}/u', ' ', $value); $value = preg_replace('/\s+/u', ' ', $value); return trim($value ?? ''); } } if (!function_exists('ktDirectElements')) { function ktDirectElements(DOMElement $node, string $tagName): array { $children = []; foreach ($node->childNodes as $child) { if ($child instanceof DOMElement && strcasecmp($child->tagName, $tagName) === 0) { $children[] = $child; } } return $children; } } if (!function_exists('ktUniqueTexts')) { function ktUniqueTexts(array $values): array { $result = []; foreach ($values as $value) { $value = ktScheduleText($value); if ($value !== '' && !in_array($value, $result, true)) { $result[] = $value; } } return $result; } } if (!function_exists('ktSpanTexts')) { function ktSpanTexts(DOMElement $node, string $className): array { $texts = []; $xpath = new DOMXPath($node->ownerDocument); $query = './/span[contains(concat(" ", normalize-space(@class), " "), " ' . $className . ' ")]'; foreach ($xpath->query($query, $node) as $span) { $text = ktScheduleText($span->textContent); if ($text !== '') { $texts[] = $text; } } return $texts; } } if (!function_exists('ktParseSlot')) { function ktParseSlot(string $text): ?array { if (!preg_match('/(\d{1,2})\.\s*(\d{2}:\d{2})\s*-\s*(\d{2}:\d{2})/u', $text, $m)) { return null; } return ['number' => (int) $m[1], 'start' => $m[2], 'end' => $m[3]]; } } if (!function_exists('ktParseCell')) { function ktParseCell(DOMElement $cell): ?array { $plain = ktScheduleText($cell->textContent); if ($plain === '') { return null; } $subjects = ktUniqueTexts(ktSpanTexts($cell, 'fach')); $teachers = ktUniqueTexts(ktSpanTexts($cell, 'dozent')); $rooms = ktUniqueTexts(ktSpanTexts($cell, 'ort')); $notes = ktUniqueTexts(ktSpanTexts($cell, 'bemerkung')); $className = $cell->getAttribute('class'); $isHoliday = stripos($className, 'Feiertag') !== false || stripos($plain, 'Feiertag') !== false || stripos($plain, 'Pfingstmontag') !== false; if (!$subjects && !$isHoliday) { $subjects = [$plain]; } return [ 'title' => $subjects ? implode(' / ', $subjects) : $plain, 'subjects' => $subjects, 'teachers' => $teachers, 'rooms' => $rooms, 'notes' => $notes, 'type' => $isHoliday ? 'holiday' : 'lesson', 'raw' => $plain, ]; } } if (!function_exists('ktLegend')) { function ktLegend(DOMXPath $xpath): array { $legend = []; $cells = $xpath->query('//table[contains(@class, "Legende")]//td'); if (!$cells) { return $legend; } for ($i = 0; $i < $cells->length - 1; $i += 2) { $short = ktScheduleText($cells->item($i)->textContent); $long = ktScheduleText($cells->item($i + 1)->textContent); if ($short !== '' && $long !== '') { $legend[$short] = $long; } } return $legend; } } if (!function_exists('ktParseSchedule')) { function ktParseSchedule(string $html, DateTimeZone $timezone): array { $dom = new DOMDocument(); @$dom->loadHTML('' . $html); $xpath = new DOMXPath($dom); $weeks = []; $events = []; $stand = preg_match('/Stand:\s*([^<]+)/u', $html, $m) ? ktScheduleText($m[1]) : null; $tables = $xpath->query('//table[contains(concat(" ", normalize-space(@class), " "), " Plan ")]'); foreach ($tables as $table) { if (!$table instanceof DOMElement) { continue; } $captionNode = $table->getElementsByTagName('caption')->item(0); $caption = $captionNode ? ktScheduleText($captionNode->textContent) : ''; if (!preg_match('/vom\s+(\d{1,2}\.\d{1,2}\.\d{4})\s*-\s*(\d{1,2}\.\d{1,2}\.\d{4})/u', $caption, $dm)) { continue; } $weekStartMutable = DateTime::createFromFormat('!d.m.Y', $dm[1], $timezone); if (!$weekStartMutable) { continue; } $weekStart = DateTimeImmutable::createFromMutable($weekStartMutable); $rows = iterator_to_array($table->getElementsByTagName('tr')); $headerIndex = null; $dayNames = []; foreach ($rows as $index => $row) { if (!$row instanceof DOMElement) { continue; } $ths = ktDirectElements($row, 'th'); if ($ths && strcasecmp(ktScheduleText($ths[0]->textContent), 'Zeit') === 0) { $headerIndex = $index; for ($i = 1; $i < count($ths); $i++) { $dayNames[] = ktScheduleText($ths[$i]->textContent); } break; } } if ($headerIndex === null || !$dayNames) { continue; } $slots = []; for ($i = $headerIndex + 1; $i < count($rows); $i++) { if (!$rows[$i] instanceof DOMElement) { continue; } $ths = ktDirectElements($rows[$i], 'th'); $slot = $ths ? ktParseSlot(ktScheduleText($ths[0]->textContent)) : null; if ($slot) { $slots[$i] = $slot; } } $week = ['caption' => $caption, 'start' => $weekStart, 'days' => []]; foreach ($dayNames as $offset => $dayName) { $week['days'][$offset] = [ 'name' => $dayName, 'date' => $weekStart->modify('+' . $offset . ' days'), 'events' => [], ]; } $rowspanDebt = array_fill(0, count($dayNames), 0); for ($rowIndex = $headerIndex + 1; $rowIndex < count($rows); $rowIndex++) { if (!isset($slots[$rowIndex]) || !$rows[$rowIndex] instanceof DOMElement) { continue; } $cells = ktDirectElements($rows[$rowIndex], 'td'); $cellPointer = 0; for ($dayIndex = 0; $dayIndex < count($dayNames); $dayIndex++) { if ($rowspanDebt[$dayIndex] > 0) { $rowspanDebt[$dayIndex]--; continue; } $cell = $cells[$cellPointer] ?? null; $cellPointer++; if (!$cell instanceof DOMElement) { continue; } $rowspan = max(1, (int) ($cell->getAttribute('rowspan') ?: 1)); $rowspanDebt[$dayIndex] = $rowspan - 1; $cellData = ktParseCell($cell); if (!$cellData) { continue; } $endRow = $rowIndex; $remaining = $rowspan - 1; for ($probe = $rowIndex + 1; $probe < count($rows) && $remaining > 0; $probe++) { if (isset($slots[$probe])) { $endRow = $probe; $remaining--; } } $date = $week['days'][$dayIndex]['date']; $start = DateTimeImmutable::createFromFormat('!Y-m-d H:i', $date->format('Y-m-d') . ' ' . $slots[$rowIndex]['start'], $timezone); $end = DateTimeImmutable::createFromFormat('!Y-m-d H:i', $date->format('Y-m-d') . ' ' . ($slots[$endRow]['end'] ?? $slots[$rowIndex]['end']), $timezone); if (!$start || !$end) { continue; } $event = array_merge($cellData, [ 'date' => $date, 'dayName' => $week['days'][$dayIndex]['name'], 'start' => $start, 'end' => $end, 'timeLabel' => $start->format('H:i') . '-' . $end->format('H:i'), 'slotCount' => $rowspan, ]); $week['days'][$dayIndex]['events'][] = $event; $events[] = $event; } } $weeks[] = $week; } usort($events, static fn ($a, $b) => $a['start'] <=> $b['start']); return ['weeks' => $weeks, 'events' => $events, 'legend' => ktLegend($xpath), 'stand' => $stand]; } } $rawHtml = cache()->remember('stundenplan_cockpit_source_v1', now()->addMinutes(15), function () use ($sourceUrl) { $context = stream_context_create([ 'http' => [ 'timeout' => 12, 'header' => "User-Agent: kotsch.tech Stundenplan Cockpit\r\n", ], ]); $html = @file_get_contents($sourceUrl, false, $context); return is_string($html) && $html !== '' ? $html : null; }); $parsed = $rawHtml ? ktParseSchedule($rawHtml, $timezone) : ['weeks' => [], 'events' => [], 'legend' => [], 'stand' => null]; $weeks = $parsed['weeks']; $legend = $parsed['legend']; $stand = $parsed['stand']; $hiddenSubjects = ['DE-ZSPLM']; $cleanEvent = static function (array $event) use ($hiddenSubjects): ?array { if ($event['type'] !== 'lesson') { return $event; } $event['subjects'] = array_values(array_filter($event['subjects'], static fn ($subject) => !in_array($subject, $hiddenSubjects, true))); if (!$event['subjects']) { return null; } $event['title'] = implode(' / ', $event['subjects']); return $event; }; foreach ($weeks as &$week) { foreach ($week['days'] as &$day) { $day['events'] = array_values(array_filter(array_map($cleanEvent, $day['events']))); } unset($day); } unset($week); $visibleEvents = []; foreach ($parsed['events'] as $event) { $event = $cleanEvent($event); if ($event) { $visibleEvents[] = $event; } } $futureEvents = array_values(array_filter($visibleEvents, static fn ($event) => $event['end'] >= $now)); $lessonEvents = array_values(array_filter($futureEvents, static fn ($event) => $event['type'] === 'lesson')); $nextEvent = collect($lessonEvents)->first(); $subjectStats = []; foreach ($lessonEvents as $event) { foreach (ktUniqueTexts($event['subjects']) as $subject) { $subjectStats[$subject] ??= ['code' => $subject, 'label' => $legend[$subject] ?? $subject, 'count' => 0, 'slots' => 0]; $subjectStats[$subject]['count']++; $subjectStats[$subject]['slots'] += $event['slotCount']; } } uasort($subjectStats, static fn ($a, $b) => [$b['count'], $a['code']] <=> [$a['count'], $b['code']]); $subjectStats = array_values($subjectStats); $palette = [ ['#2563eb', '#dbeafe'], ['#0f766e', '#ccfbf1'], ['#b45309', '#fef3c7'], ['#be123c', '#ffe4e6'], ['#6d28d9', '#ede9fe'], ['#047857', '#d1fae5'], ['#4338ca', '#e0e7ff'], ['#c2410c', '#ffedd5'], ]; $subjectStyle = []; foreach (array_column($subjectStats, 'code') as $index => $code) { $subjectStyle[$code] = $palette[$index % count($palette)]; } $detailEvents = []; foreach ($lessonEvents as $event) { $key = md5($event['start']->format(DateTimeInterface::ATOM) . '|' . $event['title'] . '|' . implode(',', $event['rooms'])); $detailEvents[$key] = [ 'key' => $key, 'title' => $event['title'], 'subjects' => $event['subjects'], 'date' => $event['date']->format('d.m.Y'), 'weekday' => $event['dayName'], 'time' => $event['timeLabel'], 'rooms' => $event['rooms'], 'teachers' => $event['teachers'], 'notes' => $event['notes'], ]; } $displayWeeks = array_values(array_filter($weeks, static function ($week) use ($now) { foreach ($week['days'] as $day) { if ($day['date'] >= $now->setTime(0, 0)) { return true; } } return false; })) ?: $weeks; $nextPayload = $nextEvent ? ['start' => $nextEvent['start']->format(DateTimeInterface::ATOM), 'end' => $nextEvent['end']->format(DateTimeInterface::ATOM)] : null; @endphp
Live aus dem BA-Glauchau-Stundenplan gelesen, als schönes Cockpit mit Wochenansicht, nächstem Termin und Fach-Counter für alles, was ab jetzt noch kommt.
Das Tool ist bereit, aber die externe Quelle konnte gerade nicht geladen werden.
{{ $stat['label'] }}
{{ $stat['slots'] }} Bloecke bis zum geladenen Planende
{{ $week['start']->format('d.m.Y') }} bis {{ $week['start']->modify('+6 days')->format('d.m.Y') }}
{{ $day['date']->format('d.m.Y') }}