MotorSignal-App/lib/services/garage/motorcycle_image_service.dart

204 lines
6.4 KiB
Dart

/// Metadata for a motorcycle photograph or reference image
class MotorcycleImageReference {
final String? assetPath;
final String? networkUrl;
final String? localFilePath;
final String caption;
final String attribution;
final bool isPhoto;
const MotorcycleImageReference({
this.assetPath,
this.networkUrl,
this.localFilePath,
required this.caption,
this.attribution = 'Technical Reference',
this.isPhoto = false,
});
bool get hasImage => assetPath != null || networkUrl != null || localFilePath != null;
}
/// Metadata descriptor for registered motorcycle reference images
class MotorcycleImageDescriptor {
final String make;
final String model;
final int? exactYear;
final int? minYear;
final int? maxYear;
final List<String> modelAliases;
final String assetPath;
final String caption;
final String attribution;
final bool isPhoto;
const MotorcycleImageDescriptor({
required this.make,
required this.model,
this.exactYear,
this.minYear,
this.maxYear,
this.modelAliases = const [],
required this.assetPath,
required this.caption,
this.attribution = 'Factory Technical Reference',
this.isPhoto = true,
});
/// Normalizes vehicle names for invariant comparison
static String normalize(String input) {
return input.toLowerCase().replaceAll(RegExp(r'[^a-z0-9]'), '');
}
/// Evaluates whether this descriptor matches the specified make, model, and year
bool matches(String targetMake, String targetModel, int? targetYear) {
final normTargetMake = normalize(targetMake);
final normMake = normalize(make);
if (normMake != normTargetMake) return false;
final normTargetModel = normalize(targetModel);
final normModel = normalize(model);
final isModelMatch = normModel == normTargetModel ||
modelAliases.any((alias) => normalize(alias) == normTargetModel);
if (!isModelMatch) return false;
// Exact year check
if (exactYear != null) {
return targetYear == exactYear;
}
// Generation / Year range check
if (minYear != null || maxYear != null) {
if (targetYear == null) return false;
if (minYear != null && targetYear < minYear!) return false;
if (maxYear != null && targetYear > maxYear!) return false;
return true;
}
return true;
}
MotorcycleImageReference toReference() {
return MotorcycleImageReference(
assetPath: assetPath,
caption: caption,
attribution: attribution,
isPhoto: isPhoto,
);
}
}
/// Central extensible repository providing authentic motorcycle reference images
class MotorcycleImageService {
static const MotorcycleImageService _instance = MotorcycleImageService._internal();
factory MotorcycleImageService() => _instance;
const MotorcycleImageService._internal();
/// List of verified physical photographic assets bundled in the APK
static const List<String> bundledPhotographicAssets = [
'assets/images/motorcycles/yamaha_yzf_r6_2001.png',
];
/// Default registry of curated motorcycle reference descriptors
static final List<MotorcycleImageDescriptor> _defaultDescriptors = [
// Tier 1 & 2: 1st Gen Yamaha YZF-R6 (1999-2002 5EB) including 2001 field test vehicle
const MotorcycleImageDescriptor(
make: 'Yamaha',
model: 'YZF-R6',
minYear: 1999,
maxYear: 2002,
modelAliases: ['R6', 'YZFR6', 'YZF R6'],
assetPath: 'assets/images/motorcycles/yamaha_yzf_r6_2001.png',
caption: '2001 Yamaha YZF-R6 (5EB)',
attribution: 'Factory Supersport Reference',
isPhoto: true,
),
];
static final List<MotorcycleImageDescriptor> _customDescriptors = [];
/// Registers a custom image descriptor (for extensions/plugins/tests)
static void registerDescriptor(MotorcycleImageDescriptor descriptor) {
_customDescriptors.add(descriptor);
}
/// Clears custom descriptors (resets to default registry)
static void resetRegistry() {
_customDescriptors.clear();
}
/// Returns all active descriptors in evaluation order (custom first, then defaults)
List<MotorcycleImageDescriptor> get allDescriptors => [
..._customDescriptors,
..._defaultDescriptors,
];
/// Resolves the best available photographic image or reference for a motorcycle
///
/// Matching Hierarchy:
/// 1. Exact Year Match (Make + Model + Exact Year)
/// 2. Generation / Year-Range Match (Make + Model + Generation Range)
/// 3. Make + Model Match (Ageless / General Model Reference)
/// 4. Dynamic Technical Schematic Fallback (Accurate make/model/year vector card)
MotorcycleImageReference getReferenceImage({
required String? make,
required String? model,
int? year,
}) {
if (make == null || model == null || make.trim().isEmpty || model.trim().isEmpty) {
return const MotorcycleImageReference(
caption: 'Motorcycle Technical Reference',
attribution: 'Schematic Vector',
isPhoto: false,
);
}
final descriptors = allDescriptors;
// Tier 1: Exact Year Match
if (year != null) {
for (final desc in descriptors) {
if (desc.exactYear != null && desc.matches(make, model, year)) {
return desc.toReference();
}
}
}
// Tier 2: Generation / Year-Range Match (e.g. 1999-2002 1st Gen R6)
if (year != null) {
for (final desc in descriptors) {
if ((desc.minYear != null || desc.maxYear != null) && desc.matches(make, model, year)) {
return desc.toReference();
}
}
}
// Tier 3: Make + Model Match (Ageless / General Model Reference)
for (final desc in descriptors) {
if (desc.exactYear == null && desc.minYear == null && desc.maxYear == null && desc.matches(make, model, year)) {
return desc.toReference();
}
}
// Tier 4: Graceful Technical Schematic Fallback
final yearPrefix = year != null && year > 1900 ? '$year ' : '';
final formattedCaption = '$yearPrefix${make.trim()} ${model.trim()}'.trim();
return MotorcycleImageReference(
caption: formattedCaption,
attribution: 'Technical Schematic',
isPhoto: false,
);
}
/// Checks whether a genuine photographic asset is registered and available for a given vehicle
bool hasPhotographicAsset({
required String? make,
required String? model,
int? year,
}) {
final ref = getReferenceImage(make: make, model: model, year: year);
return ref.isPhoto && ref.assetPath != null;
}
}