// 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: // // 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. export default { // In-memory storage for rate limiting and caching mem: { rateLimit: new Map(), cache: new Map(), }, async fetch(req) { const url = new URL(req.url); const clientIP = req.headers.get("CF-Connecting-IP") || "unknown"; // Rate limiting: Allow max 60 requests per minute per IP const now = Date.now(); const rlData = this.mem.rateLimit.get(clientIP) || { count: 0, reset: now + 60000 }; if (rlData.reset < now) { rlData.count = 0; rlData.reset = now + 60000; } if (rlData.count >= 60) { return new Response("Rate limit exceeded. Try again later.", { status: 429 }); } rlData.count += 1; this.mem.rateLimit.set(clientIP, rlData); // Caching: Cache results for 1 minute const cacheKey = url.toString(); const cachedResp = this.mem.cache.get(cacheKey); if (cachedResp && cachedResp.expiry > now) { return new Response(cachedResp.body, cachedResp.options); } // 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 const colors = { textPrim: url.searchParams.get("text_primary_color") || "black", textSec: url.searchParams.get("text_secondary_color") || "red", radSymbol: url.searchParams.get("radiation_symbol_color") || "#A9A9A9", footerLine: url.searchParams.get("footer_line_color") || "#A9A9A9", }; try { const jsonData = await this.getGMCData(paramId, numRecords); const response = this.radScreen(jsonData, timezone, bgColor, showBorder, footer, numRecords, colors); // Clone and cache the response const clonedResp = response.clone(); const body = await clonedResp.text(); // Read body for caching this.mem.cache.set(cacheKey, { body, options: { status: clonedResp.status, headers: [...clonedResp.headers], }, expiry: now + 60000, }); return response; } catch (error) { return this.errorScreen(bgColor, showBorder, footer, colors); } }, // 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 &show_border=false to disable the border (default: border is enabled). 4. Optional: Add &n=NUMBER for number of records (default: 20). 5. Optional: &bg_color=COLOR, &text_primary_color=COLOR, &text_secondary_color=COLOR, &radiation_symbol_color=COLOR, &footer_line_color=COLOR to customize colors. Example: ?param_id=1234567890&n=5&bg_color=#ffffff 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, colors) { 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, colors) { 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 svgContent = ` ${showBorder ? `` : ""} ${currentUSv.toFixed(2)} μSv/h ${CPM} CPM ${parseFloat(ACPM).toFixed(2)} ACPM ${formattedTime} ${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 averageUSv = uSvValues.reduce((a, b) => a + b, 0) / uSvValues.length; const variance = ((currentUSv - averageUSv) / averageUSv) * 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(); const localDate = new Date(utcDate.getTime() + timezone * 60 * 60 * 1000); return localDate.toLocaleString(); }, };