import '../../models/poi.dart';
import '../../models/tour.dart';
import '../localization/app_localizations.dart';
class AppTopic {
const AppTopic({
required this.id,
required this.labelKey,
required this.aliases,
this.highlightOnly = false,
});
final String id;
final String labelKey;
final List<String> aliases;
final bool highlightOnly;
String label(AppLocalizations l10n) => l10n.t(labelKey);
}
class AppTaxonomy {
const AppTaxonomy._();
static const topics = <AppTopic>[
AppTopic(
id: 'highlights',
labelKey: 'topicHighlights',
aliases: ['highlight', 'highlights', 'weimar highlights'],
highlightOnly: true),
AppTopic(
id: 'goethe_schiller',
labelKey: 'topicGoetheSchiller',
aliases: ['goethe', 'schiller', 'goethe schiller']),
AppTopic(id: 'classic', labelKey: 'topicClassic', aliases: [
'klassik',
'classic',
'aufklaerung',
'aufklärung',
'klassik aufklaerung',
'klassik aufklärung'
]),
AppTopic(
id: 'bauhaus',
labelKey: 'topicBauhaus',
aliases: ['bauhaus', 'moderne 1900 1933', 'moderne']),
AppTopic(id: 'weimar_republic', labelKey: 'topicWeimarRepublic', aliases: [
'weimarer republik',
'weimar republic',
'1919',
'demokratie'
]),
AppTopic(id: 'ns_memory', labelKey: 'topicNsMemory', aliases: [
'nationalsozialismus',
'ns geschichte',
'ns-geschichte',
'zwangsarbeit',
'gedenken',
'buchenwald'
]),
AppTopic(
id: 'ddr_contemporary',
labelKey: 'topicDdrContemporary',
aliases: ['ddr', 'gegenwart', 'zeitgeschichte', 'contemporary']),
AppTopic(
id: 'architecture',
labelKey: 'topicArchitecture',
aliases: ['architektur', 'architecture']),
AppTopic(
id: 'museums_art',
labelKey: 'topicMuseumsArt',
aliases: ['museum', 'museen', 'kunst', 'art', 'galerie']),
AppTopic(id: 'parks_nature', labelKey: 'topicParksNature', aliases: [
'parks gaerten',
'parks gärten',
'park',
'garten',
'gärten',
'gaerten',
'natur',
'bienen natur'
]),
AppTopic(
id: 'castles',
labelKey: 'topicCastles',
aliases: ['schloss', 'schlösser', 'schloesser', 'castle', 'palais']),
AppTopic(
id: 'family',
labelKey: 'topicFamily',
aliases: ['familie', 'family', 'kinder', 'kids']),
AppTopic(id: 'jewish_life', labelKey: 'topicJewishLife', aliases: [
'jüdisches leben',
'juedisches leben',
'jüdisch',
'juedisch',
'jewish'
]),
AppTopic(
id: 'music',
labelKey: 'topicMusic',
aliases: ['musik', 'music', 'liszt', 'bach']),
AppTopic(
id: 'hidden_gems',
labelKey: 'topicHiddenGems',
aliases: ['geheimtipps', 'geheimtipp', 'hidden gems', 'besonders']),
];
static AppTopic? byId(String id) {
for (final topic in topics) {
if (topic.id == id) return topic;
}
return null;
}
static bool matchesPoi(AppTopic topic, Poi poi) {
if (topic.highlightOnly && poi.highlight) return true;
final fields = <String>[
poi.category,
...poi.topicSlugs,
...poi.topicNames,
...poi.eraSlugs,
...poi.eraNames,
].map(_normalise).where((e) => e.isNotEmpty).toList();
return _matchesFields(topic, fields);
}
static bool matchesTour(AppTopic topic, Tour tour) {
if (topic.highlightOnly && tour.highlight) return true;
final fields = <String>[
tour.category,
...tour.topicSlugs,
...tour.topicNames,
...tour.eraSlugs,
...tour.eraNames,
].map(_normalise).where((e) => e.isNotEmpty).toList();
return _matchesFields(topic, fields);
}
static int scorePoi(Poi poi, Set<String> topicIds) {
var score = 0;
for (final id in topicIds) {
final topic = bySelection(id);
if (topic == null) continue;
if (topic.highlightOnly) {
if (poi.highlight) score += 34;
continue;
}
final category = _normalise(poi.category);
final topicFields =
[...poi.topicSlugs, ...poi.topicNames].map(_normalise).toList();
final eraFields =
[...poi.eraSlugs, ...poi.eraNames].map(_normalise).toList();
final title = _normalise(poi.title.get('de'));
var matched = false;
for (final rawAlias in topic.aliases) {
final alias = _normalise(rawAlias);
if (_fieldMatches(category, alias)) {
score += 30;
matched = true;
}
if (topicFields.any((field) => _fieldMatches(field, alias))) {
score += 42;
matched = true;
}
if (eraFields.any((field) => _fieldMatches(field, alias))) {
score += 38;
matched = true;
}
}
// A title hit is only a weak hint. Curated topic/era metadata wins.
if (!matched &&
topic.aliases
.any((alias) => _fieldMatches(title, _normalise(alias)))) {
score += 5;
}
}
return score;
}
static String selectionLabelForLanguage(
Set<String> ids, String languageCode) {
final selectedIds = ids
.map(bySelection)
.whereType<AppTopic>()
.map((topic) => topic.id)
.toSet();
final selected =
topics.where((topic) => selectedIds.contains(topic.id)).toList();
if (selected.isEmpty) {
return AppLocalizations.tFor(languageCode, 'freeSelection');
}
return selected
.map((topic) => AppLocalizations.tFor(languageCode, topic.labelKey))
.join(' + ');
}
static bool _matchesFields(AppTopic topic, List<String> fields) {
for (final rawAlias in topic.aliases) {
final alias = _normalise(rawAlias);
if (fields.any((field) => _fieldMatches(field, alias))) return true;
}
return false;
}
static AppTopic? bySelection(String value) {
final normalized = _normalise(value);
for (final topic in topics) {
if (_normalise(topic.id) == normalized ||
topic.aliases.any((alias) => _normalise(alias) == normalized)) {
return topic;
}
}
return null;
}
static bool _fieldMatches(String field, String alias) {
if (field.isEmpty || alias.isEmpty) return false;
if (field == alias) return true;
return field.contains(alias) || alias.contains(field);
}
static String _normalise(String value) => value
.trim()
.toLowerCase()
.replaceAll('_', ' ')
.replaceAll('-', ' ')
.replaceAll(RegExp(r'\s+'), ' ');
}

