// MIT License // // Copyright (c) 2024 HowMuchRadiation.com // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // Additional Conditions: // 1. Attribution: Users are encouraged to credit the original authors when using or redistributing this software. // 2. Modification Notices: Any modifications made to the original software should be documented. // 3. Documentation: Clear usage instructions should be provided with any distribution. // 4. Limitations of Liability: The authors are not liable for any damages resulting from the use of this software. // 5. Compliance with Laws: Users must ensure compliance with applicable laws when using this software. // 6. Contribution Guidelines: Contributions are welcome; please follow the provided guidelines. // 7. Contact Information: For questions or support, please contact HowMuchRadiation.com // // THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // // Note: This widget is not affiliated with GMCmap and operates independently. export default { async fetch(req) { const url = new URL(req.url); // Check for "param_id" in URL parameters if (!url.search || !url.searchParams.has("param_id")) { return this.helpScreen(); } const paramId = url.searchParams.get("param_id"); const timezone = parseFloat(url.searchParams.get("timezone")) || 0; const bgColor = url.searchParams.get("bg_color") || "#f5deb3"; const footer = url.searchParams.get("footer") || "HowMuchRadiation.com"; const showBorder = url.searchParams.get("show_border") !== "false"; const numRecords = parseInt(url.searchParams.get("n")) || 20; // Default n is 20 try { const jsonData = await this.getGMCData(paramId, numRecords); return this.radScreen(jsonData, timezone, bgColor, showBorder, footer, numRecords); } catch (error) { return this.errorScreen(bgColor, showBorder, footer); } }, // Fetch data from GMCmap API async getGMCData(paramId, numRecords) { const jsonUrl = `https://www.gmcmap.com/historyData-plain.asp?Param_ID=${paramId}&n=${numRecords}`; const response = await fetch(jsonUrl); if (!response.ok) { throw new Error(`Failed to fetch JSON data: ${response.status} ${response.statusText}`); } const jsonData = await response.json(); if (!jsonData || jsonData.length === 0) { throw new Error("No data available from GMCmap."); } return jsonData; }, // Generate Help Screen helpScreen() { const helpText = ` Widget Instructions 1. Add GMCmap ?param_id=1234567890 to the URL (required). 2. Optional: Add &timezone=OFFSET to specify time zone (default: 0). 3. Optional: Add &bg_color=COLOR to set background color (default: #f5deb3). 4. Optional: Add &show_border=false to disable the border (default: border is enabled). 5. Optional: Add &n=NUMBER for number of records (default: 20). 6. Example: ?param_id=1234567890&n=50 CPM: Counts Per Minute (real-time radiation count). ACPM: Average Counts Per Minute (average over the dataset). Note: This widget depends on gmcmap.com and will not work if the service is down. Disclaimer: This widget is not affiliated with or authorized by gmcmap.com. Use at your own risk. It is offered "as is" without any guarantees of accuracy, reliability, or availability. HowMuchRadiation.com `; return new Response(helpText, { headers: { "Content-Type": "image/svg+xml", "Cache-Control": "no-store" }, }); }, // Generate Error Screen errorScreen(bgColor, showBorder, footer) { const errorSvg = ` ${showBorder ? `` : ""} GMCmap not returning data. Please check param_id or try again later. ${footer} `; return new Response(errorSvg, { headers: { "Content-Type": "image/svg+xml", "Cache-Control": "no-store" }, }); }, // Generate Radiation Data Screen radScreen(jsonData, timezone, bgColor, showBorder, footer, numRecords) { const { currentUSv, variance, avgInterval, totalTime } = this.calcStats(jsonData, numRecords); const mostRecent = jsonData[0]; const { CPM, ACPM } = mostRecent; const formattedTime = this.formatTimestamp(mostRecent.time, timezone); const varianceText = numRecords > 1 ? ` Variance: ${variance.toFixed(2)}% from average Avg Update Interval: ${avgInterval.toFixed(2)} minutes Total Time: ${totalTime} ` : ""; const chartData = jsonData.map(record => parseFloat(record.uSv)).filter(value => !isNaN(value)); const chartSvg = this.asciiChart(chartData, 316, 120); // Adjust width and height as needed const svgContent = ` ${showBorder ? `` : ""} CURRENT RADIATION ${currentUSv.toFixed(2)} μSv/h ${CPM} CPM ${parseFloat(ACPM).toFixed(2)} ACPM ${formattedTime} ${chartSvg} ${varianceText} ${footer} `; return new Response(svgContent, { headers: { "Content-Type": "image/svg+xml", "Cache-Control": "no-store" }, }); }, calcStats(jsonData, numRecords) { const uSvValues = jsonData.map(record => parseFloat(record.uSv)).filter(value => !isNaN(value)); const currentUSv = parseFloat(jsonData[0].uSv); const avgUSv = uSvValues.reduce((a, b) => a + b, 0) / uSvValues.length; const variance = ((currentUSv - avgUSv) / avgUSv) * 100; const timestamps = jsonData.map(record => new Date(record.time + " UTC").getTime()); const intervals = timestamps.slice(1).map((time, i) => Math.abs((time - timestamps[i]) / 60000)); const avgInterval = intervals.length > 0 ? intervals.reduce((a, b) => a + b, 0) / intervals.length : 0; const totalMinutes = Math.abs((timestamps[timestamps.length - 1] - timestamps[0]) / 60000); const totalTime = totalMinutes <= 90 ? `${totalMinutes.toFixed(0)} minutes` : totalMinutes <= 1440 ? `${(totalMinutes / 60).toFixed(1)} hours` : `${(totalMinutes / 1440).toFixed(1)} days`; return { currentUSv, variance, avgInterval, totalTime }; }, formatTimestamp(time, timezone) { const utcDate = new Date(time + " UTC"); const localDate = new Date(utcDate.getTime() + timezone * 60 * 60 * 1000); return timezone === 0 ? `${utcDate.toISOString().replace("T", " ").split(".")[0]} UTC` : `${localDate.toISOString().replace("T", " ").split(".")[0]} UTC${timezone > 0 ? "+" : ""}${timezone}`; }, asciiChart(data, width, height) { const maxValue = Math.max(...data); const minValue = Math.min(...data); const midValue = (maxValue + minValue) / 2; const chartHeight = height; // Adjust for padding const chartWidth = width - 20; // Adjust for padding const scaleX = chartWidth / (data.length - 1); const scaleY = chartHeight / (maxValue - minValue); let pathData = data .map((value, index) => { const x = 22 + index * scaleX; // Move chart left to align with labels const y = height - 10 - (value - minValue) * scaleY; // Invert y-axis return `${index === 0 ? "M" : "L"}${x},${y}`; }) .join(" "); return ` ${minValue.toFixed(2)} ${midValue.toFixed(2)} ${maxValue.toFixed(2)} `; }, };