86 lines
3.3 KiB
Dart
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);
|
|
}
|
|
}
|