Skip to content

Commit

Permalink
fix: #1044 - now using a unique compatibility algorithm (#1046)
Browse files Browse the repository at this point in the history
Impacted files:
* `app_en.arb`: added a short "compatible" label; down to 3 longer compatibility labels
* `app_fr.arb`: added a short "compatible" label; down to 3 longer compatibility labels
* `product_compatibility_helper.dart`: removed all computations; down to 3 compatibility possibilities
* `smooth_product_card_found.dart`: now using `MatchedProduct` and its computations
* `summary_card.dart`: now using `MatchedProduct` and its computations
  • Loading branch information
monsieurtanuki authored Feb 2, 2022
1 parent 53849db commit 9b9f655
Show file tree
Hide file tree
Showing 5 changed files with 64 additions and 164 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:openfoodfacts/model/Attribute.dart';
import 'package:openfoodfacts/model/Product.dart';
import 'package:openfoodfacts/personalized_search/matched_product.dart';
import 'package:provider/provider.dart';
import 'package:smooth_app/cards/data_cards/svg_icon_chip.dart';
import 'package:smooth_app/data_models/product_preferences.dart';
Expand Down Expand Up @@ -46,8 +47,12 @@ class SmoothProductCardFound extends StatelessWidget {
for (final Attribute attribute in attributes) {
scores.add(SvgIconChip(attribute.iconUrl!, height: iconSize));
}
final ProductCompatibilityResult compatibility =
getProductCompatibility(context.watch<ProductPreferences>(), product);
final MatchedProduct matchedProduct = MatchedProduct(
product,
context.watch<ProductPreferences>(),
);
final ProductCompatibilityHelper helper =
ProductCompatibilityHelper(matchedProduct);
return GestureDetector(
onTap: onTap ??
() async {
Expand Down Expand Up @@ -105,15 +110,13 @@ class SmoothProductCardFound extends StatelessWidget {
Icon(
Icons.circle,
size: 15,
color:
getProductCompatibilityHeaderBackgroundColor(
compatibility.productCompatibility),
color: helper.getBackgroundColor(),
),
const Padding(
padding:
EdgeInsets.only(left: VERY_SMALL_SPACE)),
Text(
getSubtitle(compatibility, appLocalizations),
helper.getSubtitle(appLocalizations),
style: Theme.of(context).textTheme.bodyText2,
),
],
Expand Down
164 changes: 36 additions & 128 deletions packages/smooth_app/lib/helpers/product_compatibility_helper.dart
Original file line number Diff line number Diff line change
@@ -1,137 +1,45 @@
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:openfoodfacts/model/Attribute.dart';
import 'package:openfoodfacts/model/AttributeGroup.dart';
import 'package:openfoodfacts/model/Product.dart';
import 'package:openfoodfacts/personalized_search/preference_importance.dart';
import 'package:smooth_app/data_models/product_preferences.dart';
import 'package:smooth_app/helpers/attributes_card_helper.dart';

enum ProductCompatibility {
UNKNOWN,
INCOMPATIBLE,
BAD_COMPATIBILITY,
NEUTRAL_COMPATIBILITY,
GOOD_COMPATIBILITY,
}

class ProductCompatibilityResult {
ProductCompatibilityResult(
this.averageAttributeMatch, this.productCompatibility);
final num averageAttributeMatch;
final ProductCompatibility productCompatibility;
}

const int _BAD_COMPATIBILITY_UPPER_THRESHOLD = 33;
const int _NEUTRAL_COMPATIBILITY_UPPER_THRESHOLD = 66;

// Defines the weight of an attribute while computing the average match score
// for the product. The weight depends upon it's importance set in user prefs.
const Map<String, int> attributeImportanceWeight = <String, int>{
PreferenceImportance.ID_MANDATORY: 4,
PreferenceImportance.ID_VERY_IMPORTANT: 1, // same as important from now on
PreferenceImportance.ID_IMPORTANT: 1,
PreferenceImportance.ID_NOT_IMPORTANT: 0,
};

Color getProductCompatibilityHeaderBackgroundColor(
ProductCompatibility compatibility) {
switch (compatibility) {
case ProductCompatibility.UNKNOWN:
return Colors.grey;
case ProductCompatibility.INCOMPATIBLE:
return Colors.red;
case ProductCompatibility.BAD_COMPATIBILITY:
return Colors.orangeAccent;
case ProductCompatibility.NEUTRAL_COMPATIBILITY:
return Colors.amber;
case ProductCompatibility.GOOD_COMPATIBILITY:
return Colors.green;
}
}

String getProductCompatibilityHeaderTextWidget(
final ProductCompatibility compatibility,
final AppLocalizations appLocalizations,
) {
switch (compatibility) {
case ProductCompatibility.UNKNOWN:
return appLocalizations.product_compatibility_unknown;
case ProductCompatibility.INCOMPATIBLE:
return appLocalizations.product_compatibility_incompatible;
case ProductCompatibility.BAD_COMPATIBILITY:
return appLocalizations.product_compatibility_bad;
case ProductCompatibility.NEUTRAL_COMPATIBILITY:
return appLocalizations.product_compatibility_neutral;
case ProductCompatibility.GOOD_COMPATIBILITY:
return appLocalizations.product_compatibility_good;
import 'package:openfoodfacts/personalized_search/matched_product.dart';

class ProductCompatibilityHelper {
const ProductCompatibilityHelper(this.matchedProduct);

final MatchedProduct matchedProduct;

Color getBackgroundColor() {
switch (matchedProduct.status) {
case null:
case MatchedProductStatus.UNKNOWN:
return Colors.grey;
case MatchedProductStatus.NO:
return Colors.red;
case MatchedProductStatus.YES:
return Colors.green;
}
}
}

ProductCompatibilityResult getProductCompatibility(
ProductPreferences productPreferences,
Product product,
) {
double averageAttributeMatch = 0.0;
int numAttributesComputed = 0;
if (product.attributeGroups != null) {
for (final AttributeGroup group in product.attributeGroups!) {
if (group.attributes != null) {
for (final Attribute attribute in group.attributes!) {
final String importanceLevel =
productPreferences.getImportanceIdForAttributeId(attribute.id!);
// Check whether any mandatory attribute is incompatible
if (importanceLevel == PreferenceImportance.ID_MANDATORY &&
getAttributeEvaluation(attribute) ==
AttributeEvaluation.VERY_BAD) {
return ProductCompatibilityResult(
0, ProductCompatibility.INCOMPATIBLE);
}
if (!attributeImportanceWeight.containsKey(importanceLevel)) {
// Unknown attribute importance level. (This should ideally never happen).
// TODO(jasmeetsingh): [importanceLevel] should be an enum not a string.
continue;
}
if (attributeImportanceWeight[importanceLevel] == 0.0) {
// Skip attributes that are not important
continue;
}
if (!isMatchAvailable(attribute)) {
continue;
}
averageAttributeMatch +=
attribute.match! * attributeImportanceWeight[importanceLevel]!;
numAttributesComputed++;
}
}
String getHeaderText(final AppLocalizations appLocalizations) {
switch (matchedProduct.status) {
case null:
case MatchedProductStatus.UNKNOWN:
return appLocalizations.product_compatibility_unknown;
case MatchedProductStatus.NO:
return appLocalizations.product_compatibility_incompatible;
case MatchedProductStatus.YES:
return appLocalizations.product_compatibility_good;
}
}
if (numAttributesComputed == 0) {
return ProductCompatibilityResult(0, ProductCompatibility.INCOMPATIBLE);
}
averageAttributeMatch /= numAttributesComputed;
if (averageAttributeMatch < _BAD_COMPATIBILITY_UPPER_THRESHOLD) {
return ProductCompatibilityResult(
averageAttributeMatch, ProductCompatibility.BAD_COMPATIBILITY);
}
if (averageAttributeMatch < _NEUTRAL_COMPATIBILITY_UPPER_THRESHOLD) {
return ProductCompatibilityResult(
averageAttributeMatch, ProductCompatibility.NEUTRAL_COMPATIBILITY);
}
return ProductCompatibilityResult(
averageAttributeMatch, ProductCompatibility.GOOD_COMPATIBILITY);
}

String getSubtitle(
final ProductCompatibilityResult compatibility,
final AppLocalizations appLocalizations,
) {
if (compatibility.productCompatibility == ProductCompatibility.UNKNOWN) {
return appLocalizations.unknown;
}
if (compatibility.productCompatibility == ProductCompatibility.INCOMPATIBLE) {
return appLocalizations.incompatible;
String getSubtitle(final AppLocalizations appLocalizations) {
switch (matchedProduct.status) {
case null:
case MatchedProductStatus.UNKNOWN:
return appLocalizations.unknown;
case MatchedProductStatus.NO:
return appLocalizations.incompatible;
case MatchedProductStatus.YES:
return appLocalizations.compatible;
}
}
return appLocalizations
.pct_match(compatibility.averageAttributeMatch.toStringAsFixed(0));
}
16 changes: 6 additions & 10 deletions packages/smooth_app/lib/l10n/app_en.arb
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,10 @@
"@incompatible": {
"description": "Short label for product list view: the product is incompatible with your preferences"
},
"compatible": "Compatible",
"@compatible": {
"description": "Short label for product list view: the product is compatible with your preferences"
},
"unknown": "Unknown",
"@unknown": {
"description": "Short label for product list view: the compatibility of that product with your preferences is unknown"
Expand Down Expand Up @@ -505,19 +509,11 @@
"@product_compatibility_unknown": {
"description": "Product compatibility summary title"
},
"product_compatibility_incompatible": "Very poor Match",
"product_compatibility_incompatible": "Does not match",
"@product_compatibility_incompatible": {
"description": "Product compatibility summary title"
},
"product_compatibility_bad": "Poor Match",
"@product_compatibility_bad": {
"description": "Product compatibility summary title"
},
"product_compatibility_neutral": "Neutral Match",
"@product_compatibility_neutral": {
"description": "Product compatibility summary title"
},
"product_compatibility_good": "Great Match",
"product_compatibility_good": "Good Match",
"@product_compatibility_good": {
"description": "Product compatibility summary title"
},
Expand Down
16 changes: 6 additions & 10 deletions packages/smooth_app/lib/l10n/app_fr.arb
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,10 @@
"@incompatible": {
"description": "Short label for product list view: the product is incompatible with your preferences"
},
"compatible": "Compatible",
"@compatible": {
"description": "Short label for product list view: the product is compatible with your preferences"
},
"unknown": "Inconnu",
"@unknown": {
"description": "Short label for product list view: the compatibility of that product with your preferences is unknown"
Expand Down Expand Up @@ -505,19 +509,11 @@
"@product_compatibility_unknown": {
"description": "Product compatibility summary title"
},
"product_compatibility_incompatible": "Très mauvaise concordance",
"product_compatibility_incompatible": "Incompatible",
"@product_compatibility_incompatible": {
"description": "Product compatibility summary title"
},
"product_compatibility_bad": "Mauvaise concordance",
"@product_compatibility_bad": {
"description": "Product compatibility summary title"
},
"product_compatibility_neutral": "Concordance neutre",
"@product_compatibility_neutral": {
"description": "Product compatibility summary title"
},
"product_compatibility_good": "Excellente concordance",
"product_compatibility_good": "Compatible",
"@product_compatibility_good": {
"description": "Product compatibility summary title"
},
Expand Down
17 changes: 7 additions & 10 deletions packages/smooth_app/lib/pages/product/summary_card.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:openfoodfacts/model/Attribute.dart';
import 'package:openfoodfacts/model/AttributeGroup.dart';
import 'package:openfoodfacts/openfoodfacts.dart';
import 'package:openfoodfacts/personalized_search/matched_product.dart';
import 'package:openfoodfacts/personalized_search/preference_importance.dart';
import 'package:provider/provider.dart';
import 'package:smooth_app/cards/data_cards/score_card.dart';
Expand Down Expand Up @@ -260,14 +261,13 @@ class _SummaryCardState extends State<SummaryCard> {
}

Widget _buildProductCompatibilityHeader(BuildContext context) {
final ProductCompatibility compatibility =
getProductCompatibility(widget._productPreferences, widget._product)
.productCompatibility;
// NOTE: This is temporary and will be updated once the feature is supported
// by the server.
final MatchedProduct matchedProduct =
MatchedProduct(widget._product, widget._productPreferences);
final ProductCompatibilityHelper helper =
ProductCompatibilityHelper(matchedProduct);
return Container(
decoration: BoxDecoration(
color: getProductCompatibilityHeaderBackgroundColor(compatibility),
color: helper.getBackgroundColor(),
// Ensure that the header has the same circular radius as the SmoothCard.
borderRadius: const BorderRadius.only(
topLeft: SmoothCard.CIRCULAR_RADIUS,
Expand All @@ -278,10 +278,7 @@ class _SummaryCardState extends State<SummaryCard> {
padding: const EdgeInsets.symmetric(vertical: SMALL_SPACE),
child: Center(
child: Text(
getProductCompatibilityHeaderTextWidget(
compatibility,
AppLocalizations.of(context)!,
),
helper.getHeaderText(AppLocalizations.of(context)!),
style:
Theme.of(context).textTheme.subtitle1!.apply(color: Colors.white),
),
Expand Down

0 comments on commit 9b9f655

Please sign in to comment.