dies und das
This commit is contained in:
@@ -363,6 +363,8 @@ body.daemmerungszeiten-page main > .container {
|
||||
<label for="timezone">Zeitzone</label>
|
||||
<input id="timezone" type="text" list="timezoneSuggestions" value="<?= htmlspecialchars($initialLocation['timezone'], ENT_QUOTES, 'UTF-8') ?>">
|
||||
</div>
|
||||
</div>
|
||||
<div class="twilight-inline-fields">
|
||||
<div class="twilight-field">
|
||||
<label for="startDate">Von</label>
|
||||
<input id="startDate" type="date" value="<?= htmlspecialchars($defaultStartDate, ENT_QUOTES, 'UTF-8') ?>">
|
||||
@@ -392,7 +394,6 @@ body.daemmerungszeiten-page main > .container {
|
||||
</div>
|
||||
<div class="twilight-button-row">
|
||||
<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>
|
||||
</div>
|
||||
<datalist id="timezoneSuggestions">
|
||||
@@ -491,7 +492,6 @@ const fieldEndDate = document.getElementById('endDate');
|
||||
const fieldOverlay = document.getElementById('lineOverlay');
|
||||
const fieldPngScale = document.getElementById('pngScaleSelect');
|
||||
const renderButton = document.getElementById('renderButton');
|
||||
const geoButton = document.getElementById('geoButton');
|
||||
const exportPngButton = document.getElementById('exportPngButton');
|
||||
|
||||
let currentRenderToken = 0;
|
||||
@@ -1532,6 +1532,76 @@ function phaseColorCss(phaseIndex, 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) {
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
@@ -1554,13 +1624,26 @@ function exportCurrentChartAsPng() {
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9]+/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');
|
||||
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');
|
||||
|
||||
chartContext.scale(scale, scale);
|
||||
drawChart(currentChart, chartContext, canvas.clientWidth, canvas.clientHeight, scale, '#121726');
|
||||
|
||||
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) => {
|
||||
if (!blob) {
|
||||
@@ -1588,7 +1671,6 @@ async function renderTwilightChart() {
|
||||
|
||||
renderStatus.textContent = 'Berechnung startet …';
|
||||
renderButton.disabled = true;
|
||||
geoButton.disabled = true;
|
||||
exportPngButton.disabled = true;
|
||||
|
||||
try {
|
||||
@@ -1619,7 +1701,6 @@ async function renderTwilightChart() {
|
||||
} finally {
|
||||
if (token === currentRenderToken) {
|
||||
renderButton.disabled = false;
|
||||
geoButton.disabled = false;
|
||||
exportPngButton.disabled = false;
|
||||
}
|
||||
}
|
||||
@@ -1679,37 +1760,6 @@ fieldPreset.addEventListener('change', () => {
|
||||
renderButton.addEventListener('click', renderTwilightChart);
|
||||
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('mouseleave', hideTooltip);
|
||||
window.addEventListener('resize', () => {
|
||||
|
||||
Reference in New Issue
Block a user