// 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 = `
`;
return new Response(helpText, {
headers: { "Content-Type": "image/svg+xml", "Cache-Control": "no-store" },
});
},
// Generate Error Screen
errorScreen(bgColor, showBorder, footer, colors) {
const errorSvg = `
`;
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 = `
`;
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();
},
};