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
4 changes: 4 additions & 0 deletions docs/app/core.js
Original file line number Diff line number Diff line change
Expand Up @@ -463,6 +463,10 @@ window.S = Object.assign({
errInvalidRoute: function(msg) { return 'Invalid route file: ' + msg; },
errInvalidNavWaypoints: function(msg) { return 'Invalid nav-waypoints data: ' + msg; },
errInvalidVors: function(msg) { return 'Invalid VOR data: ' + msg; },
tbMyLocation: 'Show my location (GPS)',
tbRecordTrack: 'Record track',
geoUnsupported: 'Geolocation is not supported on this device.',
geoError: 'Location unavailable',
tbShowVor: 'Show VOR stations',
tbShowVorTitle: 'Overlay Israeli VOR/DME stations and pick a reference for radial/DME',
vorRefLabel: 'VOR ref',
Expand Down
32 changes: 32 additions & 0 deletions docs/app/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -565,6 +565,38 @@ html, body {
pointer-events: none;
display: none;
}
.leaflet-control.navaid-loc-btn,
.leaflet-control.navaid-rec-btn {
display: block;
width: 34px;
height: 34px;
margin: 8px 12px 0 0;
padding: 0;
font-size: 17px;
line-height: 32px;
text-align: center;
cursor: pointer;
color: #fff;
background: rgba(20, 18, 18, 0.88);
border: 1px solid #3a3636;
border-radius: 5px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.45);
}
.leaflet-control.navaid-loc-btn.active { background: #1d6fe0; border-color: #1d6fe0; }
.leaflet-control.navaid-rec-btn.active { background: #e23b3b; border-color: #e23b3b; }
.leaflet-control.navaid-loc-readout {
margin: 8px 12px 0 0;
padding: 4px 8px;
font: 700 13px/1 monospace;
color: #fff;
background: rgba(20, 18, 18, 0.88);
border: 1px solid #3a3636;
border-radius: 5px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.45);
white-space: nowrap;
direction: ltr;
unicode-bidi: isolate;
}
.leaflet-control.zulu-clock {
display: block;
min-width: var(--navaid-zulu-clock-min-width, 82px);
Expand Down
122 changes: 122 additions & 0 deletions docs/app/ui.js
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,128 @@ function refreshZuluClock() {
}
refreshZuluClock();
setInterval(refreshZuluClock, 1000);

// --- realtime own-position (GPS) ------------------------------------
// A topright toggle that tracks the device location via the Geolocation API
// and shows a blue dot + accuracy circle as Leaflet layers (independent of the
// route overlay). First fix pans to the position; it does not keep following.
let _locWatch = null, _locMarker = null, _locAccuracy = null, _locFollowed = false;
let _locTrack = null, _locRecording = false;
function liveLocationBtn() { return document.getElementById('navaid-loc-btn'); }
function recordTrackBtn() { return document.getElementById('navaid-rec-btn'); }
function stopLiveLocation() {
if (_locWatch != null && navigator.geolocation) navigator.geolocation.clearWatch(_locWatch);
_locWatch = null;
if (_locMarker) { map.removeLayer(_locMarker); _locMarker = null; }
if (_locAccuracy) { map.removeLayer(_locAccuracy); _locAccuracy = null; }
if (_locTrack) { map.removeLayer(_locTrack); _locTrack = null; }
const ro = document.getElementById('navaid-loc-readout');
if (ro) { ro.textContent = ''; ro.style.display = 'none'; }
_locRecording = false;
const b = liveLocationBtn();
if (b) { b.classList.remove('active'); b.setAttribute('aria-pressed', 'false'); }
const r = recordTrackBtn();
if (r) { r.classList.remove('active'); r.setAttribute('aria-pressed', 'false'); }
}
function onLiveLocationFix(pos) {
const lat = pos.coords.latitude, lng = pos.coords.longitude;
const acc = Number.isFinite(pos.coords.accuracy) ? pos.coords.accuracy : 0;
const ll = [lat, lng];
if (!_locMarker) {
_locAccuracy = L.circle(ll, { radius: acc, color: '#1d6fe0', weight: 1,
fillColor: '#1d6fe0', fillOpacity: 0.12, interactive: false }).addTo(map);
_locMarker = L.circleMarker(ll, { radius: 6, color: '#fff', weight: 2,
fillColor: '#1d6fe0', fillOpacity: 1, interactive: false }).addTo(map);
} else {
_locMarker.setLatLng(ll);
_locAccuracy.setLatLng(ll).setRadius(acc);
}
// GPS altitude (m โ†’ ft) + ground speed (m/s โ†’ kt) readout, when available.
const ro = document.getElementById('navaid-loc-readout');
if (ro) {
const parts = [];
if (Number.isFinite(pos.coords.altitude)) parts.push(Math.round(pos.coords.altitude * 3.28084) + ' ft');
if (Number.isFinite(pos.coords.speed) && pos.coords.speed >= 0) parts.push(Math.round(pos.coords.speed * 1.94384) + ' kt');
ro.textContent = parts.join(' ยท ');
ro.style.display = parts.length ? 'block' : 'none';
}
if (_locRecording) {
if (!_locTrack) {
_locTrack = L.polyline([ll], { color: '#e23b3b', weight: 3, opacity: 0.85, interactive: false }).addTo(map);
} else {
_locTrack.addLatLng(ll);
}
}
if (!_locFollowed) { _locFollowed = true; map.panTo(ll); }
}
function onLiveLocationError(e) {
if (typeof showToast === 'function') {
showToast((S.geoError || 'Location unavailable') + (e && e.message ? ': ' + e.message : ''));
}
stopLiveLocation();
}
function toggleLiveLocation() {
if (_locWatch != null) { stopLiveLocation(); return; }
if (!navigator.geolocation) {
if (typeof showToast === 'function') showToast(S.geoUnsupported || 'Geolocation not supported');
return;
}
_locFollowed = false;
const b = liveLocationBtn();
if (b) { b.classList.add('active'); b.setAttribute('aria-pressed', 'true'); }
_locWatch = navigator.geolocation.watchPosition(onLiveLocationFix, onLiveLocationError,
{ enableHighAccuracy: true, maximumAge: 2000, timeout: 15000 });
}
// Record toggle โ€” appends each GPS fix to a breadcrumb polyline. Turning it on
// also starts location tracking if it isn't already running.
function toggleRecordTrack() {
_locRecording = !_locRecording;
const r = recordTrackBtn();
if (r) { r.classList.toggle('active', _locRecording); r.setAttribute('aria-pressed', _locRecording ? 'true' : 'false'); }
if (_locRecording && _locWatch == null) toggleLiveLocation();
if (!_locRecording && _locTrack) { map.removeLayer(_locTrack); _locTrack = null; }
}
window.toggleLiveLocation = toggleLiveLocation;
window.toggleRecordTrack = toggleRecordTrack;
const liveLocationCtrl = L.control({ position: 'topright' });
liveLocationCtrl.onAdd = function () {
const b = L.DomUtil.create('button', 'leaflet-control navaid-loc-btn');
b.id = 'navaid-loc-btn';
b.type = 'button';
b.textContent = '๐Ÿ“';
b.title = S.tbMyLocation || 'Show my location';
b.setAttribute('aria-label', S.tbMyLocation || 'Show my location');
b.setAttribute('aria-pressed', 'false');
L.DomEvent.disableClickPropagation(b);
L.DomEvent.on(b, 'click', e => { L.DomEvent.preventDefault(e); toggleLiveLocation(); });
return b;
};
liveLocationCtrl.addTo(map);
const recordTrackCtrl = L.control({ position: 'topright' });
recordTrackCtrl.onAdd = function () {
const b = L.DomUtil.create('button', 'leaflet-control navaid-rec-btn');
b.id = 'navaid-rec-btn';
b.type = 'button';
b.textContent = 'โบ';
b.title = S.tbRecordTrack || 'Record track';
b.setAttribute('aria-label', S.tbRecordTrack || 'Record track');
b.setAttribute('aria-pressed', 'false');
L.DomEvent.disableClickPropagation(b);
L.DomEvent.on(b, 'click', e => { L.DomEvent.preventDefault(e); toggleRecordTrack(); });
return b;
};
recordTrackCtrl.addTo(map);
const locReadoutCtrl = L.control({ position: 'topright' });
locReadoutCtrl.onAdd = function () {
const box = L.DomUtil.create('div', 'leaflet-control navaid-loc-readout');
box.id = 'navaid-loc-readout';
box.dir = 'ltr';
box.style.display = 'none';
box.setAttribute('aria-live', 'off');
return box;
};
locReadoutCtrl.addTo(map);

// --- map legend (bottom-left) ---------------------------------------
// The legend markup lives in index.html so applyI18n() fills its text at
// boot; here we lift that element into a Leaflet control so it floats over
Expand Down
4 changes: 4 additions & 0 deletions docs/i18n/he/strings.js
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,10 @@ window.S = {
errInvalidRoute: function(msg) { return 'ืงื•ื‘ืฅ ืžืกืœื•ืœ ืœื ืชืงื™ืŸ: ' + msg; },
errInvalidNavWaypoints: function(msg) { return 'ื ืชื•ื ื™ ืฆื™ื•ื ื™ ื ื™ื•ื•ื˜ ืœื ืชืงื™ื ื™ื: ' + msg; },
errInvalidVors: function(msg) { return 'ื ืชื•ื ื™ VOR ืœื ืชืงื™ื ื™ื: ' + msg; },
tbMyLocation: 'ื”ืฆื’ ืžื™ืงื•ื ื ื•ื›ื—ื™ (GPS)',
tbRecordTrack: 'ื”ืงืœื˜ ืžืกืœื•ืœ',
geoUnsupported: 'ืื™ืชื•ืจ ืžื™ืงื•ื ืื™ื ื• ื ืชืžืš ื‘ืžื›ืฉื™ืจ ื–ื”.',
geoError: 'ืžื™ืงื•ื ืœื ื–ืžื™ืŸ',
tbShowVor: 'ื”ืฆื’ ืชื—ื ื•ืช VOR',
tbShowVorTitle: 'ื”ืฆื’ ืชื—ื ื•ืช VOR/DME ื•ื‘ื—ืจ ืชื—ื ืช ื™ื™ื—ื•ืก ืœืจื“ื™ืืœ/DME',
vorRefLabel: 'ืชื—ื ืช ื™ื™ื—ื•ืก',
Expand Down
58 changes: 58 additions & 0 deletions tests/live-location.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
// @ts-check
// Realtime own-position (GPS) + track recording (#676). Geolocation is mocked
// so the watch can be fired deterministically.
const { test, expect } = require('@playwright/test');

async function bootWithGeo(page) {
await page.addInitScript(() => {
navigator.geolocation.watchPosition = (ok) => {
window.__fireFix = c => ok({ coords: c });
return 42;
};
navigator.geolocation.clearWatch = () => {};
});
await page.goto('?lang=en');
await page.waitForFunction(() => typeof toggleLiveLocation === 'function');
}

const FIX = { latitude: 32.1, longitude: 34.8, accuracy: 30, altitude: 762, speed: 51.4 };

test('location toggle shows a position marker + alt/speed readout', async ({ page }) => {
await bootWithGeo(page);
const btn = page.locator('#navaid-loc-btn');
await expect(btn).toBeVisible();
await btn.click();
await expect(btn).toHaveClass(/active/);
await page.evaluate(f => window.__fireFix(f), FIX);
// 762 m โ†’ 2500 ft, 51.4 m/s โ†’ 100 kt.
const readout = page.locator('#navaid-loc-readout');
await expect(readout).toBeVisible();
await expect(readout).toHaveText('2500 ft ยท 100 kt');
// A position marker exists on the overlay pane.
expect(await page.locator('.leaflet-overlay-pane path').count()).toBeGreaterThan(0);
// Toggling off clears the marker + readout.
await btn.click();
await expect(btn).not.toHaveClass(/active/);
await expect(readout).toBeHidden();
});

test('record toggle starts tracking and draws a breadcrumb polyline', async ({ page }) => {
await bootWithGeo(page);
const rec = page.locator('#navaid-rec-btn');
await rec.click();
await expect(rec).toHaveClass(/active/);
// Recording auto-starts location tracking.
await expect(page.locator('#navaid-loc-btn')).toHaveClass(/active/);
await page.evaluate(f => window.__fireFix(f), FIX);
await page.evaluate(f => window.__fireFix({ ...f, latitude: 32.2, longitude: 34.85 }), FIX);
// A track polyline (โ‰ฅ2 fixes) is on the map.
const tracked = await page.evaluate(() => {
let n = 0;
document.querySelectorAll('.leaflet-overlay-pane path').forEach(p => {
const d = p.getAttribute('d') || '';
if ((d.match(/L/g) || []).length >= 1) n++;
});
return n;
});
expect(tracked).toBeGreaterThan(0);
});
Loading