import 'package:geolocator/geolocator.dart'; import 'package:geocoding/geocoding.dart' as geocoding; import 'package:hive_flutter/hive_flutter.dart'; import 'myquran_sholat_service.dart'; import '../local/hive_boxes.dart'; import '../local/models/app_settings.dart'; /// Location service with GPS + fallback to last known location. class LocationService { LocationService._(); static final LocationService instance = LocationService._(); /// Request permission and get current GPS location. Future getCurrentLocation() async { bool serviceEnabled = await Geolocator.isLocationServiceEnabled(); if (!serviceEnabled) return null; LocationPermission permission = await Geolocator.checkPermission(); if (permission == LocationPermission.denied) { permission = await Geolocator.requestPermission(); if (permission == LocationPermission.denied) return null; } if (permission == LocationPermission.deniedForever) return null; try { final position = await Geolocator.getCurrentPosition( locationSettings: const LocationSettings( accuracy: LocationAccuracy.medium, timeLimit: Duration(seconds: 10), ), ); // Save to settings for fallback await _saveLastKnown(position.latitude, position.longitude); return position; } catch (_) { return null; } } /// Get last known location from Hive settings. ({double lat, double lng})? getLastKnownLocation() { final settingsBox = Hive.box(HiveBoxes.settings); final settings = settingsBox.get('default'); if (settings?.lastLat != null && settings?.lastLng != null) { return ( lat: settings!.lastLat!, lng: settings.lastLng!, ); } return null; } /// Reverse geocode to get city name from coordinates. Future getCityName(double lat, double lng) async { try { final placemarks = await geocoding.placemarkFromCoordinates(lat, lng); if (placemarks.isNotEmpty) { final place = placemarks.first; final city = place.locality ?? place.subAdministrativeArea ?? 'Unknown'; final country = place.country ?? ''; return '$city, $country'; } } catch (_) { // Geocoding may fail offline — return coords } return '${lat.toStringAsFixed(2)}, ${lng.toStringAsFixed(2)}'; } Future<({String id, String name})?> resolveMyQuranCityFromPosition( Position position) async { return resolveMyQuranCityFromCoordinates( lat: position.latitude, lng: position.longitude, ); } Future<({String id, String name})?> resolveMyQuranCityFromCoordinates({ required double lat, required double lng, }) async { try { final placemarks = await geocoding.placemarkFromCoordinates( lat, lng, ); final place = placemarks.isNotEmpty ? placemarks.first : null; final candidates = { if (place?.locality?.trim().isNotEmpty == true) place!.locality!.trim(), if (place?.subAdministrativeArea?.trim().isNotEmpty == true) place!.subAdministrativeArea!.trim(), if (place?.administrativeArea?.trim().isNotEmpty == true) place!.administrativeArea!.trim(), }; if (candidates.isEmpty) { final label = await getCityName(lat, lng); final fallback = label.split(',').first.trim(); if (fallback.isNotEmpty) candidates.add(fallback); } Map? best; var bestScore = -1; for (final raw in candidates) { final normalizedQuery = _normalizeCityToken(raw); if (normalizedQuery.isEmpty) continue; final found = await MyQuranSholatService.instance.searchCity(raw); for (final entry in found) { final id = (entry['id'] ?? '').toString().trim(); final name = (entry['lokasi'] ?? '').toString().trim(); if (id.isEmpty || name.isEmpty) continue; final score = _cityMatchScore(normalizedQuery, name); if (score > bestScore) { bestScore = score; best = {'id': id, 'name': name}; } } } if (best != null && bestScore >= 40) { return (id: best['id'] as String, name: best['name'] as String); } return null; } catch (_) { return null; } } String _normalizeCityToken(String input) { var value = input.toLowerCase().trim(); value = value.replaceAll(RegExp(r'[^a-z0-9\s]'), ' '); value = value.replaceAll(RegExp(r'\b(kota|kabupaten|city|regency)\b'), ' '); value = value.replaceAll(RegExp(r'\s+'), ' ').trim(); return value; } int _cityMatchScore(String normalizedQuery, String candidateName) { final normalizedCandidate = _normalizeCityToken(candidateName); if (normalizedCandidate.isEmpty) return 0; if (normalizedCandidate == normalizedQuery) return 100; if (normalizedCandidate.startsWith(normalizedQuery)) return 85; if (normalizedCandidate.contains(normalizedQuery)) return 70; final queryWords = normalizedQuery.split(' ').where((w) => w.isNotEmpty); var matchCount = 0; for (final word in queryWords) { if (normalizedCandidate.contains(word)) { matchCount++; } } if (matchCount == 0) return 0; return (40 + (matchCount * 10)).clamp(0, 69); } /// Save last known position to Hive. Future _saveLastKnown(double lat, double lng) async { final settingsBox = Hive.box(HiveBoxes.settings); final settings = settingsBox.get('default'); if (settings != null) { settings.lastLat = lat; settings.lastLng = lng; await settings.save(); } } }