浏览代码

Cleaned up editor ui and added Unit management.

Added Unit objects which track measurement types and conversions
between different units.  Each unit type has a primary unit which has
a conversion factor of 1.  All other units are measured relative to
this unit.

Refactored editor panes to be more compartmentalized and generic.
Thomas Flucke 7 年之前
父节点
当前提交
dbb29234bb

+ 85 - 0
src/name/tflucke/ieat2/controllers/UnitController.java

@@ -0,0 +1,85 @@
+package name.tflucke.ieat2.controllers;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.PutMapping;
+import org.springframework.web.bind.annotation.DeleteMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestBody;
+
+import name.tflucke.ieat2.models.Unit;
+
+import org.springframework.beans.factory.annotation.Value;
+
+/**
+ * Provides APIs for managing Unit objects.
+ *
+ * @author Thomas Flucke
+ * @since 2.0.0
+ */
+@RestController("/unit/")
+public class UnitController extends AbstractController<Unit> {
+
+    /**
+     * Creates a new controller
+     */
+    public UnitController() {
+        super(Unit.class);
+    }
+    
+    @Override
+    @GetMapping("/unit/{id}")
+    public Unit get(@PathVariable("id") final String id) {
+        return super.get(id);
+    }
+
+    @GetMapping("/unit")
+    public List<Unit> list() {
+        return super.list();
+    }
+
+    @GetMapping("/unit/primary")
+    public List<Unit> listPrimary() {
+        return list().stream()
+            .filter((Unit unit) -> unit.conversion == 1.0f)
+            .collect(Collectors.toList());
+    }
+
+    @GetMapping("/unit/primary/{type}")
+    public Unit listPrimary(@PathVariable("type") final Unit.Type t) {
+        return list().stream()
+            .filter((Unit unit) -> unit.type == t &&
+                    unit.conversion == 1.0f)
+            .findFirst().get();
+    }
+    
+    @GetMapping("/unit/type/{type}")
+    public List<Unit> list(@PathVariable("type") final Unit.Type t) {
+        return list().stream()
+            .filter((Unit unit) -> unit.type == t)
+            .collect(Collectors.toList());
+    }
+
+    @Override
+    @PutMapping("/unit")
+    public Unit insert(@RequestBody Unit newElement) {
+        return super.insert(newElement);
+    }
+    
+    @Override
+    @PostMapping("/unit/{id}")
+    public Unit update(@PathVariable("id") final String id,
+                       @RequestBody Unit newElement) {
+        return super.update(id, newElement);
+    }
+
+    @Override    
+    @DeleteMapping("/unit/{id}")
+    public Unit delete(@PathVariable("id") final String id) {
+        return super.delete(id);
+    }
+}

+ 3 - 3
src/name/tflucke/ieat2/models/Food.java

@@ -8,14 +8,14 @@ import com.fasterxml.jackson.annotation.JsonProperty;
 public abstract class Food extends DBObject {
     public String name;
     //public Map<String, float> nutrients;
-    @JsonProperty("default_unit")
-    public String defaultUnit;
+    @JsonProperty("unit_type")
+    public Unit.Type unitType;
     public Long calories_p_100;
     @JsonProperty("food_group")
     public String foodGroup;
+    public boolean dry = unitType != Unit.Type.volume;
 
     public String getType() {
         return getClass().getSimpleName();
     }
-    
 }

+ 79 - 0
src/name/tflucke/ieat2/models/Unit.java

@@ -0,0 +1,79 @@
+package name.tflucke.ieat2.models;
+
+import org.mongodb.morphia.annotations.Entity;
+import org.mongodb.morphia.annotations.Transient;
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonValue;
+
+/**
+ * Model representing measuring units and handling conversions between them.
+ *
+ * Each type of unit (mass, volume, count), has a primary unit which is used
+ * as the default unit for that type and from which all others are calculated.
+ * 
+ * @author Thomas Flucke
+ * @since 2.0.0
+ */
+@Entity
+public class Unit extends DBObject {
+    public static enum Type {
+        mass, volume, count;
+
+        // TODO: Figure out how to make spring url params case insensitive
+        // TODO: Morphia serialization/deserialization case insenstive
+        // TODO: Replace these methods with spring configurations
+        @JsonCreator
+        public static Type fromString(String key) {
+            //return key == null? null : Type.valueOf(key.toUpperCase());
+            return key == null? null : Type.valueOf(key.toLowerCase());
+        }
+
+        @JsonValue
+        @Override
+        public String toString() {
+            return name().toLowerCase();
+        }
+    };
+    
+    public String name;
+    public String symbol;
+    public double conversion;
+    public Type type;
+
+    /**
+     * Converts a value measured in this unit to the primary unit for this type.
+     *
+     * @param value Value measured in current units
+     * @return The value measured in the primary unit of this type
+     */
+    public double convertToPrimary(double value) {
+        return value*conversion;
+    }
+
+    /**
+     * Converts a value measured in the primary unit for this type to this unit.
+     *
+     * @param value Value measured in primary units of this type.
+     * @return The value measured in this unit.
+     */
+    public double convertFromPrimary(double value) {
+        return value/conversion;
+    }
+
+    /**
+     * Converts a value measured in the current units another unit.
+     *
+     * @param value Value measured in current units.
+     * @param other Another unit
+     * @return The value measured in the other unit
+     * @throws UnsupportedOperationException other unit is incompatible with this unit.
+     */
+    public double convertTo(double value, Unit other) {
+        if (!getClass().equals(other.getClass())) {
+            String msg = String.format("Cannot convert from %f%s to %s.",
+                                       value, symbol, other.symbol);
+            throw new UnsupportedOperationException(msg);
+        }
+        return value*conversion/other.conversion;
+    }
+}

+ 13 - 4
web/js/basicFoodEditor.js

@@ -4,7 +4,7 @@
 (function() {
     (function() {
         try {return angular.module('ieat.ui.editors')}
-        catch {return angular.module('ieat.ui.editors', ['ndbDatabase', 'ngResource', 'Food'])}}
+        catch {return angular.module('ieat.ui.editors', ['ndbDatabase','Units'])}}
     )().component('basicFoodEditor', {
         templateUrl: 'static/templates/basicFoodEditor.html',
         bindings: {
@@ -12,14 +12,23 @@
             food: '<'
         },
         controller: [
-            '$scope', 'NDBList', 'BasicFood',
-            function($scope, NDBList, BasicFood) {
+            '$scope', 'NDBList', 'Unit', function($scope, NDBList, Unit) {
                 var self = this;
+                $scope.units_symbol = {};
+                Unit.primary(function(units) {
+                    units.forEach(function(elm) {
+                        $scope.units_symbol[elm.type] = elm.symbol;
+                    });
+                });
                 this.$onInit = function() {
-                    $scope.catagories = NDBList.get({
+                    $scope.categories = NDBList.get({
                         key: self.ndbKey,
                         type: "g"
                     });
+                    if (!self.food.unit_type) {
+                        self.food.unit_type = "Mass";
+                        self.food.dry = true;
+                    }
                 };
             }]
     });

+ 1 - 1
web/js/paginatedTable.js

@@ -22,7 +22,7 @@
         controller: ['$scope', '$timeout', function($scope, $timeout) {
             var self = this;
             this.$onInit = function() {
-                $scope.pageOffset = 0;
+                $scope.pageOffset = 1;
                 $scope.pageSize = "10";
             };
         }]

+ 50 - 26
web/js/templates/basicFoodEditor.html

@@ -4,51 +4,75 @@
     <table class="table">
       <tr>
         <td colspan="2">
-          <input type="text" style="text-align: center;" class="form-control" data-ng-model="$ctrl.food.name">
+          <input type="text"
+                 style="text-align: center;"
+                 class="form-control"
+                 data-ng-model="$ctrl.food.name">
         </td>
       </tr>
       <tr>
         <td>
           <label for="ndbno">ndbno:</label>
-          <input id="ndbno" class="form-control" type="number" data-ng-model="$ctrl.food.ndbno" readonly="readonly" />
+          <input id="ndbno"
+                 class="form-control"
+                 type="number"
+                 data-ng-model="$ctrl.food.ndbno"
+                 readonly="readonly" />
         </td>
         <td>
-          <label for="calories">Calories/100:</label>
-          <input id="calories" class="form-control" type="number" data-ng-model="$ctrl.food.calories_p_100" />
+          <label for="calories">
+            Calories/100{{units_symbol[$ctrl.food.unit_type]}}:
+          </label>
+          <input id="calories"
+                 class="form-control"
+                 type="number"
+                 data-ng-model="$ctrl.food.calories_p_100" />
         </td>
       </tr>
       <tr>
         <td>
           <label for="unit">Unit:</label>
-          <select id="unit" class="form-control" data-ng-model="$ctrl.food.default_unit">
-            <!-- TODO: Load these from server -->
-            <option value="g">g (Grams)</option>
-            <option value="ml">ml (Milliliters)</option>
+          <select id="unit"
+                  class="form-control"
+                  data-ng-model="$ctrl.food.unit_type"
+                  data-ng-change="$ctrl.food.dry = $ctrl.food.unit_type!='Volume'">
+            <option>Mass</option>
+            <option>Volume</option>
+            <option>Count</option>
           </select>
         </td>
         <td>
           <label for="group">Food Group:</label>
-          <select id="group" class="form-control" data-ng-model="$ctrl.food.food_group">
-            <option data-ng-repeat="catagory in catagories">{{catagory.name}}</option>
+          <select id="group"
+                  class="form-control"
+                  data-ng-model="$ctrl.food.food_group"
+                  data-ng-options="c.name.trim() as c.name.trim()
+                                   for c in categories">
           </select>
         </td>
       </tr>
-      <!--
-      <tr>
-        <td colspan="2" style="text-align: right;">
-          <button type="button"
-                  class="btn btn-success"
-                  data-ng-click="submit($ctrl.food);">Submit</button>
-          <button type="button" class="btn" data-ng-click="dismiss();">Cancel</button>
-          <button type="button"
-                  class="btn btn-danger"
-                  data-ng-show="$ctrl.food.id"
-                  data-ng-click="delete($ctrl.food);">
-            Delete
-          </button>
-        </td>
-      </tr>
-      -->
     </table>
+     <div class="panel-group" style="margin: 1em;">
+       <div class="panel panel-default">
+         <div class="panel-heading">
+           <div class="panel-title">
+             <a data-toggle="collapse" href="#detailsDiv">Details</a>
+           </div>
+         </div>
+         <div id="detailsDiv" class="panel-collapse collapse">
+           <div class="panel-body">
+             <div class="form-group">
+               <div class="checkbox">
+                 <label for="dry">
+                   <input id="dry" type="checkbox"
+                          data-ng-model="$ctrl.food.dry" />
+                   Dry
+                 </label>
+               </div>
+             </div>
+           </div>
+         </div>
+       </div>
+     </div>
   </div>
 </div>

+ 1 - 1
web/js/templates/paginatedTable.html

@@ -10,7 +10,7 @@
         </button>
       </th>
     </tr>
-    <tr data-ng-repeat="item in $ctrl.tableData | limitTo:pageSize:pageSize*(pageOffset-1)">
+    <tr data-ng-repeat="item in $ctrl.tableData | limitTo : pageSize : pageSize*(pageOffset-1)">
       <td data-ng-repeat="col in $ctrl.structure">
         <span data-ng-hide="col.onClick">{{ ::(item[col.col]? item[col.col]:col.defaultValue) }}</span>
         <button data-ng-show="col.onClick" data-ng-click="col.onClick(item)">

+ 39 - 0
web/js/templates/unitEditor.html

@@ -0,0 +1,39 @@
+<!-- unitEditor -->
+<div>
+  <div class="form-group">
+    <table class="table">
+      <tr>
+        <td colspan="2">
+          <input type="text"
+                 style="text-align: center;"
+                 class="form-control"
+                 data-ng-model="$ctrl.unit.name">
+        </td>
+      </tr>
+      <tr>
+        <td>
+          <label for="symbol">Symbol:</label>
+          <input id="symbol"
+                 class="form-control"
+                 type="text"
+                 data-ng-model="$ctrl.unit.symbol" />
+        </td>
+        <td>
+          <label for="conversion">Conversion:</label>
+          <input id="conversion"
+                 class="form-control"
+                 type="number"
+                 data-ng-model="$ctrl.unit.conversion" />
+        </td>
+      </tr>
+      <tr>
+        <td colspan="2">
+          <label for="type">Type:</label>
+          <select id="type" class="form-control" data-ng-model="$ctrl.unit.type">
+            <option data-ng-repeat="type in unitTypes">{{type}}</option>
+          </select>
+        </td>
+      </tr>
+    </table>
+  </div>
+</div>

+ 17 - 0
web/js/unitEditor.js

@@ -0,0 +1,17 @@
+/**
+ * A pane that displays/edits unit objects
+ */
+(function() {
+    (function() {
+        try {return angular.module('ieat.ui.editors')}
+        catch {return angular.module('ieat.ui.editors', ['ndbDatabase', 'Units'])}}
+    )().component('unitEditor', {
+        templateUrl: 'static/templates/unitEditor.html',
+        bindings: {
+            unit: '<'
+        },
+        controller: ['$scope', function($scope) {
+            $scope.unitTypes = ["Mass", "Volume", "Count"];
+        }]
+    });
+})();

+ 46 - 0
web/js/units.js

@@ -0,0 +1,46 @@
+/**
+ * Provides interface for unit apis.
+ */
+(function() {
+    var restResource = function($resource) {
+        return function() {
+            arguments[2]["create"] = {
+                method: "PUT"
+            };
+            arguments[2]["update"] = {
+                method: "POST"
+            };
+            var res = $resource.apply(null, arguments);
+            res.prototype.$save = function() {
+                return this[this.id? "$update":"$create"].apply(this, arguments);
+            };
+            return res;
+        };
+    };
+    angular.module('Units', ['ngResource'])
+    /*
+     * Collection of unit apis.
+     *  
+     * Methods:
+     * get: Gets information for a unit by id
+     * query: Get a list of all units
+     * delete: Deletes a unit with an id
+     * create: Creates a unit
+     * update: Updates a unit
+     * save: Creates a new unit or updates an existing unit
+     */
+        .factory('Unit', ['$resource', function($resource) {
+            return restResource($resource)("unit/:id", {id: "@id"}, {
+                "query": {
+                    method: 'GET',
+                    isArray: true,
+                    url: "unit/type/:type"
+                },
+                "primary": {
+                    method: 'GET',
+                    isArray: true,
+                    url: "unit/primary/:type"
+                }
+            });
+        }]);
+})();

+ 20 - 11
web/views/addFood.jsp

@@ -4,14 +4,18 @@
   <jsp:attribute name="title">Add Food</jsp:attribute>
   <jsp:attribute name="head">
     <script type="text/javascript" src="static/ndbDatabase.js"></script>
+    <script type="text/javascript" src="static/units.js"></script>
     <script type="text/javascript" src="static/basicFoodEditor.js"></script>
     <script type="text/javascript" src="static/searchBar.js"></script>
     <script type="text/javascript" src="static/paginatedTable.js"></script>
+    <script type="text/javascript" src="static/Food.js"></script>
     <script type="text/javascript">
-      var app = angular.module('ingredients', ['ndbDatabase', 'ui.bootstrap', 'ieat.ui', 'ieat.ui.editors']);
+      var app = angular.module('ingredients', [
+          'ndbDatabase', 'ui.bootstrap', 'Food', 'ieat.ui', 'ieat.ui.editors']);
       // TODO: Disable debug info in prod version
-      app.controller('SearchController', ['$scope', '$uibModal', 'NDBSearch', 'NDBFood', 'BasicFood',
-                                          function($scope, $uibModal, NDBSearch, NDBFood, BasicFood) {
+      app.controller('SearchController', [
+          '$scope', '$uibModal', 'NDBSearch', 'NDBFood', 'BasicFood',
+          function($scope, $uibModal, NDBSearch, NDBFood, BasicFood) {
               $scope.searchResults = [];
               $scope.searchFn = function(searchTerm) {
                   NDBSearch.get({
@@ -25,11 +29,13 @@
                   });
               };
 
-              var modalCtrl = ['$scope', '$uibModalInstance', 'food', function($scope, $uibModalInstance, food) {
-                  $scope.food = food;
-                  $scope.submit = $uibModalInstance.close;
-                  $scope.dismiss = $uibModalInstance.dismiss;
-              }];
+              var modalCtrl = [
+                  '$scope', '$uibModalInstance', 'food',
+                  function($scope, $uibModalInstance, food) {
+                      $scope.food = food;
+                      $scope.submit = $uibModalInstance.close;
+                      $scope.dismiss = $uibModalInstance.dismiss;
+                  }];
               
               var promptWindow = function(item) {
                   var foodRequest = NDBFood.get({
@@ -100,12 +106,15 @@
       </paginated-table>
 
       <script type="text/ng-template" id="modal">
-        <basic-food-editor data-ndb-key="${ndbKey}" data-food="food"></basic-food-editor>
-        <div style="width: 100%; text-align: right; padding-right: 1em; padding-bottom: 1em;">
+        <basic-food-editor data-ndb-key="${ndbKey}" data-food="food">
+        </basic-food-editor>
+        <div style="width: 100%; text-align: right; padding: 0 1em 1em 0;">
           <button type="button"
                   class="btn btn-success"
                   data-ng-click="submit(food);">Submit</button>
-          <button type="button" class="btn" data-ng-click="dismiss();">Cancel</button>
+          <button type="button" class="btn" data-ng-click="dismiss();">
+            Cancel
+          </button>
         </div>
       </script>
     </div>

+ 3 - 3
web/views/browseFood.jsp

@@ -4,19 +4,19 @@
   <jsp:attribute name="title">Manage Food</jsp:attribute>
   <jsp:attribute name="head">
     <script type="text/javascript" src="static/ndbDatabase.js"></script>
+    <script type="text/javascript" src="static/units.js"></script>
     <script type="text/javascript" src="static/basicFoodEditor.js"></script>
     <script type="text/javascript" src="static/searchBar.js"></script>
     <script type="text/javascript" src="static/paginatedTable.js"></script>
     <script type="text/javascript">
-      var app = angular.module('ingredients', ['ui.bootstrap', 'ieat.ui', 'ieat.ui.editors']);
+      var app = angular.module('ingredients', ['ui.bootstrap', 'Food', 'ieat.ui', 'ieat.ui.editors']);
       app.controller('SearchController', ['$scope', '$timeout', '$uibModal', 'Food', 'BasicFood', 'Recipe',
-          function($scope, $timeout, $uibModal, Food, BasicFood, Recipe) {              
+          function($scope, $timeout, $uibModal, Food, BasicFood, Recipe) {
               $scope.searchFn = function(searchTerm) {
                   if (searchTerm) {
                       this.searchTerm = searchTerm;
                   }
                   Food.query({
-                      "key": "${ndbKey}",
                       "query": this.searchTerm
                   }, function(data) {
                       $scope.searchResults = data;

+ 127 - 0
web/views/units.jsp

@@ -0,0 +1,127 @@
+<%@page contentType="text/html" pageEncoding="UTF-8"%>
+<%@taglib prefix="t" tagdir="/WEB-INF/tags" %>
+<t:template>
+  <jsp:attribute name="title">Unit Manager</jsp:attribute>
+  <jsp:attribute name="head">
+    <script type="text/javascript" src="static/units.js"></script>
+    <script type="text/javascript" src="static/ndbDatabase.js"></script>
+    <script type="text/javascript" src="static/paginatedTable.js"></script>
+    <script type="text/javascript" src="static/unitEditor.js"></script>
+    <script type="text/javascript">
+      var app = angular.module('unitManager', [
+          'Units', 'ui.bootstrap', 'ieat.ui', 'ieat.ui.editors']);
+      // TODO: Disable debug info in prod version
+      app.controller('UnitController', [
+          '$scope', '$uibModal', 'Unit',
+          function($scope, $uibModal, Unit) {
+              var loadUnitList = function(type) {
+                  type.list = Unit.query({"type": type.name.toLowerCase()});
+              }
+              
+              var promptWindow = function(newUnit) {
+                  $uibModal.open({
+                      // TODO: Figure out what these are and how they work
+                      //ariaLabelledBy: 'modal-title',
+                      //ariaDescribedBy: 'modal-body',
+                      templateUrl: "addModal",
+                      controller: ['$scope', '$uibModalInstance',
+                                   function($scope, $uibModalInstance) {
+                                       $scope.addUnit = $uibModalInstance.close;
+                                       $scope.dismiss = $uibModalInstance.dismiss;
+                                       $scope.newUnit = newUnit;
+                                   }],
+                      size: "md"
+                  }).result.then(function(unit) {
+                      unit.$save(function(resp) {
+                          loadUnitList(
+                              $scope.types.find(function(x) {
+                                  return x.name == resp.type
+                              })
+                          );
+                      }, function(err) {
+                          // TODO: Error handling
+                          console.error(err);
+                      });
+                  }, function(reason) {
+                      console.debug(reason);
+                  });
+              };
+              
+              $scope.table = [{
+                  name: "Name",
+                  col: "name"
+              }, {
+                  name: "Abv.",
+                  col: "symbol",
+                  size: 1
+              }, {
+                  name: "Conversion",
+                  col: "conversion"
+              }, {
+                  defaultValue: "Edit",
+                  onClick: promptWindow,
+                  size: 1
+              }, {
+                  name: "Add",
+                  onHeaderClick: function() {
+                      promptWindow(new Unit({
+                          type: $scope.types[$scope.activeType].name
+                      }));
+                  },
+                  defaultValue: "Delete",
+                  onClick: function(unit) {
+                      unit.$remove(function(resp) {
+                          loadUnitList(
+                              $scope.types.find(function(x) {
+                                  return x.name == resp.type
+                              })
+                          );
+                      }, function(err) {
+                          // TODO: Error handling
+                          console.error(err);
+                      });
+                  },
+                  size: 1
+              }];
+
+              $scope.types = [
+                  {name: "Mass"},
+                  {name: "Volume"},
+                  {name: "Count"}
+              ];
+              angular.forEach($scope.types, function(type) {
+                  loadUnitList(type);
+              });
+          }]);
+    </script>
+  </jsp:attribute>
+  <jsp:body>
+    <div class="section container"
+         data-ng-app="unitManager"
+         data-ng-controller="UnitController">
+      <h2>Unit Manager</h2>
+      <uib-tabset data-active="activeType">
+        <uib-tab data-ng-repeat="type in types"
+                 data-index="$index"
+                 data-heading="{{type.name}}">
+          <paginated-table data-table-data="type.list"
+                           data-structure="table">
+          </paginated-table>
+        </uib-tab>
+      </uib-tabset>
+
+      <script type="text/ng-template" id="addModal">
+        <unit-editor data-unit="newUnit">
+        </unit-editor>
+        <div style="width: 100%; text-align: right; padding: 0 1em 1em 0;">
+          <button type="button"
+                  class="btn btn-success"
+                  data-ng-click="addUnit(newUnit);">Submit</button>
+          <button type="button" class="btn" data-ng-click="dismiss();">
+            Cancel
+          </button>
+        </div>
+      </script>
+    </div>
+  </jsp:body>
+</t:template>