Upload files to "Weather"
This commit is contained in:
@@ -0,0 +1,427 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Hurricane Report (Winlink / Packet Radio)</title>
|
||||||
|
<style>
|
||||||
|
* { box-sizing: border-box; }
|
||||||
|
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, monospace; background: #eef2f7; color: #222; padding: 20px; margin: 0; }
|
||||||
|
.container { max-width: 920px; margin: 0 auto; background: #fff; padding: 24px; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); }
|
||||||
|
.header { background: #01579b; color: white; padding: 12px; border-radius: 6px; text-align: center; margin-bottom: 18px; }
|
||||||
|
.header h2 { margin: 0; font-size: 1.3rem; letter-spacing: 1px; }
|
||||||
|
.header p { margin: 4px 0 0 0; font-size: 0.85rem; opacity: 0.9; }
|
||||||
|
fieldset { border: 1px solid #81d4fa; border-radius: 6px; padding: 14px; margin-bottom: 14px; background: #f9fdff; }
|
||||||
|
legend { font-weight: bold; padding: 0 8px; font-size: 0.85rem; color: #01579b; }
|
||||||
|
.row { display: flex; flex-wrap: wrap; gap: 12px; margin-bottom: 10px; }
|
||||||
|
.col { flex: 1; min-width: 140px; display: flex; flex-direction: column; }
|
||||||
|
.label-row { display: flex; justify-content: space-between; align-items: center; margin-bottom: 4px; }
|
||||||
|
label { font-size: 0.8rem; font-weight: 600; color: #333; }
|
||||||
|
input[type="text"], select, textarea { padding: 7px 8px; font-size: 0.88rem; border: 1px solid #999; border-radius: 4px; font-family: inherit; width: 100%; }
|
||||||
|
textarea { resize: vertical; min-height: 70px; }
|
||||||
|
.btn-now { background: #4a5568; color: #fff; padding: 4px 8px; font-size: 0.72rem; border-radius: 4px; border: none; cursor: pointer; }
|
||||||
|
.btn-now:hover { background: #2d3748; }
|
||||||
|
.actions { display: flex; gap: 10px; margin: 18px 0; }
|
||||||
|
button.main-btn { flex: 1; padding: 12px; font-size: 0.95rem; font-weight: bold; border: none; border-radius: 6px; cursor: pointer; }
|
||||||
|
.btn-generate { background: #01579b; color: #fff; }
|
||||||
|
.btn-generate:hover { background: #002f6c; }
|
||||||
|
.btn-save { background: #5c6bc0; color: #fff; }
|
||||||
|
.btn-save:hover { background: #3f51b5; }
|
||||||
|
.btn-copy { background: #2e7d32; color: #fff; }
|
||||||
|
.btn-copy:hover { background: #1b5e20; }
|
||||||
|
.btn-clear { background: #c62828; color: #fff; flex: 0.35; }
|
||||||
|
pre { background: #181c20; color: #39ff14; padding: 14px; border-radius: 6px; overflow-x: auto; font-family: "Courier New", Courier, monospace; font-size: 0.82rem; line-height: 1.32; min-height: 150px; }
|
||||||
|
.nav-bar { margin-bottom: 12px; font-size: 0.85rem; }
|
||||||
|
.nav-bar a { color: #01579b; text-decoration: none; font-weight: bold; }
|
||||||
|
.notice { font-size: 0.8rem; color: #01579b; font-weight: bold; margin-bottom: 8px; }
|
||||||
|
|
||||||
|
/* Modal Save Dialog Window */
|
||||||
|
.save-modal-backdrop {
|
||||||
|
display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%;
|
||||||
|
background: rgba(0,0,0,0.55); z-index: 99999; justify-content: center; align-items: center;
|
||||||
|
}
|
||||||
|
.save-modal-backdrop.active { display: flex; }
|
||||||
|
.save-modal-window {
|
||||||
|
background: #ffffff; padding: 24px 28px; border-radius: 10px; width: 92%; max-width: 480px;
|
||||||
|
box-shadow: 0 10px 30px rgba(0,0,0,0.3); border: 1px solid #cbd5e1; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||||
|
}
|
||||||
|
.save-modal-window h3 { margin: 0 0 8px 0; font-size: 1.25rem; color: #1e293b; font-weight: bold; }
|
||||||
|
.save-modal-window p { margin: 0 0 16px 0; font-size: 0.88rem; color: #64748b; line-height: 1.4; }
|
||||||
|
.save-modal-window input[type="text"] {
|
||||||
|
width: 100%; padding: 10px 12px; font-size: 0.95rem; border: 2px solid #94a3b8; border-radius: 6px;
|
||||||
|
margin-bottom: 20px; font-family: inherit; box-sizing: border-box; text-transform: none;
|
||||||
|
}
|
||||||
|
.save-modal-window input[type="text"]:focus { border-color: #2563eb; outline: none; }
|
||||||
|
.save-modal-actions { display: flex; gap: 10px; justify-content: flex-end; }
|
||||||
|
.save-modal-btn { padding: 9px 18px; font-size: 0.9rem; font-weight: bold; border-radius: 6px; border: none; cursor: pointer; }
|
||||||
|
.save-modal-btn-confirm { background: #2563eb; color: #ffffff; }
|
||||||
|
.save-modal-btn-confirm:hover { background: #1d4ed8; }
|
||||||
|
.save-modal-btn-cancel { background: #e2e8f0; color: #334155; }
|
||||||
|
.save-modal-btn-cancel:hover { background: #cbd5e1; }
|
||||||
|
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<div class="nav-bar"><a href="../index.html">← Back to Packet Forms Hub</a></div>
|
||||||
|
<div class="header">
|
||||||
|
<h2>HURRICANE REPORT (WX4NHC / NWS)</h2>
|
||||||
|
<p>National Hurricane Center & Extreme Tropical Event Report (Packet ASCII)</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="notice">• Hurricane Watch Net (HWN) Active Frequency: 14.325 MHz / 7.268 MHz • SendTo: WX4NHC</div>
|
||||||
|
|
||||||
|
<form id="hurrForm" onsubmit="event.preventDefault(); return false;">
|
||||||
|
<fieldset>
|
||||||
|
<legend>1-4. HEADER & OBSERVER IDENTIFICATION</legend>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col">
|
||||||
|
<div class="label-row">
|
||||||
|
<label for="dtZulu">1. Date / Time (UTC)</label>
|
||||||
|
<button type="button" class="btn-now" id="btnDateNow">Current UTC</button>
|
||||||
|
</div>
|
||||||
|
<input type="text" id="dtZulu" placeholder="YYYY-MM-DD HHMMZ">
|
||||||
|
</div>
|
||||||
|
<div class="col" style="max-width: 170px;">
|
||||||
|
<label for="repType">2. Report Status</label>
|
||||||
|
<select id="repType">
|
||||||
|
<option value="First Report" selected>First Report</option>
|
||||||
|
<option value="Update Report">Update Report</option>
|
||||||
|
<option value="Final Report">Final Report</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col" style="max-width: 140px;"><label for="msgSender">3. Sender Call</label><input type="text" id="msgSender" placeholder="N0CALL"></div>
|
||||||
|
<div class="col">
|
||||||
|
<label for="isObserver">4. Are you the Observer?</label>
|
||||||
|
<select id="isObserver">
|
||||||
|
<option value="YES (Station Observer)" selected>YES (Observer)</option>
|
||||||
|
<option value="NO (Relaying for Other Observer)">NO (Relaying for Other)</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col"><label for="obsEmail">5. Observer Email / Winlink</label><input type="text" id="obsEmail"></div>
|
||||||
|
<div class="col"><label for="obsPhone">6. Observer Phone Number</label><input type="text" id="obsPhone"></div>
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
<fieldset>
|
||||||
|
<legend>6-9. GEOGRAPHIC LOCATION & COORDINATES</legend>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col"><label for="city">6. City</label><input type="text" id="city"></div>
|
||||||
|
<div class="col"><label for="county">7. County / Parish / Region</label><input type="text" id="county"></div>
|
||||||
|
<div class="col" style="max-width: 90px;"><label for="state">8. State</label><input type="text" id="state" placeholder="FL"></div>
|
||||||
|
<div class="col"><label for="country">9. Country</label><input type="text" id="country" value="USA"></div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col"><label for="lat">Latitude (Decimal)</label><input type="text" id="lat" placeholder="25.7617"></div>
|
||||||
|
<div class="col"><label for="lon">Longitude (Decimal)</label><input type="text" id="lon" placeholder="-80.1918"></div>
|
||||||
|
<div class="col"><label for="mgrs">MGRS</label><input type="text" id="mgrs" placeholder="17R NJ 8085 4995"></div>
|
||||||
|
<div class="col" style="max-width: 120px;"><label for="grid">Grid Square</label><input type="text" id="grid" placeholder="EL95ts"></div>
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
<fieldset>
|
||||||
|
<legend>10-15. MEASUREMENTS & CONDITIONS</legend>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col" style="max-width: 160px;">
|
||||||
|
<label for="measType">10. Measurements</label>
|
||||||
|
<select id="measType">
|
||||||
|
<option value="Measured (Instruments)" selected>Measured (Instruments)</option>
|
||||||
|
<option value="Estimated">Estimated</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col"><label for="instruments">11. Instruments Used</label><input type="text" id="instruments" placeholder="e.g. Davis Vantage Pro 2, Kestrel 3500"></div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col" style="max-width: 130px;"><label for="windSpeed">12. Wind Speed</label><input type="text" id="windSpeed" placeholder="65"></div>
|
||||||
|
<div class="col" style="max-width: 100px;">
|
||||||
|
<label for="windUnit">Unit</label>
|
||||||
|
<select id="windUnit"><option value="MPH" selected>MPH</option><option value="Knots">Knots</option><option value="KM/h">KM/h</option></select>
|
||||||
|
</div>
|
||||||
|
<div class="col" style="max-width: 130px;"><label for="gustSpeed">13. Gust Speed</label><input type="text" id="gustSpeed" placeholder="85"></div>
|
||||||
|
<div class="col" style="max-width: 100px;">
|
||||||
|
<label for="gustUnit">Unit</label>
|
||||||
|
<select id="gustUnit"><option value="MPH" selected>MPH</option><option value="Knots">Knots</option><option value="KM/h">KM/h</option></select>
|
||||||
|
</div>
|
||||||
|
<div class="col" style="max-width: 130px;">
|
||||||
|
<label for="windDir">14. Wind Direction</label>
|
||||||
|
<select id="windDir">
|
||||||
|
<option value="Calm">Calm</option><option value="Variable">Variable</option>
|
||||||
|
<option value="N">N</option><option value="NE" selected>NE</option><option value="E">E</option><option value="SE">SE</option>
|
||||||
|
<option value="S">S</option><option value="SW">SW</option><option value="W">W</option><option value="NW">NW</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col" style="max-width: 150px;"><label for="pressure">15. Barometer</label><input type="text" id="pressure" placeholder="29.45 / 997"></div>
|
||||||
|
<div class="col" style="max-width: 110px;">
|
||||||
|
<label for="pressUnit">Unit</label>
|
||||||
|
<select id="pressUnit"><option value="Inches (inHg)" selected>Inches (inHg)</option><option value="Millibars (mb)">Millibars (mb)</option></select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
<fieldset>
|
||||||
|
<legend>16. STORM SURGE, DAMAGE & EYEWITNESS COMMENTS</legend>
|
||||||
|
<textarea id="comments" rows="4" placeholder="Observed storm surge height, severe street flooding, rain totals, structural destruction, downed trees/lines, evacuations..."></textarea>
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
<div class="actions">
|
||||||
|
<button type="button" class="main-btn btn-generate" id="btnGen">Generate Packet ASCII</button>
|
||||||
|
<button type="button" class="main-btn btn-save" id="btnSaveHtml">Save as HTML</button>
|
||||||
|
<button type="button" class="main-btn btn-copy" id="btnCopy">Copy ASCII</button>
|
||||||
|
<button type="button" class="main-btn btn-clear" id="btnClear">Reset</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<label style="font-weight:bold;">Formatted ASCII for Packet / BBS / Winlink Transmission:</label>
|
||||||
|
<pre id="outputPre">Fill in the fields above and click "Generate Packet ASCII"...</pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function setUTC() {
|
||||||
|
const now = new Date();
|
||||||
|
const y = now.getUTCFullYear();
|
||||||
|
const m = String(now.getUTCMonth()+1).padStart(2,'0');
|
||||||
|
const d = String(now.getUTCDate()).padStart(2,'0');
|
||||||
|
const hh = String(now.getUTCHours()).padStart(2,'0');
|
||||||
|
const mm = String(now.getUTCMinutes()).padStart(2,'0');
|
||||||
|
document.getElementById('dtZulu').value = y + '-' + m + '-' + d + ' ' + hh + mm + 'Z';
|
||||||
|
}
|
||||||
|
|
||||||
|
function clean(id) {
|
||||||
|
const el = document.getElementById(id);
|
||||||
|
return el ? (el.value || '').trim() : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function pad(str, len) {
|
||||||
|
str = (str || '').toString();
|
||||||
|
return str + ' '.repeat(Math.max(0, len - str.length));
|
||||||
|
}
|
||||||
|
|
||||||
|
function wrapText(str, width) {
|
||||||
|
if (!str) return [];
|
||||||
|
let lines = [];
|
||||||
|
str.split('\n').forEach(function(paragraph) {
|
||||||
|
let words = paragraph.split(' ');
|
||||||
|
let cur = '';
|
||||||
|
words.forEach(function(w) {
|
||||||
|
if ((cur + (cur ? ' ' : '') + w).length <= width) {
|
||||||
|
cur += (cur ? ' ' : '') + w;
|
||||||
|
} else {
|
||||||
|
if (cur) lines.push(cur);
|
||||||
|
cur = w;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (cur) lines.push(cur);
|
||||||
|
});
|
||||||
|
return lines;
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateASCII() {
|
||||||
|
try {
|
||||||
|
const div = '='.repeat(78);
|
||||||
|
const sub = '-'.repeat(78);
|
||||||
|
|
||||||
|
const dt = clean('dtZulu') || 'N/A';
|
||||||
|
const rType = clean('repType').toUpperCase();
|
||||||
|
const call = clean('msgSender').toUpperCase() || 'N0CALL';
|
||||||
|
const isObs = clean('isObserver').toUpperCase();
|
||||||
|
const email = clean('obsEmail').toUpperCase() || 'N/A';
|
||||||
|
const phone = clean('obsPhone') || 'N/A';
|
||||||
|
|
||||||
|
const city = clean('city').toUpperCase();
|
||||||
|
const county = clean('county').toUpperCase();
|
||||||
|
const st = clean('state').toUpperCase();
|
||||||
|
const country = clean('country').toUpperCase();
|
||||||
|
const lat = clean('lat') || 'N/A';
|
||||||
|
const lon = clean('lon') || 'N/A';
|
||||||
|
const mgrs = clean('mgrs').toUpperCase() || 'N/A';
|
||||||
|
const grid = clean('grid').toUpperCase() || 'N/A';
|
||||||
|
|
||||||
|
const mType = clean('measType').toUpperCase();
|
||||||
|
const inst = clean('instruments').toUpperCase() || 'N/A';
|
||||||
|
const wspd = clean('windSpeed') || 'N/A';
|
||||||
|
const wunit = clean('windUnit');
|
||||||
|
const gspd = clean('gustSpeed') || 'N/A';
|
||||||
|
const gunit = clean('gustUnit');
|
||||||
|
const wdir = clean('windDir');
|
||||||
|
const press = clean('pressure') || 'N/A';
|
||||||
|
const punit = clean('pressUnit');
|
||||||
|
const cmt = clean('comments').toUpperCase() || 'NONE REPORTED';
|
||||||
|
|
||||||
|
let wrappedCmt = wrapText(cmt, 72).map(function(l){ return ' ' + l; }).join('\n');
|
||||||
|
|
||||||
|
const out = div + '\n' +
|
||||||
|
'HURRICANE / TROPICAL EVENT REPORT (PACKET ASCII)\n' +
|
||||||
|
div + '\n' +
|
||||||
|
'TO: WX4NHC (NATIONAL HURRICANE CENTER) / HWN\n' +
|
||||||
|
'STATUS / VERSION: ' + pad(rType, 18) + ' DATE/TIME (UTC): ' + dt + '\n' +
|
||||||
|
'SENDER CALLSIGN : ' + pad(call, 18) + ' OBSERVER STATUS: ' + isObs + '\n' +
|
||||||
|
'OBSERVER CONTACT: EMAIL: ' + email + ' | PHONE: ' + phone + '\n' +
|
||||||
|
sub + '\n' +
|
||||||
|
'LOCATION & COORDINATES:\n' +
|
||||||
|
'AREA : ' + (city ? city + ', ' : '') + (county ? county + ' CO, ' : '') + st + ' (' + country + ')\n' +
|
||||||
|
'COORDS: LAT: ' + pad(lat, 12) + ' LON: ' + pad(lon, 12) + ' MGRS: ' + pad(mgrs, 16) + ' GRID: ' + grid + '\n' +
|
||||||
|
sub + '\n' +
|
||||||
|
'OBSERVED METEOROLOGICAL DATA:\n' +
|
||||||
|
'MEASUREMENT TYPE: ' + mType + '\n' +
|
||||||
|
'INSTRUMENTS : ' + inst + '\n' +
|
||||||
|
'SUSTAINED WIND : ' + wspd + ' ' + wunit + ' GUST: ' + gspd + ' ' + gunit + ' DIR: ' + wdir + '\n' +
|
||||||
|
'BAROMETER : ' + press + ' ' + punit + '\n' +
|
||||||
|
sub + '\n' +
|
||||||
|
'SURGE, FLOODING, DAMAGE & EYEWITNESS REPORT:\n' +
|
||||||
|
wrappedCmt + '\n' +
|
||||||
|
div;
|
||||||
|
|
||||||
|
document.getElementById('outputPre').textContent = out;
|
||||||
|
} catch(err) {
|
||||||
|
alert("Error generating text: " + err.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function copyASCII() {
|
||||||
|
const txt = document.getElementById('outputPre').textContent;
|
||||||
|
if (navigator.clipboard && navigator.clipboard.writeText) {
|
||||||
|
navigator.clipboard.writeText(txt).then(function() {
|
||||||
|
alert("Hurricane Report copied to clipboard!");
|
||||||
|
}).catch(function() {
|
||||||
|
fallbackCopy();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
fallbackCopy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function fallbackCopy() {
|
||||||
|
const pre = document.getElementById('outputPre');
|
||||||
|
const range = document.createRange();
|
||||||
|
range.selectNode(pre);
|
||||||
|
window.getSelection().removeAllRanges();
|
||||||
|
window.getSelection().addRange(range);
|
||||||
|
document.execCommand('copy');
|
||||||
|
alert("Copied to clipboard!");
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearForm() {
|
||||||
|
if(confirm("Reset Hurricane Report form?")) {
|
||||||
|
document.getElementById('hurrForm').reset();
|
||||||
|
document.getElementById('outputPre').textContent = 'Fill in the fields above and click "Generate Packet ASCII"...';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('btnDateNow').addEventListener('click', setUTC);
|
||||||
|
document.getElementById('btnGen').addEventListener('click', generateASCII);
|
||||||
|
document.getElementById('btnSaveHtml').addEventListener('click', openSaveModal);
|
||||||
|
document.getElementById('btnCopy').addEventListener('click', copyASCII);
|
||||||
|
document.getElementById('btnClear').addEventListener('click', clearForm);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// File Name Modal Window & HTML Exporter
|
||||||
|
function openSaveModal() {
|
||||||
|
var now = new Date();
|
||||||
|
var dStr = now.toISOString().slice(0, 10);
|
||||||
|
var defName = 'hurricane_report_' + dStr + '.html';
|
||||||
|
var modal = document.getElementById('saveModalBackdrop');
|
||||||
|
var input = document.getElementById('saveModalFileName');
|
||||||
|
if (input) input.value = defName;
|
||||||
|
if (modal) modal.classList.add('active');
|
||||||
|
setTimeout(function() {
|
||||||
|
if (input) { input.focus(); input.select(); }
|
||||||
|
}, 50);
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeSaveModal() {
|
||||||
|
var modal = document.getElementById('saveModalBackdrop');
|
||||||
|
if (modal) modal.classList.remove('active');
|
||||||
|
}
|
||||||
|
|
||||||
|
function executeSaveHTML() {
|
||||||
|
var input = document.getElementById('saveModalFileName');
|
||||||
|
var chosenName = input ? input.value.trim() : '';
|
||||||
|
if (!chosenName) return;
|
||||||
|
if (!chosenName.toLowerCase().endsWith('.html') && !chosenName.toLowerCase().endsWith('.htm')) {
|
||||||
|
chosenName += '.html';
|
||||||
|
}
|
||||||
|
closeSaveModal();
|
||||||
|
|
||||||
|
document.querySelectorAll('input[type="text"]').forEach(function(el) { el.setAttribute('value', el.value); });
|
||||||
|
document.querySelectorAll('input[type="checkbox"], input[type="radio"]').forEach(function(el) {
|
||||||
|
if (el.checked) el.setAttribute('checked', 'checked');
|
||||||
|
else el.removeAttribute('checked');
|
||||||
|
});
|
||||||
|
document.querySelectorAll('textarea').forEach(function(el) { el.textContent = el.value; });
|
||||||
|
document.querySelectorAll('select').forEach(function(el) {
|
||||||
|
Array.from(el.options).forEach(function(opt) {
|
||||||
|
if (opt.value === el.value) opt.setAttribute('selected', 'selected');
|
||||||
|
else opt.removeAttribute('selected');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
var pre = document.getElementById('ascii-report') || document.getElementById('outputPre') || document.getElementById('preview') || document.getElementById('outputArea');
|
||||||
|
if (pre) pre.textContent = pre.textContent;
|
||||||
|
|
||||||
|
var clonedDoc = document.documentElement.cloneNode(true);
|
||||||
|
var modalInClone = clonedDoc.querySelector('#saveModalBackdrop');
|
||||||
|
if (modalInClone) modalInClone.classList.remove('active');
|
||||||
|
|
||||||
|
var htmlContent = '<!DOCTYPE html>\n' + clonedDoc.outerHTML;
|
||||||
|
var blob = new Blob([htmlContent], { type: 'text/html;charset=utf-8' });
|
||||||
|
var url = URL.createObjectURL(blob);
|
||||||
|
var a = document.createElement('a');
|
||||||
|
a.href = url;
|
||||||
|
a.download = chosenName;
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
document.body.removeChild(a);
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener('DOMContentLoaded', function() {
|
||||||
|
var cancelBtn = document.getElementById('saveModalCancelBtn');
|
||||||
|
var confirmBtn = document.getElementById('saveModalSaveBtn');
|
||||||
|
var modalInput = document.getElementById('saveModalFileName');
|
||||||
|
var modalBackdrop = document.getElementById('saveModalBackdrop');
|
||||||
|
|
||||||
|
if (cancelBtn) cancelBtn.addEventListener('click', closeSaveModal);
|
||||||
|
if (confirmBtn) confirmBtn.addEventListener('click', executeSaveHTML);
|
||||||
|
if (modalInput) {
|
||||||
|
modalInput.addEventListener('keydown', function(e) {
|
||||||
|
if (e.key === 'Enter') { e.preventDefault(); executeSaveHTML(); }
|
||||||
|
if (e.key === 'Escape') { e.preventDefault(); closeSaveModal(); }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (modalBackdrop) {
|
||||||
|
modalBackdrop.addEventListener('click', function(e) {
|
||||||
|
if (e.target === modalBackdrop) closeSaveModal();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
var btnSaveMain = document.getElementById('btnSaveHtml');
|
||||||
|
if (btnSaveMain) {
|
||||||
|
// Clear existing inline handlers just in case
|
||||||
|
btnSaveMain.onclick = null;
|
||||||
|
btnSaveMain.addEventListener('click', openSaveModal);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<!-- Custom File Name Modal Window -->
|
||||||
|
<div id="saveModalBackdrop" class="save-modal-backdrop">
|
||||||
|
<div class="save-modal-window">
|
||||||
|
<h3>Save Form as HTML</h3>
|
||||||
|
<p>Enter a name for the saved offline HTML file:</p>
|
||||||
|
<input type="text" id="saveModalFileName" spellcheck="false">
|
||||||
|
<div class="save-modal-actions">
|
||||||
|
<button type="button" class="save-modal-btn save-modal-btn-cancel" id="saveModalCancelBtn">Cancel</button>
|
||||||
|
<button type="button" class="save-modal-btn save-modal-btn-confirm" id="saveModalSaveBtn">Save File</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,474 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Current Local Weather Conditions (Packet Radio)</title>
|
||||||
|
<style>
|
||||||
|
* { box-sizing: border-box; }
|
||||||
|
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, monospace; background: #eef2f7; color: #222; padding: 20px; margin: 0; }
|
||||||
|
.container { max-width: 960px; margin: 0 auto; background: #fff; padding: 24px; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); }
|
||||||
|
.header { background: #0277bd; color: white; padding: 12px; border-radius: 6px; text-align: center; margin-bottom: 18px; }
|
||||||
|
.header h2 { margin: 0; font-size: 1.3rem; letter-spacing: 1px; }
|
||||||
|
.header p { margin: 4px 0 0 0; font-size: 0.85rem; opacity: 0.9; }
|
||||||
|
fieldset { border: 1px solid #81d4fa; border-radius: 6px; padding: 14px; margin-bottom: 14px; background: #f9fdff; }
|
||||||
|
legend { font-weight: bold; padding: 0 8px; font-size: 0.85rem; color: #0277bd; }
|
||||||
|
.row { display: flex; flex-wrap: wrap; gap: 12px; margin-bottom: 10px; }
|
||||||
|
.col { flex: 1; min-width: 140px; display: flex; flex-direction: column; }
|
||||||
|
.label-row { display: flex; justify-content: space-between; align-items: center; margin-bottom: 4px; }
|
||||||
|
label { font-size: 0.8rem; font-weight: 600; color: #333; }
|
||||||
|
input[type="text"], select, textarea { padding: 7px 8px; font-size: 0.88rem; border: 1px solid #999; border-radius: 4px; font-family: inherit; width: 100%; }
|
||||||
|
textarea { resize: vertical; min-height: 60px; }
|
||||||
|
.btn-now { background: #4a5568; color: #fff; padding: 4px 8px; font-size: 0.72rem; border-radius: 4px; border: none; cursor: pointer; }
|
||||||
|
.btn-now:hover { background: #2d3748; }
|
||||||
|
.conditions-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(170px, 1fr)); gap: 8px; font-size: 0.82rem; margin-top: 6px; }
|
||||||
|
.actions { display: flex; gap: 10px; margin: 18px 0; }
|
||||||
|
button.main-btn { flex: 1; padding: 12px; font-size: 0.95rem; font-weight: bold; border: none; border-radius: 6px; cursor: pointer; }
|
||||||
|
.btn-generate { background: #0277bd; color: #fff; }
|
||||||
|
.btn-generate:hover { background: #014f7c; }
|
||||||
|
.btn-save { background: #5c6bc0; color: #fff; }
|
||||||
|
.btn-save:hover { background: #3f51b5; }
|
||||||
|
.btn-copy { background: #2e7d32; color: #fff; }
|
||||||
|
.btn-copy:hover { background: #1b5e20; }
|
||||||
|
.btn-clear { background: #c62828; color: #fff; flex: 0.35; }
|
||||||
|
pre { background: #181c20; color: #39ff14; padding: 14px; border-radius: 6px; overflow-x: auto; font-family: "Courier New", Courier, monospace; font-size: 0.82rem; line-height: 1.32; min-height: 150px; }
|
||||||
|
.nav-bar { margin-bottom: 12px; font-size: 0.85rem; }
|
||||||
|
.nav-bar a { color: #0277bd; text-decoration: none; font-weight: bold; }
|
||||||
|
|
||||||
|
/* Modal Save Dialog Window */
|
||||||
|
.save-modal-backdrop {
|
||||||
|
display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%;
|
||||||
|
background: rgba(0,0,0,0.55); z-index: 99999; justify-content: center; align-items: center;
|
||||||
|
}
|
||||||
|
.save-modal-backdrop.active { display: flex; }
|
||||||
|
.save-modal-window {
|
||||||
|
background: #ffffff; padding: 24px 28px; border-radius: 10px; width: 92%; max-width: 480px;
|
||||||
|
box-shadow: 0 10px 30px rgba(0,0,0,0.3); border: 1px solid #cbd5e1; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||||
|
}
|
||||||
|
.save-modal-window h3 { margin: 0 0 8px 0; font-size: 1.25rem; color: #1e293b; font-weight: bold; }
|
||||||
|
.save-modal-window p { margin: 0 0 16px 0; font-size: 0.88rem; color: #64748b; line-height: 1.4; }
|
||||||
|
.save-modal-window input[type="text"] {
|
||||||
|
width: 100%; padding: 10px 12px; font-size: 0.95rem; border: 2px solid #94a3b8; border-radius: 6px;
|
||||||
|
margin-bottom: 20px; font-family: inherit; box-sizing: border-box; text-transform: none;
|
||||||
|
}
|
||||||
|
.save-modal-window input[type="text"]:focus { border-color: #2563eb; outline: none; }
|
||||||
|
.save-modal-actions { display: flex; gap: 10px; justify-content: flex-end; }
|
||||||
|
.save-modal-btn { padding: 9px 18px; font-size: 0.9rem; font-weight: bold; border-radius: 6px; border: none; cursor: pointer; }
|
||||||
|
.save-modal-btn-confirm { background: #2563eb; color: #ffffff; }
|
||||||
|
.save-modal-btn-confirm:hover { background: #1d4ed8; }
|
||||||
|
.save-modal-btn-cancel { background: #e2e8f0; color: #334155; }
|
||||||
|
.save-modal-btn-cancel:hover { background: #cbd5e1; }
|
||||||
|
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<div class="nav-bar"><a href="../index.html">← Back to Packet Forms Hub</a></div>
|
||||||
|
<div class="header">
|
||||||
|
<h2>CURRENT LOCAL WEATHER CONDITIONS</h2>
|
||||||
|
<p>Winlink / Packet Radio Surface Weather Observation Form</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form id="localWxForm" onsubmit="event.preventDefault(); return false;">
|
||||||
|
<fieldset>
|
||||||
|
<legend>1-7. STATION & LOCATION INFORMATION</legend>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col" style="max-width: 140px;"><label for="call">1. Callsign</label><input type="text" id="call" placeholder="N0CALL"></div>
|
||||||
|
<div class="col"><label for="repName">2. Observer Name</label><input type="text" id="repName" placeholder="Full Name"></div>
|
||||||
|
<div class="col">
|
||||||
|
<div class="label-row">
|
||||||
|
<label for="dtLocal">3. Date / Time (Local/UTC)</label>
|
||||||
|
<button type="button" class="btn-now" id="btnDateNow">Current UTC</button>
|
||||||
|
</div>
|
||||||
|
<input type="text" id="dtLocal" placeholder="YYYY-MM-DD HHMMZ">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col" style="flex: 2;"><label for="locName">4. Location Landmark / Address</label><input type="text" id="locName" placeholder="Street or landmark"></div>
|
||||||
|
<div class="col"><label for="city">5. City</label><input type="text" id="city"></div>
|
||||||
|
<div class="col" style="max-width: 80px;"><label for="state">6. State</label><input type="text" id="state" placeholder="TX" maxlength="2"></div>
|
||||||
|
<div class="col"><label for="county">7. County</label><input type="text" id="county"></div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col"><label for="lat">8. Latitude</label><input type="text" id="lat" placeholder="30.2672"></div>
|
||||||
|
<div class="col"><label for="lon">9. Longitude</label><input type="text" id="lon" placeholder="-97.7431"></div>
|
||||||
|
<div class="col"><label for="mgrs">10. MGRS</label><input type="text" id="mgrs"></div>
|
||||||
|
<div class="col" style="max-width: 120px;"><label for="grid">11. Grid</label><input type="text" id="grid" placeholder="EM10dj"></div>
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
<fieldset>
|
||||||
|
<legend>12-14. SKY COVER & CURRENT PHENOMENA</legend>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col" style="max-width: 200px;">
|
||||||
|
<label for="unitChoice">12. Default Units</label>
|
||||||
|
<select id="unitChoice">
|
||||||
|
<option value="Imperial" selected>Imperial (F, mph, inHg, in)</option>
|
||||||
|
<option value="Metric">Metric (C, km/h, mb, mm)</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col">
|
||||||
|
<label for="skyCover">13. Sky Cover</label>
|
||||||
|
<select id="skyCover">
|
||||||
|
<option value="CLEAR (0/8)">Clear (0/8)</option>
|
||||||
|
<option value="FEW CLOUDS (1/8-2/8)">Few Clouds (1/8 - 2/8)</option>
|
||||||
|
<option value="PARTLY CLOUDY (3/8-5/8)" selected>Partly Cloudy (3/8 - 5/8)</option>
|
||||||
|
<option value="MOSTLY CLOUDY (6/8-7/8)">Mostly Cloudy (6/8 - 7/8)</option>
|
||||||
|
<option value="OVERCAST (8/8)">Overcast (8/8)</option>
|
||||||
|
<option value="OBSCURED">Obscured Sky</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<label>14. Current Conditions & Phenomena (Check all that apply):</label>
|
||||||
|
<div class="conditions-grid">
|
||||||
|
<label><input type="checkbox" value="FOG"> Fog</label>
|
||||||
|
<label><input type="checkbox" value="MIST"> Mist</label>
|
||||||
|
<label><input type="checkbox" value="HAZE"> Haze</label>
|
||||||
|
<label><input type="checkbox" value="SMOKE"> Smoke</label>
|
||||||
|
<label><input type="checkbox" value="RAIN (LIGHT)"> Rain (Light)</label>
|
||||||
|
<label><input type="checkbox" value="RAIN (MOD/HEAVY)"> Rain (Mod/Heavy)</label>
|
||||||
|
<label><input type="checkbox" value="RAIN SHOWER"> Rain Shower</label>
|
||||||
|
<label><input type="checkbox" value="DRIZZLE"> Drizzle</label>
|
||||||
|
<label><input type="checkbox" value="FREEZING RAIN"> Freezing Rain</label>
|
||||||
|
<label><input type="checkbox" value="THUNDERSTORM"> Thunderstorm</label>
|
||||||
|
<label><input type="checkbox" value="TS IN VICINITY"> TS in Vicinity</label>
|
||||||
|
<label><input type="checkbox" value="HAIL"> Hail</label>
|
||||||
|
<label><input type="checkbox" value="SNOW"> Snow</label>
|
||||||
|
<label><input type="checkbox" value="BLOWING SNOW"> Blowing Snow</label>
|
||||||
|
<label><input type="checkbox" value="ICE PELLETS"> Ice Pellets</label>
|
||||||
|
<label><input type="checkbox" value="TORNADO/FUNNEL"> Funnel / Tornado</label>
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
<fieldset>
|
||||||
|
<legend>15-27. INSTRUMENT MEASUREMENTS</legend>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col"><label for="temp">15. Temperature (°F/°C)</label><input type="text" id="temp" placeholder="72"></div>
|
||||||
|
<div class="col"><label for="humidity">16. Humidity (%)</label><input type="text" id="humidity" placeholder="65"></div>
|
||||||
|
<div class="col"><label for="dewPoint">17. Dew Point (°F/°C)</label><input type="text" id="dewPoint" placeholder="58"></div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col"><label for="barometer">18. Barometer (inHg/mb)</label><input type="text" id="barometer" placeholder="29.92"></div>
|
||||||
|
<div class="col">
|
||||||
|
<label for="barTrend">19. 3-Hour Pressure Trend</label>
|
||||||
|
<select id="barTrend">
|
||||||
|
<option value="Steady" selected>Steady</option>
|
||||||
|
<option value="Falling Rapidly">Falling Rapidly</option>
|
||||||
|
<option value="Falling Slowly">Falling Slowly</option>
|
||||||
|
<option value="Rising Rapidly">Rising Rapidly</option>
|
||||||
|
<option value="Rising Slowly">Rising Slowly</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col"><label for="windSpeed">20. Wind Speed</label><input type="text" id="windSpeed" placeholder="8"></div>
|
||||||
|
<div class="col">
|
||||||
|
<label for="windDir">21. Wind Direction</label>
|
||||||
|
<select id="windDir">
|
||||||
|
<option value="Calm">Calm</option><option value="Variable">Variable</option>
|
||||||
|
<option value="N">North</option><option value="NE">NE</option><option value="E">East</option><option value="SE">SE</option>
|
||||||
|
<option value="S" selected>South</option><option value="SW">SW</option><option value="W">West</option><option value="NW">NW</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col"><label for="windGust">22. Wind Gust</label><input type="text" id="windGust" placeholder="15"></div>
|
||||||
|
<div class="col"><label for="maxGust">23. Max Gust (Past Hr)</label><input type="text" id="maxGust" placeholder="22"></div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col"><label for="rain1hr">24. Rain (Past 1 Hr)</label><input type="text" id="rain1hr" placeholder="0.25"></div>
|
||||||
|
<div class="col"><label for="rainTotal">25. Rain Total (Event)</label><input type="text" id="rainTotal" placeholder="1.10"></div>
|
||||||
|
<div class="col"><label for="snow1hr">26. Snow (Past 1 Hr)</label><input type="text" id="snow1hr" placeholder="0"></div>
|
||||||
|
<div class="col"><label for="snowTotal">27. Snow Total</label><input type="text" id="snowTotal" placeholder="0"></div>
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
<fieldset>
|
||||||
|
<legend>28-29. NWS ALERTS & NOTES</legend>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col" style="max-width: 170px;">
|
||||||
|
<label for="nwsLevel">28. NWS Alert Level</label>
|
||||||
|
<select id="nwsLevel">
|
||||||
|
<option value="NONE Issued" selected>NONE Issued</option>
|
||||||
|
<option value="ADVISORY">ADVISORY</option>
|
||||||
|
<option value="WATCH">WATCH</option>
|
||||||
|
<option value="WARNING">WARNING</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col"><label for="nwsAlertDesc">Alert Specifics</label><input type="text" id="nwsAlertDesc" placeholder="e.g. Flash Flood Warning until 2100Z"></div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col"><label for="comments">29. Notes / Local Impact</label><textarea id="comments" rows="3" placeholder="Local observations, roadway impacts, localized drainage issues..."></textarea></div>
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
<div class="actions">
|
||||||
|
<button type="button" class="main-btn btn-generate" id="btnGen">Generate Packet ASCII</button>
|
||||||
|
<button type="button" class="main-btn btn-save" id="btnSaveHtml">Save as HTML</button>
|
||||||
|
<button type="button" class="main-btn btn-copy" id="btnCopy">Copy ASCII</button>
|
||||||
|
<button type="button" class="main-btn btn-clear" id="btnClear">Reset</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<label style="font-weight:bold;">Formatted ASCII for Packet / BBS Transmission:</label>
|
||||||
|
<pre id="outputPre">Fill in the fields above and click "Generate Packet ASCII"...</pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function setUTC() {
|
||||||
|
const now = new Date();
|
||||||
|
const y = now.getUTCFullYear();
|
||||||
|
const m = String(now.getUTCMonth()+1).padStart(2,'0');
|
||||||
|
const d = String(now.getUTCDate()).padStart(2,'0');
|
||||||
|
const hh = String(now.getUTCHours()).padStart(2,'0');
|
||||||
|
const mm = String(now.getUTCMinutes()).padStart(2,'0');
|
||||||
|
document.getElementById('dtLocal').value = y + '-' + m + '-' + d + ' ' + hh + mm + 'Z';
|
||||||
|
}
|
||||||
|
|
||||||
|
function clean(id) {
|
||||||
|
const el = document.getElementById(id);
|
||||||
|
return el ? (el.value || '').trim() : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function pad(str, len) {
|
||||||
|
str = (str || '').toString();
|
||||||
|
return str + ' '.repeat(Math.max(0, len - str.length));
|
||||||
|
}
|
||||||
|
|
||||||
|
function wrapText(str, width) {
|
||||||
|
if (!str) return [];
|
||||||
|
let lines = [];
|
||||||
|
str.split('\n').forEach(function(paragraph) {
|
||||||
|
let words = paragraph.split(' ');
|
||||||
|
let cur = '';
|
||||||
|
words.forEach(function(w) {
|
||||||
|
if ((cur + (cur ? ' ' : '') + w).length <= width) {
|
||||||
|
cur += (cur ? ' ' : '') + w;
|
||||||
|
} else {
|
||||||
|
if (cur) lines.push(cur);
|
||||||
|
cur = w;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (cur) lines.push(cur);
|
||||||
|
});
|
||||||
|
return lines;
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateASCII() {
|
||||||
|
try {
|
||||||
|
const div = '='.repeat(78);
|
||||||
|
const sub = '-'.repeat(78);
|
||||||
|
|
||||||
|
const call = clean('call').toUpperCase() || 'N0CALL';
|
||||||
|
const name = clean('repName').toUpperCase() || 'N/A';
|
||||||
|
const dt = clean('dtLocal') || 'N/A';
|
||||||
|
const loc = clean('locName').toUpperCase() || 'N/A';
|
||||||
|
const city = clean('city').toUpperCase();
|
||||||
|
const st = clean('state').toUpperCase();
|
||||||
|
const county = clean('county').toUpperCase();
|
||||||
|
|
||||||
|
const lat = clean('lat') || 'N/A';
|
||||||
|
const lon = clean('lon') || 'N/A';
|
||||||
|
const mgrs = clean('mgrs').toUpperCase() || 'N/A';
|
||||||
|
const grid = clean('grid').toUpperCase() || 'N/A';
|
||||||
|
|
||||||
|
const unit = clean('unitChoice');
|
||||||
|
const sky = clean('skyCover');
|
||||||
|
|
||||||
|
let conds = [];
|
||||||
|
document.querySelectorAll('.conditions-grid input[type="checkbox"]').forEach(function(cb) {
|
||||||
|
if (cb.checked) conds.push(cb.value);
|
||||||
|
});
|
||||||
|
const condStr = conds.length ? conds.join(', ') : 'NONE';
|
||||||
|
|
||||||
|
const temp = clean('temp') || 'N/A';
|
||||||
|
const hum = clean('humidity') || 'N/A';
|
||||||
|
const dew = clean('dewPoint') || 'N/A';
|
||||||
|
const bar = clean('barometer') || 'N/A';
|
||||||
|
const trend = clean('barTrend');
|
||||||
|
const wspd = clean('windSpeed') || '0';
|
||||||
|
const wdir = clean('windDir');
|
||||||
|
const gust = clean('windGust') || '0';
|
||||||
|
const maxg = clean('maxGust') || '0';
|
||||||
|
const r1 = clean('rain1hr') || '0';
|
||||||
|
const rtot = clean('rainTotal') || '0';
|
||||||
|
const s1 = clean('snow1hr') || '0';
|
||||||
|
const stot = clean('snowTotal') || '0';
|
||||||
|
|
||||||
|
const nws = clean('nwsLevel');
|
||||||
|
const nwsDesc = clean('nwsAlertDesc').toUpperCase();
|
||||||
|
const cmt = clean('comments').toUpperCase() || 'NONE';
|
||||||
|
|
||||||
|
let wrappedCmt = wrapText(cmt, 72).map(function(l){ return ' ' + l; }).join('\n');
|
||||||
|
|
||||||
|
const out = div + '\n' +
|
||||||
|
'CURRENT LOCAL SURFACE WEATHER OBSERVATION (PACKET ASCII)\n' +
|
||||||
|
div + '\n' +
|
||||||
|
'CALLSIGN : ' + pad(call, 14) + ' OBSERVER: ' + pad(name, 22) + ' DATE/TIME: ' + dt + '\n' +
|
||||||
|
'LOCATION : ' + loc + '\n' +
|
||||||
|
'AREA : ' + (city ? city + ', ' : '') + st + (county ? ' (' + county + ' CO)' : '') + '\n' +
|
||||||
|
'COORDS : LAT: ' + pad(lat, 10) + ' LON: ' + pad(lon, 10) + ' MGRS: ' + pad(mgrs, 14) + ' GRID: ' + grid + '\n' +
|
||||||
|
sub + '\n' +
|
||||||
|
'UNITS: ' + unit + ' | SKY COVER: ' + sky + '\n' +
|
||||||
|
'CURRENT PHENOMENA: ' + condStr + '\n' +
|
||||||
|
sub + '\n' +
|
||||||
|
'TEMP : ' + pad(temp, 8) + ' HUMIDITY: ' + pad(hum + '%', 8) + ' DEW POINT: ' + dew + '\n' +
|
||||||
|
'PRESSURE: ' + pad(bar, 8) + ' 3-HR TREND: ' + trend + '\n' +
|
||||||
|
'WIND : ' + wspd + ' from ' + wdir + ' (GUST: ' + gust + ' | PEAK: ' + maxg + ')\n' +
|
||||||
|
'RAIN : 1-HR: ' + pad(r1, 8) + ' TOTAL: ' + rtot + '\n' +
|
||||||
|
'SNOW : 1-HR: ' + pad(s1, 8) + ' TOTAL: ' + stot + '\n' +
|
||||||
|
sub + '\n' +
|
||||||
|
'NWS ALERT: ' + nws + (nwsDesc ? ' (' + nwsDesc + ')' : '') + '\n' +
|
||||||
|
'NOTES / OBSERVATIONS:\n' + wrappedCmt + '\n' +
|
||||||
|
div;
|
||||||
|
|
||||||
|
document.getElementById('outputPre').textContent = out;
|
||||||
|
} catch(err) {
|
||||||
|
alert("Error generating text: " + err.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function copyASCII() {
|
||||||
|
const txt = document.getElementById('outputPre').textContent;
|
||||||
|
if (navigator.clipboard && navigator.clipboard.writeText) {
|
||||||
|
navigator.clipboard.writeText(txt).then(function() {
|
||||||
|
alert("Local Weather report copied to clipboard!");
|
||||||
|
}).catch(function() {
|
||||||
|
fallbackCopy();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
fallbackCopy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function fallbackCopy() {
|
||||||
|
const pre = document.getElementById('outputPre');
|
||||||
|
const range = document.createRange();
|
||||||
|
range.selectNode(pre);
|
||||||
|
window.getSelection().removeAllRanges();
|
||||||
|
window.getSelection().addRange(range);
|
||||||
|
document.execCommand('copy');
|
||||||
|
alert("Copied to clipboard!");
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearForm() {
|
||||||
|
if(confirm("Reset Local Weather form?")) {
|
||||||
|
document.getElementById('localWxForm').reset();
|
||||||
|
document.getElementById('outputPre').textContent = 'Fill in the fields above and click "Generate Packet ASCII"...';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('btnDateNow').addEventListener('click', setUTC);
|
||||||
|
document.getElementById('btnGen').addEventListener('click', generateASCII);
|
||||||
|
document.getElementById('btnSaveHtml').addEventListener('click', openSaveModal);
|
||||||
|
document.getElementById('btnCopy').addEventListener('click', copyASCII);
|
||||||
|
document.getElementById('btnClear').addEventListener('click', clearForm);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// File Name Modal Window & HTML Exporter
|
||||||
|
function openSaveModal() {
|
||||||
|
var now = new Date();
|
||||||
|
var dStr = now.toISOString().slice(0, 10);
|
||||||
|
var defName = 'local_weather_' + dStr + '.html';
|
||||||
|
var modal = document.getElementById('saveModalBackdrop');
|
||||||
|
var input = document.getElementById('saveModalFileName');
|
||||||
|
if (input) input.value = defName;
|
||||||
|
if (modal) modal.classList.add('active');
|
||||||
|
setTimeout(function() {
|
||||||
|
if (input) { input.focus(); input.select(); }
|
||||||
|
}, 50);
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeSaveModal() {
|
||||||
|
var modal = document.getElementById('saveModalBackdrop');
|
||||||
|
if (modal) modal.classList.remove('active');
|
||||||
|
}
|
||||||
|
|
||||||
|
function executeSaveHTML() {
|
||||||
|
var input = document.getElementById('saveModalFileName');
|
||||||
|
var chosenName = input ? input.value.trim() : '';
|
||||||
|
if (!chosenName) return;
|
||||||
|
if (!chosenName.toLowerCase().endsWith('.html') && !chosenName.toLowerCase().endsWith('.htm')) {
|
||||||
|
chosenName += '.html';
|
||||||
|
}
|
||||||
|
closeSaveModal();
|
||||||
|
|
||||||
|
document.querySelectorAll('input[type="text"]').forEach(function(el) { el.setAttribute('value', el.value); });
|
||||||
|
document.querySelectorAll('input[type="checkbox"], input[type="radio"]').forEach(function(el) {
|
||||||
|
if (el.checked) el.setAttribute('checked', 'checked');
|
||||||
|
else el.removeAttribute('checked');
|
||||||
|
});
|
||||||
|
document.querySelectorAll('textarea').forEach(function(el) { el.textContent = el.value; });
|
||||||
|
document.querySelectorAll('select').forEach(function(el) {
|
||||||
|
Array.from(el.options).forEach(function(opt) {
|
||||||
|
if (opt.value === el.value) opt.setAttribute('selected', 'selected');
|
||||||
|
else opt.removeAttribute('selected');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
var pre = document.getElementById('ascii-report') || document.getElementById('outputPre') || document.getElementById('preview') || document.getElementById('outputArea');
|
||||||
|
if (pre) pre.textContent = pre.textContent;
|
||||||
|
|
||||||
|
var clonedDoc = document.documentElement.cloneNode(true);
|
||||||
|
var modalInClone = clonedDoc.querySelector('#saveModalBackdrop');
|
||||||
|
if (modalInClone) modalInClone.classList.remove('active');
|
||||||
|
|
||||||
|
var htmlContent = '<!DOCTYPE html>\n' + clonedDoc.outerHTML;
|
||||||
|
var blob = new Blob([htmlContent], { type: 'text/html;charset=utf-8' });
|
||||||
|
var url = URL.createObjectURL(blob);
|
||||||
|
var a = document.createElement('a');
|
||||||
|
a.href = url;
|
||||||
|
a.download = chosenName;
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
document.body.removeChild(a);
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener('DOMContentLoaded', function() {
|
||||||
|
var cancelBtn = document.getElementById('saveModalCancelBtn');
|
||||||
|
var confirmBtn = document.getElementById('saveModalSaveBtn');
|
||||||
|
var modalInput = document.getElementById('saveModalFileName');
|
||||||
|
var modalBackdrop = document.getElementById('saveModalBackdrop');
|
||||||
|
|
||||||
|
if (cancelBtn) cancelBtn.addEventListener('click', closeSaveModal);
|
||||||
|
if (confirmBtn) confirmBtn.addEventListener('click', executeSaveHTML);
|
||||||
|
if (modalInput) {
|
||||||
|
modalInput.addEventListener('keydown', function(e) {
|
||||||
|
if (e.key === 'Enter') { e.preventDefault(); executeSaveHTML(); }
|
||||||
|
if (e.key === 'Escape') { e.preventDefault(); closeSaveModal(); }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (modalBackdrop) {
|
||||||
|
modalBackdrop.addEventListener('click', function(e) {
|
||||||
|
if (e.target === modalBackdrop) closeSaveModal();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
var btnSaveMain = document.getElementById('btnSaveHtml');
|
||||||
|
if (btnSaveMain) {
|
||||||
|
// Clear existing inline handlers just in case
|
||||||
|
btnSaveMain.onclick = null;
|
||||||
|
btnSaveMain.addEventListener('click', openSaveModal);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<!-- Custom File Name Modal Window -->
|
||||||
|
<div id="saveModalBackdrop" class="save-modal-backdrop">
|
||||||
|
<div class="save-modal-window">
|
||||||
|
<h3>Save Form as HTML</h3>
|
||||||
|
<p>Enter a name for the saved offline HTML file:</p>
|
||||||
|
<input type="text" id="saveModalFileName" spellcheck="false">
|
||||||
|
<div class="save-modal-actions">
|
||||||
|
<button type="button" class="save-modal-btn save-modal-btn-cancel" id="saveModalCancelBtn">Cancel</button>
|
||||||
|
<button type="button" class="save-modal-btn save-modal-btn-confirm" id="saveModalSaveBtn">Save File</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,461 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Severe Weather Report (Packet Radio)</title>
|
||||||
|
<style>
|
||||||
|
* { box-sizing: border-box; }
|
||||||
|
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, monospace; background: #fdfaf6; color: #222; padding: 20px; margin: 0; }
|
||||||
|
.container { max-width: 940px; margin: 0 auto; background: #fff; padding: 24px; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); }
|
||||||
|
.header { background: #d84315; color: white; padding: 12px; border-radius: 6px; text-align: center; margin-bottom: 18px; }
|
||||||
|
.header h2 { margin: 0; font-size: 1.3rem; letter-spacing: 1px; }
|
||||||
|
.header p { margin: 4px 0 0 0; font-size: 0.85rem; opacity: 0.9; }
|
||||||
|
fieldset { border: 1px solid #ffccbc; border-radius: 6px; padding: 14px; margin-bottom: 14px; background: #fffbf9; }
|
||||||
|
legend { font-weight: bold; padding: 0 8px; font-size: 0.85rem; color: #bf360c; }
|
||||||
|
.row { display: flex; flex-wrap: wrap; gap: 12px; margin-bottom: 10px; }
|
||||||
|
.col { flex: 1; min-width: 140px; display: flex; flex-direction: column; }
|
||||||
|
.label-row { display: flex; justify-content: space-between; align-items: center; margin-bottom: 4px; }
|
||||||
|
label { font-size: 0.8rem; font-weight: 600; color: #333; }
|
||||||
|
input[type="text"], select, textarea { padding: 7px 8px; font-size: 0.88rem; border: 1px solid #999; border-radius: 4px; font-family: inherit; width: 100%; }
|
||||||
|
textarea { resize: vertical; min-height: 70px; }
|
||||||
|
.btn-now { background: #4a5568; color: #fff; padding: 4px 8px; font-size: 0.72rem; border-radius: 4px; border: none; cursor: pointer; }
|
||||||
|
.btn-now:hover { background: #2d3748; }
|
||||||
|
.actions { display: flex; gap: 10px; margin: 18px 0; }
|
||||||
|
button.main-btn { flex: 1; padding: 12px; font-size: 0.95rem; font-weight: bold; border: none; border-radius: 6px; cursor: pointer; }
|
||||||
|
.btn-generate { background: #d84315; color: #fff; }
|
||||||
|
.btn-generate:hover { background: #bf360c; }
|
||||||
|
.btn-save { background: #5c6bc0; color: #fff; }
|
||||||
|
.btn-save:hover { background: #3f51b5; }
|
||||||
|
.btn-copy { background: #2e7d32; color: #fff; }
|
||||||
|
.btn-copy:hover { background: #1b5e20; }
|
||||||
|
.btn-clear { background: #c62828; color: #fff; flex: 0.35; }
|
||||||
|
pre { background: #181c20; color: #39ff14; padding: 14px; border-radius: 6px; overflow-x: auto; font-family: "Courier New", Courier, monospace; font-size: 0.82rem; line-height: 1.32; min-height: 150px; }
|
||||||
|
.nav-bar { margin-bottom: 12px; font-size: 0.85rem; }
|
||||||
|
.nav-bar a { color: #d84315; text-decoration: none; font-weight: bold; }
|
||||||
|
|
||||||
|
/* Modal Save Dialog Window */
|
||||||
|
.save-modal-backdrop {
|
||||||
|
display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%;
|
||||||
|
background: rgba(0,0,0,0.55); z-index: 99999; justify-content: center; align-items: center;
|
||||||
|
}
|
||||||
|
.save-modal-backdrop.active { display: flex; }
|
||||||
|
.save-modal-window {
|
||||||
|
background: #ffffff; padding: 24px 28px; border-radius: 10px; width: 92%; max-width: 480px;
|
||||||
|
box-shadow: 0 10px 30px rgba(0,0,0,0.3); border: 1px solid #cbd5e1; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||||
|
}
|
||||||
|
.save-modal-window h3 { margin: 0 0 8px 0; font-size: 1.25rem; color: #1e293b; font-weight: bold; }
|
||||||
|
.save-modal-window p { margin: 0 0 16px 0; font-size: 0.88rem; color: #64748b; line-height: 1.4; }
|
||||||
|
.save-modal-window input[type="text"] {
|
||||||
|
width: 100%; padding: 10px 12px; font-size: 0.95rem; border: 2px solid #94a3b8; border-radius: 6px;
|
||||||
|
margin-bottom: 20px; font-family: inherit; box-sizing: border-box; text-transform: none;
|
||||||
|
}
|
||||||
|
.save-modal-window input[type="text"]:focus { border-color: #2563eb; outline: none; }
|
||||||
|
.save-modal-actions { display: flex; gap: 10px; justify-content: flex-end; }
|
||||||
|
.save-modal-btn { padding: 9px 18px; font-size: 0.9rem; font-weight: bold; border-radius: 6px; border: none; cursor: pointer; }
|
||||||
|
.save-modal-btn-confirm { background: #2563eb; color: #ffffff; }
|
||||||
|
.save-modal-btn-confirm:hover { background: #1d4ed8; }
|
||||||
|
.save-modal-btn-cancel { background: #e2e8f0; color: #334155; }
|
||||||
|
.save-modal-btn-cancel:hover { background: #cbd5e1; }
|
||||||
|
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<div class="nav-bar"><a href="../index.html">← Back to Packet Forms Hub</a></div>
|
||||||
|
<div class="header">
|
||||||
|
<h2>SEVERE WEATHER REPORT (SKYWARN / NWS)</h2>
|
||||||
|
<p>Severe Convective, Wind, Hail, Flood & Winter Weather Spotter Report (Packet ASCII)</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form id="sevWxForm" onsubmit="event.preventDefault(); return false;">
|
||||||
|
<fieldset>
|
||||||
|
<legend>1-6. REPORT HEADER & SPOTTER INFO</legend>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col" style="max-width: 140px;"><label for="call">1. Sender Callsign</label><input type="text" id="call" placeholder="N0CALL"></div>
|
||||||
|
<div class="col">
|
||||||
|
<div class="label-row">
|
||||||
|
<label for="dtEvent">2. Event Date / Time (UTC)</label>
|
||||||
|
<button type="button" class="btn-now" id="btnDateNow">Current UTC</button>
|
||||||
|
</div>
|
||||||
|
<input type="text" id="dtEvent" placeholder="YYYY-MM-DD HHMMZ">
|
||||||
|
</div>
|
||||||
|
<div class="col" style="max-width: 170px;">
|
||||||
|
<label for="repVersion">3. Report Version</label>
|
||||||
|
<select id="repVersion">
|
||||||
|
<option value="First Report" selected>First Report</option>
|
||||||
|
<option value="Update Report">Update Report</option>
|
||||||
|
<option value="Final Report">Final Report</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col"><label for="repName">4. Reporting Party Name</label><input type="text" id="repName"></div>
|
||||||
|
<div class="col"><label for="phone">5. Phone Number</label><input type="text" id="phone"></div>
|
||||||
|
<div class="col"><label for="email">6. Email / Winlink</label><input type="text" id="email"></div>
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
<fieldset>
|
||||||
|
<legend>7-14. EVENT LOCATION & COORDINATES</legend>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col"><label for="city">7. City</label><input type="text" id="city"></div>
|
||||||
|
<div class="col"><label for="region">8. State / Region</label><input type="text" id="region"></div>
|
||||||
|
<div class="col"><label for="county">9. County</label><input type="text" id="county"></div>
|
||||||
|
<div class="col"><label for="otherGeo">10. Other Geographic Info</label><input type="text" id="otherGeo" placeholder="Landmarks, highway milepost"></div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col"><label for="lat">11. Latitude</label><input type="text" id="lat" placeholder="30.2672"></div>
|
||||||
|
<div class="col"><label for="lon">12. Longitude</label><input type="text" id="lon" placeholder="-97.7431"></div>
|
||||||
|
<div class="col"><label for="mgrs">13. MGRS</label><input type="text" id="mgrs"></div>
|
||||||
|
<div class="col" style="max-width: 120px;"><label for="grid">14. Grid</label><input type="text" id="grid" placeholder="EM10dj"></div>
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
<fieldset>
|
||||||
|
<legend>15-24. OBSERVED SEVERE CONDITIONS</legend>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col" style="max-width: 170px;">
|
||||||
|
<label for="unitChoice">Measurement Units</label>
|
||||||
|
<select id="unitChoice">
|
||||||
|
<option value="Imperial" selected>Imperial (mph, in)</option>
|
||||||
|
<option value="Metric">Metric (km/h, mm)</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col">
|
||||||
|
<label for="flood">15. Flooding</label>
|
||||||
|
<select id="flood">
|
||||||
|
<option value="None" selected>None</option>
|
||||||
|
<option value="Minor Street Flooding">Minor Street Flooding</option>
|
||||||
|
<option value="Low Water Crossing Flooded">Low Water Crossing Flooded</option>
|
||||||
|
<option value="Widespread Flooding">Widespread Flooding</option>
|
||||||
|
<option value="Home/Business Flooded">Home/Business Flooded</option>
|
||||||
|
<option value="Creek/Stream Out of Banks">Creek/Stream Out of Banks</option>
|
||||||
|
<option value="River Out of Banks">River Out of Banks</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col">
|
||||||
|
<label for="hail">16. Hail Size</label>
|
||||||
|
<select id="hail">
|
||||||
|
<option value="None" selected>None</option>
|
||||||
|
<option value="0.75 in (Penny)">0.75 in (Penny)</option>
|
||||||
|
<option value="0.88 in (Nickel)">0.88 in (Nickel)</option>
|
||||||
|
<option value="1.00 in (Quarter)">1.00 in (Quarter)</option>
|
||||||
|
<option value="1.25 in (Half Dollar)">1.25 in (Half Dollar)</option>
|
||||||
|
<option value="1.75 in (Golfball)">1.75 in (Golfball)</option>
|
||||||
|
<option value="2.50 in (Tennisball)">2.50 in (Tennisball)</option>
|
||||||
|
<option value="2.75 in (Baseball)">2.75 in (Baseball)</option>
|
||||||
|
<option value="4.00 in (Grapefruit)">4.00 in (Grapefruit)</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col"><label for="windSpeed">17. High Wind Speed</label><input type="text" id="windSpeed" placeholder="e.g. 55 mph"></div>
|
||||||
|
<div class="col">
|
||||||
|
<label for="tornado">18. Tornado / Funnel Cloud</label>
|
||||||
|
<select id="tornado">
|
||||||
|
<option value="None" selected>None</option>
|
||||||
|
<option value="Wall Cloud">Wall Cloud</option>
|
||||||
|
<option value="Funnel Cloud">Funnel Cloud</option>
|
||||||
|
<option value="Tornado on Ground">Tornado on Ground</option>
|
||||||
|
<option value="Waterspout">Waterspout</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col">
|
||||||
|
<label for="windDmg">19. Wind Damage</label>
|
||||||
|
<select id="windDmg">
|
||||||
|
<option value="None" selected>None</option>
|
||||||
|
<option value="Small Tree Limbs Down">Small Tree Limbs Down</option>
|
||||||
|
<option value="Large Branches Down">Large Branches Down</option>
|
||||||
|
<option value="Trees Snapped or Uprooted">Trees Snapped or Uprooted</option>
|
||||||
|
<option value="Power Lines Down">Power Lines Down</option>
|
||||||
|
<option value="Roof / Structural Damage">Roof / Structural Damage</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col">
|
||||||
|
<label for="precipType">20. Winter Precipitation Type</label>
|
||||||
|
<select id="precipType">
|
||||||
|
<option value="None" selected>None</option>
|
||||||
|
<option value="Snow">Snow</option>
|
||||||
|
<option value="Sleet">Sleet</option>
|
||||||
|
<option value="Freezing Rain">Freezing Rain</option>
|
||||||
|
<option value="Mixture">Mixture</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col"><label for="snowDepth">21. Snow Accumulation</label><input type="text" id="snowDepth" placeholder="e.g. 4.0 in"></div>
|
||||||
|
<div class="col"><label for="iceAcc">22. Freezing Rain Ice Acc.</label><input type="text" id="iceAcc" placeholder="e.g. 0.25 in"></div>
|
||||||
|
<div class="col"><label for="heavyRain">23. Heavy Rain Amount</label><input type="text" id="heavyRain" placeholder="e.g. 2.50 in"></div>
|
||||||
|
<div class="col"><label for="rainPeriod">24. Rain Time Period</label><input type="text" id="rainPeriod" placeholder="e.g. In 2 hours"></div>
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
<fieldset>
|
||||||
|
<legend>25. ADDITIONAL INFORMATION / DAMAGE DESCRIPTIONS</legend>
|
||||||
|
<textarea id="comments" rows="3" placeholder="Damage specifics, road closures, structural impact, spotter direction from event..."></textarea>
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
<div class="actions">
|
||||||
|
<button type="button" class="main-btn btn-generate" id="btnGen">Generate Packet ASCII</button>
|
||||||
|
<button type="button" class="main-btn btn-save" id="btnSaveHtml">Save as HTML</button>
|
||||||
|
<button type="button" class="main-btn btn-copy" id="btnCopy">Copy ASCII</button>
|
||||||
|
<button type="button" class="main-btn btn-clear" id="btnClear">Reset</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<label style="font-weight:bold;">Formatted ASCII for Packet / NWS Transmission:</label>
|
||||||
|
<pre id="outputPre">Fill in the fields above and click "Generate Packet ASCII"...</pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function setUTC() {
|
||||||
|
const now = new Date();
|
||||||
|
const y = now.getUTCFullYear();
|
||||||
|
const m = String(now.getUTCMonth()+1).padStart(2,'0');
|
||||||
|
const d = String(now.getUTCDate()).padStart(2,'0');
|
||||||
|
const hh = String(now.getUTCHours()).padStart(2,'0');
|
||||||
|
const mm = String(now.getUTCMinutes()).padStart(2,'0');
|
||||||
|
document.getElementById('dtEvent').value = y + '-' + m + '-' + d + ' ' + hh + mm + 'Z';
|
||||||
|
}
|
||||||
|
|
||||||
|
function clean(id) {
|
||||||
|
const el = document.getElementById(id);
|
||||||
|
return el ? (el.value || '').trim() : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function pad(str, len) {
|
||||||
|
str = (str || '').toString();
|
||||||
|
return str + ' '.repeat(Math.max(0, len - str.length));
|
||||||
|
}
|
||||||
|
|
||||||
|
function wrapText(str, width) {
|
||||||
|
if (!str) return [];
|
||||||
|
let lines = [];
|
||||||
|
str.split('\n').forEach(function(paragraph) {
|
||||||
|
let words = paragraph.split(' ');
|
||||||
|
let cur = '';
|
||||||
|
words.forEach(function(w) {
|
||||||
|
if ((cur + (cur ? ' ' : '') + w).length <= width) {
|
||||||
|
cur += (cur ? ' ' : '') + w;
|
||||||
|
} else {
|
||||||
|
if (cur) lines.push(cur);
|
||||||
|
cur = w;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (cur) lines.push(cur);
|
||||||
|
});
|
||||||
|
return lines;
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateASCII() {
|
||||||
|
try {
|
||||||
|
const div = '='.repeat(78);
|
||||||
|
const sub = '-'.repeat(78);
|
||||||
|
|
||||||
|
const call = clean('call').toUpperCase() || 'N0CALL';
|
||||||
|
const dt = clean('dtEvent') || 'N/A';
|
||||||
|
const ver = clean('repVersion').toUpperCase();
|
||||||
|
const name = clean('repName').toUpperCase() || 'N/A';
|
||||||
|
const phone = clean('phone') || 'N/A';
|
||||||
|
const email = clean('email').toUpperCase() || 'N/A';
|
||||||
|
|
||||||
|
const city = clean('city').toUpperCase();
|
||||||
|
const reg = clean('region').toUpperCase();
|
||||||
|
const county = clean('county').toUpperCase();
|
||||||
|
const other = clean('otherGeo').toUpperCase();
|
||||||
|
const lat = clean('lat') || 'N/A';
|
||||||
|
const lon = clean('lon') || 'N/A';
|
||||||
|
const mgrs = clean('mgrs').toUpperCase() || 'N/A';
|
||||||
|
const grid = clean('grid').toUpperCase() || 'N/A';
|
||||||
|
|
||||||
|
const units = clean('unitChoice');
|
||||||
|
const flood = clean('flood');
|
||||||
|
const hail = clean('hail');
|
||||||
|
const wspd = clean('windSpeed') || 'NONE';
|
||||||
|
const tor = clean('tornado');
|
||||||
|
const wdmg = clean('windDmg');
|
||||||
|
const ptype = clean('precipType');
|
||||||
|
const snow = clean('snowDepth') || 'NONE';
|
||||||
|
const ice = clean('iceAcc') || 'NONE';
|
||||||
|
const rain = clean('heavyRain') || 'NONE';
|
||||||
|
const rperiod = clean('rainPeriod') || 'N/A';
|
||||||
|
const cmt = clean('comments').toUpperCase() || 'NONE REPORTED';
|
||||||
|
|
||||||
|
let wrappedCmt = wrapText(cmt, 72).map(function(l){ return ' ' + l; }).join('\n');
|
||||||
|
|
||||||
|
const out = div + '\n' +
|
||||||
|
'SEVERE WEATHER SPOTTER REPORT (PACKET ASCII)\n' +
|
||||||
|
div + '\n' +
|
||||||
|
'1. SENDER: ' + pad(call, 14) + ' 2. DATE/TIME: ' + pad(dt, 20) + ' 3. VERSION: ' + ver + '\n' +
|
||||||
|
'4. OBSERVER: ' + pad(name, 22) + ' PHONE: ' + pad(phone, 16) + ' EMAIL: ' + email + '\n' +
|
||||||
|
sub + '\n' +
|
||||||
|
'EVENT LOCATION:\n' +
|
||||||
|
'AREA : ' + (city ? city + ', ' : '') + reg + (county ? ' (' + county + ' CO)' : '') + (other ? ' [' + other + ']' : '') + '\n' +
|
||||||
|
'COORDS: LAT: ' + pad(lat, 12) + ' LON: ' + pad(lon, 12) + ' MGRS: ' + pad(mgrs, 16) + ' GRID: ' + grid + '\n' +
|
||||||
|
sub + '\n' +
|
||||||
|
'SEVERE PHENOMENA (' + units + '):\n' +
|
||||||
|
'15. FLOODING : ' + flood + '\n' +
|
||||||
|
'16. HAIL SIZE : ' + hail + '\n' +
|
||||||
|
'17. HIGH WIND SPEED : ' + wspd + '\n' +
|
||||||
|
'18. TORNADO / FUNNEL : ' + tor + '\n' +
|
||||||
|
'19. WIND DAMAGE : ' + wdmg + '\n' +
|
||||||
|
'20. WINTER PRECIP : ' + ptype + ' (SNOW: ' + snow + ' | ICE ACC: ' + ice + ')\n' +
|
||||||
|
'23. HEAVY RAIN : ' + rain + ' (' + rperiod + ')\n' +
|
||||||
|
sub + '\n' +
|
||||||
|
'25. ADDITIONAL COMMENTS & DAMAGE DESCRIPTIONS:\n' +
|
||||||
|
wrappedCmt + '\n' +
|
||||||
|
div;
|
||||||
|
|
||||||
|
document.getElementById('outputPre').textContent = out;
|
||||||
|
} catch(err) {
|
||||||
|
alert("Error generating text: " + err.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function copyASCII() {
|
||||||
|
const txt = document.getElementById('outputPre').textContent;
|
||||||
|
if (navigator.clipboard && navigator.clipboard.writeText) {
|
||||||
|
navigator.clipboard.writeText(txt).then(function() {
|
||||||
|
alert("Severe Weather report copied to clipboard!");
|
||||||
|
}).catch(function() {
|
||||||
|
fallbackCopy();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
fallbackCopy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function fallbackCopy() {
|
||||||
|
const pre = document.getElementById('outputPre');
|
||||||
|
const range = document.createRange();
|
||||||
|
range.selectNode(pre);
|
||||||
|
window.getSelection().removeAllRanges();
|
||||||
|
window.getSelection().addRange(range);
|
||||||
|
document.execCommand('copy');
|
||||||
|
alert("Copied to clipboard!");
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearForm() {
|
||||||
|
if(confirm("Reset Severe Weather form?")) {
|
||||||
|
document.getElementById('sevWxForm').reset();
|
||||||
|
document.getElementById('outputPre').textContent = 'Fill in the fields above and click "Generate Packet ASCII"...';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('btnDateNow').addEventListener('click', setUTC);
|
||||||
|
document.getElementById('btnGen').addEventListener('click', generateASCII);
|
||||||
|
document.getElementById('btnSaveHtml').addEventListener('click', openSaveModal);
|
||||||
|
document.getElementById('btnCopy').addEventListener('click', copyASCII);
|
||||||
|
document.getElementById('btnClear').addEventListener('click', clearForm);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// File Name Modal Window & HTML Exporter
|
||||||
|
function openSaveModal() {
|
||||||
|
var now = new Date();
|
||||||
|
var dStr = now.toISOString().slice(0, 10);
|
||||||
|
var defName = 'severe_wx_' + dStr + '.html';
|
||||||
|
var modal = document.getElementById('saveModalBackdrop');
|
||||||
|
var input = document.getElementById('saveModalFileName');
|
||||||
|
if (input) input.value = defName;
|
||||||
|
if (modal) modal.classList.add('active');
|
||||||
|
setTimeout(function() {
|
||||||
|
if (input) { input.focus(); input.select(); }
|
||||||
|
}, 50);
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeSaveModal() {
|
||||||
|
var modal = document.getElementById('saveModalBackdrop');
|
||||||
|
if (modal) modal.classList.remove('active');
|
||||||
|
}
|
||||||
|
|
||||||
|
function executeSaveHTML() {
|
||||||
|
var input = document.getElementById('saveModalFileName');
|
||||||
|
var chosenName = input ? input.value.trim() : '';
|
||||||
|
if (!chosenName) return;
|
||||||
|
if (!chosenName.toLowerCase().endsWith('.html') && !chosenName.toLowerCase().endsWith('.htm')) {
|
||||||
|
chosenName += '.html';
|
||||||
|
}
|
||||||
|
closeSaveModal();
|
||||||
|
|
||||||
|
document.querySelectorAll('input[type="text"]').forEach(function(el) { el.setAttribute('value', el.value); });
|
||||||
|
document.querySelectorAll('input[type="checkbox"], input[type="radio"]').forEach(function(el) {
|
||||||
|
if (el.checked) el.setAttribute('checked', 'checked');
|
||||||
|
else el.removeAttribute('checked');
|
||||||
|
});
|
||||||
|
document.querySelectorAll('textarea').forEach(function(el) { el.textContent = el.value; });
|
||||||
|
document.querySelectorAll('select').forEach(function(el) {
|
||||||
|
Array.from(el.options).forEach(function(opt) {
|
||||||
|
if (opt.value === el.value) opt.setAttribute('selected', 'selected');
|
||||||
|
else opt.removeAttribute('selected');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
var pre = document.getElementById('ascii-report') || document.getElementById('outputPre') || document.getElementById('preview') || document.getElementById('outputArea');
|
||||||
|
if (pre) pre.textContent = pre.textContent;
|
||||||
|
|
||||||
|
var clonedDoc = document.documentElement.cloneNode(true);
|
||||||
|
var modalInClone = clonedDoc.querySelector('#saveModalBackdrop');
|
||||||
|
if (modalInClone) modalInClone.classList.remove('active');
|
||||||
|
|
||||||
|
var htmlContent = '<!DOCTYPE html>\n' + clonedDoc.outerHTML;
|
||||||
|
var blob = new Blob([htmlContent], { type: 'text/html;charset=utf-8' });
|
||||||
|
var url = URL.createObjectURL(blob);
|
||||||
|
var a = document.createElement('a');
|
||||||
|
a.href = url;
|
||||||
|
a.download = chosenName;
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
document.body.removeChild(a);
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener('DOMContentLoaded', function() {
|
||||||
|
var cancelBtn = document.getElementById('saveModalCancelBtn');
|
||||||
|
var confirmBtn = document.getElementById('saveModalSaveBtn');
|
||||||
|
var modalInput = document.getElementById('saveModalFileName');
|
||||||
|
var modalBackdrop = document.getElementById('saveModalBackdrop');
|
||||||
|
|
||||||
|
if (cancelBtn) cancelBtn.addEventListener('click', closeSaveModal);
|
||||||
|
if (confirmBtn) confirmBtn.addEventListener('click', executeSaveHTML);
|
||||||
|
if (modalInput) {
|
||||||
|
modalInput.addEventListener('keydown', function(e) {
|
||||||
|
if (e.key === 'Enter') { e.preventDefault(); executeSaveHTML(); }
|
||||||
|
if (e.key === 'Escape') { e.preventDefault(); closeSaveModal(); }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (modalBackdrop) {
|
||||||
|
modalBackdrop.addEventListener('click', function(e) {
|
||||||
|
if (e.target === modalBackdrop) closeSaveModal();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
var btnSaveMain = document.getElementById('btnSaveHtml');
|
||||||
|
if (btnSaveMain) {
|
||||||
|
// Clear existing inline handlers just in case
|
||||||
|
btnSaveMain.onclick = null;
|
||||||
|
btnSaveMain.addEventListener('click', openSaveModal);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<!-- Custom File Name Modal Window -->
|
||||||
|
<div id="saveModalBackdrop" class="save-modal-backdrop">
|
||||||
|
<div class="save-modal-window">
|
||||||
|
<h3>Save Form as HTML</h3>
|
||||||
|
<p>Enter a name for the saved offline HTML file:</p>
|
||||||
|
<input type="text" id="saveModalFileName" spellcheck="false">
|
||||||
|
<div class="save-modal-actions">
|
||||||
|
<button type="button" class="save-modal-btn save-modal-btn-cancel" id="saveModalCancelBtn">Cancel</button>
|
||||||
|
<button type="button" class="save-modal-btn save-modal-btn-confirm" id="saveModalSaveBtn">Save File</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,401 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>TPRFN - SKYWARN WINLINK GENERATOR</title>
|
||||||
|
<style>
|
||||||
|
|
||||||
|
* { box-sizing: border-box; }
|
||||||
|
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, monospace; background: #eef1f5; color: #222; padding: 20px; margin: 0; }
|
||||||
|
.container { max-width: 960px; margin: 0 auto; background: #fff; padding: 24px; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); }
|
||||||
|
.header { background: #b71c1c; color: white; padding: 12px; border-radius: 6px; text-align: center; margin-bottom: 18px; }
|
||||||
|
.header h2 { margin: 0; font-size: 1.3rem; letter-spacing: 1px; }
|
||||||
|
.header p { margin: 4px 0 0 0; font-size: 0.85rem; opacity: 0.9; }
|
||||||
|
fieldset { border: 1px solid #ef9a9a; border-radius: 6px; padding: 14px; margin-bottom: 14px; background: #fffbfb; }
|
||||||
|
legend { font-weight: bold; padding: 0 8px; font-size: 0.85rem; color: #b71c1c; }
|
||||||
|
.row { display: flex; flex-wrap: wrap; gap: 12px; margin-bottom: 10px; }
|
||||||
|
.col { flex: 1; min-width: 140px; display: flex; flex-direction: column; }
|
||||||
|
.label-row { display: flex; justify-content: space-between; align-items: center; margin-bottom: 4px; }
|
||||||
|
label { font-size: 0.8rem; font-weight: 600; color: #333; }
|
||||||
|
input[type="text"], textarea, select { padding: 7px 8px; font-size: 0.88rem; border: 1px solid #999; border-radius: 4px; font-family: inherit; width: 100%; }
|
||||||
|
textarea { resize: vertical; min-height: 50px; }
|
||||||
|
.btn-now { background: #4a5568; color: #fff; padding: 4px 8px; font-size: 0.72rem; border-radius: 4px; border: none; cursor: pointer; }
|
||||||
|
.btn-now:hover { background: #2d3748; }
|
||||||
|
.lifeline-block { background: #fafafa; border: 1px solid #e0e0e0; border-radius: 5px; padding: 10px; margin-bottom: 8px; }
|
||||||
|
.lifeline-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 6px; }
|
||||||
|
.lifeline-header span { font-size: 0.85rem; font-weight: bold; color: #333; }
|
||||||
|
.lifeline-header select { width: 170px; font-weight: bold; }
|
||||||
|
.actions { display: flex; gap: 10px; margin: 18px 0; }
|
||||||
|
button.main-btn { flex: 1; padding: 12px; font-size: 0.95rem; font-weight: bold; border: none; border-radius: 6px; cursor: pointer; }
|
||||||
|
.btn-generate { background: #b71c1c; color: #fff; }
|
||||||
|
.btn-generate:hover { background: #7f0000; }
|
||||||
|
.btn-save { background: #5c6bc0; color: #fff; }
|
||||||
|
.btn-save:hover { background: #3f51b5; }
|
||||||
|
.btn-copy { background: #2e7d32; color: #fff; }
|
||||||
|
.btn-copy:hover { background: #1b5e20; }
|
||||||
|
.btn-clear { background: #555; color: #fff; flex: 0.35; }
|
||||||
|
.btn-clear:hover { background: #333; }
|
||||||
|
pre { background: #181c20; color: #39ff14; padding: 14px; border-radius: 6px; overflow-x: auto; font-family: "Courier New", Courier, monospace; font-size: 0.82rem; line-height: 1.32; min-height: 150px; }
|
||||||
|
.nav-bar { margin-bottom: 12px; font-size: 0.85rem; }
|
||||||
|
.nav-bar a { color: #b71c1c; text-decoration: none; font-weight: bold; }
|
||||||
|
|
||||||
|
/* Modal Save Dialog Window */
|
||||||
|
.save-modal-backdrop {
|
||||||
|
display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%;
|
||||||
|
background: rgba(0,0,0,0.55); z-index: 99999; justify-content: center; align-items: center;
|
||||||
|
}
|
||||||
|
.save-modal-backdrop.active { display: flex; }
|
||||||
|
.save-modal-window {
|
||||||
|
background: #ffffff; padding: 24px 28px; border-radius: 10px; width: 92%; max-width: 480px;
|
||||||
|
box-shadow: 0 10px 30px rgba(0,0,0,0.3); border: 1px solid #cbd5e1; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||||
|
}
|
||||||
|
.save-modal-window h3 { margin: 0 0 8px 0; font-size: 1.25rem; color: #1e293b; font-weight: bold; }
|
||||||
|
.save-modal-window p { margin: 0 0 16px 0; font-size: 0.88rem; color: #64748b; line-height: 1.4; }
|
||||||
|
.save-modal-window input[type="text"] {
|
||||||
|
width: 100%; padding: 10px 12px; font-size: 0.95rem; border: 2px solid #94a3b8; border-radius: 6px;
|
||||||
|
margin-bottom: 20px; font-family: inherit; box-sizing: border-box; text-transform: none;
|
||||||
|
}
|
||||||
|
.save-modal-window input[type="text"]:focus { border-color: #2563eb; outline: none; }
|
||||||
|
.save-modal-actions { display: flex; gap: 10px; justify-content: flex-end; }
|
||||||
|
.save-modal-btn { padding: 9px 18px; font-size: 0.9rem; font-weight: bold; border-radius: 6px; border: none; cursor: pointer; }
|
||||||
|
.save-modal-btn-confirm { background: #2563eb; color: #ffffff; }
|
||||||
|
.save-modal-btn-confirm:hover { background: #1d4ed8; }
|
||||||
|
.save-modal-btn-cancel { background: #e2e8f0; color: #334155; }
|
||||||
|
.save-modal-btn-cancel:hover { background: #cbd5e1; }
|
||||||
|
|
||||||
|
|
||||||
|
.header { background: #0056b3; }
|
||||||
|
legend { color: #0056b3; }
|
||||||
|
.nav-bar a { color: #0056b3; }
|
||||||
|
.btn-generate { background: #0056b3; }
|
||||||
|
.btn-generate:hover { background: #004494; }
|
||||||
|
optgroup { font-weight: bold; color: #0056b3; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
<div class="nav-bar"><a href="../index.html">← Back to Packet Forms Hub</a></div>
|
||||||
|
<div class="header">
|
||||||
|
<h2>TPRFN SKYWARN WINLINK GENERATOR</h2>
|
||||||
|
<p><a href="https://www.tprfn.net" target="_blank" style="color:white; text-decoration:underline;">WWW.TPRFN.NET</a></p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form id="skywarnForm" onsubmit="event.preventDefault(); return false;">
|
||||||
|
<fieldset>
|
||||||
|
<legend>1. RECIPIENT & STATION INFO</legend>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col" style="flex: 2;">
|
||||||
|
<label for="nws-presets">NWS Office / Recipient</label>
|
||||||
|
<select id="nws-presets" onchange="document.getElementById('nws-office').value = this.value.toUpperCase();">
|
||||||
|
<option value="">-- SELECT OFFICE --</option>
|
||||||
|
<option value="SP N3MEL@N3MEL.#EPA.PA.USA.NOAM">(PKTNET CHECK-IN )</option>
|
||||||
|
<option value="[email protected]">[email protected] (PHILLY TACTICAL)</option>
|
||||||
|
<option value="[email protected]">[email protected] (STATE COLLEGE PA)</option>
|
||||||
|
<option value="[email protected]">[email protected] (BOSTON, MA)</option>
|
||||||
|
<option value="[email protected]">[email protected] (UPTON/NYC, NY)</option>
|
||||||
|
<option value="[email protected]">[email protected] (BALT/WASH DC)</option>
|
||||||
|
<option value="[email protected]">[email protected] (DC TACTICAL)</option>
|
||||||
|
<option value="[email protected]">[email protected] (ATLANTA, GA)</option>
|
||||||
|
<option value="[email protected]">[email protected] (NORMAN, OK)</option>
|
||||||
|
<option value="[email protected]">[email protected] (CHICAGO, IL)</option>
|
||||||
|
<option value="[email protected]">[email protected] (LOS ANGELES, CA)</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col" style="flex: 2;">
|
||||||
|
<label for="nws-office">Target Address (Manual)</label>
|
||||||
|
<input type="text" id="nws-office" placeholder="[email protected]">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col"><label for="callsign">Your Callsign</label><input type="text" id="callsign" placeholder="E.G., N3MEL"></div>
|
||||||
|
<div class="col"><label for="spotter-id">Spotter Number</label><input type="text" id="spotter-id" placeholder="E.G., CHPA-196"></div>
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
<fieldset>
|
||||||
|
<legend>2. EVENT & LOCATION</legend>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col" style="flex: 2;">
|
||||||
|
<label for="event">Event Type & Criteria</label>
|
||||||
|
<select id="event">
|
||||||
|
<option value="" selected disabled>-- SELECT ONE --</option>
|
||||||
|
<optgroup label="CHECK-IN">
|
||||||
|
<option value="PKTNET CHECK-IN">PKTNET CHECK-IN</option>
|
||||||
|
</optgroup>
|
||||||
|
<optgroup label="TESTING & DRILLS">
|
||||||
|
<option value="TEST ONLY - DISREGARD">TEST REPORT - DISREGARD / DRILL ONLY</option>
|
||||||
|
</optgroup>
|
||||||
|
<optgroup label="TORNADO & WALL CLOUDS">
|
||||||
|
<option value="TORNADO">TORNADO (ANY ROTATION/DURATION)</option>
|
||||||
|
<option value="FUNNEL CLOUD">FUNNEL CLOUD (ANY ROTATION/DURATION)</option>
|
||||||
|
<option value="ROTATING WALL CLOUD">ROTATING WALL CLOUD (SUSTAINED VERTICAL)</option>
|
||||||
|
</optgroup>
|
||||||
|
<optgroup label="HAIL SIZES">
|
||||||
|
<option value="HAIL: PENNY (0.75 IN)">HAIL: PENNY (0.75 IN)</option>
|
||||||
|
<option value="HAIL: NICKEL (0.88 IN)">HAIL: NICKEL (0.88 IN)</option>
|
||||||
|
<option value="HAIL: QUARTER (1.00 IN)">HAIL: QUARTER (1.00 IN - SEVERE)</option>
|
||||||
|
<option value="HAIL: GOLF BALL (1.75 IN)">HAIL: GOLF BALL (1.75 IN)</option>
|
||||||
|
</optgroup>
|
||||||
|
<optgroup label="WIND & RAIN">
|
||||||
|
<option value="WIND GUST: 40-50 MPH">WIND GUST: 40-50 MPH (MEASURED)</option>
|
||||||
|
<option value="WIND DAMAGE: ESTIMATED">WIND DAMAGE: ESTIMATED (TREES/STRUCTURAL)</option>
|
||||||
|
<option value="RAIN: 1+ IN/HOUR">HEAVY RAIN: 1 INCH OR MORE PER HOUR</option>
|
||||||
|
<option value="FLOODING: ROADS/STREAMS">FLOODING: STREETS, STREAMS, OR ROADS</option>
|
||||||
|
</optgroup>
|
||||||
|
<optgroup label="DAMAGE & INJURY">
|
||||||
|
<option value="DAMAGE: INJURY">DAMAGE: WEATHER-RELATED INJURY</option>
|
||||||
|
<option value="DAMAGE: PROPERTY">DAMAGE: SIGNIFICANT PROPERTY DAMAGE</option>
|
||||||
|
</optgroup>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col">
|
||||||
|
<div class="label-row">
|
||||||
|
<label for="time">Date & Time</label>
|
||||||
|
<button type="button" class="btn-now" id="btnDateNow">Current Time</button>
|
||||||
|
</div>
|
||||||
|
<input type="text" id="time" placeholder="YYYY-MM-DD HH:MM L">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col" style="flex: 2;"><label for="location">Location (Town, State)</label><input type="text" id="location" placeholder="E.G., DOWNINGTOWN, PA"></div>
|
||||||
|
<div class="col"><label for="grid">Grid Square</label><input type="text" id="grid" placeholder="E.G., FM29DX"></div>
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
<fieldset>
|
||||||
|
<legend>3. CHANGEOVER REPORT (OPTIONAL)</legend>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col"><label for="from-type">From Precip/Event Type</label><input type="text" id="from-type" placeholder="E.G., HEAVY RAIN"></div>
|
||||||
|
<div class="col"><label for="to-type">To Precip/Event Type</label><input type="text" id="to-type" placeholder="E.G., FREEZING RAIN"></div>
|
||||||
|
<div class="col">
|
||||||
|
<div class="label-row">
|
||||||
|
<label for="change-time">Changeover Time</label>
|
||||||
|
<button type="button" class="btn-now" id="btnChangeNow">Set Now</button>
|
||||||
|
</div>
|
||||||
|
<input type="text" id="change-time" placeholder="E.G., 14:15 L">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
<fieldset>
|
||||||
|
<legend>4. REMARKS</legend>
|
||||||
|
<textarea id="details" rows="3" placeholder="SPECIFICS: MOVEMENT, POWER LINES DOWN, ETC."></textarea>
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
<div class="actions">
|
||||||
|
<button type="button" class="main-btn btn-generate" id="btnGen">Generate Packet ASCII</button>
|
||||||
|
<button type="button" class="main-btn btn-save" id="btnSaveHtml">Save as HTML</button>
|
||||||
|
<button type="button" class="main-btn btn-copy" id="btnCopy">Copy ASCII</button>
|
||||||
|
<button type="button" class="main-btn btn-clear" id="btnClear">Reset</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<label style="font-weight:bold;">Formatted ASCII for Packet / BBS Transmission:</label>
|
||||||
|
<pre id="outputPre">Fill in the fields above and click "Generate Packet ASCII"...</pre>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div id="saveModalBackdrop" class="save-modal-backdrop">
|
||||||
|
<div class="save-modal-window">
|
||||||
|
<h3>Save Form as HTML</h3>
|
||||||
|
<p>Enter a name for the saved offline HTML file:</p>
|
||||||
|
<input type="text" id="saveModalFileName" spellcheck="false">
|
||||||
|
<div class="save-modal-actions">
|
||||||
|
<button type="button" class="save-modal-btn save-modal-btn-cancel" id="saveModalCancelBtn">Cancel</button>
|
||||||
|
<button type="button" class="save-modal-btn save-modal-btn-confirm" id="saveModalSaveBtn">Save File</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function pad(str, len) {
|
||||||
|
str = (str || '').toString();
|
||||||
|
return str + ' '.repeat(Math.max(0, len - str.length));
|
||||||
|
}
|
||||||
|
|
||||||
|
function setCurrentTime(targetId) {
|
||||||
|
var now = new Date();
|
||||||
|
var year = now.getFullYear();
|
||||||
|
var month = String(now.getMonth() + 1).padStart(2, '0');
|
||||||
|
var day = String(now.getDate()).padStart(2, '0');
|
||||||
|
var hours = String(now.getHours()).padStart(2, '0');
|
||||||
|
var minutes = String(now.getMinutes()).padStart(2, '0');
|
||||||
|
document.getElementById(targetId).value = (year + '-' + month + '-' + day + ' ' + hours + ':' + minutes + ' L').toUpperCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateReport() {
|
||||||
|
var eventSelect = document.getElementById('event');
|
||||||
|
if (!eventSelect.value) {
|
||||||
|
alert("PLEASE SELECT AN EVENT TYPE BEFORE GENERATING THE REPORT.");
|
||||||
|
eventSelect.focus();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var toAddr = document.getElementById('nws-office').value.toUpperCase() || "[email protected]";
|
||||||
|
var call = document.getElementById('callsign').value.toUpperCase() || "NOCALL";
|
||||||
|
var spotter = document.getElementById('spotter-id').value.toUpperCase() || "N/A";
|
||||||
|
var event = eventSelect.value.toUpperCase();
|
||||||
|
var loc = document.getElementById('location').value.toUpperCase() || "N/A";
|
||||||
|
var grid = document.getElementById('grid').value.toUpperCase() || "N/A";
|
||||||
|
var time = document.getElementById('time').value.toUpperCase() || "N/A";
|
||||||
|
var details = document.getElementById('details').value.toUpperCase() || "NONE";
|
||||||
|
|
||||||
|
var fromType = document.getElementById('from-type').value.toUpperCase() || "N/A";
|
||||||
|
var toType = document.getElementById('to-type').value.toUpperCase() || "N/A";
|
||||||
|
var changeTime = document.getElementById('change-time').value.toUpperCase() || "N/A";
|
||||||
|
|
||||||
|
var subject = "WX REPORT - " + event + " - " + loc;
|
||||||
|
|
||||||
|
var div = "--------------------------------------------------------------------------";
|
||||||
|
|
||||||
|
var body = "*** TPRFN SKYWARN SEVERE WEATHER REPORT ***\n" +
|
||||||
|
div + "\n" +
|
||||||
|
pad("TO ADDRESS", 16) + " : " + toAddr + "\n" +
|
||||||
|
pad("SUBJECT", 16) + " : " + subject + "\n" +
|
||||||
|
div + "\n" +
|
||||||
|
pad("SENDER CALL", 16) + " : " + call + "\n" +
|
||||||
|
pad("SPOTTER ID", 16) + " : " + spotter + "\n" +
|
||||||
|
pad("EVENT TYPE", 16) + " : " + event + "\n" +
|
||||||
|
pad("DATE/TIME", 16) + " : " + time + "\n" +
|
||||||
|
pad("GRID SQUARE", 16) + " : " + grid + "\n" +
|
||||||
|
pad("LOCATION", 16) + " : " + loc + "\n";
|
||||||
|
|
||||||
|
if (fromType !== "N/A" || toType !== "N/A" || changeTime !== "N/A") {
|
||||||
|
body += "\n--- CHANGEOVER REPORT ---\n" +
|
||||||
|
pad("FROM TYPE", 16) + " : " + fromType + "\n" +
|
||||||
|
pad("TO TYPE", 16) + " : " + toType + "\n" +
|
||||||
|
pad("TIME/REM", 16) + " : " + changeTime + "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
body += "\nREMARKS :\n " + details + "\n" +
|
||||||
|
div + "\n" +
|
||||||
|
"SENT VIA PACKET RMS / TPRFN NETWORK\n" +
|
||||||
|
"WWW.TPRFN.NET";
|
||||||
|
|
||||||
|
document.getElementById('outputPre').textContent = body;
|
||||||
|
}
|
||||||
|
|
||||||
|
function copyASCII() {
|
||||||
|
var txt = document.getElementById('outputPre').textContent;
|
||||||
|
if (navigator.clipboard && navigator.clipboard.writeText) {
|
||||||
|
navigator.clipboard.writeText(txt).then(function() {
|
||||||
|
alert("Report copied to clipboard!");
|
||||||
|
}).catch(function() {
|
||||||
|
fallbackCopy();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
fallbackCopy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function fallbackCopy() {
|
||||||
|
var pre = document.getElementById('outputPre');
|
||||||
|
var range = document.createRange();
|
||||||
|
range.selectNode(pre);
|
||||||
|
window.getSelection().removeAllRanges();
|
||||||
|
window.getSelection().addRange(range);
|
||||||
|
document.execCommand('copy');
|
||||||
|
alert("Copied to clipboard!");
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearForm() {
|
||||||
|
if(confirm("CLEAR ALL FIELDS?")) {
|
||||||
|
document.getElementById('skywarnForm').reset();
|
||||||
|
document.getElementById('outputPre').textContent = 'Fill in the fields above and click "Generate Packet ASCII"...';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SAVE AS HTML LOGIC
|
||||||
|
function openSaveModal() {
|
||||||
|
var now = new Date();
|
||||||
|
var dStr = now.toISOString().slice(0, 10);
|
||||||
|
var defName = 'tprfn_skywarn_' + dStr + '.html';
|
||||||
|
var modal = document.getElementById('saveModalBackdrop');
|
||||||
|
var input = document.getElementById('saveModalFileName');
|
||||||
|
if (input) input.value = defName;
|
||||||
|
if (modal) modal.classList.add('active');
|
||||||
|
setTimeout(function() {
|
||||||
|
if (input) { input.focus(); input.select(); }
|
||||||
|
}, 50);
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeSaveModal() {
|
||||||
|
var modal = document.getElementById('saveModalBackdrop');
|
||||||
|
if (modal) modal.classList.remove('active');
|
||||||
|
}
|
||||||
|
|
||||||
|
function executeSaveHTML() {
|
||||||
|
var input = document.getElementById('saveModalFileName');
|
||||||
|
var chosenName = input ? input.value.trim() : '';
|
||||||
|
if (!chosenName) return;
|
||||||
|
if (!chosenName.toLowerCase().endsWith('.html') && !chosenName.toLowerCase().endsWith('.htm')) {
|
||||||
|
chosenName += '.html';
|
||||||
|
}
|
||||||
|
closeSaveModal();
|
||||||
|
|
||||||
|
document.querySelectorAll('input[type="text"]').forEach(function(el) { el.setAttribute('value', el.value); });
|
||||||
|
document.querySelectorAll('input[type="checkbox"], input[type="radio"]').forEach(function(el) {
|
||||||
|
if (el.checked) el.setAttribute('checked', 'checked');
|
||||||
|
else el.removeAttribute('checked');
|
||||||
|
});
|
||||||
|
document.querySelectorAll('textarea').forEach(function(el) { el.textContent = el.value; });
|
||||||
|
document.querySelectorAll('select').forEach(function(el) {
|
||||||
|
Array.from(el.options).forEach(function(opt) {
|
||||||
|
if (opt.value === el.value) opt.setAttribute('selected', 'selected');
|
||||||
|
else opt.removeAttribute('selected');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
var pre = document.getElementById('outputPre');
|
||||||
|
if (pre) pre.textContent = pre.textContent;
|
||||||
|
|
||||||
|
var clonedDoc = document.documentElement.cloneNode(true);
|
||||||
|
var modalInClone = clonedDoc.querySelector('#saveModalBackdrop');
|
||||||
|
if (modalInClone) modalInClone.classList.remove('active');
|
||||||
|
|
||||||
|
var htmlContent = '<!DOCTYPE html>\n' + clonedDoc.outerHTML;
|
||||||
|
var blob = new Blob([htmlContent], { type: 'text/html;charset=utf-8' });
|
||||||
|
var url = URL.createObjectURL(blob);
|
||||||
|
var a = document.createElement('a');
|
||||||
|
a.href = url;
|
||||||
|
a.download = chosenName;
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
document.body.removeChild(a);
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
var cancelBtn = document.getElementById('saveModalCancelBtn');
|
||||||
|
var confirmBtn = document.getElementById('saveModalSaveBtn');
|
||||||
|
var modalInput = document.getElementById('saveModalFileName');
|
||||||
|
var modalBackdrop = document.getElementById('saveModalBackdrop');
|
||||||
|
|
||||||
|
if (cancelBtn) cancelBtn.addEventListener('click', closeSaveModal);
|
||||||
|
if (confirmBtn) confirmBtn.addEventListener('click', executeSaveHTML);
|
||||||
|
if (modalInput) {
|
||||||
|
modalInput.addEventListener('keydown', function(e) {
|
||||||
|
if (e.key === 'Enter') { e.preventDefault(); executeSaveHTML(); }
|
||||||
|
if (e.key === 'Escape') { e.preventDefault(); closeSaveModal(); }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (modalBackdrop) {
|
||||||
|
modalBackdrop.addEventListener('click', function(e) {
|
||||||
|
if (e.target === modalBackdrop) closeSaveModal();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('btnSaveHtml').addEventListener('click', openSaveModal);
|
||||||
|
|
||||||
|
// Attach main buttons
|
||||||
|
document.getElementById('btnDateNow').addEventListener('click', function() { setCurrentTime('time'); });
|
||||||
|
document.getElementById('btnChangeNow').addEventListener('click', function() { setCurrentTime('change-time'); });
|
||||||
|
document.getElementById('btnGen').addEventListener('click', generateReport);
|
||||||
|
document.getElementById('btnCopy').addEventListener('click', copyASCII);
|
||||||
|
document.getElementById('btnClear').addEventListener('click', clearForm);
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Reference in New Issue
Block a user