summaryrefslogtreecommitdiff
path: root/script.js
diff options
context:
space:
mode:
Diffstat (limited to 'script.js')
-rw-r--r--script.js240
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}
+}