Skip to content

Commit 0421380

Browse files
Added track length to ACE checkbox
1 parent d3dd244 commit 0421380

2 files changed

Lines changed: 153 additions & 21 deletions

File tree

static/css/style.css

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -969,6 +969,7 @@ ion-icon+span {
969969

970970
#ace-results .ace-list li {
971971
display: flex;
972+
flex-flow: column;
972973
justify-content: space-between;
973974
align-items: baseline;
974975
gap: .5rem;
@@ -988,6 +989,60 @@ ion-icon+span {
988989
white-space: nowrap;
989990
}
990991

992+
/* ACE header and controls */
993+
#ace-results .ace-header {
994+
margin: .25rem 0;
995+
display: flex;
996+
justify-content: space-between;
997+
align-items: center;
998+
gap: .5rem;
999+
}
1000+
1001+
#ace-results .ace-controls {
1002+
display: flex;
1003+
gap: .5rem;
1004+
align-items: center;
1005+
}
1006+
1007+
#ace-results .ace-controls label {
1008+
display: flex;
1009+
align-items: center;
1010+
gap: .3rem;
1011+
font-size: .85em;
1012+
opacity: .9;
1013+
padding-right: .5rem;
1014+
}
1015+
1016+
#ace-results .ace-sort-toggle {
1017+
background: rgba(255,255,255,0.1);
1018+
border: 1px solid rgba(255,255,255,0.2);
1019+
border-radius: .3rem;
1020+
color: inherit;
1021+
cursor: pointer;
1022+
font-size: .75em;
1023+
padding: .2rem .4rem;
1024+
transition: background .2s;
1025+
}
1026+
#ace-results .ace-sort-toggle:hover {
1027+
background: rgba(255,255,255,0.2);
1028+
}
1029+
1030+
#ace-results .ace-total-length {
1031+
font-size: .9em;
1032+
opacity: .9;
1033+
margin-top: .25rem;
1034+
}
1035+
1036+
#ace-results .ace-storm-item {
1037+
margin-bottom: .5rem;
1038+
}
1039+
1040+
#ace-results .ace-storm-length {
1041+
margin-top: .25rem;
1042+
opacity: .85;
1043+
font-size: .9em;
1044+
}
1045+
9911046
@media (max-width: 640px) {
9921047
#ace-results.ace-card {
9931048
left: .75rem;

static/js/generate.js

Lines changed: 98 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -562,7 +562,33 @@ function normalizeLongitude(lng) {
562562
return ((lng + 180) % 360 + 360) % 360 - 180;
563563
}
564564

565-
// ACE helpers
565+
// utilities: convert lat/lon strings (ex.: "12.3N") to signed decimal degrees
566+
function coordToDecimal(coord) {
567+
if (!coord || typeof coord !== 'string') return NaN;
568+
const s = coord.trim();
569+
if (!s.length) return NaN;
570+
const last = s.slice(-1).toUpperCase();
571+
const hasDir = ['N', 'S', 'E', 'W'].includes(last);
572+
let val = parseFloat(hasDir ? s.slice(0, -1) : s);
573+
if (isNaN(val)) return NaN;
574+
if (hasDir) {
575+
if (last === 'S' || last === 'W') val = -Math.abs(val);
576+
}
577+
return val;
578+
}
579+
580+
// haversine formula: distance in kilometers between two lat/lon points
581+
function haversineKm(lat1, lon1, lat2, lon2) {
582+
const toRad = (deg) => deg * Math.PI / 180;
583+
const R = 6371.0; // Earth's radius in km
584+
const dLat = toRad(lat2 - lat1);
585+
const dLon = toRad(lon2 - lon1);
586+
const a = Math.sin(dLat/2)**2 + Math.cos(toRad(lat1))*Math.cos(toRad(lat2)) * Math.sin(dLon/2)**2;
587+
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
588+
return R * c;
589+
}
590+
591+
// ACE + track length calculator
566592
function computeACEByStorm(points) {
567593
const groups = points.reduce((acc, p) => {
568594
const k = p.name || "Unnamed";
@@ -571,9 +597,14 @@ function computeACEByStorm(points) {
571597
}, {});
572598
const storms = [];
573599
let totalRaw = 0;
600+
let totalLengthKm = 0;
574601

575602
Object.entries(groups).forEach(([name, arr]) => {
576603
let sum = 0, pts = 0, tsPts = 0;
604+
// for length calc we parse coords to decimals and compute distances between consecutive valid points
605+
let lengthKm = 0;
606+
let prevLat = NaN, prevLon = NaN;
607+
577608
arr.forEach(p => {
578609
const v = Number(p.speed);
579610
if (!Number.isFinite(v)) return;
@@ -586,14 +617,40 @@ function computeACEByStorm(points) {
586617
sum += v5 * v5;
587618
tsPts++;
588619
}
620+
621+
// length: parse coordinates and compute distance from previous point
622+
const lat = coordToDecimal(p.latitude);
623+
const lon = coordToDecimal(p.longitude);
624+
if (Number.isFinite(lat) && Number.isFinite(lon)) {
625+
if (Number.isFinite(prevLat) && Number.isFinite(prevLon)) {
626+
lengthKm += haversineKm(prevLat, prevLon, lat, lon);
627+
}
628+
prevLat = lat;
629+
prevLon = lon;
630+
} else {
631+
// if coordinate invalid, reset prev to avoid incorrect jump
632+
prevLat = NaN;
633+
prevLon = NaN;
634+
}
589635
});
636+
590637
const rawAce = sum / 10000;
591638
const ace = +rawAce.toFixed(4);
592-
storms.push({ name, ace, points: pts, tsPoints: tsPts });
639+
const lengthNm = +(lengthKm / 1.852).toFixed(2);
640+
641+
storms.push({
642+
name,
643+
ace,
644+
points: pts,
645+
tsPoints: tsPts,
646+
lengthKm: +lengthKm.toFixed(2),
647+
lengthNm
648+
});
593649
totalRaw += rawAce;
650+
totalLengthKm += lengthKm;
594651
});
595652

596-
return { totalAce: +totalRaw.toFixed(4), storms };
653+
return { totalAce: +totalRaw.toFixed(4), totalLengthKm: +totalLengthKm.toFixed(2), totalLengthNm: +(totalLengthKm / 1.852).toFixed(2), storms };
597654
}
598655

599656
function sortStormsByNumber(storms) {
@@ -616,37 +673,58 @@ function sortStormsByACE(storms) {
616673
function renderACEResults(ace, sortByNumber = true) {
617674
const container = document.getElementById("ace-results");
618675
if (!container) return;
619-
676+
677+
const showLengthPref = JSON.parse(localStorage.getItem("ace_show_length") || "false");
678+
620679
const sortedStorms = sortByNumber ? sortStormsByNumber(ace.storms) : sortStormsByACE(ace.storms);
621680
const sortIcon = sortByNumber ? '🔢' : '📊';
622681
const sortLabel = sortByNumber ? 'by number' : 'by ACE';
623682
const nextSort = sortByNumber ? 'ACE' : 'number';
624-
683+
625684
container.classList.remove("hidden-2");
626685
container.innerHTML = `
627-
<h3 style="margin:.25rem 0; display:flex; justify-content:space-between; align-items:center;">
628-
<span>ACE</span>
629-
<button id="ace-sort-toggle" style="background:rgba(255,255,255,0.1); border:1px solid rgba(255,255,255,0.2); border-radius:.3rem; color:inherit; cursor:pointer; font-size:.75em; padding:.2rem .4rem; transition:background .2s;" title="Sort by ${nextSort}">
630-
${sortIcon} ${sortLabel}
631-
</button>
686+
<h3 class="ace-header">
687+
<span class="ace-title">ACE</span>
688+
<div class="ace-controls">
689+
<label>
690+
<input type="checkbox" id="ace-show-length" ${showLengthPref ? "checked" : ""} /> <span style="margin-left:.25rem">Show length</span>
691+
</label>
692+
<button id="ace-sort-toggle" class="ace-sort-toggle" title="Sort by ${nextSort}">
693+
${sortIcon} ${sortLabel}
694+
</button>
695+
</div>
632696
</h3>
633-
<div class="ace-total">Total: ${ace.totalAce}</div>
697+
698+
<div class="ace-total">Total ACE: ${ace.totalAce}</div>
699+
${showLengthPref ? `<div class="ace-total-length">Total length: ${ace.totalLengthKm} km (${ace.totalLengthNm} nm)</div>` : ''}
634700
<ul class="ace-list">
635-
${sortedStorms.map(s => `<li>${s.name || "Unnamed"}: ${s.ace} <span>(pts: ${s.tsPoints}/${s.points})</span></li>`).join("")}
701+
${sortedStorms.map(s => `
702+
<li class="ace-storm-item">
703+
<div>
704+
<strong>${s.name || "Unnamed"}</strong>
705+
<span style="margin-left:.5rem;">${s.ace}</span>
706+
<span style="opacity:.75; margin-left:.5rem;">(pts: ${s.tsPoints}/${s.points})</span>
707+
</div>
708+
${showLengthPref ? `<div class="ace-storm-length">Length: ${s.lengthKm} km (${s.lengthNm} nm)</div>` : ''}
709+
</li>
710+
`).join("")}
636711
</ul>
637712
`;
638-
639-
// click handler for the sort toggle button
713+
714+
// Sort toggle handler
640715
const sortToggle = document.getElementById("ace-sort-toggle");
641716
if (sortToggle) {
642717
sortToggle.addEventListener("click", () => {
643718
renderACEResults(ace, !sortByNumber);
644719
});
645-
sortToggle.addEventListener("mouseenter", (e) => {
646-
e.target.style.background = "rgba(255,255,255,0.2)";
647-
});
648-
sortToggle.addEventListener("mouseleave", (e) => {
649-
e.target.style.background = "rgba(255,255,255,0.1)";
720+
}
721+
722+
// Length toggle handler
723+
const lengthToggle = document.getElementById("ace-show-length");
724+
if (lengthToggle) {
725+
lengthToggle.addEventListener("change", (e) => {
726+
localStorage.setItem("ace_show_length", JSON.stringify(e.target.checked));
727+
renderACEResults(ace, sortByNumber);
650728
});
651729
}
652730
}
@@ -1010,8 +1088,7 @@ function createMap(data, accessible) {
10101088
output.classList.remove("hidden");
10111089

10121090
try {
1013-
const ace = computeACEByStorm(data);
1014-
renderACEResults(ace);
1091+
// only compute and render ACE + length if user requested it
10151092
if (shouldComputeAce) {
10161093
const ace = computeACEByStorm(data);
10171094
renderACEResults(ace);

0 commit comments

Comments
 (0)