118 lines
3.9 KiB
Dart
118 lines
3.9 KiB
Dart
import 'dart:convert';
|
|
import 'package:flutter/services.dart' show rootBundle;
|
|
import 'package:http/http.dart' as http;
|
|
import 'package:latlong2/latlong.dart';
|
|
import '../hoogerheide_streets.dart';
|
|
|
|
class GeocodingService {
|
|
static final GeocodingService _instance = GeocodingService._internal();
|
|
factory GeocodingService() => _instance;
|
|
GeocodingService._internal();
|
|
|
|
static const _nominatimBaseUrl = 'https://nominatim.openstreetmap.org';
|
|
static const _userAgent = 'DeliveryApp/1.0 (delivery-app-openclaw@users.noreply.github.com)';
|
|
|
|
Map<String, dynamic>? _addressCache;
|
|
|
|
String _normalize(String name) =>
|
|
name.replaceAll(RegExp(r'[–—]'), ' ').replaceAll(RegExp(r'\s+'), ' ').trim().toLowerCase();
|
|
|
|
Future<Map<String, dynamic>> _loadAddressCache() async {
|
|
if (_addressCache != null) return _addressCache!;
|
|
try {
|
|
final jsonStr = await rootBundle.loadString('assets/hoogerheide_addresses.json');
|
|
_addressCache = jsonDecode(jsonStr);
|
|
return _addressCache!;
|
|
} catch (_) {
|
|
return {'addresses': []};
|
|
}
|
|
}
|
|
|
|
/// Geocode a street + house number to LatLng.
|
|
/// Priority: local JSON cache → Nominatim → hoogerheide_streets.dart → default
|
|
Future<LatLng> geocode(String street, String houseNum) async {
|
|
final ns = _normalize(street);
|
|
final hn = houseNum.trim();
|
|
|
|
// 1. Local address cache (fast, no rate limits)
|
|
try {
|
|
final cache = await _loadAddressCache();
|
|
final addresses = cache['addresses'] as List? ?? [];
|
|
for (final addr in addresses) {
|
|
final addrStreet = (addr['street'] as String).toLowerCase();
|
|
final addrNum = addr['housenumber'] as String;
|
|
if (addrStreet == ns && addrNum == hn) {
|
|
return LatLng(addr['lat'] as double, addr['lon'] as double);
|
|
}
|
|
}
|
|
} catch (_) {}
|
|
|
|
// 2. Nominatim API
|
|
try {
|
|
final query = Uri.encodeComponent('$street $houseNum, Hoogerheide, Netherlands');
|
|
final url = '$_nominatimBaseUrl/search?format=jsonv2&q=$query&limit=1';
|
|
final resp = await http.get(
|
|
Uri.parse(url),
|
|
headers: {'User-Agent': _userAgent},
|
|
).timeout(const Duration(seconds: 5));
|
|
|
|
if (resp.statusCode == 200) {
|
|
final data = jsonDecode(resp.body);
|
|
if (data is List && data.isNotEmpty) {
|
|
return LatLng(
|
|
double.parse(data[0]['lat']),
|
|
double.parse(data[0]['lon']),
|
|
);
|
|
}
|
|
}
|
|
} catch (_) {}
|
|
|
|
// 3. hoogerheide_streets.dart lookup (exact then partial match)
|
|
final streetResult = _lookupInStreets(street);
|
|
if (streetResult != null) {
|
|
// Offset by house number to spread markers along the street
|
|
final hnInt = int.tryParse(houseNum.replaceAll(RegExp(r'[A-Za-z]'), '')) ?? 1;
|
|
return LatLng(
|
|
streetResult.latitude + (hnInt - 1) * 0.00015,
|
|
streetResult.longitude + ((hnInt / 25).floor() % 2 == 0 ? 0 : 0.0002),
|
|
);
|
|
}
|
|
|
|
// 4. Default to Hoogerheide center
|
|
return const LatLng(51.4243390, 4.3238380);
|
|
}
|
|
|
|
LatLng? _lookupInStreets(String street) {
|
|
final ns = _normalize(street);
|
|
|
|
// Exact match
|
|
for (final entry in HoogerheideStreets.streetCoords.entries) {
|
|
if (_normalize(entry.key) == ns) {
|
|
return LatLng(entry.value[0], entry.value[1]);
|
|
}
|
|
}
|
|
|
|
// Partial match
|
|
for (final entry in HoogerheideStreets.streetCoords.entries) {
|
|
final ek = _normalize(entry.key);
|
|
if (ns.contains(ek) || ek.contains(ns)) {
|
|
return LatLng(entry.value[0], entry.value[1]);
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/// Batch-geocode a list of stops, updating coords only when they changed.
|
|
Future<List<LatLng?>> geocodeStops(List<(String street, String houseNum)> addresses) async {
|
|
final results = <LatLng?>[];
|
|
for (final (street, houseNum) in addresses) {
|
|
try {
|
|
results.add(await geocode(street, houseNum));
|
|
} catch (_) {
|
|
results.add(null);
|
|
}
|
|
}
|
|
return results;
|
|
}
|
|
}
|