import 'dart:convert'; import 'package:http/http.dart' as http; import 'clinic.dart'; import 'geo_utils.dart'; /// Consultórios dentários encontrados perto de uma morada, já separados /// pela regra de negócio "menos de 5km" vs "mais distantes" — a única /// classe que conhece esse limite; os ecrãs só mostram o que recebem. typedef NearbyClinics = ({List near, List far}); /// Pesquisa de consultórios dentários via Overpass API (dados do /// OpenStreetMap). `overpass-api.de` é um servidor público e comunitário, /// sem SLA — se se tornar um problema em produção, a solução é apontar /// para outra instância (ex.: overpass.kumi.systems), não redesenhar isto. class OverpassService { const OverpassService._(); static const String _endpoint = 'https://overpass-api.de/api/interpreter'; static const String _userAgent = 'CheckTeethKids/1.0 (caducorreia740@gmail.com)'; /// Raio de pesquisa efetivo — mais largo que os 5km usados para /// distinguir "perto"/"longe", para haver o que mostrar em "mais /// distantes" mesmo em zonas menos densas. static const int _searchRadiusMeters = 18000; static const double _nearThresholdKm = 5; static Future fetchNearbyClinics({ required double lat, required double lon, }) async { const overpassTimeoutSeconds = 25; final query = '[out:json][timeout:$overpassTimeoutSeconds];' '(' 'node["amenity"="dentist"](around:$_searchRadiusMeters,$lat,$lon);' 'way["amenity"="dentist"](around:$_searchRadiusMeters,$lat,$lon);' 'node["healthcare"="dentist"](around:$_searchRadiusMeters,$lat,$lon);' 'way["healthcare"="dentist"](around:$_searchRadiusMeters,$lat,$lon);' ');' 'out center tags;'; // O timeout do lado do cliente fica acima do [timeout:] pedido à // Overpass — assim é sempre a própria Overpass a desistir primeiro // (com uma resposta 5xx tratável abaixo), em vez de uma corrida entre // os dois a cortar a ligação a meio de uma resposta que já vinha a // caminho. final response = await http .post( Uri.parse(_endpoint), headers: {'User-Agent': _userAgent}, body: {'data': query}, ) .timeout(const Duration(seconds: overpassTimeoutSeconds + 10)); if (response.statusCode != 200) { throw Exception( 'Overpass respondeu com status ${response.statusCode}: ' '${response.body.substring(0, response.body.length.clamp(0, 300))}', ); } final decoded = jsonDecode(response.body); final elements = (decoded is Map ? decoded['elements'] : null) as List?; if (elements == null) { throw Exception('Resposta inesperada da Overpass API'); } final clinics = elements .cast>() .map(Clinic.fromOverpassElement) .toList(); final withDistance = clinics .map((c) => c.copyWithDistance(haversineDistanceKm(lat, lon, c.lat, c.lon))) .toList() ..sort((a, b) => a.distanceKm!.compareTo(b.distanceKm!)); final near = withDistance.where((c) => c.distanceKm! < _nearThresholdKm).toList(); final far = withDistance.where((c) => c.distanceKm! >= _nearThresholdKm).toList(); return (near: near, far: far); } }