/// Seed-saving know-how per crop: how a plant reproduces and what it takes to /// keep a variety true when saving its seed. Bundled reference data (curated /// from public seed-saving literature), keyed by botanical family with /// per-species overrides. Pure Dart (only `dart:convert`) — no Flutter binding /// — so it is trivially testable. library; import 'dart:convert'; /// How long the plant takes to make seed. enum LifeCycle { annual, biennial, perennial } /// Whether a plant fertilises itself or needs another plant. enum Pollination { self, cross, mixed } /// What carries the pollen when it does cross. enum PollinationVector { self, insect, wind } /// How the seed is cleaned once ripe. enum SeedProcessing { dry, wet } /// Roughly how hard it is to save true seed at home. enum SavingDifficulty { easy, medium, hard } /// The saving guidance for one crop. Every field is optional — a starter entry /// may only know some of them. Distances are in metres; plant counts are how /// many plants to save seed from to keep enough genetic diversity. class SeedSavingGuide { const SeedSavingGuide({ this.lifeCycle, this.pollination, this.vector, this.isolationMinM, this.isolationMaxM, this.minPlants, this.recommendedPlants, this.processing, this.difficulty, this.note = const {}, }); final LifeCycle? lifeCycle; final Pollination? pollination; final PollinationVector? vector; final int? isolationMinM; final int? isolationMaxM; final int? minPlants; final int? recommendedPlants; final SeedProcessing? processing; final SavingDifficulty? difficulty; /// Locale-keyed short tip (`{"es": "…", "en": "…"}`), extensible to any /// language — never assume es/en only. final Map note; /// True when there's at least one fact worth showing. bool get hasAny => lifeCycle != null || pollination != null || vector != null || isolationMinM != null || minPlants != null || processing != null || difficulty != null || note.isNotEmpty; /// This guide with [other]'s non-null fields laid over it (species over /// family). Notes merge, with [other]'s languages winning. SeedSavingGuide mergedWith(SeedSavingGuide other) => SeedSavingGuide( lifeCycle: other.lifeCycle ?? lifeCycle, pollination: other.pollination ?? pollination, vector: other.vector ?? vector, isolationMinM: other.isolationMinM ?? isolationMinM, isolationMaxM: other.isolationMaxM ?? isolationMaxM, minPlants: other.minPlants ?? minPlants, recommendedPlants: other.recommendedPlants ?? recommendedPlants, processing: other.processing ?? processing, difficulty: other.difficulty ?? difficulty, note: {...note, ...other.note}, ); /// The tip in [lang], falling back to English then any language. String? noteFor(String lang) { if (note.isEmpty) return null; return note[lang] ?? note['en'] ?? note.values.first; } } /// The whole bundled dataset: family defaults plus per-taxon overrides (keyed by /// scientific name OR bare genus). Lookups are case-insensitive. class SeedSavingData { SeedSavingData({ required Map families, required Map taxa, }) : _families = { for (final e in families.entries) e.key.toLowerCase().trim(): e.value, }, _taxa = { for (final e in taxa.entries) e.key.toLowerCase().trim(): e.value, }; final Map _families; final Map _taxa; /// The best guide for a crop: the family default, overlaid by a per-species /// (or per-genus) entry when one exists. Returns null when nothing matches. SeedSavingGuide? guideFor({String? scientificName, String? family}) { SeedSavingGuide? base = family == null ? null : _families[family.toLowerCase().trim()]; if (scientificName != null) { final sci = scientificName.toLowerCase().trim(); final genus = sci.split(RegExp(r'\s+')).first; final species = _taxa[sci] ?? (genus.isEmpty ? null : _taxa[genus]); if (species != null) base = base == null ? species : base.mergedWith(species); } return base; } } T? _enumByName(List values, Object? name) { if (name is! String) return null; for (final v in values) { if (v.name == name) return v; } return null; } /// Parses one guide from a JSON map (already decoded). SeedSavingGuide parseGuide(Map m) => SeedSavingGuide( lifeCycle: _enumByName(LifeCycle.values, m['lifeCycle']), pollination: _enumByName(Pollination.values, m['pollination']), vector: _enumByName(PollinationVector.values, m['vector']), isolationMinM: (m['isolationMinM'] as num?)?.toInt(), isolationMaxM: (m['isolationMaxM'] as num?)?.toInt(), minPlants: (m['minPlants'] as num?)?.toInt(), recommendedPlants: (m['recommendedPlants'] as num?)?.toInt(), processing: _enumByName(SeedProcessing.values, m['processing']), difficulty: _enumByName(SavingDifficulty.values, m['difficulty']), note: (m['note'] as Map? ?? const {}) .map((k, v) => MapEntry(k, v as String)), ); /// Parses the bundled seed-saving catalog JSON into [SeedSavingData]. SeedSavingData parseSeedSaving(String jsonString) { final root = jsonDecode(jsonString) as Map; Map section(String key) { final raw = root[key] as Map? ?? const {}; return raw.map( (k, v) => MapEntry(k, parseGuide((v as Map).cast())), ); } return SeedSavingData(families: section('families'), taxa: section('species')); }