New files for stdev calculation for v0.2
This commit is contained in:
+5
-10
@@ -1,14 +1,9 @@
|
||||
{
|
||||
"_comment": "Naue Roll Calculator - General Configuration. Article data stays in article-data.json.",
|
||||
"_comment": "Naue Roll Calculator - Configuration",
|
||||
"forklift_rules": {
|
||||
"_comment": "Weight-based forklift and core diameter rules. Applies per product category.",
|
||||
"applicable_categories": [
|
||||
"bentofix"
|
||||
],
|
||||
"applicable_sites": [
|
||||
"espelkamp",
|
||||
"malaysia"
|
||||
],
|
||||
"_comment": "Weight-based forklift and core diameter rules",
|
||||
"applicable_categories": ["bentofix"],
|
||||
"applicable_sites": ["espelkamp", "malaysia"],
|
||||
"rules": [
|
||||
{
|
||||
"id": "heavy_roll",
|
||||
@@ -56,4 +51,4 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,211 @@
|
||||
/**
|
||||
* DIRECT CALCULATION WITH STDDEV INTEGRATION
|
||||
* ==========================================
|
||||
* Replace the d-calc button handler in roll_calculator.html with this code
|
||||
*
|
||||
* This includes:
|
||||
* 1. Diameter calculation with ±2σ ranges
|
||||
* 2. Weight calculation with ±2σ ranges
|
||||
* 3. Forklift check with worst-case weight
|
||||
* 4. Load Optimizer integration with nominal values
|
||||
*/
|
||||
|
||||
// This code should replace the existing d-calc button handler
|
||||
// Find: document.getElementById('d-calc').addEventListener('click', () => { ... });
|
||||
// Replace with the code below:
|
||||
|
||||
document.getElementById('d-calc')?.addEventListener('click', () => {
|
||||
console.log('[DirectCalc] Starting calculation...');
|
||||
|
||||
const mode = document.querySelector('.mode-btn.active')?.dataset.mode || 'diameter';
|
||||
const d_mm = parseFloat(document.getElementById('d-co').value) || 0;
|
||||
const t_mm = parseFloat(document.getElementById('d-th').value) || 0;
|
||||
const tol = parseFloat(document.getElementById('d-tol').value) || 0;
|
||||
const L_m = parseFloat(document.getElementById('d-le').value) || 0;
|
||||
const D_mm = parseFloat(document.getElementById('d-di').value) || 0;
|
||||
const category = (document.getElementById('d-cat')?.value || '').toLowerCase();
|
||||
|
||||
const resBox = document.getElementById('d-result');
|
||||
const resLabel = document.getElementById('d-res-label');
|
||||
const resVal = document.getElementById('d-res-val');
|
||||
const resRange = document.getElementById('d-res-range');
|
||||
|
||||
// =====================================================
|
||||
// MODE: DIAMETER CALCULATION (with stddev)
|
||||
// =====================================================
|
||||
if (mode === 'diameter') {
|
||||
if (!d_mm || !t_mm || !L_m) {
|
||||
alert('Please enter: Core Diameter (d), Product Thickness (t), and Product Length (L).');
|
||||
return;
|
||||
}
|
||||
|
||||
// Get stddev from data attribute (set during article selection)
|
||||
const t_stddev = parseFloat(document.getElementById('d-th')?.dataset.stddev || 0);
|
||||
|
||||
// Calculate diameter ranges using StdDev calculator
|
||||
let diameterRanges = null;
|
||||
if (window.StdDevCalculator && t_stddev > 0) {
|
||||
diameterRanges = window.StdDevCalculator.calculateDiameterRanges(d_mm, t_mm, t_stddev, L_m);
|
||||
console.log('[DirectCalc] Diameter with stddev:', diameterRanges);
|
||||
} else {
|
||||
// Fallback: simple calculation without stddev
|
||||
const D_nom = Math.sqrt(d_mm*d_mm + (4*L_m*1000*t_mm)/Math.PI);
|
||||
diameterRanges = {
|
||||
min: D_nom, nom: D_nom, max: D_nom,
|
||||
t_min: t_mm, t_nom: t_mm, t_max: t_mm
|
||||
};
|
||||
console.log('[DirectCalc] Diameter without stddev (nominal only):', D_nom.toFixed(1));
|
||||
}
|
||||
|
||||
// Display diameter result
|
||||
resLabel.textContent = 'Roll Diameter (D)';
|
||||
resVal.textContent = diameterRanges.nom.toFixed(1) + ' mm';
|
||||
|
||||
if (t_stddev > 0) {
|
||||
const d_low = Math.min(diameterRanges.min, diameterRanges.max);
|
||||
const d_high = Math.max(diameterRanges.min, diameterRanges.max);
|
||||
let rangeText = `Range (±2σ): ${d_low.toFixed(1)}–${d_high.toFixed(1)} mm`;
|
||||
|
||||
if (tol > 0) {
|
||||
const d_tol_min = diameterRanges.nom - tol;
|
||||
const d_tol_max = diameterRanges.nom + tol;
|
||||
rangeText += ` | Tolerance: ${d_tol_min.toFixed(1)}–${d_tol_max.toFixed(1)} mm`;
|
||||
}
|
||||
resRange.textContent = rangeText;
|
||||
} else {
|
||||
if (tol > 0) {
|
||||
const D_min = Math.sqrt(d_mm*d_mm + (4*L_m*1000*(t_mm-tol))/Math.PI);
|
||||
const D_max = Math.sqrt(d_mm*d_mm + (4*L_m*1000*(t_mm+tol))/Math.PI);
|
||||
resRange.textContent = `Tolerance: ${D_min.toFixed(1)}–${D_max.toFixed(1)} mm`;
|
||||
} else {
|
||||
resRange.textContent = '';
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById('d-di').value = diameterRanges.nom.toFixed(1);
|
||||
resBox.style.display = 'block';
|
||||
|
||||
// =====================================================
|
||||
// WEIGHT CALCULATION WITH STDDEV
|
||||
// =====================================================
|
||||
const wi = parseFloat(document.getElementById('de-wi').value) || 0;
|
||||
const aw = parseFloat(document.getElementById('d-aw').value) || 0;
|
||||
const aw_stddev = parseFloat(document.getElementById('d-aw')?.dataset.stddev || 0);
|
||||
|
||||
let weightRanges = null;
|
||||
if (wi > 0 && aw > 0) {
|
||||
if (window.StdDevCalculator && aw_stddev > 0) {
|
||||
weightRanges = window.StdDevCalculator.calculateWeightRanges(aw, aw_stddev, L_m, wi);
|
||||
console.log('[DirectCalc] Weight with stddev:', weightRanges);
|
||||
} else {
|
||||
const w = (aw * L_m * wi / 1000);
|
||||
weightRanges = { min: w, nom: w, max: w };
|
||||
console.log('[DirectCalc] Weight without stddev:', w.toFixed(1), 'kg');
|
||||
}
|
||||
|
||||
// Store weight in field with data attributes
|
||||
const woEl = document.getElementById('d-wo');
|
||||
woEl.value = weightRanges.nom.toFixed(1);
|
||||
woEl.dataset.weight_nom = weightRanges.nom.toFixed(1);
|
||||
woEl.dataset.weight_min = weightRanges.min.toFixed(1);
|
||||
woEl.dataset.weight_max = weightRanges.max.toFixed(1);
|
||||
}
|
||||
|
||||
// =====================================================
|
||||
// FORKLIFT CHECK WITH WORST-CASE WEIGHT
|
||||
// =====================================================
|
||||
if (window.ForkliftChecker && weightRanges) {
|
||||
const worstCaseWeight = Math.max(weightRanges.nom, weightRanges.max);
|
||||
const allowed = window.ForkliftChecker.evaluate(worstCaseWeight, d_mm, category);
|
||||
|
||||
if (!allowed) {
|
||||
resBox.style.display = 'none'; // Hide result if blocked
|
||||
}
|
||||
|
||||
console.log('[DirectCalc] Forklift check:', {
|
||||
nominal: weightRanges.nom.toFixed(1),
|
||||
worst_case: worstCaseWeight.toFixed(1),
|
||||
allowed: allowed
|
||||
});
|
||||
}
|
||||
|
||||
// =====================================================
|
||||
// LOAD OPTIMIZER INTEGRATION (nominal values)
|
||||
// =====================================================
|
||||
if (window.loadOptimizerUI && diameterRanges && weightRanges) {
|
||||
window.loadOptimizerUI.optimizer.setRollData({
|
||||
rollDiameter: diameterRanges.nom,
|
||||
coreDiameter: d_mm,
|
||||
rollWidth: wi,
|
||||
rollWeight: weightRanges.nom, // Use nominal for capacity planning
|
||||
m2PerRoll: L_m * wi,
|
||||
productLength: L_m
|
||||
});
|
||||
console.log('[DirectCalc] Load Optimizer updated with nominal values');
|
||||
}
|
||||
}
|
||||
|
||||
// =====================================================
|
||||
// MODE: PRODUCT LENGTH CALCULATION
|
||||
// =====================================================
|
||||
else if (mode === 'length') {
|
||||
if (!d_mm || !t_mm || !D_mm) {
|
||||
alert('Please enter: Core Diameter (d), Product Thickness (t), and Roll Diameter (D).');
|
||||
return;
|
||||
}
|
||||
|
||||
const L = (Math.PI / (4 * t_mm)) * (D_mm*D_mm - d_mm*d_mm) / 1000;
|
||||
resLabel.textContent = 'Product Length (L)';
|
||||
resVal.textContent = L.toFixed(2) + ' m';
|
||||
|
||||
if (tol > 0) {
|
||||
const L_min = (Math.PI / (4*(t_mm+tol))) * (D_mm*D_mm - d_mm*d_mm) / 1000;
|
||||
const L_max = (Math.PI / (4*(t_mm-tol))) * (D_mm*D_mm - d_mm*d_mm) / 1000;
|
||||
resRange.textContent = `Range: ${L_min.toFixed(2)}–${L_max.toFixed(2)} m`;
|
||||
} else {
|
||||
resRange.textContent = '';
|
||||
}
|
||||
|
||||
document.getElementById('d-le').value = L.toFixed(2);
|
||||
resBox.style.display = 'block';
|
||||
}
|
||||
|
||||
// =====================================================
|
||||
// MODE: TARGET DIAMETER
|
||||
// =====================================================
|
||||
else if (mode === 'target') {
|
||||
if (!d_mm || !t_mm) {
|
||||
alert('Please enter: Core Diameter (d) and Product Thickness (t).');
|
||||
return;
|
||||
}
|
||||
|
||||
const D_target = D_mm || 800;
|
||||
const L = (Math.PI / (4 * t_mm)) * (D_target*D_target - d_mm*d_mm) / 1000;
|
||||
resLabel.textContent = `Product Length for Ø ${D_target} mm`;
|
||||
resVal.textContent = L.toFixed(2) + ' m';
|
||||
resRange.textContent = '';
|
||||
|
||||
document.getElementById('d-le').value = L.toFixed(2);
|
||||
resBox.style.display = 'block';
|
||||
}
|
||||
|
||||
// =====================================================
|
||||
// MODE: PRODUCT THICKNESS
|
||||
// =====================================================
|
||||
else if (mode === 'thickness') {
|
||||
if (!d_mm || !D_mm || !L_m) {
|
||||
alert('Please enter: Core Diameter (d), Roll Diameter (D), and Product Length (L).');
|
||||
return;
|
||||
}
|
||||
|
||||
const t = (Math.PI / (4 * L_m * 1000)) * (D_mm*D_mm - d_mm*d_mm);
|
||||
resLabel.textContent = 'Product Thickness (t)';
|
||||
resVal.textContent = t.toFixed(3) + ' mm';
|
||||
resRange.textContent = '';
|
||||
|
||||
document.getElementById('d-th').value = t.toFixed(3);
|
||||
resBox.style.display = 'block';
|
||||
}
|
||||
|
||||
console.log('[DirectCalc] Calculation complete');
|
||||
});
|
||||
@@ -0,0 +1,128 @@
|
||||
/**
|
||||
* ROLLCALC IMPROVEMENTS V14
|
||||
* Article selection + automatic weight calculation
|
||||
*/
|
||||
|
||||
function populateArticleDropdowns_v14() {
|
||||
const data = window.ARTICLE_DATA || [];
|
||||
console.log('[ArticleDropdown] Populating with', data.length, 'articles');
|
||||
|
||||
['d-art', 'e-art'].forEach(function(inputId) {
|
||||
const dl = document.getElementById(inputId + '-list');
|
||||
if (!dl) return;
|
||||
|
||||
data.forEach(function(article) {
|
||||
const displayText = article.nr + ' – ' + article.name;
|
||||
const opt = document.createElement('option');
|
||||
opt.value = displayText;
|
||||
opt.dataset.articleNr = article.nr;
|
||||
opt.dataset.thickness = article.thickness || 0;
|
||||
opt.dataset.areaWeight = article.area_weight || 0;
|
||||
opt.dataset.width = extractWidthFromName(article.name) || 0;
|
||||
opt.dataset.stddev_thickness = article.thickness_stddev || 0;
|
||||
opt.dataset.stddev_areaweight = article.area_weight_stddev || 0;
|
||||
dl.appendChild(opt);
|
||||
});
|
||||
});
|
||||
|
||||
const dArt = document.getElementById('d-art');
|
||||
if (dArt) {
|
||||
dArt.addEventListener('input', function() {
|
||||
const inputVal = dArt.value.trim();
|
||||
const match = data.find(function(a) {
|
||||
const displayText = a.nr + ' – ' + a.name;
|
||||
return displayText === inputVal || inputVal.startsWith(a.nr + ' –') || inputVal === a.nr;
|
||||
});
|
||||
|
||||
if (match) {
|
||||
document.getElementById('d-th').value = match.thickness || 0;
|
||||
document.getElementById('d-th').dataset.stddev = match.thickness_stddev || 0;
|
||||
|
||||
if (match.area_weight && match.area_weight > 0) {
|
||||
document.getElementById('d-aw').value = match.area_weight;
|
||||
document.getElementById('d-aw').dataset.stddev = match.area_weight_stddev || 0;
|
||||
}
|
||||
|
||||
const extractedWidth = extractWidthFromName(match.name);
|
||||
if (extractedWidth > 0) {
|
||||
document.getElementById('de-wi').value = extractedWidth;
|
||||
}
|
||||
|
||||
setTimeout(function() {
|
||||
triggerAutoWeightCalculation();
|
||||
}, 100);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
document.querySelectorAll('.core-preset-btn').forEach(function(btn) {
|
||||
btn.addEventListener('click', function() {
|
||||
const coreVal = btn.dataset.core;
|
||||
const pane = btn.closest('.tab-pane');
|
||||
if (pane) {
|
||||
const inp = pane.querySelector('input[id$="-co"]');
|
||||
if (inp) {
|
||||
inp.value = coreVal;
|
||||
pane.querySelectorAll('.core-preset-btn').forEach(function(b) { b.classList.remove('active'); });
|
||||
btn.classList.add('active');
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function extractWidthFromName(productName) {
|
||||
if (!productName) return 0;
|
||||
const match = productName.match(/(\d+[,\.]\d+)\s*x\s*\d+\s*m/i);
|
||||
if (match && match[1]) {
|
||||
const widthStr = match[1].replace(',', '.');
|
||||
const width = parseFloat(widthStr);
|
||||
if (width > 0 && width < 100) return width;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
function triggerAutoWeightCalculation() {
|
||||
const wiEl = document.getElementById('de-wi');
|
||||
const awEl = document.getElementById('d-aw');
|
||||
const LEL = document.getElementById('d-le');
|
||||
const resultEl = document.getElementById('d-wo');
|
||||
|
||||
if (!wiEl || !awEl || !LEL || !resultEl) return;
|
||||
|
||||
const wi = parseFloat(wiEl.value) || 0;
|
||||
const aw = parseFloat(awEl.value) || 0;
|
||||
const L = parseFloat(LEL.value) || 0;
|
||||
|
||||
if (wi > 0 && aw > 0 && L > 0) {
|
||||
const weight = (aw * L * wi / 1000).toFixed(1);
|
||||
resultEl.value = weight;
|
||||
console.log('[AutoCalc] Weight: ' + weight + ' kg');
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function setupWeightCalculationListeners() {
|
||||
const inputIds = ['de-wi', 'd-aw', 'd-le'];
|
||||
inputIds.forEach(function(id) {
|
||||
const el = document.getElementById(id);
|
||||
if (el) {
|
||||
el.addEventListener('change', triggerAutoWeightCalculation);
|
||||
el.addEventListener('blur', triggerAutoWeightCalculation);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
window.RollCalcV14 = {
|
||||
populateArticleDropdowns_v14: populateArticleDropdowns_v14,
|
||||
triggerAutoWeightCalculation: triggerAutoWeightCalculation,
|
||||
extractWidthFromName: extractWidthFromName
|
||||
};
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
if (window.ARTICLE_DATA && window.ARTICLE_DATA.length > 0) {
|
||||
populateArticleDropdowns_v14();
|
||||
setupWeightCalculationListeners();
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,274 @@
|
||||
/**
|
||||
* INTEGRATION PATCH FOR STDDEV RANGES
|
||||
* ====================================
|
||||
*
|
||||
* This shows how to integrate the StdDev calculator into the existing
|
||||
* Direct Calculation button handler in roll_calculator.html
|
||||
*
|
||||
* CHANGES:
|
||||
* 1. Load stddev data from article-data.json when article is selected
|
||||
* 2. Modify the d-calc button handler to use ranges
|
||||
* 3. Update Forklift checker to use worst-case weight
|
||||
* 4. Keep everything backward compatible
|
||||
*/
|
||||
|
||||
// ===================================================================
|
||||
// PATCH 1: STORE STDDEV IN HIDDEN FIELDS DURING ARTICLE SELECTION
|
||||
// ===================================================================
|
||||
// Add this to populateArticleDropdowns_v14() in rollcalc_improvements.js
|
||||
// In the article selection handler for d-art:
|
||||
|
||||
/*
|
||||
if (match) {
|
||||
console.log('[Article Select] Found:', match.nr, '- Thickness:', match.thickness, 'Area Weight:', match.area_weight);
|
||||
|
||||
// 1. Auto-fill Thickness
|
||||
document.getElementById('d-th').value = match.thickness;
|
||||
|
||||
// 2. Auto-fill Area Weight
|
||||
if (match.area_weight && match.area_weight > 0) {
|
||||
document.getElementById('d-aw').value = match.area_weight;
|
||||
}
|
||||
|
||||
// 3. Auto-fill Roll Width
|
||||
var widthMatch = match.name.match(/(\d+[,\.]\d+)\s*x\s*\d+/);
|
||||
if (widthMatch) {
|
||||
var width = widthMatch[1].replace(',', '.');
|
||||
document.getElementById('de-wi').value = width;
|
||||
}
|
||||
|
||||
// NEW: Store stddev values in hidden attributes
|
||||
document.getElementById('d-th').dataset.stddev = match.thickness_stddev || 0;
|
||||
document.getElementById('d-aw').dataset.stddev = match.area_weight_stddev || 0;
|
||||
|
||||
console.log('[StdDev] Stored:', {
|
||||
thickness_stddev: match.thickness_stddev,
|
||||
area_weight_stddev: match.area_weight_stddev
|
||||
});
|
||||
|
||||
setTimeout(function() {
|
||||
triggerAutoWeightCalculation();
|
||||
}, 50);
|
||||
}
|
||||
*/
|
||||
|
||||
// ===================================================================
|
||||
// PATCH 2: MODIFY d-calc BUTTON HANDLER FOR DIAMETER MODE
|
||||
// ===================================================================
|
||||
// Replace the diameter calculation section in the d-calc click handler:
|
||||
|
||||
/*
|
||||
OLD CODE (diameter mode):
|
||||
if (mode === 'diameter') {
|
||||
if (!d_mm || !t_mm || !L_m) { alert('Please enter d, t and L.'); return; }
|
||||
const D_nom = Math.sqrt(d_mm*d_mm + (4*L_m*1000*t_mm)/Math.PI);
|
||||
resLabel.textContent = 'Roll Diameter (D)';
|
||||
resVal.textContent = D_nom.toFixed(1) + ' mm';
|
||||
if (tol) {
|
||||
const D_min = Math.sqrt(d_mm*d_mm + (4*L_m*1000*(t_mm-tol))/Math.PI);
|
||||
const D_max = Math.sqrt(d_mm*d_mm + (4*L_m*1000*(t_mm+tol))/Math.PI);
|
||||
resRange.textContent = 'Min: ' + D_min.toFixed(1) + ' mm | Max: ' + D_max.toFixed(1) + ' mm';
|
||||
} else { resRange.textContent = ''; }
|
||||
document.getElementById('d-di').value = D_nom.toFixed(1);
|
||||
calculatedLength = L_m;
|
||||
}
|
||||
|
||||
NEW CODE (with stddev):
|
||||
*/
|
||||
|
||||
// Example replacement for diameter mode:
|
||||
function calculateDiameterWithStddev(d_mm, t_mm, t_stddev, L_m, tol) {
|
||||
if (!d_mm || !t_mm || !L_m) {
|
||||
alert('Please enter d, t and L.');
|
||||
return false;
|
||||
}
|
||||
|
||||
// Get stddev from data attribute (set during article selection)
|
||||
const thField = document.getElementById('d-th');
|
||||
const actualStddev = t_stddev || parseFloat(thField?.dataset.stddev || 0);
|
||||
|
||||
const resBox = document.getElementById('d-result');
|
||||
const resLabel = document.getElementById('d-res-label');
|
||||
const resVal = document.getElementById('d-res-val');
|
||||
const resRange = document.getElementById('d-res-range');
|
||||
const diField = document.getElementById('d-di');
|
||||
|
||||
// Use StdDev calculator
|
||||
const diameterRanges = window.StdDevCalculator.calculateDiameterRanges(
|
||||
d_mm, t_mm, actualStddev, L_m
|
||||
);
|
||||
|
||||
resLabel.textContent = 'Roll Diameter (D)';
|
||||
resVal.textContent = diameterRanges.nom.toFixed(1) + ' mm';
|
||||
|
||||
// Show ±2σ range
|
||||
const d_low = Math.min(diameterRanges.min, diameterRanges.max);
|
||||
const d_high = Math.max(diameterRanges.min, diameterRanges.max);
|
||||
|
||||
let rangeText = `Range (±2σ): ${d_low.toFixed(1)} – ${d_high.toFixed(1)} mm`;
|
||||
|
||||
if (tol && tol > 0) {
|
||||
const d_tol_min = diameterRanges.nom - tol;
|
||||
const d_tol_max = diameterRanges.nom + tol;
|
||||
rangeText += ` | Tol: ${d_tol_min.toFixed(1)} – ${d_tol_max.toFixed(1)} mm`;
|
||||
}
|
||||
|
||||
resRange.textContent = rangeText;
|
||||
diField.value = diameterRanges.nom.toFixed(1);
|
||||
resBox.style.display = 'block';
|
||||
|
||||
console.log('[StdDev-Calc] Diameter with ranges:', diameterRanges);
|
||||
|
||||
return {
|
||||
calculatedLength: L_m,
|
||||
diameterRanges: diameterRanges
|
||||
};
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
// PATCH 3: MODIFY WEIGHT CALCULATION FOR STDDEV RANGES
|
||||
// ===================================================================
|
||||
// Replace the weight calculation in calcWeight():
|
||||
|
||||
/*
|
||||
OLD:
|
||||
function calcWeight(length) {
|
||||
const wi = parseFloat(document.getElementById('de-wi').value) || 0;
|
||||
const aw = parseFloat(document.getElementById('d-aw').value) || 0;
|
||||
if (wi > 0 && aw > 0 && length > 0) {
|
||||
const w = (aw * length * wi / 1000).toFixed(1);
|
||||
document.getElementById('d-wo').value = w;
|
||||
return parseFloat(w);
|
||||
}
|
||||
document.getElementById('d-wo').value = '';
|
||||
return null;
|
||||
}
|
||||
|
||||
NEW:
|
||||
*/
|
||||
|
||||
function calcWeightWithStddev(length) {
|
||||
const wi = parseFloat(document.getElementById('de-wi').value) || 0;
|
||||
const aw = parseFloat(document.getElementById('d-aw').value) || 0;
|
||||
const awField = document.getElementById('d-aw');
|
||||
const awStddev = parseFloat(awField?.dataset.stddev || 0);
|
||||
|
||||
if (wi > 0 && aw > 0 && length > 0) {
|
||||
// Calculate weight ranges
|
||||
const weightRanges = window.StdDevCalculator.calculateWeightRanges(
|
||||
aw, awStddev, length, wi
|
||||
);
|
||||
|
||||
// Store in field and data attributes
|
||||
const woField = document.getElementById('d-wo');
|
||||
woField.value = weightRanges.nom.toFixed(1);
|
||||
woField.dataset.weight_nom = weightRanges.nom.toFixed(1);
|
||||
woField.dataset.weight_min = weightRanges.min.toFixed(1);
|
||||
woField.dataset.weight_max = weightRanges.max.toFixed(1);
|
||||
|
||||
console.log('[StdDev-Weight] Calculated ranges:', {
|
||||
nom: weightRanges.nom.toFixed(1),
|
||||
min: weightRanges.min.toFixed(1),
|
||||
max: weightRanges.max.toFixed(1)
|
||||
});
|
||||
|
||||
return weightRanges;
|
||||
}
|
||||
|
||||
document.getElementById('d-wo').value = '';
|
||||
return null;
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
// PATCH 4: UPDATE FORKLIFT CHECKER TO USE WORST-CASE
|
||||
// ===================================================================
|
||||
// In ForkliftChecker.evaluate(), use worst-case weight:
|
||||
|
||||
/*
|
||||
OLD:
|
||||
evaluate(weightKg, coreMm, category) {
|
||||
const box = document.getElementById('forkliftCheck');
|
||||
// ... check if weightKg > limit
|
||||
}
|
||||
|
||||
NEW:
|
||||
*/
|
||||
|
||||
function evaluateForkliftWithWorstCase(coreMm, category) {
|
||||
const woField = document.getElementById('d-wo');
|
||||
if (!woField) return true;
|
||||
|
||||
// Get worst-case weight
|
||||
const weight_nom = parseFloat(woField.value) || 0;
|
||||
const weight_max = parseFloat(woField.dataset.weight_max) || weight_nom;
|
||||
|
||||
// Use maximum for safety
|
||||
const weightToCheck = Math.max(weight_nom, weight_max);
|
||||
|
||||
console.log('[Forklift-StdDev] Using worst-case weight:', {
|
||||
nominal: weight_nom.toFixed(1),
|
||||
worst_case: weight_max.toFixed(1),
|
||||
checking: weightToCheck.toFixed(1)
|
||||
});
|
||||
|
||||
if (window.ForkliftChecker) {
|
||||
return window.ForkliftChecker.evaluate(weightToCheck, coreMm, category);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
// COMPLETE EXAMPLE: NEW d-calc HANDLER WITH STDDEV
|
||||
// ===================================================================
|
||||
/*
|
||||
document.getElementById('d-calc').addEventListener('click', () => {
|
||||
const mode = document.querySelector('.mode-btn.active')?.dataset.mode || 'diameter';
|
||||
const d_mm = parseFloat(document.getElementById('d-co').value) || 0;
|
||||
const t_mm = parseFloat(document.getElementById('d-th').value) || 0;
|
||||
const tol = parseFloat(document.getElementById('d-tol').value) || 0;
|
||||
const L_m = parseFloat(document.getElementById('d-le').value) || 0;
|
||||
const D_mm = parseFloat(document.getElementById('d-di').value) || 0;
|
||||
const category = (document.getElementById('d-cat')?.value || '').toLowerCase();
|
||||
|
||||
if (mode === 'diameter') {
|
||||
// Use new function with stddev
|
||||
const result = calculateDiameterWithStddev(d_mm, t_mm, null, L_m, tol);
|
||||
if (!result) return;
|
||||
|
||||
const calculatedLength = result.calculatedLength;
|
||||
const diameterRanges = result.diameterRanges;
|
||||
|
||||
// Calculate weight with stddev
|
||||
const weightRanges = calcWeightWithStddev(calculatedLength);
|
||||
|
||||
// Evaluate forklift with worst-case weight
|
||||
evaluateForkliftWithWorstCase(d_mm, category);
|
||||
|
||||
// Store for Load Optimizer (use nominal)
|
||||
if (weightRanges) {
|
||||
window.loadOptimizerUI.optimizer.setRollData({
|
||||
rollDiameter: diameterRanges.nom,
|
||||
coreDiameter: d_mm,
|
||||
rollWidth: parseFloat(document.getElementById('de-wi').value) || 0,
|
||||
rollWeight: weightRanges.nom, // Nominal for optimizer
|
||||
m2PerRoll: calculatedLength * (parseFloat(document.getElementById('de-wi').value) || 0),
|
||||
productLength: calculatedLength
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ... handle other modes ...
|
||||
});
|
||||
*/
|
||||
|
||||
// ===================================================================
|
||||
// EXPORT FUNCTIONS
|
||||
// ===================================================================
|
||||
window.StdDevIntegration = {
|
||||
calculateDiameterWithStddev: calculateDiameterWithStddev,
|
||||
calcWeightWithStddev: calcWeightWithStddev,
|
||||
evaluateForkliftWithWorstCase: evaluateForkliftWithWorstCase
|
||||
};
|
||||
|
||||
console.log('[StdDevIntegration] Integration functions ready');
|
||||
@@ -0,0 +1,210 @@
|
||||
/**
|
||||
* ROLLCALC V14 - QUALITY OF LIFE UPDATE
|
||||
* Standard Deviation Ranges (±2σ = 95% confidence)
|
||||
* ====================================================
|
||||
*
|
||||
* Features:
|
||||
* 1. Direct Calc: Show MIN/NOM/MAX for diameter & weight (±2σ)
|
||||
* 2. Forklift Checker: Use worst-case weight (MAX)
|
||||
* 3. Load Optimizer: Use nominal values (current behavior)
|
||||
*/
|
||||
|
||||
// ===================================================================
|
||||
// ENHANCED WEIGHT CALCULATION WITH STDDEV
|
||||
// ===================================================================
|
||||
function calculateWeightRanges(areaWeight, areaWeightStddev, length, width) {
|
||||
if (!areaWeight || !length || !width) return null;
|
||||
|
||||
const sigma = areaWeightStddev || 0;
|
||||
const aw_min = Math.max(0, areaWeight - 2 * sigma); // Worst case
|
||||
const aw_nom = areaWeight; // Nominal
|
||||
const aw_max = areaWeight + 2 * sigma; // Best case
|
||||
|
||||
return {
|
||||
min: (aw_min * length * width / 1000),
|
||||
nom: (aw_nom * length * width / 1000),
|
||||
max: (aw_max * length * width / 1000),
|
||||
aw_min: aw_min,
|
||||
aw_nom: aw_nom,
|
||||
aw_max: aw_max
|
||||
};
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
// ENHANCED DIAMETER CALCULATION WITH STDDEV
|
||||
// ===================================================================
|
||||
function calculateDiameterRanges(coreDiameter, thickness, thicknessStddev, productLength) {
|
||||
if (!coreDiameter || !thickness || !productLength) return null;
|
||||
|
||||
const d = coreDiameter;
|
||||
const L = productLength * 1000; // Convert to mm
|
||||
const sigma = thicknessStddev || 0;
|
||||
|
||||
// ±2σ = 95% confidence interval
|
||||
const t_min = Math.max(0, thickness - 2 * sigma); // Thinnest (largest diameter)
|
||||
const t_nom = thickness; // Nominal
|
||||
const t_max = thickness + 2 * sigma; // Thickest (smallest diameter)
|
||||
|
||||
// D = √(d² + 4Lt/π)
|
||||
const d_nom = Math.sqrt(d*d + (4*L*t_nom)/Math.PI);
|
||||
const d_min = Math.sqrt(d*d + (4*L*t_min)/Math.PI); // Thinnest product → largest roll
|
||||
const d_max = Math.sqrt(d*d + (4*L*t_max)/Math.PI); // Thickest product → smallest roll
|
||||
|
||||
return {
|
||||
min: d_min, // Actually MAX diameter (thinnest product)
|
||||
nom: d_nom, // Nominal diameter
|
||||
max: d_max, // Actually MIN diameter (thickest product)
|
||||
t_min: t_min,
|
||||
t_nom: t_nom,
|
||||
t_max: t_max,
|
||||
note: "Note: Min/Max are swapped (thinner product → larger roll)"
|
||||
};
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
// DISPLAY RESULT WITH STDDEV RANGES
|
||||
// ===================================================================
|
||||
function displayDiameterResult(diameterRanges, weightRanges, tolerance = 0) {
|
||||
if (!diameterRanges || !weightRanges) return false;
|
||||
|
||||
const resBox = document.getElementById('d-result');
|
||||
const resLabel = document.getElementById('d-res-label');
|
||||
const resVal = document.getElementById('d-res-val');
|
||||
const resRange = document.getElementById('d-res-range');
|
||||
|
||||
if (!resBox || !resLabel || !resVal || !resRange) {
|
||||
console.warn('[StdDev] Result elements not found');
|
||||
return false;
|
||||
}
|
||||
|
||||
resLabel.textContent = 'Roll Diameter (D)';
|
||||
resVal.textContent = diameterRanges.nom.toFixed(1) + ' mm';
|
||||
|
||||
// Build range text with ±2σ
|
||||
const d_low = Math.min(diameterRanges.min, diameterRanges.max);
|
||||
const d_high = Math.max(diameterRanges.min, diameterRanges.max);
|
||||
|
||||
let rangeText = `Range (±2σ): ${d_low.toFixed(1)} – ${d_high.toFixed(1)} mm`;
|
||||
|
||||
if (tolerance && tolerance > 0) {
|
||||
const d_tol_min = diameterRanges.nom - tolerance;
|
||||
const d_tol_max = diameterRanges.nom + tolerance;
|
||||
rangeText += ` | Tol: ${d_tol_min.toFixed(1)} – ${d_tol_max.toFixed(1)} mm`;
|
||||
}
|
||||
|
||||
resRange.textContent = rangeText;
|
||||
resBox.style.display = 'block';
|
||||
|
||||
console.log('[StdDev] Diameter results displayed:', {
|
||||
nom: diameterRanges.nom.toFixed(1),
|
||||
min: d_low.toFixed(1),
|
||||
max: d_high.toFixed(1)
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
// DISPLAY WEIGHT WITH STDDEV RANGES
|
||||
// ===================================================================
|
||||
function displayWeightRanges(weightRanges) {
|
||||
if (!weightRanges) return false;
|
||||
|
||||
const woField = document.getElementById('d-wo');
|
||||
if (!woField) {
|
||||
console.warn('[StdDev] Weight field not found');
|
||||
return false;
|
||||
}
|
||||
|
||||
// Display nominal weight with data attributes for stddev
|
||||
woField.value = weightRanges.nom.toFixed(1);
|
||||
|
||||
// Store ranges in data attributes for Forklift Checker to use
|
||||
woField.dataset.weight_nom = weightRanges.nom.toFixed(1);
|
||||
woField.dataset.weight_min = weightRanges.min.toFixed(1);
|
||||
woField.dataset.weight_max = weightRanges.max.toFixed(1);
|
||||
|
||||
console.log('[StdDev] Weight ranges stored:', {
|
||||
nom: weightRanges.nom.toFixed(1),
|
||||
min: weightRanges.min.toFixed(1),
|
||||
max: weightRanges.max.toFixed(1)
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
// ENHANCED FORKLIFT CHECKER - Use worst-case weight
|
||||
// ===================================================================
|
||||
function evaluateForkliftWithStddev(coreMm, category) {
|
||||
const woField = document.getElementById('d-wo');
|
||||
if (!woField) return;
|
||||
|
||||
// Get worst-case weight (maximum)
|
||||
const weight_nom = parseFloat(woField.value) || 0;
|
||||
const weight_max = parseFloat(woField.dataset.weight_max) || weight_nom;
|
||||
|
||||
// Use worst-case for safety
|
||||
const weightToCheck = Math.max(weight_nom, weight_max);
|
||||
|
||||
console.log('[StdDev-Forklift] Evaluating with worst-case weight:', {
|
||||
nominal: weight_nom.toFixed(1),
|
||||
worst_case: weight_max.toFixed(1),
|
||||
using: weightToCheck.toFixed(1)
|
||||
});
|
||||
|
||||
if (window.ForkliftChecker) {
|
||||
return window.ForkliftChecker.evaluate(weightToCheck, coreMm, category);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
// EXPORT FOR USE IN MAIN CALCULATOR
|
||||
// ===================================================================
|
||||
window.StdDevCalculator = {
|
||||
calculateWeightRanges: calculateWeightRanges,
|
||||
calculateDiameterRanges: calculateDiameterRanges,
|
||||
displayDiameterResult: displayDiameterResult,
|
||||
displayWeightRanges: displayWeightRanges,
|
||||
evaluateForkliftWithStddev: evaluateForkliftWithStddev
|
||||
};
|
||||
|
||||
console.log('[StdDevCalculator] Module loaded');
|
||||
|
||||
// ===================================================================
|
||||
// INTEGRATION PATCH FOR DIRECT CALCULATION
|
||||
// ===================================================================
|
||||
// This should be added to the main d-calc button handler
|
||||
|
||||
/*
|
||||
USAGE IN DIRECT CALC:
|
||||
|
||||
// After article selection, when user clicks "Calculate":
|
||||
|
||||
const mode = document.querySelector('.mode-btn.active')?.dataset.mode || 'diameter';
|
||||
const d_mm = parseFloat(document.getElementById('d-co').value) || 0;
|
||||
const t_mm = parseFloat(document.getElementById('d-th').value) || 0;
|
||||
const t_stddev = parseFloat(document.getElementById('d-th-stddev')?.value) || 0; // NEW
|
||||
const L_m = parseFloat(document.getElementById('d-le').value) || 0;
|
||||
const wi = parseFloat(document.getElementById('de-wi').value) || 0;
|
||||
const aw = parseFloat(document.getElementById('d-aw').value) || 0;
|
||||
const aw_stddev = parseFloat(document.getElementById('d-aw-stddev')?.value) || 0; // NEW
|
||||
const category = (document.getElementById('d-cat')?.value || '').toLowerCase();
|
||||
|
||||
// Calculate ranges
|
||||
const diameterRanges = window.StdDevCalculator.calculateDiameterRanges(
|
||||
d_mm, t_mm, t_stddev, L_m
|
||||
);
|
||||
const weightRanges = window.StdDevCalculator.calculateWeightRanges(
|
||||
aw, aw_stddev, L_m, wi
|
||||
);
|
||||
|
||||
// Display results with ranges
|
||||
window.StdDevCalculator.displayDiameterResult(diameterRanges, weightRanges);
|
||||
window.StdDevCalculator.displayWeightRanges(weightRanges);
|
||||
|
||||
// Forklift check with worst-case weight
|
||||
window.StdDevCalculator.evaluateForkliftWithStddev(d_mm, category);
|
||||
*/
|
||||
@@ -0,0 +1,232 @@
|
||||
/**
|
||||
* ROLLCALC V14 - ARTICLE SELECTION & AUTO-CALCULATION
|
||||
* ====================================================
|
||||
* Handles article selection, auto-fill of dimensions, and weight calculation
|
||||
* Integrates with stddev_calculator.js for range calculations
|
||||
*/
|
||||
|
||||
/**
|
||||
* Populate article dropdowns from ARTICLE_DATA
|
||||
*/
|
||||
function initializeArticleDropdowns() {
|
||||
const data = window.ARTICLE_DATA || [];
|
||||
console.log('[ArticleDropdown] Initializing with', data.length, 'articles');
|
||||
|
||||
// Populate datalists for both tabs
|
||||
['d-art', 'e-art'].forEach(inputId => {
|
||||
const datalist = document.getElementById(inputId + '-list');
|
||||
if (!datalist) {
|
||||
console.warn('[ArticleDropdown] Datalist not found:', inputId + '-list');
|
||||
return;
|
||||
}
|
||||
|
||||
datalist.innerHTML = ''; // Clear existing
|
||||
data.forEach(article => {
|
||||
const option = document.createElement('option');
|
||||
option.value = `${article.nr} – ${article.name}`;
|
||||
option.dataset.articleNr = article.nr;
|
||||
option.dataset.thickness = article.thickness || 0;
|
||||
option.dataset.areaWeight = article.area_weight || 0;
|
||||
option.dataset.thicknessStddev = article.thickness_stddev || 0;
|
||||
option.dataset.areaWeightStddev = article.area_weight_stddev || 0;
|
||||
datalist.appendChild(option);
|
||||
});
|
||||
});
|
||||
|
||||
// Setup event listeners for article selection
|
||||
setupArticleSelectionListeners();
|
||||
setupWeightCalculationListeners();
|
||||
|
||||
console.log('[ArticleDropdown] Initialization complete');
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup listeners for when user selects an article
|
||||
*/
|
||||
function setupArticleSelectionListeners() {
|
||||
const dArtInput = document.getElementById('d-art');
|
||||
if (!dArtInput) return;
|
||||
|
||||
const dArtList = document.getElementById('d-art-list');
|
||||
const data = window.ARTICLE_DATA || [];
|
||||
|
||||
dArtInput.addEventListener('input', function() {
|
||||
const inputValue = dArtInput.value.trim();
|
||||
|
||||
// Find matching article
|
||||
const match = data.find(a =>
|
||||
inputValue === `${a.nr} – ${a.name}` ||
|
||||
inputValue === a.nr ||
|
||||
inputValue.startsWith(a.nr + ' –')
|
||||
);
|
||||
|
||||
if (match) {
|
||||
console.log('[ArticleSelection] Found:', match.nr, '-', match.name);
|
||||
|
||||
// 1. Auto-fill thickness
|
||||
const thField = document.getElementById('d-th');
|
||||
if (thField) {
|
||||
thField.value = match.thickness || 0;
|
||||
thField.dataset.stddev = match.thickness_stddev || 0;
|
||||
}
|
||||
|
||||
// 2. Auto-fill area weight
|
||||
const awField = document.getElementById('d-aw');
|
||||
if (awField && match.area_weight && match.area_weight > 0) {
|
||||
awField.value = match.area_weight;
|
||||
awField.dataset.stddev = match.area_weight_stddev || 0;
|
||||
}
|
||||
|
||||
// 3. Extract width from article name (e.g., "5.0 x 5m" → 5.0)
|
||||
const widthMatch = match.name.match(/(\d+[,\.]\d+)\s*x\s*\d+\s*m/i);
|
||||
if (widthMatch) {
|
||||
const width = parseFloat(widthMatch[1].replace(',', '.'));
|
||||
const wiField = document.getElementById('de-wi');
|
||||
if (wiField && width > 0 && width < 100) {
|
||||
wiField.value = width;
|
||||
}
|
||||
}
|
||||
|
||||
console.log('[ArticleSelection] Auto-filled:', {
|
||||
thickness: match.thickness,
|
||||
area_weight: match.area_weight,
|
||||
thickness_stddev: match.thickness_stddev,
|
||||
area_weight_stddev: match.area_weight_stddev
|
||||
});
|
||||
|
||||
// Trigger auto-calculation after short delay
|
||||
setTimeout(triggerAutoWeightCalculation, 100);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup listeners for weight field changes (Roll Width, Area Weight, Product Length)
|
||||
*/
|
||||
function setupWeightCalculationListeners() {
|
||||
const fieldIds = ['de-wi', 'd-aw', 'd-le'];
|
||||
|
||||
fieldIds.forEach(id => {
|
||||
const el = document.getElementById(id);
|
||||
if (el) {
|
||||
el.addEventListener('change', triggerAutoWeightCalculation);
|
||||
el.addEventListener('blur', triggerAutoWeightCalculation);
|
||||
el.addEventListener('input', () => {
|
||||
// Debounce: recalculate after user stops typing
|
||||
clearTimeout(el._autoCalcTimeout);
|
||||
el._autoCalcTimeout = setTimeout(triggerAutoWeightCalculation, 300);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Auto-calculate weight when width, area_weight, or length changes
|
||||
*/
|
||||
function triggerAutoWeightCalculation() {
|
||||
const wiEl = document.getElementById('de-wi');
|
||||
const awEl = document.getElementById('d-aw');
|
||||
const leEl = document.getElementById('d-le');
|
||||
const woEl = document.getElementById('d-wo');
|
||||
|
||||
if (!wiEl || !awEl || !leEl || !woEl) return;
|
||||
|
||||
const wi = parseFloat(wiEl.value) || 0;
|
||||
const aw = parseFloat(awEl.value) || 0;
|
||||
const le = parseFloat(leEl.value) || 0;
|
||||
|
||||
if (wi > 0 && aw > 0 && le > 0) {
|
||||
// Calculate with stddev if available
|
||||
const aw_stddev = parseFloat(awEl.dataset.stddev) || 0;
|
||||
|
||||
if (window.StdDevCalculator && aw_stddev > 0) {
|
||||
// Calculate weight ranges
|
||||
const ranges = window.StdDevCalculator.calculateWeightRanges(aw, aw_stddev, le, wi);
|
||||
woEl.value = ranges.nom.toFixed(1);
|
||||
woEl.dataset.weight_nom = ranges.nom.toFixed(1);
|
||||
woEl.dataset.weight_min = ranges.min.toFixed(1);
|
||||
woEl.dataset.weight_max = ranges.max.toFixed(1);
|
||||
|
||||
console.log('[AutoCalc] Weight with stddev:', {
|
||||
nom: ranges.nom.toFixed(1),
|
||||
min: ranges.min.toFixed(1),
|
||||
max: ranges.max.toFixed(1)
|
||||
});
|
||||
} else {
|
||||
// Simple calculation without stddev
|
||||
const weight = (aw * le * wi / 1000).toFixed(1);
|
||||
woEl.value = weight;
|
||||
woEl.dataset.weight_nom = weight;
|
||||
woEl.dataset.weight_max = weight;
|
||||
|
||||
console.log('[AutoCalc] Weight (no stddev):', weight, 'kg');
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
woEl.value = '';
|
||||
delete woEl.dataset.weight_nom;
|
||||
delete woEl.dataset.weight_max;
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup core preset buttons
|
||||
*/
|
||||
function setupCorePresetButtons() {
|
||||
document.querySelectorAll('.core-preset-btn').forEach(btn => {
|
||||
btn.addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
const coreValue = btn.dataset.core;
|
||||
const tabPane = btn.closest('.tab-pane');
|
||||
|
||||
if (tabPane) {
|
||||
const coreInput = tabPane.querySelector('input[id$="-co"]');
|
||||
if (coreInput) {
|
||||
coreInput.value = coreValue;
|
||||
|
||||
// Update active state
|
||||
tabPane.querySelectorAll('.core-preset-btn').forEach(b => b.classList.remove('active'));
|
||||
btn.classList.add('active');
|
||||
|
||||
console.log('[CorePreset] Set core diameter:', coreValue, 'mm');
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize everything when DOM is ready
|
||||
*/
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
console.log('[RollCalcV14] Initializing...');
|
||||
|
||||
// Wait for article data to be loaded
|
||||
const maxRetries = 50;
|
||||
let retries = 0;
|
||||
|
||||
const waitForData = setInterval(function() {
|
||||
if (window.ARTICLE_DATA && window.ARTICLE_DATA.length > 0) {
|
||||
clearInterval(waitForData);
|
||||
initializeArticleDropdowns();
|
||||
setupCorePresetButtons();
|
||||
console.log('[RollCalcV14] Ready');
|
||||
} else if (retries++ > maxRetries) {
|
||||
clearInterval(waitForData);
|
||||
console.warn('[RollCalcV14] Article data not loaded after', maxRetries, 'retries');
|
||||
}
|
||||
}, 100);
|
||||
});
|
||||
|
||||
// Export functions
|
||||
window.RollCalcV14 = {
|
||||
initializeArticleDropdowns,
|
||||
setupArticleSelectionListeners,
|
||||
setupWeightCalculationListeners,
|
||||
triggerAutoWeightCalculation,
|
||||
setupCorePresetButtons
|
||||
};
|
||||
|
||||
console.log('[RollCalcV14] Module loaded');
|
||||
+26
-124
@@ -1,37 +1,22 @@
|
||||
/**
|
||||
* SERVICE WORKER FOR NAUE ROLL CALCULATOR
|
||||
* ========================================
|
||||
* Caching strategy:
|
||||
* 1. CACHE_FIRST for static assets (CSS, JS, images)
|
||||
* 2. NETWORK_FIRST for API/data (with fallback to cache)
|
||||
* 3. STALE_WHILE_REVALIDATE for article data
|
||||
* Offline support + intelligent caching
|
||||
*/
|
||||
|
||||
const CACHE_NAME = 'rollcalc-v14-' + new Date().getTime();
|
||||
const STATIC_CACHE = 'rollcalc-static-v14';
|
||||
const DATA_CACHE = 'rollcalc-data-v14';
|
||||
|
||||
// Assets to cache on install
|
||||
const STATIC_ASSETS = [
|
||||
'/',
|
||||
'/index.html',
|
||||
'/static/article-data.json',
|
||||
'/static/config.json'
|
||||
];
|
||||
const STATIC_ASSETS = ['/', '/index.html', '/static/article-data.json', '/static/config.json'];
|
||||
|
||||
// ===================================================================
|
||||
// INSTALL EVENT - Cache static assets
|
||||
// ===================================================================
|
||||
self.addEventListener('install', event => {
|
||||
console.log('[ServiceWorker] Installing...');
|
||||
|
||||
event.waitUntil(
|
||||
caches.open(STATIC_CACHE)
|
||||
.then(cache => {
|
||||
console.log('[ServiceWorker] Caching static assets');
|
||||
return cache.addAll(STATIC_ASSETS).catch(err => {
|
||||
console.warn('[ServiceWorker] Some assets failed to cache:', err);
|
||||
// Don't fail installation if some assets can't be cached
|
||||
return Promise.resolve();
|
||||
});
|
||||
})
|
||||
@@ -39,19 +24,13 @@ self.addEventListener('install', event => {
|
||||
);
|
||||
});
|
||||
|
||||
// ===================================================================
|
||||
// ACTIVATE EVENT - Clean up old caches
|
||||
// ===================================================================
|
||||
self.addEventListener('activate', event => {
|
||||
console.log('[ServiceWorker] Activating...');
|
||||
|
||||
event.waitUntil(
|
||||
caches.keys().then(cacheNames => {
|
||||
return Promise.all(
|
||||
cacheNames.map(cacheName => {
|
||||
if (cacheName !== STATIC_CACHE &&
|
||||
cacheName !== DATA_CACHE &&
|
||||
cacheName !== CACHE_NAME) {
|
||||
if (cacheName !== STATIC_CACHE && cacheName !== DATA_CACHE && cacheName !== CACHE_NAME) {
|
||||
console.log('[ServiceWorker] Deleting old cache:', cacheName);
|
||||
return caches.delete(cacheName);
|
||||
}
|
||||
@@ -61,38 +40,23 @@ self.addEventListener('activate', event => {
|
||||
);
|
||||
});
|
||||
|
||||
// ===================================================================
|
||||
// FETCH EVENT - Intelligent caching strategy
|
||||
// ===================================================================
|
||||
self.addEventListener('fetch', event => {
|
||||
const { request } = event;
|
||||
const url = new URL(request.url);
|
||||
|
||||
// Skip cross-origin requests
|
||||
if (url.origin !== location.origin) {
|
||||
return;
|
||||
}
|
||||
if (url.origin !== location.origin) return;
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// 1. JSON DATA (article-data.json, config.json)
|
||||
// STALE_WHILE_REVALIDATE: Use cache first, update in background
|
||||
// ---------------------------------------------------------------
|
||||
// JSON data - STALE_WHILE_REVALIDATE
|
||||
if (url.pathname.includes('.json')) {
|
||||
event.respondWith(
|
||||
caches.open(DATA_CACHE).then(cache => {
|
||||
return cache.match(request).then(response => {
|
||||
const fetchPromise = fetch(request).then(networkResponse => {
|
||||
// Update cache with fresh version
|
||||
if (networkResponse && networkResponse.status === 200) {
|
||||
cache.put(request, networkResponse.clone());
|
||||
}
|
||||
return networkResponse;
|
||||
}).catch(() => {
|
||||
// Network failed, return cached version
|
||||
return response || cacheNotFound('JSON data not found');
|
||||
});
|
||||
|
||||
// Return cached response immediately, update in background
|
||||
}).catch(() => response || cacheNotFound('JSON data'));
|
||||
return response || fetchPromise;
|
||||
});
|
||||
})
|
||||
@@ -100,55 +64,35 @@ self.addEventListener('fetch', event => {
|
||||
return;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// 2. HTML PAGES
|
||||
// NETWORK_FIRST: Try network, fall back to cache
|
||||
// ---------------------------------------------------------------
|
||||
if (request.method === 'GET' &&
|
||||
(url.pathname === '/' || url.pathname.endsWith('.html'))) {
|
||||
// HTML - NETWORK_FIRST
|
||||
if (request.method === 'GET' && (url.pathname === '/' || url.pathname.endsWith('.html'))) {
|
||||
event.respondWith(
|
||||
fetch(request)
|
||||
.then(response => {
|
||||
if (response && response.status === 200) {
|
||||
const cache_copy = response.clone();
|
||||
caches.open(STATIC_CACHE).then(cache => {
|
||||
cache.put(request, cache_copy);
|
||||
});
|
||||
caches.open(STATIC_CACHE).then(cache => cache.put(request, response.clone()));
|
||||
}
|
||||
return response;
|
||||
})
|
||||
.catch(() => {
|
||||
return caches.match(request)
|
||||
.then(response => response || cacheNotFound('Page not found'));
|
||||
})
|
||||
.catch(() => caches.match(request).then(response => response || cacheNotFound('Page')))
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// 3. STATIC ASSETS (CSS, JS, images)
|
||||
// CACHE_FIRST: Use cache if available, fall back to network
|
||||
// ---------------------------------------------------------------
|
||||
// Static assets - CACHE_FIRST
|
||||
if (request.method === 'GET') {
|
||||
event.respondWith(
|
||||
caches.open(STATIC_CACHE).then(cache => {
|
||||
return cache.match(request).then(response => {
|
||||
if (response) {
|
||||
return response;
|
||||
}
|
||||
|
||||
if (response) return response;
|
||||
return fetch(request).then(networkResponse => {
|
||||
// Only cache successful responses
|
||||
if (networkResponse && networkResponse.status === 200) {
|
||||
cache.put(request, networkResponse.clone());
|
||||
}
|
||||
return networkResponse;
|
||||
}).catch(() => {
|
||||
// Return offline placeholder
|
||||
if (request.destination === 'image') {
|
||||
return createPlaceholderImage();
|
||||
}
|
||||
return cacheNotFound('Asset not found');
|
||||
if (request.destination === 'image') return createPlaceholderImage();
|
||||
return cacheNotFound('Asset');
|
||||
});
|
||||
});
|
||||
})
|
||||
@@ -156,71 +100,29 @@ self.addEventListener('fetch', event => {
|
||||
return;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// 4. POST/PUT/DELETE (API calls)
|
||||
// NETWORK_ONLY: Do not cache, but handle errors gracefully
|
||||
// ---------------------------------------------------------------
|
||||
// POST/PUT/DELETE - NETWORK_ONLY
|
||||
if (['POST', 'PUT', 'DELETE'].includes(request.method)) {
|
||||
event.respondWith(
|
||||
fetch(request)
|
||||
.catch(() => {
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
error: 'You are offline. API calls are not available.',
|
||||
status: 'offline'
|
||||
}),
|
||||
{
|
||||
status: 503,
|
||||
statusText: 'Service Unavailable',
|
||||
headers: new Headers({
|
||||
'Content-Type': 'application/json'
|
||||
})
|
||||
}
|
||||
);
|
||||
})
|
||||
fetch(request).catch(() => {
|
||||
return new Response(
|
||||
JSON.stringify({ error: 'You are offline. API calls are not available.', status: 'offline' }),
|
||||
{ status: 503, statusText: 'Service Unavailable', headers: { 'Content-Type': 'application/json' } }
|
||||
);
|
||||
})
|
||||
);
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
// ===================================================================
|
||||
// HELPER FUNCTIONS
|
||||
// ===================================================================
|
||||
|
||||
function cacheNotFound(message) {
|
||||
return new Response(
|
||||
`<html><body style="font-family: sans-serif; padding: 20px; color: #666;">
|
||||
<h2>Offline</h2>
|
||||
<p>${message}</p>
|
||||
<p style="font-size: 12px; color: #999;">Roll Calculator is offline or the resource is not cached.</p>
|
||||
</body></html>`,
|
||||
{
|
||||
status: 404,
|
||||
statusText: 'Not Found',
|
||||
headers: new Headers({
|
||||
'Content-Type': 'text/html'
|
||||
})
|
||||
}
|
||||
`<html><body style="font-family: sans-serif; padding: 20px; color: #666;"><h2>Offline</h2><p>${message} not found or you are offline.</p></body></html>`,
|
||||
{ status: 404, statusText: 'Not Found', headers: { 'Content-Type': 'text/html' } }
|
||||
);
|
||||
}
|
||||
|
||||
function createPlaceholderImage() {
|
||||
const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100">
|
||||
<rect fill="#f0f0f0" width="100" height="100"/>
|
||||
<text x="50" y="50" text-anchor="middle" dy=".3em" fill="#999" font-size="12">
|
||||
Image offline
|
||||
</text>
|
||||
</svg>`;
|
||||
|
||||
return new Response(svg, {
|
||||
headers: {
|
||||
'Content-Type': 'image/svg+xml',
|
||||
'Cache-Control': 'public, max-age=86400'
|
||||
}
|
||||
});
|
||||
const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100"><rect fill="#f0f0f0" width="100" height="100"/><text x="50" y="50" text-anchor="middle" dy=".3em" fill="#999" font-size="12">Image offline</text></svg>`;
|
||||
return new Response(svg, { headers: { 'Content-Type': 'image/svg+xml', 'Cache-Control': 'public, max-age=86400' } });
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
// LOG SERVICE WORKER STATUS
|
||||
// ===================================================================
|
||||
console.log('[ServiceWorker] Naue Roll Calculator Service Worker loaded');
|
||||
console.log('[ServiceWorker] RollCalc Service Worker loaded');
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
/**
|
||||
* ROLLCALC V14 - STANDARD DEVIATION CALCULATOR
|
||||
* =============================================
|
||||
* Calculates diameter & weight ranges (±2σ = 95% confidence)
|
||||
* For use with article-data.json containing thickness_stddev & area_weight_stddev
|
||||
*/
|
||||
|
||||
class StdDevCalculator {
|
||||
/**
|
||||
* Calculate weight ranges from area weight and stddev
|
||||
* @param {number} areaWeight - Area weight in g/m²
|
||||
* @param {number} areaWeightStddev - Standard deviation in g/m²
|
||||
* @param {number} length - Product length in meters
|
||||
* @param {number} width - Roll width in meters
|
||||
* @returns {Object} {min, nom, max, aw_min, aw_nom, aw_max}
|
||||
*/
|
||||
static calculateWeightRanges(areaWeight, areaWeightStddev, length, width) {
|
||||
if (!areaWeight || !length || !width) return null;
|
||||
|
||||
const sigma = areaWeightStddev || 0;
|
||||
const aw_min = Math.max(0, areaWeight - 2 * sigma); // Worst case (lightest)
|
||||
const aw_nom = areaWeight; // Nominal
|
||||
const aw_max = areaWeight + 2 * sigma; // Best case (heaviest)
|
||||
|
||||
return {
|
||||
min: (aw_min * length * width / 1000), // kg
|
||||
nom: (aw_nom * length * width / 1000), // kg
|
||||
max: (aw_max * length * width / 1000), // kg
|
||||
aw_min: aw_min,
|
||||
aw_nom: aw_nom,
|
||||
aw_max: aw_max
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate diameter ranges from thickness and stddev
|
||||
* NOTE: Thinner product → larger roll, thicker product → smaller roll
|
||||
* @param {number} coreDiameter - Core diameter in mm
|
||||
* @param {number} thickness - Product thickness in mm
|
||||
* @param {number} thicknessStddev - Standard deviation in mm
|
||||
* @param {number} productLength - Product length in meters
|
||||
* @returns {Object} {min, nom, max, t_min, t_nom, t_max}
|
||||
*/
|
||||
static calculateDiameterRanges(coreDiameter, thickness, thicknessStddev, productLength) {
|
||||
if (!coreDiameter || !thickness || !productLength) return null;
|
||||
|
||||
const d = coreDiameter;
|
||||
const L = productLength * 1000; // Convert to mm
|
||||
const sigma = thicknessStddev || 0;
|
||||
|
||||
// ±2σ = 95% confidence interval
|
||||
const t_min = Math.max(0, thickness - 2 * sigma); // Thinnest product
|
||||
const t_nom = thickness; // Nominal
|
||||
const t_max = thickness + 2 * sigma; // Thickest product
|
||||
|
||||
// D = √(d² + 4Lt/π)
|
||||
// Thinner product → larger D
|
||||
// Thicker product → smaller D
|
||||
const d_nom = Math.sqrt(d*d + (4*L*t_nom)/Math.PI);
|
||||
const d_thin = Math.sqrt(d*d + (4*L*t_min)/Math.PI); // LARGEST roll
|
||||
const d_thick = Math.sqrt(d*d + (4*L*t_max)/Math.PI); // SMALLEST roll
|
||||
|
||||
return {
|
||||
min: d_thick, // SMALLEST (thickest product)
|
||||
nom: d_nom, // NOMINAL
|
||||
max: d_thin, // LARGEST (thinnest product)
|
||||
t_min: t_min,
|
||||
t_nom: t_nom,
|
||||
t_max: t_max
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Format diameter result with ±2σ ranges and tolerance
|
||||
*/
|
||||
static formatDiameterResult(diameterRanges, tolerance = 0) {
|
||||
if (!diameterRanges) return '';
|
||||
|
||||
const d_low = Math.min(diameterRanges.min, diameterRanges.max);
|
||||
const d_high = Math.max(diameterRanges.min, diameterRanges.max);
|
||||
|
||||
let text = `${diameterRanges.nom.toFixed(1)} mm (±2σ: ${d_low.toFixed(1)}–${d_high.toFixed(1)} mm)`;
|
||||
|
||||
if (tolerance && tolerance > 0) {
|
||||
const d_tol_min = diameterRanges.nom - tolerance;
|
||||
const d_tol_max = diameterRanges.nom + tolerance;
|
||||
text += ` | Tolerance: ${d_tol_min.toFixed(1)}–${d_tol_max.toFixed(1)} mm`;
|
||||
}
|
||||
|
||||
return text;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format weight result
|
||||
*/
|
||||
static formatWeightResult(weightRanges) {
|
||||
if (!weightRanges) return '';
|
||||
return `${weightRanges.nom.toFixed(1)} kg (${weightRanges.min.toFixed(1)}–${weightRanges.max.toFixed(1)} kg)`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get worst-case weight for safety checks (Forklift, etc.)
|
||||
*/
|
||||
static getWorstCaseWeight(weightRanges) {
|
||||
if (!weightRanges) return 0;
|
||||
return Math.max(weightRanges.nom, weightRanges.max);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get nominal weight for capacity planning
|
||||
*/
|
||||
static getNominalWeight(weightRanges) {
|
||||
if (!weightRanges) return 0;
|
||||
return weightRanges.nom;
|
||||
}
|
||||
}
|
||||
|
||||
// Export globally
|
||||
window.StdDevCalculator = StdDevCalculator;
|
||||
console.log('[StdDevCalculator] Module loaded');
|
||||
Reference in New Issue
Block a user