1// Weâll request alerts for CAZ087, CAZ287 and PZZ655 2window.addEventListener('DOMContentLoaded', () => { 3 const zoneList = ['CAZ087', 'CAZ287', 'PZZ655']; 4 5 // Build an array of fetch promises, one per zone 6 const fetchPromises = zoneList.map((zoneId) => { 7 const apiUrl = `https://api.weather.gov/alerts/active?zone=${zoneId}`; 8 return fetch(apiUrl) 9 .then((response) => { 10 if (!response.ok) { 11 throw new Error(`Network error: ${response.status} (zone: ${zoneId})`); 12 } 13 return response.json(); 14 }) 15 .catch((err) => { 16 console.error(`Error fetching zone ${zoneId}:`, err); 17 // Return a fallback structure so Promise.all doesn't fail entirely 18 return { features: [] }; 19 }); 20 }); 21 22 // Wait for all fetches to complete 23 Promise.all(fetchPromises) 24 .then((results) => { 25 // Combine 'features' from each zoneâs result 26 let allFeatures = []; 27 results.forEach((zoneData) => { 28 if (zoneData.features) { 29 allFeatures = allFeatures.concat(zoneData.features); 30 } 31 }); 32 33 // Filter out test alerts more robustly 34 allFeatures = allFeatures.filter((feature) => { 35 const { messageType, event, headline, description } = feature.properties; 36 37 // Convert to lowercase just once for simpler checks 38 const lowerMessageType = (messageType || '').toLowerCase(); 39 const lowerEvent = (event || '').toLowerCase(); 40 const lowerHeadline = (headline || '').toLowerCase(); 41 const lowerDescription = (description || '').toLowerCase(); 42 43 // If messageType is 'Test' 44 if (lowerMessageType === 'test') { 45 return false; 46 } 47 48 // If event, headline or description contains 'test' 49 if ( 50 lowerEvent.includes('test') || 51 lowerHeadline.includes('test') || 52 lowerDescription.includes('test') 53 ) { 54 return false; 55 } 56 57 return true; 58 }); 59 60 // If no real alerts remain, bail out 61 if (allFeatures.length === 0) { 62 console.log('No active alerts for either zone.'); 63 return; 64 } 65 66 // Convert raw features into simplified alert objects. 67 // For each alert, check if it has PZZ655 in its affectedZones. 68 const simplifiedAlerts = allFeatures.map((feature) => { 69 const props = feature.properties; 70 // affectedZones may be an array of URLs, so we check if any include PZZ655. 71 const affectedZones = props.affectedZones || []; 72 const isPZZ655 = affectedZones.some((zoneUrl) => zoneUrl.includes('PZZ655')); 73 const alertName = props.event || props.headline || 'Weather Alert'; 74 75 // Potential date fields 76 const startTime = props.start || props.effective || props.onset; 77 const endTime = props.end || props.expires || props.ends; 78 const apStart = startTime ? formatDateApStyle(startTime) : ''; 79 const apEnd = endTime ? formatDateApStyle(endTime) : ''; 80 81 let dateText = ''; 82 if (apStart || apEnd) { 83 dateText = `issued ${apStart}`; 84 if (apEnd) { 85 dateText += ` until ${apEnd}`; 86 } 87 } 88 89 return { 90 shortName: alertName, 91 dateString: dateText, 92 isPZZ655: isPZZ655 93 }; 94 }); 95 96 // Now that we have the merged list, create the carousel 97 createAlertCarousel(simplifiedAlerts); 98 }) 99 .catch((error) => { 100 console.error('Error fetching alerts for multiple zones:', error); 101 }); 102}); 103 104/** 105 * Return the PNG file URL based on the alert name 106 * (Adjust paths/filenames to your actual icons) 107 */ 108function getIconURLForAlert(alertName) { 109 const lower = alertName.toLowerCase(); 110 111 if (lower.includes('marine')) { 112 return 'https://catalinaconservancy.org/wp-content/uploads/2025/01/sailboat-solid.svg'; 113 } else if (lower.includes('wind')) { 114 return 'https://catalinaconservancy.org/wp-content/uploads/2025/01/wind-solid1.svg'; 115 } else if ( 116 lower.includes('cold') || 117 lower.includes('freeze') || 118 lower.includes('frost') 119 ) { 120 return 'https://catalinaconservancy.org/wp-content/uploads/2025/01/temperature-low-solid.svg'; 121 } else if (lower.includes('fog')) { 122 return 'https://catalinaconservancy.org/wp-content/uploads/2025/01/fog-solid.svg'; 123 } else if (lower.includes('winter')) { 124 return 'https://catalinaconservancy.org/wp-content/uploads/2025/01/snowflake-regular.svg'; 125 } else if (lower.includes('red flag')) { 126 return 'https://catalinaconservancy.org/wp-content/uploads/2025/01/fire-solid1.svg'; 127 } else if (lower.includes('flood')) { 128 return 'https://catalinaconservancy.org/wp-content/uploads/2025/01/water-solid.svg'; 129 } else if (lower.includes('thunderstorm')) { 130 return 'https://catalinaconservancy.org/wp-content/uploads/2025/01/cloud-bolt-solid.svg'; 131 } else if (lower.includes('craft')) { 132 return 'https://catalinaconservancy.org/wp-content/uploads/2025/01/sailboat-solid.svg'; 133 } 134 // Default icon 135 return 'https://catalinaconservancy.org/wp-content/uploads/2025/01/triangle-exclamation-solid1.svg'; 136} 137 138/** 139 * Create one bar for all alerts, rotating if multiple 140 */ 141function createAlertCarousel(alertsArray) { 142 const appDiv = document.getElementById('nws-app'); 143 if (!appDiv) return; 144 145 const bar = document.createElement('div'); 146 bar.className = 'nws-alert-bar'; 147 148 const contentDiv = document.createElement('div'); 149 contentDiv.className = 'nws-alert-content'; 150 151 // PNG icon (instead of Font Awesome) 152 const iconEl = document.createElement('img'); 153 iconEl.className = 'alert-icon'; 154 155 const alertParagraph = document.createElement('p'); 156 alertParagraph.className = 'alert-text'; 157 158 const alertAnchor = document.createElement('a'); 159 alertAnchor.className = 'alert-link'; 160 // Default link; it will be updated based on the alert type. 161 alertAnchor.href = 162 'https://forecast.weather.gov/MapClick.php?CityName=Avalon&state=CA&site=LOX&textField1=33.3428&textField2=-118.327&e=1'; 163 alertAnchor.target = '_blank'; 164 alertAnchor.rel = 'noopener noreferrer'; 165 166 const dateSpan = document.createElement('span'); 167 dateSpan.className = 'date-span'; 168 169 alertParagraph.appendChild(alertAnchor); 170 alertParagraph.appendChild(dateSpan); 171 172 const closeBtn = document.createElement('button'); 173 closeBtn.className = 'nws-alert-close'; 174 closeBtn.innerHTML = '×';
175 closeBtn.addEventListener('click', () => { 176 bar.remove(); 177 if (window.alertInterval) { 178 clearInterval(window.alertInterval); 179 } 180 }); 181 182 contentDiv.appendChild(iconEl); 183 contentDiv.appendChild(alertParagraph); 184 contentDiv.appendChild(closeBtn); 185 bar.appendChild(contentDiv); 186 appDiv.insertBefore(bar, appDiv.firstChild); 187 188 let currentIndex = 0; 189 const totalAlerts = alertsArray.length; 190 191 function showAlert(index) { 192 const alertObj = alertsArray[index]; 193 iconEl.src = getIconURLForAlert(alertObj.shortName); 194 alertAnchor.textContent = alertObj.shortName; 195 dateSpan.textContent = alertObj.dateString || ''; 196 197 // If the alert is from PZZ655, use the custom URL. 198 if (alertObj.isPZZ655) { 199 alertAnchor.href = 200 'https://forecast.weather.gov/MapClick.php?lon=-118.32097917792319&lat=33.34556497647978'; 201 } else { 202 alertAnchor.href = 203 'https://forecast.weather.gov/MapClick.php?CityName=Avalon&state=CA&site=LOX&textField1=33.3428&textField2=-118.327&e=1'; 204 } 205 } 206 207 // Show the first alert 208 showAlert(currentIndex); 209 210 // Rotate every 5 seconds if multiple alerts 211 if (totalAlerts > 1) { 212 window.alertInterval = setInterval(() => { 213 currentIndex = (currentIndex + 1) % totalAlerts; 214 showAlert(currentIndex); 215 }, 5000); 216 } 217} 218 219/** 220 * Convert an ISO date/time to AP style (e.g. "Jan. 7 at 3 a.m.") 221 */ 222function formatDateApStyle(isoString) { 223 const date = new Date(isoString); 224 if (isNaN(date.getTime())) return ''; 225 226 const months = [ 227 'Jan.', 'Feb.', 'March', 'April', 'May', 'June', 228 'July', 'Aug.', 'Sept.', 'Oct.', 'Nov.', 'Dec.' 229 ];
230 const month = date.getMonth(); 231 const day = date.getDate(); 232 233 let hours = date.getHours(); 234 const minutes = date.getMinutes(); 235 const isPM = hours >= 12; 236 const ampm = isPM ? 'p.m.' : 'a.m.'; 237 hours = hours % 12 || 12; 238 const minutesFormatted = 239 minutes > 0 ? `:${String(minutes).padStart(2, '0')}` : ''; 240 241 const timeString = `${hours}${minutesFormatted} ${ampm}`; 242 return `${months[month]} ${day} at ${timeString}`; 243}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.