Skip to content

Commit

Permalink
feat: 5568 - optimized search for price locations (openfoodfacts#5587)
Browse files Browse the repository at this point in the history
Impacted files:
* `app_en.arb`: added a label for "location broader search"
* `app_fr.arb`: added a label for "location broader search"
* `dao_osm_location.dart`: added columns osmKey and osmValue
* `local_database.dart`: upgraded the version in order to add columns to location table
* `location_list_supplier.dart`: added optional parameters for optimized search; added fields osmKey and osmValue
* `location_query_model.dart`: now we use 2 suppliers - optimized and broader
* `location_query_page.dart`: now display optimized results first, then broader results after a button click
* `osm_location.dart`: added fields osmKey and osmValue
  • Loading branch information
monsieurtanuki authored Sep 25, 2024
1 parent 93fc899 commit 8e5ea75
Show file tree
Hide file tree
Showing 8 changed files with 109 additions and 26 deletions.
21 changes: 17 additions & 4 deletions packages/smooth_app/lib/data_models/location_list_supplier.dart
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,22 @@ import 'package:smooth_app/query/product_query.dart';
class LocationListSupplier {
LocationListSupplier(
this.query,
this.optimizedSearch,
);

/// Query text.
final String query;

/// True if we want to focus on shops.
final bool optimizedSearch;

/// Locations as result.
final List<OsmLocation> locations = <OsmLocation>[];

/// Returns additional query parameters.
String _getAdditionalParameters() =>
optimizedSearch ? '&osm_tag=shop&osm_tag=amenity' : '';

/// Returns null if OK, or the message error
Future<String?> asyncLoad() async {
// don't ask me why, but it looks like we need to explicitly set a language,
Expand All @@ -35,10 +45,9 @@ class LocationListSupplier {
scheme: 'https',
host: 'photon.komoot.io',
path: 'api',
queryParameters: <String, String>{
'q': query,
'lang': getQueryLanguage().offTag,
},
query: 'q=${Uri.encodeComponent(query)}'
'&lang=${getQueryLanguage().offTag}'
'${_getAdditionalParameters()}',
),
);
if (response.statusCode != 200) {
Expand Down Expand Up @@ -70,6 +79,8 @@ class LocationListSupplier {
final String? countryCode = properties['countrycode'];
final String? country = properties['country'];
final String? postCode = properties['postcode'];
final String? osmKey = properties['osm_key'];
final String? osmValue = properties['osm_value'];
final OsmLocation osmLocation = OsmLocation(
osmId: osmId,
osmType: osmType,
Expand All @@ -81,6 +92,8 @@ class LocationListSupplier {
street: street,
country: country,
countryCode: countryCode,
osmKey: osmKey,
osmValue: osmValue,
);
locations.add(osmLocation);
}
Expand Down
47 changes: 33 additions & 14 deletions packages/smooth_app/lib/data_models/location_query_model.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,13 @@ import 'package:smooth_app/pages/locations/osm_location.dart';
import 'package:smooth_app/pages/product/common/loading_status.dart';

/// Location query model.
///
/// We use 2 location suppliers:
/// * the first one optimized on shops, as it's what we want
/// * an optional one with no restrictions, in case OSM data is a bit clumsy
class LocationQueryModel with ChangeNotifier {
LocationQueryModel(this.query) {
_asyncLoad(notify: true);
_asyncLoad(_supplierOptimized);
}

final String query;
Expand All @@ -15,39 +19,54 @@ class LocationQueryModel with ChangeNotifier {
String? _loadingError;
List<OsmLocation> displayedResults = <OsmLocation>[];

bool _isOptimized = true;
bool get isOptimized => _isOptimized;

bool isEmpty() => displayedResults.isEmpty;

String? get loadingError => _loadingError;
LoadingStatus get loadingStatus => _loadingStatus;

late final LocationListSupplier supplier = LocationListSupplier(query);
/// A location supplier focused on shops.
late final LocationListSupplier _supplierOptimized =
LocationListSupplier(query, true);

/// A location supplier without restrictions.
late final LocationListSupplier _supplierBroader =
LocationListSupplier(query, false);

Future<bool> _asyncLoad({
final bool notify = false,
final bool fromScratch = false,
}) async {
Future<bool> _asyncLoad(final LocationListSupplier supplier) async {
_loadingStatus = LoadingStatus.LOADING;
notifyListeners();
_loadingError = await supplier.asyncLoad();
if (_loadingError != null) {
_loadingStatus = LoadingStatus.ERROR;
} else {
await _process(supplier.locations, fromScratch);
await _process(supplier.locations);
_loadingStatus = LoadingStatus.LOADED;
}
if (notify) {
notifyListeners();
}
notifyListeners();
return _loadingStatus == LoadingStatus.LOADED;
}

final Set<String> _locationKeys = <String>{};

Future<void> _process(
final List<OsmLocation> locations,
final bool fromScratch,
) async {
if (fromScratch) {
displayedResults.clear();
for (final OsmLocation location in locations) {
final String primaryKey = location.primaryKey;
if (_locationKeys.contains(primaryKey)) {
continue;
}
displayedResults.add(location);
_locationKeys.add(primaryKey);
}
displayedResults.addAll(locations);
_loadingStatus = LoadingStatus.LOADED;
}

Future<void> loadMore() async {
_isOptimized = false;
_asyncLoad(_supplierBroader);
}
}
12 changes: 12 additions & 0 deletions packages/smooth_app/lib/database/dao_osm_location.dart
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ class DaoOsmLocation extends AbstractSqlDao {
static const String _columnPostCode = 'post_code';
static const String _columnCountry = 'country';
static const String _columnCountryCode = 'country_code';
static const String _columnOsmKey = 'osm_key';
static const String _columnOsmValue = 'osm_value';
static const String _columnLastAccess = 'last_access';

static const List<String> _columns = <String>[
Expand All @@ -33,6 +35,8 @@ class DaoOsmLocation extends AbstractSqlDao {
_columnPostCode,
_columnCountry,
_columnCountryCode,
_columnOsmKey,
_columnOsmValue,
_columnLastAccess,
];

Expand All @@ -58,6 +62,10 @@ class DaoOsmLocation extends AbstractSqlDao {
',PRIMARY KEY($_columnId,$_columnType) on conflict replace'
')');
}
if (oldVersion < 7) {
await db.execute('alter table $_table add column $_columnOsmKey TEXT');
await db.execute('alter table $_table add column $_columnOsmValue TEXT');
}
}

/// Deletes the [OsmLocation] that matches the key.
Expand Down Expand Up @@ -100,6 +108,8 @@ class DaoOsmLocation extends AbstractSqlDao {
_columnPostCode: osmLocation.postcode,
_columnCountry: osmLocation.country,
_columnCountryCode: osmLocation.countryCode,
_columnOsmKey: osmLocation.osmKey,
_columnOsmValue: osmLocation.osmValue,
_columnLastAccess: LocalDatabase.nowInMillis(),
},
);
Expand All @@ -122,6 +132,8 @@ class DaoOsmLocation extends AbstractSqlDao {
postcode: row[_columnPostCode] as String?,
country: row[_columnCountry] as String?,
countryCode: row[_columnCountryCode] as String?,
osmKey: row[_columnOsmKey] as String?,
osmValue: row[_columnOsmValue] as String?,
);
}
}
2 changes: 1 addition & 1 deletion packages/smooth_app/lib/database/local_database.dart
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ class LocalDatabase extends ChangeNotifier {
final String databasePath = join(databasesRootPath, 'smoothie.db');
final Database database = await openDatabase(
databasePath,
version: 6,
version: 7,
singleInstance: true,
onUpgrade: _onUpgrade,
);
Expand Down
1 change: 1 addition & 0 deletions packages/smooth_app/lib/l10n/app_en.arb
Original file line number Diff line number Diff line change
Expand Up @@ -1911,6 +1911,7 @@
"prices_location_subtitle": "Shop",
"prices_location_find": "Find a shop",
"prices_location_mandatory": "You need to select a shop!",
"prices_location_search_broader": "Couldn't find what you were looking for? Let's try a broader search!",
"prices_proof_subtitle": "Proof",
"prices_proof_find": "Select a proof",
"prices_proof_receipt": "Receipt",
Expand Down
1 change: 1 addition & 0 deletions packages/smooth_app/lib/l10n/app_fr.arb
Original file line number Diff line number Diff line change
Expand Up @@ -1899,6 +1899,7 @@
"prices_location_subtitle": "Magasin",
"prices_location_find": "Chercher un magasin",
"prices_location_mandatory": "Vous devez choisir un magasin !",
"prices_location_search_broader": "Vous n'avez pas trouvé ce que vous cherchiez ? Essayons une recherche plus large !",
"prices_proof_subtitle": "Preuve",
"prices_proof_find": "Choisir une preuve",
"prices_proof_receipt": "Ticket de caisse",
Expand Down
39 changes: 32 additions & 7 deletions packages/smooth_app/lib/pages/locations/location_query_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:matomo_tracker/matomo_tracker.dart';
import 'package:provider/provider.dart';
import 'package:smooth_app/data_models/location_query_model.dart';
import 'package:smooth_app/generic_lib/buttons/smooth_large_button_with_icon.dart';
import 'package:smooth_app/generic_lib/design_constants.dart';
import 'package:smooth_app/generic_lib/widgets/smooth_back_button.dart';
import 'package:smooth_app/generic_lib/widgets/smooth_card.dart';
import 'package:smooth_app/generic_lib/widgets/smooth_error_card.dart';
import 'package:smooth_app/pages/locations/search_location_preloaded_item.dart';
import 'package:smooth_app/pages/product/common/loading_status.dart';
Expand Down Expand Up @@ -67,7 +69,7 @@ class _LocationQueryPageState extends State<LocationQueryPage>
}
break;
case LoadingStatus.LOADED:
if (_model.isEmpty()) {
if (_model.isEmpty() && !_model.isOptimized) {
return SearchEmptyScreen(
name: widget.query,
emptiness: _getEmptyText(
Expand Down Expand Up @@ -114,12 +116,35 @@ class _LocationQueryPageState extends State<LocationQueryPage>
textColor: Theme.of(context).colorScheme.onSurface,
),
child: ListView.builder(
itemBuilder: (BuildContext context, int index) =>
SearchLocationPreloadedItem(
_model.displayedResults[index],
popFirst: true,
).getWidget(context),
itemCount: _model.displayedResults.length,
itemBuilder: (BuildContext context, int index) {
if (index >= _model.displayedResults.length) {
if (_model.isOptimized) {
return SmoothCard(
child: SmoothLargeButtonWithIcon(
text: appLocalizations.prices_location_search_broader,
icon: Icons.search,
onPressed: () => _model.loadMore(),
),
);
}
return const Padding(
padding: EdgeInsets.only(top: SMALL_SPACE),
child: Center(
child: CircularProgressIndicator.adaptive(),
),
);
}
return SearchLocationPreloadedItem(
_model.displayedResults[index],
popFirst: true,
).getWidget(context);
},
itemCount: _model.displayedResults.length +
(_model.isOptimized
? 1
: _model.loadingStatus == LoadingStatus.LOADING
? 1
: 0),
),
),
);
Expand Down
12 changes: 12 additions & 0 deletions packages/smooth_app/lib/pages/locations/osm_location.dart
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ class OsmLocation {
this.postcode,
this.country,
this.countryCode,
this.osmKey,
this.osmValue,
});

final int osmId;
Expand All @@ -26,6 +28,8 @@ class OsmLocation {
final String? postcode;
final String? country;
final String? countryCode;
final String? osmKey;
final String? osmValue;

LatLng getLatLng() => LatLng(latitude, longitude);

Expand Down Expand Up @@ -65,9 +69,17 @@ class OsmLocation {
}
result.write(country);
}
if (osmKey != null && osmValue != null) {
if (result.isNotEmpty) {
result.write(', ');
}
result.write('$osmKey:$osmValue');
}
if (result.isEmpty) {
return null;
}
return result.toString();
}

String get primaryKey => '${osmType.offTag}$osmId';
}

0 comments on commit 8e5ea75

Please sign in to comment.