feat(species): auto-classify variety species from its label
Infer the catalog species a free-text variety label names ("Maiz de la
abuela" -> Zea mays) and prefill the category from the species family.
- Pure, testable matcher (domain/species_autoclassify.dart): whole-word,
accent/case-insensitive, Unicode-aware (any script), longest-name-wins,
ambiguous names left unclassified, light plural fold.
- Quick-add and draft naming auto-link the species when the field is empty
(non-destructive; an explicit category is kept).
- Edit sheet offers a one-tap suggestion from the typed name.
Tests: matcher unit, repository integration, SpeciesRepository.classifyLabel,
and an edit-sheet widget test.
This commit is contained in:
parent
0e6ef13d00
commit
b840b83c42
15 changed files with 510 additions and 3 deletions
|
|
@ -2,6 +2,7 @@ import 'package:commons_core/commons_core.dart';
|
|||
import 'package:drift/drift.dart';
|
||||
|
||||
import '../db/database.dart';
|
||||
import '../domain/species_autoclassify.dart';
|
||||
|
||||
/// One entry from the bundled catalog, before it is stored.
|
||||
class SpeciesSeed {
|
||||
|
|
@ -249,6 +250,44 @@ class SpeciesRepository {
|
|||
.replaceAll('%', r'\%')
|
||||
.replaceAll('_', r'\_');
|
||||
|
||||
/// Auto-classification for the UI: infers the single species a free-text
|
||||
/// variety [label] names (see [matchSpeciesInLabel]) and returns it as a
|
||||
/// [SpeciesMatch] with a best common name for [languageCode]. Returns null
|
||||
/// when the label names no known species or the match is ambiguous — so the
|
||||
/// caller can offer it as a one-tap suggestion, never a silent guess.
|
||||
Future<SpeciesMatch?> classifyLabel(
|
||||
String label, {
|
||||
String languageCode = 'en',
|
||||
}) async {
|
||||
final species = await (_db.select(
|
||||
_db.species,
|
||||
)..where((s) => s.isDeleted.equals(false))).get();
|
||||
final commons = await (_db.select(
|
||||
_db.speciesCommonNames,
|
||||
)..where((n) => n.isDeleted.equals(false))).get();
|
||||
|
||||
final names = <SpeciesNameEntry>[
|
||||
for (final s in species)
|
||||
SpeciesNameEntry(speciesId: s.id, name: s.scientificName),
|
||||
for (final c in commons)
|
||||
SpeciesNameEntry(speciesId: c.speciesId, name: c.name),
|
||||
];
|
||||
final id = matchSpeciesInLabel(label, names);
|
||||
if (id == null) return null;
|
||||
|
||||
final match = species.firstWhere((s) => s.id == id);
|
||||
final matchNames = [
|
||||
for (final c in commons)
|
||||
if (c.speciesId == id) (name: c.name, language: c.language),
|
||||
];
|
||||
return SpeciesMatch(
|
||||
id: match.id,
|
||||
scientificName: match.scientificName,
|
||||
family: match.family,
|
||||
commonName: _bestName(matchNames, languageCode),
|
||||
);
|
||||
}
|
||||
|
||||
String? _bestName(
|
||||
List<({String name, String? language})> names,
|
||||
String languageCode,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue