import 'dart:convert'; import 'package:flutter/foundation.dart'; import 'package:http/http.dart' as http; class MotorcycleImage { final String imageUrl; final String sourceUrl; final String? credit; final String? license; const MotorcycleImage({ required this.imageUrl, required this.sourceUrl, this.credit, this.license, }); } /// Resolves a real motorcycle photograph for a make/model/year selection. /// /// Known exact matches are preferred first. Other motorcycles are resolved /// from Wikimedia Commons using its public API, with licensing metadata kept /// alongside the image for attribution in the UI. class MotorcycleImageService { MotorcycleImageService._(); static final MotorcycleImageService instance = MotorcycleImageService._(); final Map _cache = {}; static const _exactMatches = { '2001|yamaha|yzf-r6': MotorcycleImage( imageUrl: 'https://upload.wikimedia.org/wikipedia/commons/e/e0/2001_Yamaha_YZFR600.jpg', sourceUrl: 'https://commons.wikimedia.org/wiki/File:2001_Yamaha_YZFR600.jpg', credit: 'James McCauley / Wikimedia Commons', license: 'CC BY 2.0', ), }; Future resolve({ required int year, required String make, required String model, }) async { final key = _key(year, make, model); if (_cache.containsKey(key)) return _cache[key]; final exact = _exactMatches[key]; if (exact != null) { _cache[key] = exact; return exact; } try { final query = '$year $make $model motorcycle'; final uri = Uri.https('commons.wikimedia.org', '/w/api.php', { 'action': 'query', 'format': 'json', 'origin': '*', 'generator': 'search', 'gsrnamespace': '6', 'gsrsearch': query, 'gsrlimit': '8', 'prop': 'imageinfo', 'iiprop': 'url|extmetadata', 'iiurlwidth': '900', 'iiextmetadatafilter': 'Artist|Credit|LicenseShortName|LicenseUrl', }); final response = await http.get(uri, headers: const { 'Accept': 'application/json', 'User-Agent': 'MotorSignal/1.0 (motorcycle garage image lookup)', }).timeout(const Duration(seconds: 8)); if (response.statusCode != 200) { _cache[key] = null; return null; } final json = jsonDecode(response.body) as Map; final pages = (json['query']?['pages'] as Map?)?.values; if (pages == null) { _cache[key] = null; return null; } MotorcycleImage? best; var bestScore = -1; final normalizedModel = _normalize(model); final normalizedMake = _normalize(make); final normalizedYear = year.toString(); for (final rawPage in pages) { final page = Map.from(rawPage as Map); final title = (page['title']?.toString() ?? '').toLowerCase(); final infoList = page['imageinfo']; if (infoList is! List || infoList.isEmpty) continue; final info = Map.from(infoList.first as Map); final thumb = info['thumburl']?.toString() ?? info['url']?.toString(); final source = info['descriptionurl']?.toString(); if (thumb == null || source == null) continue; final metadata = Map.from( (info['extmetadata'] as Map?) ?? const {}, ); final license = _metadataValue(metadata, 'LicenseShortName'); final licenseLower = (license ?? '').toLowerCase(); final allowed = licenseLower.contains('cc by') || licenseLower.contains('public domain') || licenseLower.contains('cc0'); if (!allowed) continue; var score = 0; if (title.contains(normalizedModel)) score += 5; if (title.contains(normalizedMake)) score += 3; if (title.contains(normalizedYear)) score += 4; if (title.contains('motorcycle') || title.contains('motorbike')) score += 1; if (score > bestScore) { bestScore = score; best = MotorcycleImage( imageUrl: thumb, sourceUrl: source, credit: _metadataValue(metadata, 'Artist') ?? _metadataValue(metadata, 'Credit'), license: license, ); } } _cache[key] = best; return best; } catch (error) { debugPrint('Motorcycle image lookup failed: $error'); _cache[key] = null; return null; } } String _key(int year, String make, String model) => '$year|${_normalize(make)}|${_normalize(model)}'; String _normalize(String value) => value .toLowerCase() .replaceAll(RegExp(r'[^a-z0-9]+'), '-') .replaceAll(RegExp(r'^-+|-+$'), ''); String? _metadataValue(Map metadata, String key) { final value = metadata[key]; if (value is Map) return value['value']?.toString(); return value?.toString(); } }