import '../strings/consultorios_strings.dart'; /// Um consultório dentário devolvido pela Overpass API. Os campos de texto /// nunca são nulos — qualquer tag em falta no OpenStreetMap já é resolvida /// para um texto de substituição aqui — mas [hasPhone]/[hasOpeningHours] /// dizem se esse texto é um valor real ou só o substituto, para os widgets /// decidirem o que é "acionável" (ex.: só ligar quando há telefone real). class Clinic { const Clinic({ required this.id, required this.name, required this.lat, required this.lon, required this.address, required this.phoneNumbers, required this.openingHours, required this.hasOpeningHours, this.distanceKm, }); final String id; final String name; final double lat; final double lon; final String address; /// Um consultório na OSM pode ter mais do que um contacto na mesma tag /// `phone` (separados por `;`/`,`) — já vêm separados aqui, para quem usa /// isto poder oferecer escolha em vez de tentar ligar para o texto todo /// de uma vez. Lista vazia quando não há telefone. final List phoneNumbers; final String openingHours; final bool hasOpeningHours; bool get hasPhone => phoneNumbers.isNotEmpty; /// Distância (km) até à morada guardada — só preenchida depois de /// [OverpassService.fetchNearbyClinics] a calcular; `null` até lá. final double? distanceKm; Clinic copyWithDistance(double distanceKm) { return Clinic( id: id, name: name, lat: lat, lon: lon, address: address, phoneNumbers: phoneNumbers, openingHours: openingHours, hasOpeningHours: hasOpeningHours, distanceKm: distanceKm, ); } /// Constrói a partir de um elemento da resposta da Overpass API (`node` /// ou `way`, com `out center tags;` para que `way` também tenha /// coordenadas via `element['center']`). factory Clinic.fromOverpassElement(Map element) { final tags = (element['tags'] as Map?)?.cast() ?? const {}; final double lat; final double lon; if (element['type'] == 'node') { lat = (element['lat'] as num).toDouble(); lon = (element['lon'] as num).toDouble(); } else { final center = (element['center'] as Map?)?.cast() ?? const {}; lat = ((center['lat'] as num?) ?? 0).toDouble(); lon = ((center['lon'] as num?) ?? 0).toDouble(); } final name = (tags['name'] as String?)?.trim(); final addressParts = [ [ tags['addr:street'], tags['addr:housenumber'], ].whereType().join(', '), tags['addr:postcode'], (tags['addr:city'] ?? tags['addr:suburb']), ].whereType().where((s) => s.trim().isNotEmpty).toList(); final phoneRaw = (tags['phone'] as String?)?.trim() ?? (tags['contact:phone'] as String?)?.trim(); final phoneNumbers = (phoneRaw == null || phoneRaw.isEmpty) ? const [] : phoneRaw .split(RegExp(r'[;,]')) .map((s) => s.trim()) .where((s) => s.isNotEmpty) .toList(); final openingHours = (tags['opening_hours'] as String?)?.trim(); final hasOpeningHours = openingHours != null && openingHours.isNotEmpty; return Clinic( id: '${element['type']}/${element['id']}', name: (name != null && name.isNotEmpty) ? name : ConsultoriosStrings.unnamedClinic, lat: lat, lon: lon, address: addressParts.isNotEmpty ? addressParts.join(' • ') : ConsultoriosStrings.noAddress, phoneNumbers: phoneNumbers, openingHours: hasOpeningHours ? openingHours : ConsultoriosStrings.noOpeningHours, hasOpeningHours: hasOpeningHours, ); } }