Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/review.yml
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ jobs:
run: |
npx --yes lighthouse@12 http://localhost:8080 \
--chrome-flags="--headless --no-sandbox --disable-gpu" \
--blocked-url-patterns="googletagmanager|google-analytics|doubleclick" \
--output=json --output=html --output-path=./lighthouse \
--only-categories=performance,accessibility,best-practices,seo
- uses: actions/upload-artifact@v4
Expand Down
2 changes: 2 additions & 0 deletions docs/core.js
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,8 @@ window.S = Object.assign({
fpTotal: 'Total',
fpClose: 'Close',
fpPrint: 'Print',
fpPin: 'Pin',
fpUnpin: 'Unpin',
fpFuel: 'Fuel',
tbAircraft: 'Aircraft',
tbGph: 'Gallons per Hour',
Expand Down
2 changes: 2 additions & 0 deletions docs/he/strings.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ window.S = {
fpReturn: 'ΧžΧ‘ΧœΧ•Χœ Χ—Χ–Χ¨Χ”',
fpClose: 'Χ‘Χ’Χ•Χ¨',
fpPrint: 'Χ”Χ“Χ€Χ‘',
fpPin: 'Χ Χ’Χ₯',
fpUnpin: 'Χ©Χ—Χ¨Χ¨',
fpFuel: 'Χ“ΧœΧ§',
tbAircraft: 'Χ›ΧœΧ™ Χ˜Χ™Χ‘',
tbGph: 'Χ’ΧœΧ•Χ Χ™Χ ΧœΧ©Χ’Χ”',
Expand Down
263 changes: 263 additions & 0 deletions docs/io.js
Original file line number Diff line number Diff line change
Expand Up @@ -837,6 +837,143 @@ function showFlightPlan() {
box.appendChild(btns);
addModalCloseX(box, closeFlightPlan);

// Pin button β€” toggles flight plan visibility on PNG export.
const pinBtn = document.createElement('button');
pinBtn.className = 'modal-pin';
pinBtn.type = 'button';
pinBtn.textContent = '\uD83D\uDCCC';
const PLANPIN_KEY = 'navaid.planPin';
let planPinned = localStorage.getItem(PLANPIN_KEY) === '1';
pinBtn.title = planPinned ? S.fpUnpin : S.fpPin;
if (planPinned) { pinBtn.classList.add('active'); box.classList.add('pinned'); applyPlanFontSize(); }
if (planPinned) printBtn.style.display = 'none';
pinBtn.onclick = function () {
planPinned = !planPinned;
pinBtn.classList.toggle('active', planPinned);
box.classList.toggle('pinned', planPinned);
pinBtn.title = planPinned ? S.fpUnpin : S.fpPin;
printBtn.style.display = planPinned ? 'none' : '';
try { localStorage.setItem(PLANPIN_KEY, planPinned ? '1' : '0'); } catch (e) {}
if (planPinned) {
applyPlanFontSize();
} else {
box.style.width = '';
box.style.height = '';
box.style.removeProperty('--fp-font-size');
try { localStorage.removeItem('navaid.planW'); localStorage.removeItem('navaid.planH'); } catch (e) {}
}
};
box.appendChild(pinBtn);

// Resize handles β€” all four corners.
function computePlanFontSize() {
var numRows = (state.legs || []).length + 2;
if (numRows < 1) return 13;
var titleEl = box.querySelector('.modal-title');
var acEl = box.querySelector('.fp-aircraft');
var btnsEl = box.querySelector('.modal-btns');
var titleH = titleEl ? titleEl.offsetHeight : 40;
var acH = acEl ? acEl.offsetHeight : 0;
var btnsH = btnsEl ? btnsEl.offsetHeight : 36;
var avail = box.offsetHeight - titleH - acH - btnsH - 28;
if (avail < 6) return 2;
var rowH = avail / numRows;
return Math.max(2, Math.min(13, rowH - 0.3));
}
function applyPlanFontSize() {
if (!box.classList.contains('pinned')) return;
box.style.setProperty('--fp-font-size', computePlanFontSize() + 'px');
}
var rCleanups = [];
function makeCornerHandle(corner) {
var el = document.createElement('div');
el.className = 'resize-handle corner-' + corner;
box.appendChild(el);
var rx = 0, ry = 0, rw = 0, rh = 0, rl = 0, rt = 0, rDrag = false;
function rStart(cx, cy) {
var r = box.getBoundingClientRect();
rx = cx; ry = cy;
rw = r.width; rh = r.height;
rl = r.left; rt = r.top;
rDrag = true;
}
function clampViewport(l, t, w, h) {
var maxL = window.innerWidth - w;
var maxT = window.innerHeight - h;
l = Math.max(0, Math.min(maxL, l));
t = Math.max(0, Math.min(maxT, t));
return { l: l, t: t, w: w, h: h };
}
function rMove(cx, cy) {
if (!rDrag) return;
var dx = cx - rx, dy = cy - ry;
var nl = rl, nt = rt, nw = rw, nh = rh;
if (corner.indexOf('e') !== -1) nw = Math.max(300, rw + dx);
if (corner.indexOf('w') !== -1) { nw = Math.max(300, rw - dx); nl = rl + (rw - nw); }
if (corner.indexOf('s') !== -1) nh = Math.max(200, rh + dy);
if (corner.indexOf('n') !== -1) { nh = Math.max(200, rh - dy); nt = rt + (rh - nh); }
var c = clampViewport(nl, nt, nw, nh);
box.style.left = c.l + 'px';
box.style.top = c.t + 'px';
box.style.width = c.w + 'px';
box.style.height = c.h + 'px';
box.style.margin = '0';
applyPlanFontSize();
}
function rEnd() {
if (!rDrag) return;
rDrag = false;
try { localStorage.setItem('navaid.planW', box.offsetWidth); localStorage.setItem('navaid.planH', box.offsetHeight);
var rect = box.getBoundingClientRect(); localStorage.setItem('navaid.fpPos', JSON.stringify({ x: rect.left, y: rect.top })); } catch (e) {}
}
function rTouchStart(e) {
if (e.touches.length !== 1) return;
e.preventDefault();
rStart(e.touches[0].clientX, e.touches[0].clientY);
}
function rTouchMove(e) {
if (!rDrag || e.touches.length !== 1) return;
e.preventDefault();
rMove(e.touches[0].clientX, e.touches[0].clientY);
}
el.addEventListener('mousedown', function (e) {
e.preventDefault();
rStart(e.clientX, e.clientY);
var onMove = function (ev) { rMove(ev.clientX, ev.clientY); };
var onUp = function () { rEnd(); window.removeEventListener('mousemove', onMove); window.removeEventListener('mouseup', onUp); };
window.addEventListener('mousemove', onMove);
window.addEventListener('mouseup', onUp);
});
el.addEventListener('touchstart', rTouchStart, { passive: false });
window.addEventListener('touchmove', rTouchMove, { passive: false });
window.addEventListener('touchend', rEnd);
window.addEventListener('touchcancel', rEnd);
rCleanups.push(function () {
el.removeEventListener('touchstart', rTouchStart, { passive: false });
window.removeEventListener('touchmove', rTouchMove, { passive: false });
window.removeEventListener('touchend', rEnd);
window.removeEventListener('touchcancel', rEnd);
});
}
makeCornerHandle('se');
makeCornerHandle('sw');
makeCornerHandle('ne');
makeCornerHandle('nw');
// Extend flightPlanCleanup with resize-teardown.
var prevCleanup = flightPlanCleanup;
flightPlanCleanup = function () {
if (typeof prevCleanup === 'function') prevCleanup();
for (var i = 0; i < rCleanups.length; i++) rCleanups[i]();
};

// Restore saved size.
try {
const sw = localStorage.getItem('navaid.planW');
const sh = localStorage.getItem('navaid.planH');
if (sw) box.style.width = sw + 'px';
if (sh) box.style.height = sh + 'px';
} catch (e) {}

back.appendChild(box);
// Close via the Close button or Escape (#86).
document.body.appendChild(back);
Expand Down Expand Up @@ -1160,6 +1297,116 @@ function showExportModal() {
document.addEventListener('keydown', onEsc);
}

// Draw the flight-plan table on an export canvas at the given position and
// size. Called from exportPNG() when the flight plan is pinned to the map.
// `ctx` is the output-canvas 2d context at identity transform; x/y/w/h are
// output-canvas pixels.
function drawFlightPlanTable(ctx, x, y, w, h) {
const legs = state.legs || [];
const wpts = state.waypoints || [];
if (!legs.length || wpts.length < 2) return;
const ac = aircraft;
const taxiFuel = ac && ac.taxiGal && isAirport(wpts[0]) ? ac.taxiGal : 0;
// Compute rows.
const rows = [];
let totDist = 0, totTime = 0, totFuel = 0;
for (let i = 0; i < legs.length; i++) {
const A = wpts[i], B = wpts[i + 1];
if (!A || !B) continue;
const { dist, brg } = geo(A, B);
const hdg = toMagnetic(brg);
const dur = legs[i].flightSpeed > 0 ? dist / legs[i].flightSpeed : 0;
let fuel = ac ? dur * ac.gph : 0;
if (i === 0 && taxiFuel) fuel += taxiFuel;
totDist += dist;
totTime += dur;
totFuel += fuel;
const fLabel = ac ? fuel.toFixed(1) + (i === 0 && taxiFuel ? ' *' : '') : '--';
rows.push({ num: i + 1, from: navName((A.name || '').trim()) || S.wpPrefix + (i + 1),
to: navName((B.name || '').trim()) || S.wpPrefix + (i + 2),
hdg: pad3(hdg) + '\u00B0M', dist: dist.toFixed(1),
speed: String(legs[i].flightSpeed), alt: String(legs[i].inboundAltitude),
time: dur > 0 ? toHMS(dur) : '--', fuel: fLabel });
}
if (!rows.length) return;
const headers = S.fpHeaders;
const numCols = headers.length;
// Column width fractions (sum to 1).
const colFrac = [0.04, 0.17, 0.17, 0.11, 0.10, 0.10, 0.10, 0.10, 0.11];
const numRows = rows.length + 2; // header + data + total
const rowH = h / numRows;
const fontSize = Math.min(rowH * 0.55, 13);
const padX = 5;
// Text alignment per column.
const aligns = ['center', 'left', 'left', 'center', 'right', 'right', 'right', 'center', 'right'];
ctx.save();
ctx.font = fontSize + 'px sans-serif';
// Background.
ctx.fillStyle = 'rgba(42,38,38,0.92)';
ctx.fillRect(x, y, w, h);
// Border.
ctx.strokeStyle = '#4a4646';
ctx.lineWidth = 1;
ctx.strokeRect(x, y, w, h);
// Helper: draw one cell.
function cell(row, col, text, bold, bg) {
const cx = x + colFrac.slice(0, col).reduce(function (a, b) { return a + b; }, 0) * w;
const cy = y + row * rowH;
const cw = colFrac[col] * w;
if (bg) { ctx.fillStyle = bg; ctx.fillRect(cx, cy, cw, rowH); }
// Right/bottom grid lines (avoid overdraw on last column/row).
ctx.strokeStyle = '#4a4646';
ctx.lineWidth = 0.5;
if (col < numCols - 1) { ctx.beginPath(); ctx.moveTo(cx + cw, cy); ctx.lineTo(cx + cw, cy + rowH); ctx.stroke(); }
if (row < numRows - 1) { ctx.beginPath(); ctx.moveTo(cx, cy + rowH); ctx.lineTo(cx + cw, cy + rowH); ctx.stroke(); }
// Text.
ctx.fillStyle = bold ? '#e8e8e8' : '#d0d0d0';
ctx.font = (bold ? 'bold ' : '') + fontSize + 'px sans-serif';
ctx.textBaseline = 'middle';
var a = aligns[col];
ctx.textAlign = a;
var tx = a === 'right' ? cx + cw - padX : a === 'center' ? cx + cw / 2 : cx + padX;
ctx.fillText(text, tx, cy + rowH / 2);
}
// Header.
for (var c = 0; c < numCols; c++) cell(0, c, headers[c], true, '#3a3636');
// Data rows.
for (var r = 0; r < rows.length; r++) {
var rd = rows[r];
var vals = [rd.num, rd.from, rd.to, rd.hdg, rd.dist, rd.speed, rd.alt, rd.time, rd.fuel];
for (var c2 = 0; c2 < numCols; c2++) {
cell(r + 1, c2, String(vals[c2]), false, r % 2 === 1 ? 'rgba(255,255,255,0.03)' : null);
}
}
// Total row: "Total" spans first 4 columns.
var tr = rows.length + 1;
var totalLabel = S.fpTotal;
var totX0 = x;
var totX1 = x + colFrac.slice(0, 4).reduce(function (a, b) { return a + b; }, 0) * w;
var totCY = y + tr * rowH;
ctx.fillStyle = '#333030';
ctx.fillRect(totX0, totCY, totX1 - totX0, rowH);
ctx.fillStyle = '#e8e8e8';
ctx.font = 'bold ' + fontSize + 'px sans-serif';
ctx.textAlign = 'left';
ctx.textBaseline = 'middle';
ctx.fillText(totalLabel, totX0 + padX, totCY + rowH / 2);
// Grid lines for total-row cells.
for (var c3 = 0; c3 < numCols; c3++) {
var cx3 = x + colFrac.slice(0, c3).reduce(function (a, b) { return a + b; }, 0) * w;
ctx.strokeStyle = '#4a4646';
ctx.lineWidth = 0.5;
if (c3 < numCols - 1) { ctx.beginPath(); ctx.moveTo(cx3 + colFrac[c3] * w, totCY); ctx.lineTo(cx3 + colFrac[c3] * w, totCY + rowH); ctx.stroke(); }
ctx.beginPath(); ctx.moveTo(cx3, totCY + rowH); ctx.lineTo(cx3 + colFrac[c3] * w, totCY + rowH); ctx.stroke();
}
// Remaining total cells.
var totVals = { 4: totDist.toFixed(1), 7: totTime > 0 ? toHMS(totTime) : '--', 8: ac ? totFuel.toFixed(1) : '--' };
for (var c4 = 4; c4 < numCols; c4++) {
if (totVals[c4] !== undefined) cell(tr, c4, String(totVals[c4]), true, null);
}
ctx.restore();
}

// Save the framed map + route as a PNG, rendered at the highest practical
// native tile zoom (not the on-screen zoom) for maximum quality. flight-maps
// tiles are not CORS-enabled, so each tile is fetched through the weserv image
Expand Down Expand Up @@ -1379,6 +1626,22 @@ function exportPNG() {
octx = prevOctx;
}

// Pinned flight plan overlay.
var planPinned = localStorage.getItem('navaid.planPin') === '1';
if (planPinned) {
var fpBox = document.querySelector('.modal-back.flight-plan > .modal');
if (fpBox) {
var fpRect = fpBox.getBoundingClientRect();
var fpx = (fpRect.left - fr.x) * s;
var fpy = (fpRect.top - fr.y) * s;
var fpw = fpRect.width * s;
var fph = fpRect.height * s;
if (fpx + fpw > 0 && fpy + fph > 0 && fpx < W && fpy < H) {
drawFlightPlanTable(o, Math.max(0, fpx), Math.max(0, fpy), Math.min(fpw, W - fpx), Math.min(fph, H - fpy));
}
}
}

out.toBlob(b => {
btn.textContent = btnLabel;
btn.disabled = false;
Expand Down
Loading