µWave Activity Day Planner
Microwave QSO Planning · 1.2 GHz — 76 GHz
Reference Grid
Ref Call
← Back
Stations 0
📡
No stations yet.
Click + Add Station to begin.
Stations: 0
LINK BUDGET
TERRAIN
MATRIX
EXPORT
⛰️
Select a station
to view terrain profile.
📊
Add stations to see the QSO matrix.
Reference Station Summary
Export bearing, distance, link budgets for all stations from your reference grid/callsign.
All Station Data Pack
Full data for all participants — bearings, distances, link margins, comms info — one row per station pair.
Printable Activity Sheet
Opens a printer-friendly summary for your reference station.
Station Database — Server Sync
Stations are saved to the server automatically whenever you add, edit or delete a station. All participants reload to see changes instantly.

Files needed on server:
uwave-stations.json — the database
uwave-save.php — the write endpoint
ACTIVITY CHAT
0 online
µWave Activity Day Chat — messages appear here
You:
`); win.document.close(); } function dl(content,filename,type) { const blob=new Blob([content],{type}); const a=document.createElement('a'); a.href=URL.createObjectURL(blob); a.download=filename; a.click(); } // ═══════════════════════════════════════════════════════════════════ // CHAT SYSTEM // ═══════════════════════════════════════════════════════════════════ const CHAT_URL = 'uwave-chat.php'; const CHAT_POLL_MS = 5000; let chatLastId = 0; let chatTimer = null; let chatMyCall = '', chatMyName = '', chatMyGrid = ''; function chatIdentity() { const call = (document.getElementById('refCall').value || '').toUpperCase().trim(); const grid = (document.getElementById('refGrid').value || '').toUpperCase().trim(); const st = stations.find(s => s.callsign.toUpperCase() === call); chatMyCall = call || 'ANON'; chatMyName = st ? (st.name || '') : ''; chatMyGrid = grid || (st ? st.grid : '') || ''; const ce = document.getElementById('chatMyCall'); const ge = document.getElementById('chatMyGrid'); if (ce) ce.textContent = chatMyCall; if (ge) ge.textContent = chatMyGrid ? '(' + chatMyGrid + ')' : ''; } async function chatPoll() { chatIdentity(); try { const url = CHAT_URL + '?action=get&since=' + chatLastId + '&callsign=' + encodeURIComponent(chatMyCall) + '&name=' + encodeURIComponent(chatMyName) + '&grid=' + encodeURIComponent(chatMyGrid); const resp = await fetch(url, { cache: 'no-store' }); if (!resp.ok) throw new Error('HTTP ' + resp.status); const data = await resp.json(); renderOnline(data.online || []); if (data.messages && data.messages.length) { appendMessages(data.messages); chatLastId = Math.max(chatLastId, ...data.messages.map(m => m.id)); } const se = document.getElementById('chatStatus'); if (se) se.textContent = ''; } catch(e) { const se = document.getElementById('chatStatus'); if (se) se.textContent = 'Chat: ' + e.message; } chatTimer = setTimeout(chatPoll, CHAT_POLL_MS); } function renderOnline(users) { const ce = document.getElementById('chatOnlineCount'); const le = document.getElementById('chatOnlineList'); if (ce) ce.textContent = users.length; if (!le) return; le.innerHTML = users.map(u => { const me = u.callsign.toUpperCase() === chatMyCall.toUpperCase(); return '' + u.callsign + ''; }).join(''); } function appendMessages(msgs) { const box = document.getElementById('chatMsgs'); if (!box) return; const atBottom = box.scrollHeight - box.scrollTop - box.clientHeight < 80; const ph = box.querySelector('.chat-sys'); if (ph && msgs.length) ph.remove(); msgs.forEach(m => { const me = m.callsign.toUpperCase() === chatMyCall.toUpperCase(); const wrap = document.createElement('div'); wrap.className = 'chat-msg' + (me ? ' me' : ''); const meta = document.createElement('div'); meta.className = 'chat-meta'; meta.innerHTML = '' + m.callsign + '' + (m.name ? '' + m.name + '' : '') + (m.grid ? '' + m.grid + '' : '') + '' + (m.tsStr || '') + ''; const bub = document.createElement('div'); bub.className = 'chat-bubble'; bub.textContent = m.text; wrap.appendChild(meta); wrap.appendChild(bub); box.appendChild(wrap); }); if (atBottom) box.scrollTop = box.scrollHeight; } async function sendChatMsg() { chatIdentity(); const inp = document.getElementById('chatInput'); const text = (inp ? inp.value : '').trim(); if (!text) return; if (!chatMyCall || chatMyCall === 'ANON') { const se = document.getElementById('chatStatus'); if (se) se.textContent = 'Set Reference Callsign first'; document.getElementById('refCall').focus(); return; } const btn = document.getElementById('chatSendBtn'); if (btn) btn.disabled = true; if (inp) inp.value = ''; const se = document.getElementById('chatStatus'); if (se) se.textContent = 'Sending...'; try { const resp = await fetch(CHAT_URL + '?action=post', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Save-Token': SAVE_TOKEN }, body: JSON.stringify({ callsign: chatMyCall, name: chatMyName, grid: chatMyGrid, text }) }); const result = await resp.json(); if (!result.ok) throw new Error(result.error || 'Failed'); if (se) se.textContent = ''; clearTimeout(chatTimer); chatPoll(); } catch(e) { if (se) se.textContent = 'Send failed: ' + e.message; if (inp) inp.value = text; } finally { if (btn) btn.disabled = false; if (inp) inp.focus(); } } function chatKeyDown(e) { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); sendChatMsg(); } } async function clearChat() { if (!confirm('Clear all chat messages?')) return; try { await fetch(CHAT_URL + '?action=clear', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Save-Token': SAVE_TOKEN }, body: JSON.stringify({}) }); const box = document.getElementById('chatMsgs'); if (box) { box.innerHTML = ''; const d = document.createElement('div'); d.className='chat-sys'; d.textContent='Chat cleared'; box.appendChild(d); } chatLastId = 0; } catch(e) { alert('Clear failed: ' + e.message); } } function startChat() { chatIdentity(); if (!chatTimer) chatPoll(); } // ═══════════════════════════════════════════════════════════════════ // INIT // ═══════════════════════════════════════════════════════════════════ document.addEventListener('DOMContentLoaded', async ()=>{ // Init map FIRST before anything else initMap(); // Then force re-layout after browser paint requestAnimationFrame(() => { if(map) map.invalidateSize(false); }); setTimeout(() => { if(map) map.invalidateSize(false); }, 200); setTimeout(() => { if(map) map.invalidateSize(false); }, 800); renderStationList(); buildMatrix(); // Demo stations for first run const demo=[ {id:'d1',callsign:'K6VHF',name:'Alex',grid:'DN31ux',bands:[ {mhz:10368,label:'10 GHz',alias:'3cm',power:5,antType:'Dish',dishSize:0.6,antGain:parseFloat(dishGain(0.6,10368).toFixed(1)),cableLoss:.5}, {mhz:5760,label:'5.7 GHz',alias:'6cm',power:10,antType:'Dish',dishSize:0.6,antGain:parseFloat(dishGain(0.6,5760).toFixed(1)),cableLoss:.5}, {mhz:1296,label:'1.3 GHz',alias:'23cm',power:100,antType:'Yagi',yElements:11,antGain:parseFloat(yagiGain(11).toFixed(1)),cableLoss:1}, ],comms:['Cell Phone','2m Net'],phone:'',email:'',repeater:'146.520',dmr:'',notes:''}, {id:'d2',callsign:'W6ABC',name:'Jim',grid:'DM04',bands:[ {mhz:10368,label:'10 GHz',alias:'3cm',power:3,antType:'Horn',dishSize:0.25,antGain:parseFloat(dishGain(0.25,10368,0.75).toFixed(1)),cableLoss:.3}, {mhz:1296,label:'1.3 GHz',alias:'23cm',power:50,antType:'Loop Yagi',yElements:16,antGain:parseFloat(yagiGain(16).toFixed(1)),cableLoss:1.5}, ],comms:['Cell Phone','Echolink'],phone:'',email:'',repeater:'',dmr:'',notes:''}, {id:'d3',callsign:'N7XYZ',name:'Bob',grid:'DN40',bands:[ {mhz:10368,label:'10 GHz',alias:'3cm',power:8,antType:'Dish',dishSize:0.9,antGain:parseFloat(dishGain(0.9,10368).toFixed(1)),cableLoss:.4}, {mhz:5760,label:'5.7 GHz',alias:'6cm',power:5,antType:'Dish',dishSize:0.9,antGain:parseFloat(dishGain(0.9,5760).toFixed(1)),cableLoss:.5}, {mhz:24048,label:'24 GHz',alias:'1.25cm',power:.5,antType:'Dish',dishSize:0.3,antGain:parseFloat(dishGain(0.3,24048).toFixed(1)),cableLoss:.8}, ],comms:['DMR','2m Net'],phone:'',email:'',repeater:'',dmr:'310123',notes:'Operating from 8am-6pm MDT'}, ]; startChat(); logVisit();; // Try to load from server first; fall back to demo data if not found setLoadingStatus('Connecting to server database…'); const serverLoaded = await fetchServerDB(false); if (!serverLoaded) { // No server DB found — load demo data stations = demo; document.getElementById('refCall').value = 'K6VHF'; document.getElementById('refGrid').value = 'DN31ux'; onRefChange(); renderStationList(); refreshMarkers(); buildMatrix(); const bounds = stations.map(s=>gridToLL(s.grid)).filter(Boolean).map(ll=>[ll.lat,ll.lon]); if (bounds.length) map.fitBounds(L.latLngBounds(bounds).pad(0.4)); setLoadingStatus('Demo data loaded — upload uwave-stations.json to server to share with participants', 'warn'); } });
🔐 Authentication Required
This action is protected.
Enter the operator PIN to continue.
Delete Station