Files
Carlos Correia fc875d559c CTK 1.2.0
2026-07-30 00:53:51 +01:00

113 lines
3.8 KiB
Dart

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<String> 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<String, dynamic> element) {
final tags = (element['tags'] as Map?)?.cast<String, dynamic>() ?? 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<String, dynamic>() ?? 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<String>().join(', '),
tags['addr:postcode'],
(tags['addr:city'] ?? tags['addr:suburb']),
].whereType<String>().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 <String>[]
: 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,
);
}
}