CTK 1.1.0
This commit is contained in:
85
lib/consultorios/overpass_service.dart
Normal file
85
lib/consultorios/overpass_service.dart
Normal file
@@ -0,0 +1,85 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user