dies und das
This commit is contained in:
@@ -53,6 +53,15 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && (string) ($_POST['ajax'] ?? '') ===
|
|||||||
trim((string) ($_POST['sourceUnit'] ?? '')),
|
trim((string) ($_POST['sourceUnit'] ?? '')),
|
||||||
trim((string) ($_POST['sourceValue'] ?? '')),
|
trim((string) ($_POST['sourceValue'] ?? '')),
|
||||||
]);
|
]);
|
||||||
|
} elseif ($kind === 'tools') {
|
||||||
|
$pythonResult = runPythonApi('astronomical_conversions', [
|
||||||
|
'tools',
|
||||||
|
trim((string) ($_POST['tool'] ?? '')),
|
||||||
|
trim((string) ($_POST['dateTimeLocal'] ?? '')),
|
||||||
|
trim((string) ($_POST['latitude'] ?? '')),
|
||||||
|
trim((string) ($_POST['longitude'] ?? '')),
|
||||||
|
trim((string) ($_POST['timeZone'] ?? '')),
|
||||||
|
]);
|
||||||
} else {
|
} else {
|
||||||
echo json_encode([
|
echo json_encode([
|
||||||
'ok' => false,
|
'ok' => false,
|
||||||
@@ -79,6 +88,15 @@ $timeLocationName = !empty($currentLocation['name']) ? (string) $currentLocation
|
|||||||
$timeLocationLongitude = isset($currentLocation['longitude']) && is_numeric($currentLocation['longitude'])
|
$timeLocationLongitude = isset($currentLocation['longitude']) && is_numeric($currentLocation['longitude'])
|
||||||
? (float) $currentLocation['longitude']
|
? (float) $currentLocation['longitude']
|
||||||
: null;
|
: null;
|
||||||
|
$toolLocationName = !empty($currentLocation['name']) ? (string) $currentLocation['name'] : 'Standard-Standort';
|
||||||
|
$toolLocationLatitude = isset($currentLocation['latitude']) && is_numeric($currentLocation['latitude'])
|
||||||
|
? (float) $currentLocation['latitude']
|
||||||
|
: 50.3597;
|
||||||
|
$toolLocationLongitude = isset($currentLocation['longitude']) && is_numeric($currentLocation['longitude'])
|
||||||
|
? (float) $currentLocation['longitude']
|
||||||
|
: 11.1907;
|
||||||
|
$toolLocationTimezone = !empty($currentLocation['timezone']) ? (string) $currentLocation['timezone'] : 'Europe/Berlin';
|
||||||
|
$toolDefaultLocalDateTime = (new DateTimeImmutable('now', new DateTimeZone($toolLocationTimezone)))->format('Y-m-d\TH:i');
|
||||||
?>
|
?>
|
||||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/flatpickr/dist/flatpickr.min.css">
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/flatpickr/dist/flatpickr.min.css">
|
||||||
|
|
||||||
@@ -228,6 +246,7 @@ $timeLocationLongitude = isset($currentLocation['longitude']) && is_numeric($cur
|
|||||||
<button type="button" class="astro-conv-tab" data-tab-target="timeTab" role="tab" aria-selected="false">Zeit</button>
|
<button type="button" class="astro-conv-tab" data-tab-target="timeTab" role="tab" aria-selected="false">Zeit</button>
|
||||||
<button type="button" class="astro-conv-tab" data-tab-target="coordinatesTab" role="tab" aria-selected="false">Koordinaten</button>
|
<button type="button" class="astro-conv-tab" data-tab-target="coordinatesTab" role="tab" aria-selected="false">Koordinaten</button>
|
||||||
<button type="button" class="astro-conv-tab" data-tab-target="angleTab" role="tab" aria-selected="false">Winkel</button>
|
<button type="button" class="astro-conv-tab" data-tab-target="angleTab" role="tab" aria-selected="false">Winkel</button>
|
||||||
|
<button type="button" class="astro-conv-tab" data-tab-target="toolsTab" role="tab" aria-selected="false">Diverse Tools</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<section class="card astro-conv-panel astro-conv-tab-panel is-active" id="distanceTab" role="tabpanel">
|
<section class="card astro-conv-panel astro-conv-tab-panel is-active" id="distanceTab" role="tabpanel">
|
||||||
@@ -497,6 +516,58 @@ $timeLocationLongitude = isset($currentLocation['longitude']) && is_numeric($cur
|
|||||||
<button type="button" class="btn btn-primary" id="angleArcsecond">1 Bogensekunde</button>
|
<button type="button" class="btn btn-primary" id="angleArcsecond">1 Bogensekunde</button>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<section class="card astro-conv-panel astro-conv-tab-panel" id="toolsTab" role="tabpanel">
|
||||||
|
<h2>Diverse Tools</h2>
|
||||||
|
<p>Sonnenstand für den Standard-Standort aus dem Header berechnen. Datum und Uhrzeit werden in der lokalen Zeitzone des Standorts interpretiert.</p>
|
||||||
|
|
||||||
|
<div class="astro-conv-grid" id="toolsConverter">
|
||||||
|
<div class="astro-conv-field">
|
||||||
|
<label for="toolsDateTimeLocal">Datum und Uhrzeit lokal</label>
|
||||||
|
<input class="astro-conv-input" id="toolsDateTimeLocal" type="datetime-local" value="<?= h($toolDefaultLocalDateTime) ?>">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="astro-conv-field">
|
||||||
|
<label for="toolsLocationName">Standort</label>
|
||||||
|
<input class="astro-conv-input" id="toolsLocationName" type="text" value="<?= h($toolLocationName) ?>" readonly>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="astro-conv-field">
|
||||||
|
<label for="toolsTimeZone">Zeitzone</label>
|
||||||
|
<input class="astro-conv-input" id="toolsTimeZone" type="text" value="<?= h($toolLocationTimezone) ?>" readonly>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="astro-conv-field">
|
||||||
|
<label for="toolsLatitude">Breitengrad</label>
|
||||||
|
<input class="astro-conv-input" id="toolsLatitude" type="text" value="<?= h(number_format($toolLocationLatitude, 4, '.', '')) ?>" readonly>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="astro-conv-field">
|
||||||
|
<label for="toolsLongitude">Längengrad</label>
|
||||||
|
<input class="astro-conv-input" id="toolsLongitude" type="text" value="<?= h(number_format($toolLocationLongitude, 4, '.', '')) ?>" readonly>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="astro-conv-field">
|
||||||
|
<label for="toolsDateTimeUtc">UTC</label>
|
||||||
|
<input class="astro-conv-input" id="toolsDateTimeUtc" type="text" readonly>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="astro-conv-field">
|
||||||
|
<label for="toolsSunAltitude">Sonnenhöhe Alt (°)</label>
|
||||||
|
<input class="astro-conv-input" id="toolsSunAltitude" type="text" readonly>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="astro-conv-field">
|
||||||
|
<label for="toolsSunAzimuth">Azimut Azi (°)</label>
|
||||||
|
<input class="astro-conv-input" id="toolsSunAzimuth" type="text" readonly>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="astro-conv-actions">
|
||||||
|
<button type="button" class="btn btn-secondary" id="toolsClear">Leeren</button>
|
||||||
|
<button type="button" class="btn btn-primary" id="toolsNow">Jetzt am Standort</button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="js/number_to_german_words.js"></script>
|
<script src="js/number_to_german_words.js"></script>
|
||||||
@@ -509,6 +580,12 @@ $timeLocationLongitude = isset($currentLocation['longitude']) && is_numeric($cur
|
|||||||
name: <?= json_encode($timeLocationName, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?>,
|
name: <?= json_encode($timeLocationName, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?>,
|
||||||
longitude: <?= $timeLocationLongitude !== null ? json_encode($timeLocationLongitude) : 'null' ?>
|
longitude: <?= $timeLocationLongitude !== null ? json_encode($timeLocationLongitude) : 'null' ?>
|
||||||
};
|
};
|
||||||
|
const USER_TOOL_LOCATION = {
|
||||||
|
name: <?= json_encode($toolLocationName, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?>,
|
||||||
|
latitude: <?= json_encode($toolLocationLatitude) ?>,
|
||||||
|
longitude: <?= json_encode($toolLocationLongitude) ?>,
|
||||||
|
timeZone: <?= json_encode($toolLocationTimezone, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?>
|
||||||
|
};
|
||||||
const SPEED_OF_LIGHT_KM_S = 299792.458;
|
const SPEED_OF_LIGHT_KM_S = 299792.458;
|
||||||
const H0 = 70;
|
const H0 = 70;
|
||||||
const OMEGA_M = 0.3;
|
const OMEGA_M = 0.3;
|
||||||
@@ -570,6 +647,16 @@ $timeLocationLongitude = isset($currentLocation['longitude']) && is_numeric($cur
|
|||||||
arcsec: document.getElementById('angleArcsec'),
|
arcsec: document.getElementById('angleArcsec'),
|
||||||
rad: document.getElementById('angleRad')
|
rad: document.getElementById('angleRad')
|
||||||
};
|
};
|
||||||
|
const toolFields = {
|
||||||
|
dateTimeLocal: document.getElementById('toolsDateTimeLocal'),
|
||||||
|
locationName: document.getElementById('toolsLocationName'),
|
||||||
|
timeZone: document.getElementById('toolsTimeZone'),
|
||||||
|
latitude: document.getElementById('toolsLatitude'),
|
||||||
|
longitude: document.getElementById('toolsLongitude'),
|
||||||
|
dateTimeUtc: document.getElementById('toolsDateTimeUtc'),
|
||||||
|
altitude: document.getElementById('toolsSunAltitude'),
|
||||||
|
azimuth: document.getElementById('toolsSunAzimuth')
|
||||||
|
};
|
||||||
let isSyncingTimePicker = false;
|
let isSyncingTimePicker = false;
|
||||||
let timeUtcPicker = null;
|
let timeUtcPicker = null;
|
||||||
let distanceRequestId = 0;
|
let distanceRequestId = 0;
|
||||||
@@ -577,6 +664,7 @@ $timeLocationLongitude = isset($currentLocation['longitude']) && is_numeric($cur
|
|||||||
let timeRequestId = 0;
|
let timeRequestId = 0;
|
||||||
let coordinateRequestId = 0;
|
let coordinateRequestId = 0;
|
||||||
let angleRequestId = 0;
|
let angleRequestId = 0;
|
||||||
|
let toolRequestId = 0;
|
||||||
|
|
||||||
const DECIMAL_DIVISION_PRECISION = 32;
|
const DECIMAL_DIVISION_PRECISION = 32;
|
||||||
const pow10Cache = [1n];
|
const pow10Cache = [1n];
|
||||||
@@ -1573,6 +1661,71 @@ $timeLocationLongitude = isset($currentLocation['longitude']) && is_numeric($cur
|
|||||||
updateAngleFrom('arcsec');
|
updateAngleFrom('arcsec');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const clearToolOutputs = function () {
|
||||||
|
toolFields.dateTimeUtc.value = '';
|
||||||
|
toolFields.altitude.value = '';
|
||||||
|
toolFields.azimuth.value = '';
|
||||||
|
updateTooltip(toolFields.dateTimeUtc);
|
||||||
|
updateTooltip(toolFields.altitude);
|
||||||
|
updateTooltip(toolFields.azimuth);
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateSolarTool = async function () {
|
||||||
|
const localDateTime = toolFields.dateTimeLocal.value.trim();
|
||||||
|
if (localDateTime === '') {
|
||||||
|
clearToolOutputs();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const requestId = ++toolRequestId;
|
||||||
|
try {
|
||||||
|
const result = await requestConversion({
|
||||||
|
kind: 'tools',
|
||||||
|
tool: 'solar_altaz',
|
||||||
|
dateTimeLocal: localDateTime,
|
||||||
|
latitude: USER_TOOL_LOCATION.latitude,
|
||||||
|
longitude: USER_TOOL_LOCATION.longitude,
|
||||||
|
timeZone: USER_TOOL_LOCATION.timeZone
|
||||||
|
});
|
||||||
|
|
||||||
|
if (requestId !== toolRequestId || !result.valid) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
toolFields.dateTimeUtc.value = typeof result.fields.utc === 'string' ? result.fields.utc : '';
|
||||||
|
toolFields.altitude.value = typeof result.fields.altitude === 'string' ? result.fields.altitude : '';
|
||||||
|
toolFields.azimuth.value = typeof result.fields.azimuth === 'string' ? result.fields.azimuth : '';
|
||||||
|
updateTooltip(toolFields.dateTimeUtc);
|
||||||
|
updateTooltip(toolFields.altitude);
|
||||||
|
updateTooltip(toolFields.azimuth);
|
||||||
|
} catch (error) {
|
||||||
|
clearToolOutputs();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
toolFields.dateTimeLocal.addEventListener('input', updateSolarTool);
|
||||||
|
|
||||||
|
document.getElementById('toolsClear').addEventListener('click', function () {
|
||||||
|
toolFields.dateTimeLocal.value = '';
|
||||||
|
clearToolOutputs();
|
||||||
|
toolFields.dateTimeLocal.focus();
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('toolsNow').addEventListener('click', function () {
|
||||||
|
const now = new Date();
|
||||||
|
const formatter = new Intl.DateTimeFormat('sv-SE', {
|
||||||
|
timeZone: USER_TOOL_LOCATION.timeZone,
|
||||||
|
year: 'numeric',
|
||||||
|
month: '2-digit',
|
||||||
|
day: '2-digit',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
hourCycle: 'h23'
|
||||||
|
});
|
||||||
|
toolFields.dateTimeLocal.value = formatter.format(now).replace(' ', 'T');
|
||||||
|
updateSolarTool();
|
||||||
|
});
|
||||||
|
|
||||||
const tabs = Array.from(document.querySelectorAll('.astro-conv-tab'));
|
const tabs = Array.from(document.querySelectorAll('.astro-conv-tab'));
|
||||||
const tabPanels = Array.from(document.querySelectorAll('.astro-conv-tab-panel'));
|
const tabPanels = Array.from(document.querySelectorAll('.astro-conv-tab-panel'));
|
||||||
|
|
||||||
@@ -1599,6 +1752,7 @@ $timeLocationLongitude = isset($currentLocation['longitude']) && is_numeric($cur
|
|||||||
updateTimeFrom('utc');
|
updateTimeFrom('utc');
|
||||||
updateCoordinatesFrom('equatorial');
|
updateCoordinatesFrom('equatorial');
|
||||||
updateAngleFrom('deg');
|
updateAngleFrom('deg');
|
||||||
|
updateSolarTool();
|
||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -363,6 +363,8 @@ body.daemmerungszeiten-page main > .container {
|
|||||||
<label for="timezone">Zeitzone</label>
|
<label for="timezone">Zeitzone</label>
|
||||||
<input id="timezone" type="text" list="timezoneSuggestions" value="<?= htmlspecialchars($initialLocation['timezone'], ENT_QUOTES, 'UTF-8') ?>">
|
<input id="timezone" type="text" list="timezoneSuggestions" value="<?= htmlspecialchars($initialLocation['timezone'], ENT_QUOTES, 'UTF-8') ?>">
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="twilight-inline-fields">
|
||||||
<div class="twilight-field">
|
<div class="twilight-field">
|
||||||
<label for="startDate">Von</label>
|
<label for="startDate">Von</label>
|
||||||
<input id="startDate" type="date" value="<?= htmlspecialchars($defaultStartDate, ENT_QUOTES, 'UTF-8') ?>">
|
<input id="startDate" type="date" value="<?= htmlspecialchars($defaultStartDate, ENT_QUOTES, 'UTF-8') ?>">
|
||||||
@@ -392,7 +394,6 @@ body.daemmerungszeiten-page main > .container {
|
|||||||
</div>
|
</div>
|
||||||
<div class="twilight-button-row">
|
<div class="twilight-button-row">
|
||||||
<button class="twilight-btn" id="renderButton" type="button">Grafik neu berechnen</button>
|
<button class="twilight-btn" id="renderButton" type="button">Grafik neu berechnen</button>
|
||||||
<button class="twilight-btn secondary" id="geoButton" type="button">Browser-Standort</button>
|
|
||||||
<button class="twilight-btn secondary" id="exportPngButton" type="button">Als PNG exportieren</button>
|
<button class="twilight-btn secondary" id="exportPngButton" type="button">Als PNG exportieren</button>
|
||||||
</div>
|
</div>
|
||||||
<datalist id="timezoneSuggestions">
|
<datalist id="timezoneSuggestions">
|
||||||
@@ -491,7 +492,6 @@ const fieldEndDate = document.getElementById('endDate');
|
|||||||
const fieldOverlay = document.getElementById('lineOverlay');
|
const fieldOverlay = document.getElementById('lineOverlay');
|
||||||
const fieldPngScale = document.getElementById('pngScaleSelect');
|
const fieldPngScale = document.getElementById('pngScaleSelect');
|
||||||
const renderButton = document.getElementById('renderButton');
|
const renderButton = document.getElementById('renderButton');
|
||||||
const geoButton = document.getElementById('geoButton');
|
|
||||||
const exportPngButton = document.getElementById('exportPngButton');
|
const exportPngButton = document.getElementById('exportPngButton');
|
||||||
|
|
||||||
let currentRenderToken = 0;
|
let currentRenderToken = 0;
|
||||||
@@ -1532,6 +1532,76 @@ function phaseColorCss(phaseIndex, alpha) {
|
|||||||
return `rgba(${color[0]}, ${color[1]}, ${color[2]}, ${alpha})`;
|
return `rgba(${color[0]}, ${color[1]}, ${color[2]}, ${alpha})`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function drawExportHeader(context, width, chart) {
|
||||||
|
const title = 'Dämmerungszeiten';
|
||||||
|
const subtitle = `${formatDateGerman(chart.state.dayDates[0], chart.state.timeZone)} bis ${formatDateGerman(chart.state.dayDates[chart.totalDays - 1], chart.state.timeZone)}`;
|
||||||
|
const locationLine = `Standort: ${chart.state.name} · ${chart.state.latitude.toFixed(4)}°, ${chart.state.longitude.toFixed(4)}°`;
|
||||||
|
|
||||||
|
context.save();
|
||||||
|
context.textAlign = 'center';
|
||||||
|
context.textBaseline = 'top';
|
||||||
|
|
||||||
|
context.fillStyle = '#f1d88a';
|
||||||
|
context.font = '700 24px Segoe UI, sans-serif';
|
||||||
|
context.fillText(title, width / 2, 18);
|
||||||
|
|
||||||
|
context.fillStyle = 'rgba(221, 227, 245, 0.80)';
|
||||||
|
context.font = '14px Segoe UI, sans-serif';
|
||||||
|
context.fillText(subtitle, width / 2, 50);
|
||||||
|
|
||||||
|
context.fillStyle = 'rgba(221, 227, 245, 0.68)';
|
||||||
|
context.font = '13px Segoe UI, sans-serif';
|
||||||
|
context.fillText(locationLine, width / 2, 72);
|
||||||
|
context.restore();
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawExportLegend(context, width, startY) {
|
||||||
|
const items = PHASES.map((phase) => ({
|
||||||
|
label: phase.label,
|
||||||
|
color: `rgba(${phase.color[0]}, ${phase.color[1]}, ${phase.color[2]}, ${phase.color[3] / 255})`
|
||||||
|
}));
|
||||||
|
const paddingX = 20;
|
||||||
|
const rowGap = 14;
|
||||||
|
const itemGap = 18;
|
||||||
|
const swatchSize = 14;
|
||||||
|
const textGap = 8;
|
||||||
|
const lineHeight = 18;
|
||||||
|
|
||||||
|
context.save();
|
||||||
|
context.font = '14px Segoe UI, sans-serif';
|
||||||
|
context.textAlign = 'left';
|
||||||
|
context.textBaseline = 'middle';
|
||||||
|
|
||||||
|
let cursorX = paddingX;
|
||||||
|
let cursorY = startY + 16;
|
||||||
|
|
||||||
|
items.forEach((item) => {
|
||||||
|
const textWidth = context.measureText(item.label).width;
|
||||||
|
const itemWidth = swatchSize + textGap + textWidth;
|
||||||
|
|
||||||
|
if (cursorX + itemWidth > width - paddingX) {
|
||||||
|
cursorX = paddingX;
|
||||||
|
cursorY += lineHeight + rowGap;
|
||||||
|
}
|
||||||
|
|
||||||
|
context.fillStyle = item.color;
|
||||||
|
context.strokeStyle = 'rgba(255,255,255,0.14)';
|
||||||
|
context.lineWidth = 1;
|
||||||
|
context.beginPath();
|
||||||
|
context.roundRect(cursorX, cursorY - (swatchSize / 2), swatchSize, swatchSize, 3);
|
||||||
|
context.fill();
|
||||||
|
context.stroke();
|
||||||
|
|
||||||
|
context.fillStyle = 'rgba(221, 227, 245, 0.82)';
|
||||||
|
context.fillText(item.label, cursorX + swatchSize + textGap, cursorY);
|
||||||
|
cursorX += itemWidth + itemGap;
|
||||||
|
});
|
||||||
|
|
||||||
|
context.restore();
|
||||||
|
|
||||||
|
return cursorY + 16;
|
||||||
|
}
|
||||||
|
|
||||||
function downloadBlob(filename, blob) {
|
function downloadBlob(filename, blob) {
|
||||||
const url = URL.createObjectURL(blob);
|
const url = URL.createObjectURL(blob);
|
||||||
const link = document.createElement('a');
|
const link = document.createElement('a');
|
||||||
@@ -1554,13 +1624,26 @@ function exportCurrentChartAsPng() {
|
|||||||
.toLowerCase()
|
.toLowerCase()
|
||||||
.replace(/[^a-z0-9]+/g, '-')
|
.replace(/[^a-z0-9]+/g, '-')
|
||||||
.replace(/^-+|-+$/g, '');
|
.replace(/^-+|-+$/g, '');
|
||||||
|
const headerHeight = 98;
|
||||||
|
const legendHeight = 92;
|
||||||
|
const chartCanvas = document.createElement('canvas');
|
||||||
|
chartCanvas.width = Math.max(1, Math.floor(canvas.clientWidth * scale));
|
||||||
|
chartCanvas.height = Math.max(1, Math.floor(canvas.clientHeight * scale));
|
||||||
|
const chartContext = chartCanvas.getContext('2d');
|
||||||
const exportCanvas = document.createElement('canvas');
|
const exportCanvas = document.createElement('canvas');
|
||||||
exportCanvas.width = Math.max(1, Math.floor(canvas.clientWidth * scale));
|
exportCanvas.width = Math.max(1, Math.floor(canvas.clientWidth * scale));
|
||||||
exportCanvas.height = Math.max(1, Math.floor(canvas.clientHeight * scale));
|
exportCanvas.height = Math.max(1, Math.floor((headerHeight + canvas.clientHeight + legendHeight) * scale));
|
||||||
const exportContext = exportCanvas.getContext('2d');
|
const exportContext = exportCanvas.getContext('2d');
|
||||||
|
|
||||||
|
chartContext.scale(scale, scale);
|
||||||
|
drawChart(currentChart, chartContext, canvas.clientWidth, canvas.clientHeight, scale, '#121726');
|
||||||
|
|
||||||
exportContext.scale(scale, scale);
|
exportContext.scale(scale, scale);
|
||||||
drawChart(currentChart, exportContext, canvas.clientWidth, canvas.clientHeight, scale, '#121726');
|
exportContext.fillStyle = '#121726';
|
||||||
|
exportContext.fillRect(0, 0, canvas.clientWidth, headerHeight + canvas.clientHeight + legendHeight);
|
||||||
|
drawExportHeader(exportContext, canvas.clientWidth, currentChart);
|
||||||
|
exportContext.drawImage(chartCanvas, 0, headerHeight, canvas.clientWidth, canvas.clientHeight);
|
||||||
|
drawExportLegend(exportContext, canvas.clientWidth, headerHeight + canvas.clientHeight + 10);
|
||||||
|
|
||||||
exportCanvas.toBlob((blob) => {
|
exportCanvas.toBlob((blob) => {
|
||||||
if (!blob) {
|
if (!blob) {
|
||||||
@@ -1588,7 +1671,6 @@ async function renderTwilightChart() {
|
|||||||
|
|
||||||
renderStatus.textContent = 'Berechnung startet …';
|
renderStatus.textContent = 'Berechnung startet …';
|
||||||
renderButton.disabled = true;
|
renderButton.disabled = true;
|
||||||
geoButton.disabled = true;
|
|
||||||
exportPngButton.disabled = true;
|
exportPngButton.disabled = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -1619,7 +1701,6 @@ async function renderTwilightChart() {
|
|||||||
} finally {
|
} finally {
|
||||||
if (token === currentRenderToken) {
|
if (token === currentRenderToken) {
|
||||||
renderButton.disabled = false;
|
renderButton.disabled = false;
|
||||||
geoButton.disabled = false;
|
|
||||||
exportPngButton.disabled = false;
|
exportPngButton.disabled = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1679,37 +1760,6 @@ fieldPreset.addEventListener('change', () => {
|
|||||||
renderButton.addEventListener('click', renderTwilightChart);
|
renderButton.addEventListener('click', renderTwilightChart);
|
||||||
exportPngButton.addEventListener('click', exportCurrentChartAsPng);
|
exportPngButton.addEventListener('click', exportCurrentChartAsPng);
|
||||||
|
|
||||||
geoButton.addEventListener('click', () => {
|
|
||||||
if (!navigator.geolocation) {
|
|
||||||
alert('Die Standortbestimmung wird von diesem Browser nicht unterstützt.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
geoButton.disabled = true;
|
|
||||||
renderStatus.textContent = 'Standort wird ermittelt …';
|
|
||||||
|
|
||||||
navigator.geolocation.getCurrentPosition(
|
|
||||||
(position) => {
|
|
||||||
selectedLocation = {
|
|
||||||
...selectedLocation,
|
|
||||||
latitude: Number(position.coords.latitude.toFixed(4)),
|
|
||||||
longitude: Number(position.coords.longitude.toFixed(4))
|
|
||||||
};
|
|
||||||
fieldPreset.value = '';
|
|
||||||
fieldName.value = 'Browser-Standort';
|
|
||||||
fieldTimezone.value = Intl.DateTimeFormat().resolvedOptions().timeZone || 'Europe/Berlin';
|
|
||||||
geoButton.disabled = false;
|
|
||||||
renderTwilightChart();
|
|
||||||
},
|
|
||||||
(error) => {
|
|
||||||
geoButton.disabled = false;
|
|
||||||
renderStatus.textContent = 'Standort konnte nicht geladen werden';
|
|
||||||
alert(`Standort konnte nicht ermittelt werden: ${error.message}`);
|
|
||||||
},
|
|
||||||
{ enableHighAccuracy: true, timeout: 10000 }
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
canvas.addEventListener('mousemove', showTooltip);
|
canvas.addEventListener('mousemove', showTooltip);
|
||||||
canvas.addEventListener('mouseleave', hideTooltip);
|
canvas.addEventListener('mouseleave', hideTooltip);
|
||||||
window.addEventListener('resize', () => {
|
window.addEventListener('resize', () => {
|
||||||
|
|||||||
@@ -258,6 +258,17 @@ def action_astronomical_conversions(args: list[str]) -> dict:
|
|||||||
)
|
)
|
||||||
payload["sourceUnit"] = args[1]
|
payload["sourceUnit"] = args[1]
|
||||||
payload["sourceValue"] = args[2]
|
payload["sourceValue"] = args[2]
|
||||||
|
elif kind == "tools":
|
||||||
|
if len(args) != 6:
|
||||||
|
fail(
|
||||||
|
"Aktion astronomical_conversions fuer tools erwartet 6 Argumente: kind tool dateTimeLocal latitude longitude timeZone",
|
||||||
|
extra={"argv": args},
|
||||||
|
)
|
||||||
|
payload["tool"] = args[1]
|
||||||
|
payload["dateTimeLocal"] = args[2]
|
||||||
|
payload["latitude"] = args[3]
|
||||||
|
payload["longitude"] = args[4]
|
||||||
|
payload["timeZone"] = args[5]
|
||||||
else:
|
else:
|
||||||
fail(
|
fail(
|
||||||
"Unbekannter Umrechnungsbereich fuer astronomical_conversions.",
|
"Unbekannter Umrechnungsbereich fuer astronomical_conversions.",
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
import math
|
import math
|
||||||
from datetime import datetime, timedelta, timezone
|
from datetime import datetime, timedelta, timezone
|
||||||
from decimal import Decimal, InvalidOperation, ROUND_HALF_UP, localcontext
|
from decimal import Decimal, InvalidOperation, ROUND_HALF_UP, localcontext
|
||||||
|
from zoneinfo import ZoneInfo
|
||||||
|
|
||||||
import astronomy
|
import astronomy
|
||||||
try:
|
try:
|
||||||
@@ -57,6 +58,7 @@ REDSHIFT_ORDER = [
|
|||||||
TIME_ORDER = ["utc", "ut1", "tai", "jd", "mjd", "unix", "trueSolar", "sidereal"]
|
TIME_ORDER = ["utc", "ut1", "tai", "jd", "mjd", "unix", "trueSolar", "sidereal"]
|
||||||
COORDINATE_ORDER = ["ra", "dec", "galLon", "galLat"]
|
COORDINATE_ORDER = ["ra", "dec", "galLon", "galLat"]
|
||||||
ANGLE_ORDER = ["deg", "arcmin", "arcsec", "rad"]
|
ANGLE_ORDER = ["deg", "arcmin", "arcsec", "rad"]
|
||||||
|
TOOLS_ORDER = ["utc", "altitude", "azimuth"]
|
||||||
ANGLE_TO_DEGREES = {
|
ANGLE_TO_DEGREES = {
|
||||||
"deg": Decimal("1"),
|
"deg": Decimal("1"),
|
||||||
"arcmin": Decimal("0.01666666666666666666666666667"),
|
"arcmin": Decimal("0.01666666666666666666666666667"),
|
||||||
@@ -215,6 +217,27 @@ def parse_utc_datetime(value: str) -> datetime | None:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def parse_local_datetime(value: str, time_zone_name: str) -> datetime | None:
|
||||||
|
text = str(value or "").strip()
|
||||||
|
if text == "":
|
||||||
|
return None
|
||||||
|
|
||||||
|
normalized = text.replace(" ", "T")
|
||||||
|
try:
|
||||||
|
zone = ZoneInfo(time_zone_name)
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
|
||||||
|
for fmt in ("%Y-%m-%dT%H:%M:%S", "%Y-%m-%dT%H:%M"):
|
||||||
|
try:
|
||||||
|
parsed = datetime.strptime(normalized, fmt)
|
||||||
|
return parsed.replace(tzinfo=zone)
|
||||||
|
except ValueError:
|
||||||
|
continue
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
def dt_to_time(dt_utc: datetime) -> astronomy.Time:
|
def dt_to_time(dt_utc: datetime) -> astronomy.Time:
|
||||||
dt_utc = dt_utc.astimezone(timezone.utc)
|
dt_utc = dt_utc.astimezone(timezone.utc)
|
||||||
return astronomy.Time.Make(
|
return astronomy.Time.Make(
|
||||||
@@ -509,6 +532,44 @@ def convert_angle(payload: dict) -> dict:
|
|||||||
return {"valid": True, "fields": fields}
|
return {"valid": True, "fields": fields}
|
||||||
|
|
||||||
|
|
||||||
|
def convert_tools(payload: dict) -> dict:
|
||||||
|
tool = str(payload.get("tool") or "").strip().lower()
|
||||||
|
if tool != "solar_altaz":
|
||||||
|
raise ValueError("Unbekanntes Tool im Bereich Diverse Tools.")
|
||||||
|
|
||||||
|
local_dt = parse_local_datetime(str(payload.get("dateTimeLocal") or ""), str(payload.get("timeZone") or ""))
|
||||||
|
latitude_decimal = parse_decimal_input(payload.get("latitude", ""))
|
||||||
|
longitude_decimal = parse_decimal_input(payload.get("longitude", ""))
|
||||||
|
|
||||||
|
if local_dt is None or latitude_decimal is None or longitude_decimal is None:
|
||||||
|
return {"valid": False, "fields": {key: "" for key in TOOLS_ORDER}}
|
||||||
|
|
||||||
|
latitude = float(latitude_decimal)
|
||||||
|
longitude = float(longitude_decimal)
|
||||||
|
if (
|
||||||
|
not math.isfinite(latitude)
|
||||||
|
or not math.isfinite(longitude)
|
||||||
|
or latitude < -90.0
|
||||||
|
or latitude > 90.0
|
||||||
|
or longitude < -180.0
|
||||||
|
or longitude > 180.0
|
||||||
|
):
|
||||||
|
return {"valid": False, "fields": {key: "" for key in TOOLS_ORDER}}
|
||||||
|
|
||||||
|
dt_utc = local_dt.astimezone(timezone.utc)
|
||||||
|
time_value = dt_to_time(dt_utc)
|
||||||
|
observer = astronomy.Observer(latitude, longitude, 0.0)
|
||||||
|
equator = astronomy.Equator(astronomy.Body.Sun, time_value, observer, True, True)
|
||||||
|
horizon = astronomy.Horizon(time_value, observer, equator.ra, equator.dec, astronomy.Refraction.Normal)
|
||||||
|
|
||||||
|
fields = {
|
||||||
|
"utc": format_utc_datetime(dt_utc),
|
||||||
|
"altitude": format_decimal_german(horizon.altitude, max_fraction_digits=4),
|
||||||
|
"azimuth": format_decimal_german(normalize_angle_degrees(horizon.azimuth), max_fraction_digits=4),
|
||||||
|
}
|
||||||
|
return {"valid": True, "fields": fields}
|
||||||
|
|
||||||
|
|
||||||
def handle_request(payload: dict) -> dict:
|
def handle_request(payload: dict) -> dict:
|
||||||
kind = str(payload.get("kind") or "").strip().lower()
|
kind = str(payload.get("kind") or "").strip().lower()
|
||||||
if kind == "distance":
|
if kind == "distance":
|
||||||
@@ -521,6 +582,8 @@ def handle_request(payload: dict) -> dict:
|
|||||||
result = convert_coordinates(payload)
|
result = convert_coordinates(payload)
|
||||||
elif kind == "angle":
|
elif kind == "angle":
|
||||||
result = convert_angle(payload)
|
result = convert_angle(payload)
|
||||||
|
elif kind == "tools":
|
||||||
|
result = convert_tools(payload)
|
||||||
else:
|
else:
|
||||||
raise ValueError("Unbekannter Umrechnungsbereich.")
|
raise ValueError("Unbekannter Umrechnungsbereich.")
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user