www/js/rec_edit.js

changeset 667
1246550451ca
parent 662
4bb005694ce7
child 710
9646123ea063
--- a/www/js/rec_edit.js	Fri May 01 21:37:23 2020 +0200
+++ b/www/js/rec_edit.js	Wed May 06 14:14:14 2020 +0200
@@ -48,7 +48,12 @@
  MMNaHCO3 = 84.007,
  MMNa2CO3 = 105.996,
  MMNaCl = 58.443,
- MMCaOH2 = 74.06268;
+ MMCaOH2 = 74.06268,
+ SpecificHeatWater = 1.0,
+ SpecificHeatMalt = 0.399, //cal/g.°C
+ SlakingHeat = 10.318, //cal/g.°C
+ eq_tun_weight = 2.0, // 2 Kg pot
+ eq_tun_specific_heat = 0.110, // Stainless Steel
  data_loaded = 0;
 
 function createDelElements() {
@@ -348,18 +353,90 @@
 }
 
 
+function swapMash(r1, r2) {
+
+ console.log('swap mash rows ' + r1 + ' ' + r2);
+ var row1 = $('#mashGrid').jqxGrid('getrowdata', r1);
+ var row2 = $('#mashGrid').jqxGrid('getrowdata', r2);
+ var obj1 = { step_name: row1.step_name, step_type: row1.step_type, step_volume: row1.step_volume, step_infuse_amount: row1.step_infuse_amount,
+              step_infuse_temp: row1.step_infuse_temp, step_temp: row1.step_temp, step_time: row1.step_time,
+              ramp_time: row1.ramp_time, end_temp: row1.end_temp, step_wg_ratio: row1.step_wg_ratio };
+ var obj2 = { step_name: row2.step_name, step_type: row2.step_type, step_volume: row2.step_volume, step_infuse_amount: row2.step_infuse_amount,
+              step_infuse_temp: row2.step_infuse_temp, step_temp: row2.step_temp, step_time: row2.step_time,
+              ramp_time: row2.ramp_time, end_temp: row2.end_temp, step_wg_ratio: row2.step_wg_ratio };
+ $("#mashGrid").jqxGrid('updaterow', r1, obj2);
+ $("#mashGrid").jqxGrid('updaterow', r2, obj1);
+}
+
+
+function infusionVol(step_infused, step_mashkg, infuse_temp, step_temp, last_temp) {
+ var a = last_temp * (eq_tun_weight * eq_tun_specific_heat + step_infused * SpecificHeatWater + step_mashkg * SpecificHeatMalt);
+ var b = step_temp * (eq_tun_weight * eq_tun_specific_heat + step_infused * SpecificHeatWater + step_mashkg * SpecificHeatMalt);
+ var vol = Round(((b - a) / ((infuse_temp - step_temp) * SpecificHeatWater)), 2);
+ console.log('infusionVol(' + step_infused + ', ' + step_mashkg + ', ' + infuse_temp + ', ' + step_temp + ', ' + last_temp + '): ' + vol);
+ return vol;
+}
+
+
+function decoctionVol(step_volume, step_temp, prev_temp) {
+ var a = (eq_tun_weight * eq_tun_specific_heat + step_volume * SpecificHeatWater) * (step_temp - prev_temp);
+ var b = SpecificHeatWater * (99 - step_temp);
+ var vol = 0;
+ if (b > 0)
+  vol = Round(a / b, 6);
+ console.log('decoctionVol(' + step_volume + ', ' + step_temp + ', ' + prev_temp + '): ' + vol);
+ return vol;
+}
+
+
 function calcMash() {
- var infused = 0, i, row, rows;
- if (!(rows = $('#mashGrid').jqxGrid('getrows')))
-  return;
- if (mashkg == 0)
-  return;
+ var infused = 0, vol, i, j, n, a, b, row, rows, temp;
+ var lasttemp = 18.0;
+ var graintemp = 18.0;
+ var tuntemp = 18.0;
 
- for (i = 0; i < rows.length; i++) {
-  row = $('#mashGrid').jqxGrid('getrowdata', i);
-  if (row.step_type == 0) // Infusion
-   infused += row.step_infuse_amount;
-  $('#mashGrid').jqxGrid('setcellvalue', i, 'step_thickness', infused / mashkg);
+ if ((rows = $('#mashGrid').jqxGrid('getrows')) && (mashkg > 0)) {
+  console.log('calcMash()');
+  for (i = 0; i < rows.length; i++) {
+   row = $('#mashGrid').jqxGrid('getrowdata', i);
+   if (row.step_type == 0) { // Infusion
+    if (i == 0) {
+      // First mash step, temperature from the mashtun and malt.
+      n = 20; // tun is preheated.
+      tuntemp = row.step_temp;
+      for (j = 0; j < n; j++) {
+       a = mashkg * graintemp * SpecificHeatMalt + eq_tun_weight * tuntemp * eq_tun_specific_heat;
+       b = row.step_temp * (eq_tun_weight * eq_tun_specific_heat + row.step_infuse_amount * SpecificHeatWater + mashkg * SpecificHeatMalt) - SlakingHeat * mashkg;
+       if (row.step_infuse_amount > 0) {
+        temp = (b - a) / (row.step_infuse_amount * SpecificHeatWater);
+       } else {
+        temp = 99;
+       }
+       tuntemp += (temp - tuntemp) / 2;
+       row.step_infuse_temp = Round(temp, 6);
+      }
+      console.log('init infuse temp: ' + row.step_infuse_temp);
+    } else {
+      // Calculate amount of infusion water.
+      row.step_infuse_amount = infusionVol(infused, mashkg, row.step_infuse_temp, row.step_temp, lasttemp);
+      //console.log('vol: ' + row.step_infuse_amount + ' temp: ' + row.step_infuse_temp);
+    }
+    infused += row.step_infuse_amount;
+   } else if (row.step_type == 1) { // Temperature
+     if (i > 0)
+      row.step_infuse_amount = 0;
+     row.step_infuse_temp = 0;
+   } else if (row.step_type == 2) { // Decoction
+     row.step_infuse_amount = decoctionVol(infused, row.step_temp, lasttemp);
+     row.step_infuse_temp = 99;
+   }
+    row.step_volume = infused;
+    //console.log(i + ' type: ' + row.step_type + ' volume: ' + row.step_infuse_amount + ' temp: ' + row.step_infuse_temp);
+    lasttemp = row.step_temp;
+    mashtime += row.step_time + row.ramp_time;
+    row.step_wg_ratio = Round(infused / mashkg, 6);
+    $('#mashGrid').jqxGrid('updaterow', i, row);
+  }
  }
 }
 
@@ -1685,18 +1762,6 @@
 
  // inline mash editor
  var editMash = function(data) {
-  var generaterow = function() {
-   var row = {};
-   row['step_name'] = 'Stap 1';
-   row['step_type'] = 0;
-   row['step_infuse_amount'] = 15;
-   row['step_temp'] = 62.0;
-   row['step_time'] = 20.0;
-   row['step_thickness'] = 0;
-   row['ramp_time'] = 1.0;
-   row['end_temp'] = 62.0;
-   return row;
-  };
   var mashSource = {
    localdata: data.mashs,
    datatype: 'local',
@@ -1705,19 +1770,17 @@
    datafields: [
     { name: 'step_name', type: 'string' },
     { name: 'step_type', type: 'int' },
+    { name: 'step_volume', type: 'float' },
     { name: 'step_infuse_amount', type: 'float' },
+    { name: 'step_infuse_temp', type: 'float' },
     { name: 'step_temp', type: 'float' },
     { name: 'step_time', type: 'float' },
-    { name: 'step_thickness', type: 'float' },
+    { name: 'step_wg_ratio', type: 'float' },
     { name: 'ramp_time', type: 'float' },
     { name: 'end_temp', type: 'float' }
    ],
-   addrow: function(rowid, rowdata, position, commit) {
-    commit(true);
-   },
-   deleterow: function(rowid, commit) {
-    commit(true);
-   }
+   addrow: function(rowid, rowdata, position, commit) { commit(true); },
+   deleterow: function(rowid, commit) { commit(true); }
   },
   mashAdapter = new $.jqx.dataAdapter(mashSource, {
    beforeLoadComplete: function(records) {
@@ -1727,7 +1790,7 @@
      row = records[i];
      if (row.step_type == 0) // Infusion
       mash_infuse += parseFloat(row.step_infuse_amount);
-     row.step_thickness = 0; // Init this field.
+     row.step_wg_ratio = 0; // Init this field.
      data.push(row);
     }
    },
@@ -1747,8 +1810,25 @@
     container.append('<input style="float: left; margin-left: 565px;" id="sdeleterowbutton" type="button" value="Verwijder stap" />');
     $('#saddrowbutton').jqxButton({ template: 'primary', theme: theme, height: 27, width: 150 });
     $('#saddrowbutton').on('click', function() {
-     var datarow = generaterow();
+     var row = {}, rowscount = $('#mashGrid').jqxGrid('getdatainformation').rowscount;
+     row['step_name'] = 'Stap ' + (rowscount + 1);
+     if (rowscount > 0) {
+      row['step_type'] = 1;
+      row['step_infuse_amount'] = 0;
+      row['step_volume'] = mash_infuse;
+     } else {
+      row['step_type'] = 0;
+      row['step_infuse_amount'] = 15;
+      row['step_volume'] = 15;
+     }
+     row['step_infuse_temp'] = 0;
+     row['step_temp'] = 62.0;
+     row['step_time'] = 20.0;
+     row['step_wg_ratio'] = 0;
+     row['ramp_time'] = 1.0;
+     row['end_temp'] = 62.0;
      $('#mashGrid').jqxGrid('addrow', null, datarow);
+     calcMash();
     });
     // delete selected yeast.
     $('#sdeleterowbutton').jqxButton({ template: 'danger', theme: theme, height: 27, width: 150 });
@@ -1758,6 +1838,7 @@
      if (selectedrowindex >= 0 && selectedrowindex < rowscount) {
       id = $('#mashGrid').jqxGrid('getrowid', selectedrowindex);
       $('#mashGrid').jqxGrid('deleterow', id);
+      calcMash();
      }
     });
    },
@@ -1779,27 +1860,82 @@
     { text: 'Eind &deg;C', datafield: 'end_temp', width: 90, align: 'right', cellsalign: 'right', cellsformat: 'f1' },
     { text: 'Rust min.', datafield: 'step_time', width: 90, align: 'right', cellsalign: 'right' },
     { text: 'Stap min.', datafield: 'ramp_time', width: 90, align: 'right', cellsalign: 'right' },
-    { text: 'Infuse L.', datafield: 'step_infuse_amount', width: 90, align: 'right', cellsalign: 'right', cellsformat: 'f1' },
-    { text: 'L/Kg.', datafield: 'step_thickness', width: 90, align: 'right', cellsalign: 'right', cellsformat: 'f2' },
-    { text: '', datafield: 'Edit', columntype: 'button', width: 100, align: 'center',
+    { text: 'Inf/dec L.', datafield: 'step_infuse_amount', width: 90, align: 'right',
+      cellsrenderer: function(row, columnfield, value, defaulthtml, columnproperties, rowdata) {
+       if (rowdata.step_type == 1)
+        return '<span></span>';
+       return '<span style="margin: 4px; margin-top: 6px; float: right;">' + dataAdapter.formatNumber(value, 'f1') + '</span>';
+      }
+    },
+    { text: 'Inf/dec &deg;C', datafield: 'step_infuse_temp', width: 90, align: 'right',
+      cellsrenderer: function(row, columnfield, value, defaulthtml, columnproperties, rowdata) {
+       if (rowdata.step_type == 1)
+        return '<span></span>';
+       return '<span style="margin: 4px; margin-top: 6px; float: right;">' + dataAdapter.formatNumber(value, 'f2') + '</span>';
+      }
+    },
+    { text: 'L/Kg.', datafield: 'step_wg_ratio', width: 90, align: 'right',
+      cellsrenderer: function(row, columnfield, value, defaulthtml, columnproperties, rowdata) {
+       var color = '#ffffff';
+       if (value < 2.0 || value > 6.0)
+        color = '#ff4040';
+       return '<span style="margin: 4px; margin-top: 6px; float: right; color: ' + color + ';">' + dataAdapter.formatNumber(value, 'f2') + '</span>';
+      }
+    },
+    { text: '', columntype: 'button', width: 15, align: 'center',
+     cellsrenderer: function(row) {
+      if (row < 2)
+       return ' ';
+      return '▴';
+     }, buttonclick: function(row) {
+      if (row >= 2) {
+       swapMash(row, row-1);
+      }
+     }
+    },
+    { text: '', columntype: 'button', width: 15, align: 'center',
+     cellsrenderer: function(row) {
+      rowscount = $('#mashGrid').jqxGrid('getdatainformation').rowscount;
+      if (row < 1 || row > (rowscount -2))
+       return ' ';
+      return '▾';
+     }, buttonclick: function(row) {
+      rowscount = $('#mashGrid').jqxGrid('getdatainformation').rowscount;
+      if (row >= 1 && row <= (rowscount -2)) {
+       swapMash(row, row+1);
+      }
+     }
+    },
+    { text: '', datafield: 'Edit', columntype: 'button', width: 80, align: 'center',
      cellsrenderer: function() {
       return 'Wijzig';
      }, buttonclick: function(row) {
       mashRow = row;
       mashData = $('#mashGrid').jqxGrid('getrowdata', mashRow);
+      if (mashRow == 0)
+       $("#wstep_type").jqxDropDownList('disableAt', 2);
+      else
+       $("#wstep_type").jqxDropDownList('enableAt', 2);
       $('#wstep_name').val(mashData.step_name);
       $('#wstep_type').val(mashData.step_type);
       $('#wstep_infuse_amount').val(mashData.step_infuse_amount);
+      $('#wstep_infuse_temp').val(mashData.step_infuse_temp);
       $('#wstep_temp').val(mashData.step_temp);
       $('#wend_temp').val(mashData.end_temp);
       $('#wstep_time').val(mashData.step_time);
       $('#wramp_time').val(mashData.ramp_time);
+      $('#wstep_infuse_amount').hide(); // Hide all untile we need it.
+      $('#wstep_infuse_temp').hide();
+      $('#wstep_pmpt_amount').hide();
+      $('#wstep_pmpt_temp').hide();
       if (mashData.step_type == 0) {
-       $('#wstep_infuse_amount').show();
-       $('#wstep_pmpt').show();
-      } else {
-       $('#wstep_infuse_amount').hide();
-       $('#wstep_pmpt').hide();
+       if (mashRow == 0) {
+        $('#wstep_infuse_amount').show();
+        $('#wstep_pmpt_amount').show();
+       } else {
+        $('#wstep_infuse_temp').show();
+        $('#wstep_pmpt_temp').show();
+       }
       }
       // show the popup window.
       $('#popupMash').jqxWindow('open');
@@ -1826,7 +1962,6 @@
   theme: theme
  });
 
-
  function setWaterAgent(name, amount) {
 
   var record, records, miscs, i, id, row, found = false, rows = $('#miscGrid').jqxGrid('getrows');
@@ -3582,7 +3717,7 @@
  });
  $('#mash_select').on('select', function(event) {
   if (event.args) {
-   var data, datarecord, i, row, rows, rowIDs, index = event.args.index;
+   var infused = 0, data, datarecord, i, row, rows, rowIDs, index = event.args.index;
    // First delete all current steps
    rowIDs = new Array();
    rows = $('#mashGrid').jqxGrid('getdisplayrows');
@@ -3598,29 +3733,37 @@
     data = datarecord.steps[i];
     row = {};
     row['step_name'] = data.step_name;
-    row['step_type'] = data.step_type;
-    // For now, but this must be smarter.
-    if (mash_infuse == 0 && dataRecord.w1_amount > 0)
-     mash_infuse = dataRecord.w1_amount;
-    if (i == 0)
-     row['step_infuse_amount'] = mash_infuse;
+    row['step_type'] = parseInt(data.step_type);
+    row['step_temp'] = parseFloat(data.step_temp);
+    row['end_temp'] = parseFloat(data.end_temp);
+    row['step_time'] = parseFloat(data.step_time);
+    row['ramp_time'] = parseFloat(data.ramp_time);
+    row['step_infuse_temp'] = 0.0;
+    row['step_infuse_amount'] = 0.0;
+    if (mash_infuse == 0 && dataRecord.wg_amount > 0)
+     mash_infuse = dataRecord.wg_amount;
+    if (data.step_type == 0) { // Infusion
+     if (i == 0) {
+      row['step_infuse_amount'] = parseFloat(mash_infuse);
+     } else {
+      row['step_infuse_temp'] = 99.0;
+     }
+    }
+    //console.log(i + ' type: ' + row['step_type'] + ' start infusion: ' + parseFloat(row['step_infuse_amount']) + ' mash_infuse: ' + mash_infuse);
+    infused += parseFloat(row['step_infuse_amount']);
+    row['step_volume'] = infused;
+    if (mashkg > 0)
+     row['step_wg_ratio'] = Round(parseFloat(mash_infuse / mashkg), 2);
     else
-     row['step_infuse_amount'] = 0;
-    row['step_temp'] = data.step_temp;
-    if (mashkg > 0)
-     row['step_thickness'] = parseFloat(mash_infuse / mashkg);
-    else
-     row['step_thickness'] = 0;
-    row['end_temp'] = data.end_temp;
-    row['step_time'] = data.step_time;
-    row['ramp_time'] = data.ramp_time;
+     row['step_wg_ratio'] = 0;
     $('#mashGrid').jqxGrid('addrow', null, row);
    }
+   calcMash();
   }
  });
  $('#popupMash').jqxWindow({
   width: 800,
-  height: 350,
+  height: 375,
   position: { x: 230, y: 100 },
   resizable: false,
   theme: theme,
@@ -3636,7 +3779,7 @@
  $('#wstep_name').jqxInput({ theme: theme, width: 320, height: 23 });
  $('#wstep_name').on('change', function(event) {
   var rowdata = $('#mashGrid').jqxGrid('getrowdata', mashRow);
-  rowdata.step_name = event.args.value;
+  rowdata.step_name = $('#wstep_name').val();
  });
  $('#wstep_type').jqxDropDownList({
   theme: theme,
@@ -3651,28 +3794,61 @@
   if (event.args) {
    var rowdata, rows, i, row, index = event.args.index;
    rowdata = $('#mashGrid').jqxGrid('getrowdata', mashRow);
-   rowdata.step_type = index;
-   if (index == 0) {
-    $('#wstep_infuse_amount').show();
-    $('#wstep_pmpt').show();
-   } else {
-    rowdata.step_infuse_amount = 0;
+   if (rowdata.step_type != index) {
+    rowdata.step_type = index;
     $('#wstep_infuse_amount').hide();
-    $('#wstep_pmpt').hide();
-   }
-   mash_infuse = 0;
-   rows = $('#mashGrid').jqxGrid('getrows');
-   for (i = 0; i < rows.length; i++) {
-    row = rows[i];
-    if (row.step_type == 0) // Infusion
-     mash_infuse += parseFloat(row.step_infuse_amount);
+    $('#wstep_infuse_temp').hide();
+    $('#wstep_pmpt_amount').hide();
+    $('#wstep_pmpt_temp').hide();
+    if (index == 0) { // Infusion
+     if (mashRow == 0) {
+      $('#wstep_infuse_amount').show();
+      $('#wstep_pmpt_amount').show();
+     } else {
+      $('#wstep_infuse_temp').show();
+      $('#wstep_pmpt_temp').show();
+     }
+    }
+    if (index == 1) { // Temperature
+     if (mashRow > 0)
+      rowdata.step_infuse_amount = 0;
+     rowdata.step_infuse_temp = 0;
+    }
+    if (index == 2) { // Decoction
+     var rowprev = $('#mashGrid').jqxGrid('getrowdata', mashRow-1);
+     rowdata.step_infuse_temp = 99;
+     rowdata.step_infuse_amount = decoctionVol(rowdata.step_volume, rowdata.step_temp, rowprev.end_temp);
+     console.log('decoction: ' + rowdata.step_infuse_amount + '/' + rowdata.step_infuse_temp);
+    }
+    $('#mashGrid').jqxGrid('updaterow', mashRow, rowdata);
+    mash_infuse = 0;
+    rows = $('#mashGrid').jqxGrid('getrows');
+    for (i = 0; i < rows.length; i++) {
+     row = rows[i];
+     if (row.step_type == 0) // Infusion
+      mash_infuse += parseFloat(row.step_infuse_amount);
+    }
+    calcMash();
    }
   }
  });
  $('#wstep_temp').jqxNumberInput(Spin1dec);
  $('#wstep_temp').on('change', function(event) {
   var rowdata = $('#mashGrid').jqxGrid('getrowdata', mashRow);
-  rowdata.step_temp = parseFloat(event.args.value);
+  if (rowdata.step_type == 2) { // Decoction
+   var rowprev = $('#mashGrid').jqxGrid('getrowdata', mashRow-1);
+   var a = (eq_tun_weight * eq_tun_specific_heat + rowdata.step_volume * SpecificHeatWater) *
+           (parseFloat(event.args.value) - rowprev.end_temp);
+   var b = SpecificHeatWater * (99 - parseFloat(event.args.value));
+   if (b > 0) {
+    rowdata.step_temp = parseFloat(event.args.value);
+    rowdata.step_infuse_amount = Round(a / b, 2);
+   } else
+    rowdata.step_infuse_amount = 0;
+   console.log('change temp ' + rowdata.step_temp + ' decoction: ' + rowdata.step_infuse_amount + '/' + rowdata.step_infuse_temp);
+  } else {
+   rowdata.step_temp = parseFloat(event.args.value);
+  }
  });
  $('#wend_temp').jqxNumberInput(Spin1dec);
  $('#wend_temp').on('change', function(event) {
@@ -3693,23 +3869,40 @@
  $('#wstep_infuse_amount').on('change', function(event) {
   var i, rows, row, rowdata = $('#mashGrid').jqxGrid('getrowdata', mashRow);
   rowdata.step_infuse_amount = parseFloat(event.args.value);
-  mash_infuse = 0;
-  rows = $('#mashGrid').jqxGrid('getrows');
-  for (i = 0; i < rows.length; i++) {
-   row = rows[i];
-   if (row.step_type == 0) // Infusion
-    mash_infuse += parseFloat(row.step_infuse_amount);
+  if (mashRow == 0) {
+   rowdata.step_infuse_amount = parseFloat(event.args.value);
+   mash_infuse = 0;
+   rows = $('#mashGrid').jqxGrid('getrows');
+   for (i = 0; i < rows.length; i++) {
+    row = rows[i];
+    if (row.step_type == 0) // Infusion
+     mash_infuse += parseFloat(row.step_infuse_amount);
+   }
+   if (dataRecord.w2_amount == 0) {
+    dataRecord.w1_amount = mash_infuse;
+    $('#w1_amount').val(mash_infuse);
+   } else {
+    var w1_amount = (dataRecord.w1_amount / (dataRecord.w1_amount + dataRecord.w2_amount)) * mash_infuse;
+    var w2_amount = (dataRecord.w2_amount / (dataRecord.w1_amount + dataRecord.w2_amount)) * mash_infuse;
+    dataRecord.w1_amount = Round(w1_amount, 3);
+    dataRecord.w2_amount = Round(w2_amount, 3);
+    $('#w1_amount').val(dataRecord.w1_amount);
+    $('#w2_amount').val(dataRecord.w2_amount);
+   }
+   $('#wg_amount').val(mash_infuse);
+   console.log('new infuse amount: ' + mash_infuse);
+   calcWater();
   }
-  if (dataRecord.w2_amount == 0) {
-   dataRecord.w1_amount = mash_infuse;
-   $('#w1_amount').val(mash_infuse);
-  } else {
-   dataRecord.w1_amount = (dataRecord.w1_amount / (dataRecord.w1_amount + dataRecord.w2_amount)) * mash_infuse;
-   dataRecord.w2_amount = (dataRecord.w2_amount / (dataRecord.w1_amount + dataRecord.w2_amount)) * mash_infuse;
-   $('#w1_amount').val(dataRecord.w1_amount);
-   $('#w2_amount').val(dataRecord.w2_amount);
-  }
-  $('#wg_amount').val(mash_infuse);
+ });
+ $('#wstep_infuse_temp').jqxNumberInput(Spin1dec);
+ $('#wstep_infuse_temp').on('change', function(event) {
+  var prevdata = $('#mashGrid').jqxGrid('getrowdata', mashRow-1);
+  var rowdata = $('#mashGrid').jqxGrid('getrowdata', mashRow);
+  rowdata.step_infuse_temp = parseFloat(event.args.value);
+  var vol = infusionVol(prevdata.step_volume, mashkg, rowdata.step_infuse_temp, rowdata.step_temp, prevdata.end_temp);
+  console.log('new vol: ' + vol);
+  rowdata.step_infuse_amount = vol;
+  $('#wstep_infuse_amount').val(vol);
  });
 
  // Tab 7, Water

mercurial