diff options
Diffstat (limited to 'script.js')
-rw-r--r-- | script.js | 240 |
1 files changed, 240 insertions, 0 deletions
diff --git a/script.js b/script.js new file mode 100644 index 0000000..ba26dac --- /dev/null +++ b/script.js @@ -0,0 +1,240 @@ +"use strict"; + +var ingredientsm = {} +var allIngredients = [] +for (let recipe of allRecipes) { + for (let ingredient of recipe.ingredients) { + if (ingredient.special) { + continue + } + if (!ingredientsm[ingredient.ingredient]) { + allIngredients.push(ingredient.ingredient) + ingredientsm[ingredient.ingredient] = 1 + } + } +} +allIngredients.sort() +var ingredientsLookup = {} +for (let [idx, ingredient] of allIngredients.entries()) { + ingredientsLookup[ingredient] = idx +} + +var globret = undefined; + +window.addEventListener('load', function(evt) { + glpkPromise.then(function(glpk) { + + const worker = new Worker('./node_modules/glpk.js/glpk-worker.js') + worker.onerror = (err) => { console.log(err) } + worker.onmessage = (evt) => { + if (evt.data.initialized) {} + if (evt.data.result) { + hide(document.getElementById('waiting')) + show(document.getElementById('solution')) + renderSolution(glpk, evt.data, globret) + } + } + + document.getElementById('optimal').addEventListener('click', function(evt) { + evt.preventDefault() + window.globret = constructILP(glpk) + solveILP(worker, globret) + }) + }) +}) + + +function renderSolution(glpk, sol, ret) { + let ingredients = ret.ingredients + let recipes = JSON.parse(JSON.stringify(ret.recipes)) + let res = [] + let batches = [] + let origbatch = [] + for (let [r, recipe] of recipes.entries()) { + if (recipe.length == 0) { + origbatch.push(r) + } + } + for (let [varname, on] of Object.entries(sol.result.vars)) { + if (!on) { continue } + if (!varname.startsWith('y')) { continue } + let y = Number.parseInt(varname.split('_')[1]) + if (res.indexOf(y) != -1) { continue } + res.push(y) + let batch = [] + for (let [r, recipe] of recipes.entries()) { + if (recipe.length == 1 & recipe[0] == y) { + batch.push(r) + } + } + for (let [r, recipe] of recipes.entries()) { + recipes[r] = recipe.filter(it => it != y) + } + batches.push(batch) + } + let ol = document.createElement('ol') + + let li = document.createElement('li') + li.appendChild(document.createTextNode('On-hand')) + if (origbatch.length) { + let ul = document.createElement('ul') + drinks = origbatch.map(b => allRecipes[b].name).join(', ') + let sli = document.createElement('li') + sli.appendChild(document.createTextNode(drinks)) + ul.appendChild(sli) + li.appendChild(ul) + } + ol.appendChild(li) + + for (let [idx, y] of res.entries()) { + let batch = batches[idx] + let li = document.createElement('li') + li.appendChild(document.createTextNode(ingredients[y])) + if (batch.length) { + let ul = document.createElement('ul') + let drinks = batch.map(b => allRecipes[b].name).join(', ') + let sli = document.createElement('li') + sli.appendChild(document.createTextNode(drinks)) + ul.appendChild(sli) + li.appendChild(ul) + } + ol.appendChild(li) + } + document.getElementById('solution').innerHTML = '' + let desc = document.createElement('p') + if (sol.result.status == glpk.GLP_UNDEF) { + desc.appendChild(document.createTextNode('Error: Failed to find feasible solution. Try increasing time limit.')) + } else if (sol.result.status == glpk.GLP_FEAS) { + desc.appendChild(document.createTextNode(`Found feasible solution of score ${-sol.result.z}.`)) + } else if (sol.result.status == glpk.GLP_INFEAS) { + desc.appendChild(document.createTextNode('Error: Did not find feasible solution.')) + } else if (sol.result.status == glpk.GLP_NOFEAS) { + desc.appendChild(document.createTextNode('Error: Problem has no feasible solution.')) + } else if (sol.result.status == glpk.GLP_OPT) { + desc.appendChild(document.createTextNode(`Found optimal solution of score ${-sol.result.z}.`)) + } else if (sol.result.status == glpk.GLP_UNBND) { + desc.appendChild(document.createTextNode('Error: Problem is unbounded.')) + } + document.getElementById('solution').appendChild(desc) + if (sol.result.status == glpk.GLP_FEAS || sol.result.status == glpk.GLP_OPT) { + document.getElementById('solution').appendChild(ol) + } +} + +let hide = e => e.style.display = 'none'; +let show = e => e.style.display = 'inline-block'; + +function solveILP(worker, ret) { + let seconds = Number.parseInt(document.getElementById('limit').value) + document.getElementById('waiting').innerHTML = seconds + ' seconds...' + hide(document.getElementById('solution')) + show(document.getElementById('waiting')) + let incrSeconds = () => { + document.getElementById('waiting').innerHTML = seconds + ' seconds...' + seconds-- + } + setInterval(incrSeconds, 1000) + worker.postMessage(ret.lp) +} + +function constructILP(glpk) { + let skip = Object.values(document.querySelector('form')).reduce((obj, field) => { + if (field.getAttribute('x-ingredient') && field.checked) { + obj[field.getAttribute('x-ingredient')] = 1 + } + return obj + }, {}) + + let ingredientsset = {} + let recipes = [] + + for (let recipe of allRecipes) { + for (let ingredient of recipe.ingredients) { + if (ingredient.special) { continue } + if (skip[ingredientsLookup[ingredient.ingredient]]) { continue } + ingredientsset[ingredient.ingredient] = 1 + } + } + let ingredients = Object.keys(ingredientsset) + + for (let recipe of allRecipes) { + let x = [] + for (let ingredient of recipe.ingredients) { + if (ingredient.special) { continue } + if (skip[ingredientsLookup[ingredient.ingredient]]) { continue } + x.push(ingredients.indexOf(ingredient.ingredient)) + } + recipes.push(x) + } + let n_ingredients = ingredients.length + + let makeStepR = (step, r) => `r${step}_${r}` + let makeStepY = (step, y) => `y${step}_${y}` + + let jsonvars = [] + for (let step = 0; step < n_ingredients; step++) { + for (let [r, recipe] of recipes.entries()) { + jsonvars.push({name: makeStepR(step, r), coef: -1}) + } + } + let jsonobjective = { + direction: glpk.GLP_MIN, + vars: jsonvars, + } + let jsonconstraints = [] + for (let step = 0; step < n_ingredients; step++) { + let vars = [] + for (let y = 0; y < n_ingredients; y++) { + vars.push({name: makeStepY(step, y), coef: 1}) + } + jsonconstraints.push({vars, bnds: {type: glpk.GLP_FX, lb: step+1}}) + } + for (let y = 0; y < n_ingredients; y++) { + for (let step = 0; step < n_ingredients - 1; step++) { + jsonconstraints.push({ + vars: [{name: makeStepY(step+1, y), coef: 1}, {name: makeStepY(step, y), coef: -1}], + bnds: {type: glpk.GLP_LO, lb: 0} + }) + } + } + for (let [r, recipe] of recipes.entries()) { + for (let step = 0; step < n_ingredients; step++) { + let total = [] + for (let y of recipe) { + total.push({name: makeStepY(step, y), coef: -1}) + } + total.push({name: makeStepR(step, r), coef: 1}) + jsonconstraints.push({ + vars: total, + bnds: {type: glpk.GLP_LO, lb: (-recipe.length + 1)} + }) + for (let y of recipe) { + jsonconstraints.push({ + vars: [{name: makeStepR(step, r), coef: 1}, {name: makeStepY(step, y), coef: -1}], + bnds: {type: glpk.GLP_UP, ub: 0} + }) + } + } + } + let binaries = [] + for (let y = 0; y < n_ingredients; y++) { + for (let step = 0; step < n_ingredients; step++) { + binaries.push(makeStepY(step, y)) + } + } + for (let r = 0; r < recipes.length; r++) { + for (let step = 0; step < n_ingredients; step++) { + binaries.push(makeStepR(step, r)) + } + } + let lp = { + objective: jsonobjective, + subjectTo: jsonconstraints, + binaries, + options: { + msglev: 2, + tmlim: Number.parseInt(document.getElementById('limit').value), + } + } + return {ingredients, lp, recipes} +} |