class StationWord { final int id; final String word; final String emoji; final String pos; final String category; final String desc; const StationWord({ required this.id, required this.word, this.emoji = '', this.pos = '', this.category = '', this.desc = '', }); factory StationWord.fromMap(Map map) { return StationWord( id: (map['id'] as num?)?.toInt() ?? 0, word: map['word'] as String? ?? '', emoji: map['emoji'] as String? ?? '', pos: map['pos'] as String? ?? '', category: map['category'] as String? ?? '', desc: map['desc'] as String? ?? '', ); } } class TraditionStation { final String id; final String name; final String symbol; final String group; final String lore; final String? hebrew; final String? hanzi; final String? arabic; final String? arcanum; final StationWord? primary; final List cluster; final List> parallels; final List dots; const TraditionStation({ required this.id, required this.name, required this.symbol, required this.group, required this.lore, this.hebrew, this.hanzi, this.arabic, this.arcanum, this.primary, this.cluster = const [], this.parallels = const [], this.dots = const [], }); /// Four geomantic rows (1 or 2 dots each), head → feet. Empty if not a figure. List get geomancyRows { if (dots.length == 4 && dots.every((n) => n == 1 || n == 2)) return dots; final lines = symbol .split('\n') .map((s) => s.trim()) .where((s) => s.isNotEmpty) .toList(); if (lines.length != 4) return const []; final rows = []; for (final line in lines) { final n = '·'.allMatches(line).length; if (n != 1 && n != 2) return const []; rows.add(n); } return rows; } String get groupKey { final sep = group.indexOf(' · '); if (sep <= 0) return group; return group.substring(0, sep); } String get groupSub { final sep = group.indexOf(' · '); if (sep < 0) return ''; return group.substring(sep + 3); } factory TraditionStation.fromMap(Map map) { final primaryMap = map['primary']; final clusterRaw = map['cluster']; return TraditionStation( id: map['id'] as String? ?? '', name: map['name'] as String? ?? '', symbol: map['symbol'] as String? ?? '', group: map['group'] as String? ?? '', lore: map['lore'] as String? ?? '', hebrew: map['hebrew'] as String?, hanzi: map['hanzi'] as String?, arabic: map['arabic'] as String?, arcanum: map['arcanum'] as String?, primary: primaryMap is Map ? StationWord.fromMap(primaryMap) : null, cluster: clusterRaw is List ? clusterRaw .whereType>() .map(StationWord.fromMap) .toList() : const [], dots: (map['dots'] as List? ?? const []) .whereType() .map((n) => n.toInt()) .toList(), parallels: (map['parallels'] as List? ?? const []) .whereType() .map((p) => { 'system': '${p['system'] ?? ''}', 'system_name': '${p['system_name'] ?? ''}', 'id': '${p['id'] ?? ''}', 'name': '${p['name'] ?? ''}', 'symbol': '${p['symbol'] ?? ''}', }) .toList(), ); } bool matchesQuery(String query) { if (query.isEmpty) return true; final q = query.toLowerCase(); if (name.toLowerCase().contains(q)) return true; if (lore.toLowerCase().contains(q)) return true; if (symbol.contains(query)) return true; if (hebrew != null && hebrew!.contains(query)) return true; if (hanzi != null && hanzi!.contains(query)) return true; if (arabic != null && arabic!.contains(query)) return true; if (primary != null && primary!.word.toLowerCase().contains(q)) return true; return cluster.any((c) => c.word.toLowerCase().contains(q)); } } class TraditionCanon { final String id; final String name; final String icon; final int count; final String description; final List stations; const TraditionCanon({ required this.id, required this.name, required this.icon, required this.count, required this.description, required this.stations, }); factory TraditionCanon.fromMap(Map map) { final raw = map['stations']; return TraditionCanon( id: map['id'] as String? ?? '', name: map['name'] as String? ?? '', icon: map['icon'] as String? ?? '', count: (map['count'] as num?)?.toInt() ?? 0, description: map['description'] as String? ?? '', stations: raw is List ? raw .whereType>() .map(TraditionStation.fromMap) .toList() : const [], ); } }