Files
CheckTheethKids/lib/consultorios/overpass_service.dart
Carlos Correia 89b27e5e86 CTK 1.1.0
2026-07-22 01:34:26 +01:00

86 lines
3.3 KiB
Dart

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<Clinic> near, List<Clinic> 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<NearbyClinics> 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<String, dynamic>>()
.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);
}
}