TCrB Diagramm verbessert
This commit is contained in:
@@ -0,0 +1,67 @@
|
|||||||
|
{
|
||||||
|
"U": {
|
||||||
|
"wavelength_nm": 366.3,
|
||||||
|
"color": "#1B2A6B",
|
||||||
|
"description": "Johnson U, Ultraviolett, ca. 366,3 nm"
|
||||||
|
},
|
||||||
|
"B": {
|
||||||
|
"wavelength_nm": 436.1,
|
||||||
|
"color": "#1D00FF",
|
||||||
|
"description": "Johnson B, Blau, ca. 436,1 nm"
|
||||||
|
},
|
||||||
|
"SG": {
|
||||||
|
"wavelength_nm": 477.0,
|
||||||
|
"color": "#00C8FF",
|
||||||
|
"description": "Sloan g, ca. 477,0 nm"
|
||||||
|
},
|
||||||
|
"TB": {
|
||||||
|
"wavelength_nm": 440.0,
|
||||||
|
"color": "#0000FF",
|
||||||
|
"description": "DSLR/OSC Blaukanal, ca. 440,0 nm"
|
||||||
|
},
|
||||||
|
"V": {
|
||||||
|
"wavelength_nm": 544.8,
|
||||||
|
"color": "#92FF00",
|
||||||
|
"description": "Johnson V, visuell/gruen, ca. 544,8 nm"
|
||||||
|
},
|
||||||
|
"TG": {
|
||||||
|
"wavelength_nm": 544.8,
|
||||||
|
"color": "#92FF00",
|
||||||
|
"description": "DSLR/OSC Gruenkanal, ca. 544,8 nm"
|
||||||
|
},
|
||||||
|
"CV": {
|
||||||
|
"wavelength_nm": 544.8,
|
||||||
|
"color": "#92FF00",
|
||||||
|
"description": "Clear auf V referenziert, ca. 544,8 nm"
|
||||||
|
},
|
||||||
|
"SR": {
|
||||||
|
"wavelength_nm": 623.1,
|
||||||
|
"color": "#FF6B00",
|
||||||
|
"description": "Sloan r, ca. 623,1 nm"
|
||||||
|
},
|
||||||
|
"R": {
|
||||||
|
"wavelength_nm": 640.7,
|
||||||
|
"color": "#FF1D00",
|
||||||
|
"description": "Cousins R, Rot, ca. 640,7 nm"
|
||||||
|
},
|
||||||
|
"TR": {
|
||||||
|
"wavelength_nm": 640.7,
|
||||||
|
"color": "#FF1D00",
|
||||||
|
"description": "DSLR/OSC Rotkanal, ca. 640,7 nm"
|
||||||
|
},
|
||||||
|
"SI": {
|
||||||
|
"wavelength_nm": 762.5,
|
||||||
|
"color": "#870000",
|
||||||
|
"description": "Sloan i, ca. 762,5 nm"
|
||||||
|
},
|
||||||
|
"I": {
|
||||||
|
"wavelength_nm": 798.0,
|
||||||
|
"color": "#7A0018",
|
||||||
|
"description": "Cousins I, nahes Infrarot, ca. 798,0 nm"
|
||||||
|
},
|
||||||
|
"SZ": {
|
||||||
|
"wavelength_nm": 909.7,
|
||||||
|
"color": "#7A0018",
|
||||||
|
"description": "Sloan z, ca. 909,7 nm"
|
||||||
|
}
|
||||||
|
}
|
||||||
Binary file not shown.
@@ -0,0 +1,117 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
FILTER_WAVELENGTHS_NM: dict[str, float] = {
|
||||||
|
"U": 366.3,
|
||||||
|
"B": 436.1,
|
||||||
|
"SG": 477.0,
|
||||||
|
"TB": 440.0,
|
||||||
|
"V": 544.8,
|
||||||
|
"TG": 544.8,
|
||||||
|
"CV": 544.8,
|
||||||
|
"SR": 623.1,
|
||||||
|
"R": 640.7,
|
||||||
|
"TR": 640.7,
|
||||||
|
"SI": 762.5,
|
||||||
|
"I": 798.0,
|
||||||
|
"SZ": 909.7,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
FILTER_DESCRIPTIONS: dict[str, str] = {
|
||||||
|
"U": "Johnson U, Ultraviolett, ca. 366,3 nm",
|
||||||
|
"B": "Johnson B, Blau, ca. 436,1 nm",
|
||||||
|
"SG": "Sloan g, ca. 477,0 nm",
|
||||||
|
"TB": "DSLR/OSC Blaukanal, ca. 440,0 nm",
|
||||||
|
"V": "Johnson V, visuell/gruen, ca. 544,8 nm",
|
||||||
|
"TG": "DSLR/OSC Gruenkanal, ca. 544,8 nm",
|
||||||
|
"CV": "Clear auf V referenziert, ca. 544,8 nm",
|
||||||
|
"SR": "Sloan r, ca. 623,1 nm",
|
||||||
|
"R": "Cousins R, Rot, ca. 640,7 nm",
|
||||||
|
"TR": "DSLR/OSC Rotkanal, ca. 640,7 nm",
|
||||||
|
"SI": "Sloan i, ca. 762,5 nm",
|
||||||
|
"I": "Cousins I, nahes Infrarot, ca. 798,0 nm",
|
||||||
|
"SZ": "Sloan z, ca. 909,7 nm",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def wavelength_to_hex_color(wavelength_nm: float) -> str:
|
||||||
|
if wavelength_nm < 380:
|
||||||
|
return "#1B2A6B"
|
||||||
|
|
||||||
|
if wavelength_nm > 780:
|
||||||
|
return "#7A0018"
|
||||||
|
|
||||||
|
red = 0.0
|
||||||
|
green = 0.0
|
||||||
|
blue = 0.0
|
||||||
|
|
||||||
|
if wavelength_nm < 440:
|
||||||
|
red = -((wavelength_nm - 440) / (440 - 380))
|
||||||
|
blue = 1.0
|
||||||
|
elif wavelength_nm < 490:
|
||||||
|
green = (wavelength_nm - 440) / (490 - 440)
|
||||||
|
blue = 1.0
|
||||||
|
elif wavelength_nm < 510:
|
||||||
|
green = 1.0
|
||||||
|
blue = -((wavelength_nm - 510) / (510 - 490))
|
||||||
|
elif wavelength_nm < 580:
|
||||||
|
red = (wavelength_nm - 510) / (580 - 510)
|
||||||
|
green = 1.0
|
||||||
|
elif wavelength_nm < 645:
|
||||||
|
red = 1.0
|
||||||
|
green = -((wavelength_nm - 645) / (645 - 580))
|
||||||
|
else:
|
||||||
|
red = 1.0
|
||||||
|
|
||||||
|
factor = 1.0
|
||||||
|
if wavelength_nm < 420:
|
||||||
|
factor = 0.3 + 0.7 * ((wavelength_nm - 380) / (420 - 380))
|
||||||
|
elif wavelength_nm > 700:
|
||||||
|
factor = 0.3 + 0.7 * ((780 - wavelength_nm) / (780 - 700))
|
||||||
|
|
||||||
|
gamma = 0.8
|
||||||
|
|
||||||
|
def convert(channel: float) -> int:
|
||||||
|
if channel <= 0.0:
|
||||||
|
return 0
|
||||||
|
return round(255 * pow(channel * factor, gamma))
|
||||||
|
|
||||||
|
return "#{:02X}{:02X}{:02X}".format(convert(red), convert(green), convert(blue))
|
||||||
|
|
||||||
|
|
||||||
|
def build_filter_metadata() -> dict[str, dict[str, object]]:
|
||||||
|
metadata: dict[str, dict[str, object]] = {}
|
||||||
|
for filter_code, wavelength_nm in FILTER_WAVELENGTHS_NM.items():
|
||||||
|
metadata[filter_code] = {
|
||||||
|
"wavelength_nm": wavelength_nm,
|
||||||
|
"color": wavelength_to_hex_color(wavelength_nm),
|
||||||
|
"description": FILTER_DESCRIPTIONS.get(filter_code, filter_code),
|
||||||
|
}
|
||||||
|
return metadata
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
parser = argparse.ArgumentParser(description="Erzeugt Filter-Metadaten mit Wellenlaengen und RGB-Farben.")
|
||||||
|
parser.add_argument("--output", help="Pfad zur JSON-Ausgabedatei.")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
payload = build_filter_metadata()
|
||||||
|
json_text = json.dumps(payload, ensure_ascii=False, indent=2) + "\n"
|
||||||
|
|
||||||
|
if args.output:
|
||||||
|
output_path = Path(args.output)
|
||||||
|
output_path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
output_path.write_text(json_text, encoding="utf-8")
|
||||||
|
else:
|
||||||
|
print(json_text, end="")
|
||||||
|
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
+170
-93
@@ -108,8 +108,9 @@ $summary = [
|
|||||||
'max_mag' => null,
|
'max_mag' => null,
|
||||||
];
|
];
|
||||||
|
|
||||||
if (empty($errors) && !empty($selectedFilters)) {
|
if (empty($errors) && !empty($availableFilters)) {
|
||||||
$placeholders = implode(',', array_fill(0, count($selectedFilters), '?'));
|
$queryFilters = array_column($availableFilters, 'code');
|
||||||
|
$placeholders = implode(',', array_fill(0, count($queryFilters), '?'));
|
||||||
$stmt = $pdo->prepare("
|
$stmt = $pdo->prepare("
|
||||||
SELECT
|
SELECT
|
||||||
`observed_at_utc`,
|
`observed_at_utc`,
|
||||||
@@ -123,7 +124,7 @@ if (empty($errors) && !empty($selectedFilters)) {
|
|||||||
WHERE `filter_code` IN ({$placeholders})
|
WHERE `filter_code` IN ({$placeholders})
|
||||||
ORDER BY `observed_at_utc` ASC, `jd` ASC
|
ORDER BY `observed_at_utc` ASC, `jd` ASC
|
||||||
");
|
");
|
||||||
$stmt->execute($selectedFilters);
|
$stmt->execute($queryFilters);
|
||||||
$rows = $stmt->fetchAll();
|
$rows = $stmt->fetchAll();
|
||||||
|
|
||||||
foreach ($rows as $row) {
|
foreach ($rows as $row) {
|
||||||
@@ -155,23 +156,33 @@ if (empty($errors) && empty($observations)) {
|
|||||||
$errors[] = 'Fuer die ausgewaehlten Filter wurden keine Daten gefunden.';
|
$errors[] = 'Fuer die ausgewaehlten Filter wurden keine Daten gefunden.';
|
||||||
}
|
}
|
||||||
|
|
||||||
$filterPalette = [
|
$filterMetadata = [];
|
||||||
'U' => '#4c6ef5',
|
$filterMetadataPath = __DIR__ . '/../config/filter_colors.json';
|
||||||
'B' => '#1c7ed6',
|
if (is_file($filterMetadataPath)) {
|
||||||
'V' => '#2b8a3e',
|
$filterMetadataRaw = file_get_contents($filterMetadataPath);
|
||||||
'R' => '#c92a2a',
|
$filterMetadataDecoded = is_string($filterMetadataRaw) ? json_decode($filterMetadataRaw, true) : null;
|
||||||
'I' => '#862e9c',
|
if (is_array($filterMetadataDecoded)) {
|
||||||
'CV' => '#495057',
|
$filterMetadata = $filterMetadataDecoded;
|
||||||
'TG' => '#0b7285',
|
}
|
||||||
'TB' => '#364fc7',
|
}
|
||||||
'TR' => '#d9480f',
|
|
||||||
];
|
$filterPalette = [];
|
||||||
|
$filterDescriptions = [];
|
||||||
|
foreach ($filterMetadata as $filterCode => $filterConfig) {
|
||||||
|
if (!is_array($filterConfig)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$filterPalette[$filterCode] = (string) ($filterConfig['color'] ?? '');
|
||||||
|
$filterDescriptions[$filterCode] = (string) ($filterConfig['description'] ?? $filterCode);
|
||||||
|
}
|
||||||
|
|
||||||
$chartDatasets = [];
|
$chartDatasets = [];
|
||||||
foreach ($selectedFilters as $selectedFilter) {
|
foreach ($availableFilters as $availableFilter) {
|
||||||
$chartDatasets[$selectedFilter] = [
|
$filterCode = $availableFilter['code'];
|
||||||
'filter' => $selectedFilter,
|
$chartDatasets[$filterCode] = [
|
||||||
'color' => $filterPalette[$selectedFilter] ?? sprintf('#%06X', crc32($selectedFilter) & 0xFFFFFF),
|
'filter' => $filterCode,
|
||||||
|
'color' => $filterPalette[$filterCode] ?? sprintf('#%06X', crc32($filterCode) & 0xFFFFFF),
|
||||||
'points' => [],
|
'points' => [],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
@@ -247,74 +258,10 @@ foreach ($chartDatasets as $dataset) {
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
$meanSourcePoints = [];
|
|
||||||
foreach ($chartDatasets as $dataset) {
|
|
||||||
foreach ($dataset['points'] as $point) {
|
|
||||||
$meanSourcePoints[] = $point;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
usort($meanSourcePoints, static function (array $a, array $b): int {
|
|
||||||
return strcmp((string) $a['observed_at_utc'], (string) $b['observed_at_utc']);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (count($meanSourcePoints) >= 3) {
|
|
||||||
$windowSize = min(11, count($meanSourcePoints));
|
|
||||||
if ($windowSize % 2 === 0) {
|
|
||||||
$windowSize--;
|
|
||||||
}
|
|
||||||
|
|
||||||
$halfWindow = intdiv($windowSize, 2);
|
|
||||||
$meanX = [];
|
|
||||||
$meanY = [];
|
|
||||||
$meanCustomData = [];
|
|
||||||
|
|
||||||
foreach ($meanSourcePoints as $index => $point) {
|
|
||||||
$start = max(0, $index - $halfWindow);
|
|
||||||
$end = min(count($meanSourcePoints) - 1, $index + $halfWindow);
|
|
||||||
$sum = 0.0;
|
|
||||||
$count = 0;
|
|
||||||
|
|
||||||
for ($i = $start; $i <= $end; $i++) {
|
|
||||||
$sum += (float) $meanSourcePoints[$i]['magnitude'];
|
|
||||||
$count++;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($count === 0) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
$meanValue = $sum / $count;
|
|
||||||
$meanX[] = (string) $point['observed_at_utc'];
|
|
||||||
$meanY[] = $meanValue;
|
|
||||||
$meanCustomData[] = [$count];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!empty($meanX)) {
|
|
||||||
$plotlyTraces[] = [
|
|
||||||
'type' => 'scatter',
|
|
||||||
'mode' => 'lines',
|
|
||||||
'name' => 'Mittelkurve',
|
|
||||||
'x' => $meanX,
|
|
||||||
'y' => $meanY,
|
|
||||||
'line' => [
|
|
||||||
'color' => '#f4d35e',
|
|
||||||
'width' => 3,
|
|
||||||
'dash' => 'solid',
|
|
||||||
'shape' => 'spline',
|
|
||||||
'smoothing' => 0.7,
|
|
||||||
],
|
|
||||||
'customdata' => $meanCustomData,
|
|
||||||
'hovertemplate' => 'Mittelkurve<br>'
|
|
||||||
. 'Magnitude %{y:.3f}<br>'
|
|
||||||
. 'UTC %{x}<br>'
|
|
||||||
. 'Fenster %{customdata[0]} Punkte<extra></extra>',
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$plotlyPayload = [
|
$plotlyPayload = [
|
||||||
'traces' => $plotlyTraces,
|
'traces' => $plotlyTraces,
|
||||||
|
'datasets' => $chartDatasets,
|
||||||
|
'selectedFilters' => $selectedFilters,
|
||||||
'summary' => $summary,
|
'summary' => $summary,
|
||||||
];
|
];
|
||||||
?>
|
?>
|
||||||
@@ -342,6 +289,17 @@ $plotlyPayload = [
|
|||||||
gap: 0.55rem;
|
gap: 0.55rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tcrb-filter-actions {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.6rem;
|
||||||
|
margin-bottom: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tcrb-filter-actions .btn {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
}
|
||||||
|
|
||||||
.tcrb-filter-option {
|
.tcrb-filter-option {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -460,15 +418,20 @@ $plotlyPayload = [
|
|||||||
<div class="tcrb-layout">
|
<div class="tcrb-layout">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h2>Filter</h2>
|
<h2>Filter</h2>
|
||||||
<form method="get" action="" class="form-stack">
|
<form method="get" action="" class="form-stack" id="tcrbFilterForm">
|
||||||
|
<div class="tcrb-filter-actions">
|
||||||
|
<button type="button" class="btn btn-secondary" id="tcrbSelectAll">Alle an</button>
|
||||||
|
<button type="button" class="btn btn-secondary" id="tcrbSelectNone">Alle aus</button>
|
||||||
|
</div>
|
||||||
<div class="tcrb-filter-list">
|
<div class="tcrb-filter-list">
|
||||||
<?php foreach ($availableFilters as $filter): ?>
|
<?php foreach ($availableFilters as $filter): ?>
|
||||||
<?php
|
<?php
|
||||||
$filterCode = $filter['code'];
|
$filterCode = $filter['code'];
|
||||||
$filterColor = $filterPalette[$filterCode] ?? sprintf('#%06X', crc32($filterCode) & 0xFFFFFF);
|
$filterColor = $filterPalette[$filterCode] ?? sprintf('#%06X', crc32($filterCode) & 0xFFFFFF);
|
||||||
|
$filterDescription = $filterDescriptions[$filterCode] ?? $filterCode;
|
||||||
?>
|
?>
|
||||||
<div class="tcrb-filter-option">
|
<div class="tcrb-filter-option" title="<?= h($filterDescription) ?>">
|
||||||
<label>
|
<label title="<?= h($filterDescription) ?>">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
name="filters[]"
|
name="filters[]"
|
||||||
@@ -482,7 +445,6 @@ $plotlyPayload = [
|
|||||||
</div>
|
</div>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
</div>
|
</div>
|
||||||
<button type="submit" class="btn btn-primary">Diagramm aktualisieren</button>
|
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -509,7 +471,7 @@ $plotlyPayload = [
|
|||||||
</div>
|
</div>
|
||||||
<div class="tcrb-summary-box">
|
<div class="tcrb-summary-box">
|
||||||
<strong><?= h($nextPossibleEruptionUtc->format('d.m.Y')) ?></strong>
|
<strong><?= h($nextPossibleEruptionUtc->format('d.m.Y')) ?></strong>
|
||||||
<span>Naechster moeglicher Ausbruch<br>(in <?= number_format($daysUntilNextEruption, 1, ',', '.') ?> Tagen)</span>
|
<span>Nächster möglicher Ausbruch<br>(in <?= number_format($daysUntilNextEruption, 1, ',', '.') ?> Tagen)</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -519,7 +481,8 @@ $plotlyPayload = [
|
|||||||
|
|
||||||
<div class="tcrb-chart-legend">
|
<div class="tcrb-chart-legend">
|
||||||
<?php foreach ($chartDatasets as $dataset): ?>
|
<?php foreach ($chartDatasets as $dataset): ?>
|
||||||
<div class="tcrb-chart-legend-item">
|
<?php $filterDescription = $filterDescriptions[$dataset['filter']] ?? $dataset['filter']; ?>
|
||||||
|
<div class="tcrb-chart-legend-item" title="<?= h($filterDescription) ?>">
|
||||||
<span style="background: <?= h($dataset['color']) ?>"></span>
|
<span style="background: <?= h($dataset['color']) ?>"></span>
|
||||||
<?= h($dataset['filter']) ?>
|
<?= h($dataset['filter']) ?>
|
||||||
</div>
|
</div>
|
||||||
@@ -536,11 +499,21 @@ $plotlyPayload = [
|
|||||||
(function () {
|
(function () {
|
||||||
const payload = <?= json_encode($plotlyPayload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?>;
|
const payload = <?= json_encode($plotlyPayload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?>;
|
||||||
const plotEl = document.getElementById('tcrbPlot');
|
const plotEl = document.getElementById('tcrbPlot');
|
||||||
|
const filterForm = document.getElementById('tcrbFilterForm');
|
||||||
|
const selectAllEl = document.getElementById('tcrbSelectAll');
|
||||||
|
const selectNoneEl = document.getElementById('tcrbSelectNone');
|
||||||
|
const filterCheckboxes = filterForm ? Array.from(filterForm.querySelectorAll('input[name="filters[]"]')) : [];
|
||||||
|
|
||||||
if (!plotEl || typeof Plotly === 'undefined' || !payload || !Array.isArray(payload.traces) || payload.traces.length === 0) {
|
if (!plotEl || typeof Plotly === 'undefined' || !payload || !Array.isArray(payload.datasets) || payload.datasets.length === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (filterForm) {
|
||||||
|
filterForm.addEventListener('submit', function (event) {
|
||||||
|
event.preventDefault();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const layout = {
|
const layout = {
|
||||||
margin: { t: 20, r: 24, b: 56, l: 72 },
|
margin: { t: 20, r: 24, b: 56, l: 72 },
|
||||||
paper_bgcolor: 'rgba(0,0,0,0)',
|
paper_bgcolor: 'rgba(0,0,0,0)',
|
||||||
@@ -556,7 +529,7 @@ $plotlyPayload = [
|
|||||||
zeroline: false
|
zeroline: false
|
||||||
},
|
},
|
||||||
yaxis: {
|
yaxis: {
|
||||||
title: { text: 'Magnitude (kleiner = heller)' },
|
title: { text: 'Magnitude' },
|
||||||
autorange: 'reversed',
|
autorange: 'reversed',
|
||||||
gridcolor: 'rgba(255,255,255,0.12)',
|
gridcolor: 'rgba(255,255,255,0.12)',
|
||||||
linecolor: '#f8fafc',
|
linecolor: '#f8fafc',
|
||||||
@@ -574,7 +547,111 @@ $plotlyPayload = [
|
|||||||
modeBarButtonsToRemove: ['select2d', 'lasso2d']
|
modeBarButtonsToRemove: ['select2d', 'lasso2d']
|
||||||
};
|
};
|
||||||
|
|
||||||
Plotly.newPlot(plotEl, payload.traces, layout, config);
|
function buildDatasetTrace(dataset) {
|
||||||
|
const errorValues = [];
|
||||||
|
let hasErrors = false;
|
||||||
|
|
||||||
|
dataset.points.forEach(function (point) {
|
||||||
|
const errorValue = point.magnitude_error !== null ? Number(point.magnitude_error) : 0;
|
||||||
|
errorValues.push(errorValue);
|
||||||
|
if (errorValue > 0) {
|
||||||
|
hasErrors = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: 'scatter',
|
||||||
|
mode: 'lines+markers',
|
||||||
|
name: dataset.filter,
|
||||||
|
x: dataset.points.map(function (point) { return point.observed_at_utc; }),
|
||||||
|
y: dataset.points.map(function (point) { return Number(point.magnitude); }),
|
||||||
|
line: {
|
||||||
|
color: dataset.color,
|
||||||
|
width: 2,
|
||||||
|
shape: 'spline',
|
||||||
|
smoothing: 0.65
|
||||||
|
},
|
||||||
|
marker: {
|
||||||
|
color: dataset.color,
|
||||||
|
size: 3,
|
||||||
|
line: {
|
||||||
|
color: '#0f172a',
|
||||||
|
width: 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error_y: {
|
||||||
|
type: 'data',
|
||||||
|
array: errorValues,
|
||||||
|
visible: hasErrors,
|
||||||
|
thickness: 1.1,
|
||||||
|
width: 3,
|
||||||
|
color: dataset.color
|
||||||
|
},
|
||||||
|
customdata: dataset.points.map(function (point) {
|
||||||
|
return [
|
||||||
|
point.jd,
|
||||||
|
point.observer_code !== '' ? point.observer_code : '-',
|
||||||
|
point.magnitude_error !== null ? Number(point.magnitude_error).toFixed(3) : '-'
|
||||||
|
];
|
||||||
|
}),
|
||||||
|
hovertemplate: 'Filter %{fullData.name}<br>'
|
||||||
|
+ 'Magnitude %{y:.3f}<br>'
|
||||||
|
+ 'Fehler %{customdata[2]}<br>'
|
||||||
|
+ 'UTC %{x}<br>'
|
||||||
|
+ 'JD %{customdata[0]}<br>'
|
||||||
|
+ 'Observer %{customdata[1]}<extra></extra>'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function getActiveFilterCodes() {
|
||||||
|
return filterCheckboxes
|
||||||
|
.filter(function (checkbox) { return checkbox.checked; })
|
||||||
|
.map(function (checkbox) { return checkbox.value; });
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderPlot() {
|
||||||
|
const activeFilterCodes = getActiveFilterCodes();
|
||||||
|
const activeDatasets = payload.datasets.filter(function (dataset) {
|
||||||
|
return activeFilterCodes.includes(dataset.filter);
|
||||||
|
});
|
||||||
|
const traces = activeDatasets.map(buildDatasetTrace);
|
||||||
|
|
||||||
|
if (traces.length === 0) {
|
||||||
|
Plotly.react(plotEl, [], layout, config);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Plotly.react(plotEl, traces, layout, config);
|
||||||
|
}
|
||||||
|
|
||||||
|
const initiallySelected = Array.isArray(payload.selectedFilters) && payload.selectedFilters.length > 0
|
||||||
|
? payload.selectedFilters
|
||||||
|
: ['V'];
|
||||||
|
|
||||||
|
filterCheckboxes.forEach(function (checkbox) {
|
||||||
|
checkbox.checked = initiallySelected.includes(checkbox.value);
|
||||||
|
checkbox.addEventListener('change', renderPlot);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (selectAllEl) {
|
||||||
|
selectAllEl.addEventListener('click', function () {
|
||||||
|
filterCheckboxes.forEach(function (checkbox) {
|
||||||
|
checkbox.checked = true;
|
||||||
|
});
|
||||||
|
renderPlot();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selectNoneEl) {
|
||||||
|
selectNoneEl.addEventListener('click', function () {
|
||||||
|
filterCheckboxes.forEach(function (checkbox) {
|
||||||
|
checkbox.checked = false;
|
||||||
|
});
|
||||||
|
renderPlot();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
renderPlot();
|
||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ $stateFile = __DIR__ . DIRECTORY_SEPARATOR . 'telegram_cron_state.json';
|
|||||||
$dbConfig = require $rootDir . DIRECTORY_SEPARATOR . 'config' . DIRECTORY_SEPARATOR . 'database.php';
|
$dbConfig = require $rootDir . DIRECTORY_SEPARATOR . 'config' . DIRECTORY_SEPARATOR . 'database.php';
|
||||||
$telegramConfig = require $rootDir . DIRECTORY_SEPARATOR . 'config' . DIRECTORY_SEPARATOR . 'telegram.php';
|
$telegramConfig = require $rootDir . DIRECTORY_SEPARATOR . 'config' . DIRECTORY_SEPARATOR . 'telegram.php';
|
||||||
require_once $rootDir . DIRECTORY_SEPARATOR . 'config' . DIRECTORY_SEPARATOR . 'telegram_message_footer.php';
|
require_once $rootDir . DIRECTORY_SEPARATOR . 'config' . DIRECTORY_SEPARATOR . 'telegram_message_footer.php';
|
||||||
|
$filterColorConfigPath = $rootDir . DIRECTORY_SEPARATOR . 'config' . DIRECTORY_SEPARATOR . 'filter_colors.json';
|
||||||
|
|
||||||
$dsn = sprintf(
|
$dsn = sprintf(
|
||||||
'mysql:host=%s;port=%d;dbname=%s;charset=%s',
|
'mysql:host=%s;port=%d;dbname=%s;charset=%s',
|
||||||
@@ -547,6 +548,12 @@ function formatGermanDateTime(?string $value): string
|
|||||||
|
|
||||||
function buildTcrbVChartSvg(array $observations, array $latestObservation): string
|
function buildTcrbVChartSvg(array $observations, array $latestObservation): string
|
||||||
{
|
{
|
||||||
|
$filterColors = loadFilterColors();
|
||||||
|
$vColor = (string) ($filterColors['V'] ?? '#92FF00');
|
||||||
|
$vStrokeColor = darkenHexColor($vColor, 0.35);
|
||||||
|
$vHighlightColor = lightenHexColor($vColor, 0.35);
|
||||||
|
$vHighlightStrokeColor = lightenHexColor($vColor, 0.6);
|
||||||
|
|
||||||
$width = 1200;
|
$width = 1200;
|
||||||
$height = 700;
|
$height = 700;
|
||||||
$paddingLeft = 90;
|
$paddingLeft = 90;
|
||||||
@@ -639,14 +646,15 @@ function buildTcrbVChartSvg(array $observations, array $latestObservation): stri
|
|||||||
$yTop = $scaleY($magnitude - $error);
|
$yTop = $scaleY($magnitude - $error);
|
||||||
$yBottom = $scaleY($magnitude + $error);
|
$yBottom = $scaleY($magnitude + $error);
|
||||||
$errorBars[] = sprintf(
|
$errorBars[] = sprintf(
|
||||||
'<line x1="%1$.2f" y1="%2$.2f" x2="%1$.2f" y2="%3$.2f" stroke="#8fd694" stroke-width="2" opacity="0.9" />' .
|
'<line x1="%1$.2f" y1="%2$.2f" x2="%1$.2f" y2="%3$.2f" stroke="%6$s" stroke-width="2" opacity="0.9" />' .
|
||||||
'<line x1="%4$.2f" y1="%2$.2f" x2="%5$.2f" y2="%2$.2f" stroke="#8fd694" stroke-width="2" opacity="0.9" />' .
|
'<line x1="%4$.2f" y1="%2$.2f" x2="%5$.2f" y2="%2$.2f" stroke="%6$s" stroke-width="2" opacity="0.9" />' .
|
||||||
'<line x1="%4$.2f" y1="%3$.2f" x2="%5$.2f" y2="%3$.2f" stroke="#8fd694" stroke-width="2" opacity="0.9" />',
|
'<line x1="%4$.2f" y1="%3$.2f" x2="%5$.2f" y2="%3$.2f" stroke="%6$s" stroke-width="2" opacity="0.9" />',
|
||||||
$x,
|
$x,
|
||||||
$yTop,
|
$yTop,
|
||||||
$yBottom,
|
$yBottom,
|
||||||
$x - 7,
|
$x - 7,
|
||||||
$x + 7
|
$x + 7,
|
||||||
|
htmlspecialchars($vHighlightColor, ENT_QUOTES, 'UTF-8')
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -658,8 +666,8 @@ function buildTcrbVChartSvg(array $observations, array $latestObservation): stri
|
|||||||
$x,
|
$x,
|
||||||
$y,
|
$y,
|
||||||
$isLatest ? 7.0 : 4.2,
|
$isLatest ? 7.0 : 4.2,
|
||||||
$isLatest ? '#f4d35e' : '#7bd389',
|
$isLatest ? $vHighlightColor : $vColor,
|
||||||
$isLatest ? '#fff3bf' : '#d9f99d',
|
$isLatest ? $vHighlightStrokeColor : $vHighlightColor,
|
||||||
$isLatest ? 2.6 : 1.4
|
$isLatest ? 2.6 : 1.4
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -691,13 +699,13 @@ function buildTcrbVChartSvg(array $observations, array $latestObservation): stri
|
|||||||
<text x="%d" y="70" fill="#cbd5e1" font-size="19">letzte 2 Monate</text>
|
<text x="%d" y="70" fill="#cbd5e1" font-size="19">letzte 2 Monate</text>
|
||||||
<text x="%d" y="%d" fill="#cbd5e1" font-size="18">%s</text>
|
<text x="%d" y="%d" fill="#cbd5e1" font-size="18">%s</text>
|
||||||
<text x="%d" y="%d" fill="#cbd5e1" font-size="18" text-anchor="end">%s</text>
|
<text x="%d" y="%d" fill="#cbd5e1" font-size="18" text-anchor="end">%s</text>
|
||||||
<text x="%d" y="48" fill="#f4d35e" font-size="22" text-anchor="end">Aktuell: %s%s</text>
|
<text x="%d" y="48" fill="%s" font-size="22" text-anchor="end">Aktuell: %s%s</text>
|
||||||
<text x="%d" y="74" fill="#cbd5e1" font-size="18" text-anchor="end">%s UTC</text>
|
<text x="%d" y="74" fill="#cbd5e1" font-size="18" text-anchor="end">%s UTC</text>
|
||||||
%s
|
%s
|
||||||
<line x1="%d" y1="%d" x2="%d" y2="%d" stroke="#88a4bf" stroke-width="1.6"/>
|
<line x1="%d" y1="%d" x2="%d" y2="%d" stroke="#88a4bf" stroke-width="1.6"/>
|
||||||
<line x1="%d" y1="%d" x2="%d" y2="%d" stroke="#88a4bf" stroke-width="1.6"/>
|
<line x1="%d" y1="%d" x2="%d" y2="%d" stroke="#88a4bf" stroke-width="1.6"/>
|
||||||
%s
|
%s
|
||||||
<path d="%s" fill="none" stroke="#7bd389" stroke-width="3" stroke-linejoin="round" stroke-linecap="round"/>
|
<path d="%s" fill="none" stroke="%s" stroke-width="3" stroke-linejoin="round" stroke-linecap="round"/>
|
||||||
%s
|
%s
|
||||||
</svg>
|
</svg>
|
||||||
SVG,
|
SVG,
|
||||||
@@ -720,6 +728,7 @@ SVG,
|
|||||||
$height - 28,
|
$height - 28,
|
||||||
htmlspecialchars($lastLabel, ENT_QUOTES, 'UTF-8'),
|
htmlspecialchars($lastLabel, ENT_QUOTES, 'UTF-8'),
|
||||||
$width - $paddingRight,
|
$width - $paddingRight,
|
||||||
|
htmlspecialchars($vHighlightColor, ENT_QUOTES, 'UTF-8'),
|
||||||
htmlspecialchars($latestMagnitude, ENT_QUOTES, 'UTF-8'),
|
htmlspecialchars($latestMagnitude, ENT_QUOTES, 'UTF-8'),
|
||||||
htmlspecialchars($latestError, ENT_QUOTES, 'UTF-8'),
|
htmlspecialchars($latestError, ENT_QUOTES, 'UTF-8'),
|
||||||
$width - $paddingRight,
|
$width - $paddingRight,
|
||||||
@@ -735,10 +744,75 @@ SVG,
|
|||||||
$plotBottomY,
|
$plotBottomY,
|
||||||
$errorBarsSvg,
|
$errorBarsSvg,
|
||||||
$pathSvg,
|
$pathSvg,
|
||||||
|
htmlspecialchars($vStrokeColor, ENT_QUOTES, 'UTF-8'),
|
||||||
$pointsSvgMarkup
|
$pointsSvgMarkup
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function loadFilterColors(): array
|
||||||
|
{
|
||||||
|
static $colors = null;
|
||||||
|
global $filterColorConfigPath;
|
||||||
|
|
||||||
|
if (is_array($colors)) {
|
||||||
|
return $colors;
|
||||||
|
}
|
||||||
|
|
||||||
|
$colors = [];
|
||||||
|
if (!is_string($filterColorConfigPath) || !is_file($filterColorConfigPath)) {
|
||||||
|
return $colors;
|
||||||
|
}
|
||||||
|
|
||||||
|
$raw = file_get_contents($filterColorConfigPath);
|
||||||
|
$decoded = is_string($raw) ? json_decode($raw, true) : null;
|
||||||
|
if (!is_array($decoded)) {
|
||||||
|
return $colors;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($decoded as $filterCode => $config) {
|
||||||
|
if (is_array($config) && !empty($config['color'])) {
|
||||||
|
$colors[(string) $filterCode] = (string) $config['color'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $colors;
|
||||||
|
}
|
||||||
|
|
||||||
|
function darkenHexColor(string $hexColor, float $amount): string
|
||||||
|
{
|
||||||
|
return adjustHexColor($hexColor, -abs($amount));
|
||||||
|
}
|
||||||
|
|
||||||
|
function lightenHexColor(string $hexColor, float $amount): string
|
||||||
|
{
|
||||||
|
return adjustHexColor($hexColor, abs($amount));
|
||||||
|
}
|
||||||
|
|
||||||
|
function adjustHexColor(string $hexColor, float $amount): string
|
||||||
|
{
|
||||||
|
$hex = ltrim(trim($hexColor), '#');
|
||||||
|
if (!preg_match('/^[0-9a-fA-F]{6}$/', $hex)) {
|
||||||
|
return '#92FF00';
|
||||||
|
}
|
||||||
|
|
||||||
|
$channels = [
|
||||||
|
hexdec(substr($hex, 0, 2)),
|
||||||
|
hexdec(substr($hex, 2, 2)),
|
||||||
|
hexdec(substr($hex, 4, 2)),
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($channels as $index => $value) {
|
||||||
|
if ($amount >= 0) {
|
||||||
|
$channels[$index] = (int) round($value + ((255 - $value) * $amount));
|
||||||
|
} else {
|
||||||
|
$channels[$index] = (int) round($value * (1 + $amount));
|
||||||
|
}
|
||||||
|
$channels[$index] = max(0, min(255, $channels[$index]));
|
||||||
|
}
|
||||||
|
|
||||||
|
return sprintf('#%02X%02X%02X', $channels[0], $channels[1], $channels[2]);
|
||||||
|
}
|
||||||
|
|
||||||
function buildTcrbObservationSignature(array $observation): string
|
function buildTcrbObservationSignature(array $observation): string
|
||||||
{
|
{
|
||||||
return implode('|', [
|
return implode('|', [
|
||||||
|
|||||||
Reference in New Issue
Block a user