Skip to content

Commit eec51c6

Browse files
committed
feat(geocoding): add multi-provider support for LocationIQ and Geoapify
- Add GeocodingProvider enum (nominatim, locationIQ, geoapify) with display names, descriptions, and rate limits per provider - Refactor geocoding service to route requests based on active provider with per-provider URL builders, response parsers, and throttle intervals - Add provider preference to SettingsService with ValueNotifier for reactive UI updates - Add 'Map API Provider' section in Settings screen with live tile selector showing rate limits and API key validation warnings
1 parent 1a98432 commit eec51c6

5 files changed

Lines changed: 375 additions & 126 deletions

File tree

lib/src/core/di/injection.config.dart

Lines changed: 12 additions & 12 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
enum GeocodingProvider {
2+
nominatim,
3+
locationIQ,
4+
geoapify;
5+
6+
String get displayName {
7+
switch (this) {
8+
case GeocodingProvider.nominatim:
9+
return 'Nominatim (OpenStreetMap)';
10+
case GeocodingProvider.locationIQ:
11+
return 'LocationIQ';
12+
case GeocodingProvider.geoapify:
13+
return 'Geoapify';
14+
}
15+
}
16+
17+
String get description {
18+
switch (this) {
19+
case GeocodingProvider.nominatim:
20+
return 'Free, no API key. Limit: 1 req/sec.';
21+
case GeocodingProvider.locationIQ:
22+
return 'Free tier: 5,000 req/day, 2 req/sec.';
23+
case GeocodingProvider.geoapify:
24+
return 'Free tier: 3,000 req/day, 5 req/sec.';
25+
}
26+
}
27+
28+
bool get requiresApiKey =>
29+
this == GeocodingProvider.locationIQ ||
30+
this == GeocodingProvider.geoapify;
31+
}

lib/src/presentation/screens/settings_screen.dart

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import '../../core/di/injection.dart';
99
import '../../l10n/app_localizations.dart';
1010
import '../../models/discount_type.dart';
1111
import '../../models/fare_formula.dart';
12+
import '../../models/geocoding_provider.dart';
1213
import '../../models/transport_mode.dart';
1314
import '../../repositories/fare_repository.dart';
1415
import '../../services/offline/offline_map_service.dart';
@@ -51,6 +52,7 @@ class _SettingsScreenState extends State<SettingsScreen>
5152
Locale _currentLocale = const Locale('en');
5253
bool _isLoading = true;
5354

55+
GeocodingProvider _geocodingProvider = GeocodingProvider.nominatim;
5456
bool _offlineModeEnabled = false;
5557
bool _autoCacheEnabled = true;
5658
bool _autoCacheWifiOnly = true;
@@ -113,6 +115,7 @@ class _SettingsScreenState extends State<SettingsScreen>
113115
final trafficFactor = await _settingsService.getTrafficFactor();
114116
final themeMode = await _settingsService.getThemeMode();
115117
final discountType = await _settingsService.getUserDiscountType();
118+
final geocodingProvider = await _settingsService.getGeocodingProvider();
116119
final hiddenModes = await _settingsService.getHiddenTransportModes();
117120
final hasSetModePrefs = await _settingsService
118121
.hasSetTransportModePreferences();
@@ -154,6 +157,7 @@ class _SettingsScreenState extends State<SettingsScreen>
154157
_themeMode = themeMode;
155158
_trafficFactor = trafficFactor;
156159
_discountType = discountType;
160+
_geocodingProvider = geocodingProvider;
157161
_currentLocale = locale;
158162
_hiddenTransportModes = hiddenModes;
159163
_hasSetTransportModePreferences = hasSetModePrefs;
@@ -230,6 +234,28 @@ class _SettingsScreenState extends State<SettingsScreen>
230234
),
231235
const SizedBox(height: 24),
232236

237+
// Map API Provider Section
238+
_buildSectionHeader(
239+
context,
240+
icon: Icons.map_rounded,
241+
title: 'Map API Provider',
242+
),
243+
const SizedBox(height: 8),
244+
_buildSettingsCard(
245+
context,
246+
children: [
247+
_buildGeocodingProviderTile(
248+
context, GeocodingProvider.nominatim),
249+
const Divider(height: 1, indent: 56),
250+
_buildGeocodingProviderTile(
251+
context, GeocodingProvider.locationIQ),
252+
const Divider(height: 1, indent: 56),
253+
_buildGeocodingProviderTile(
254+
context, GeocodingProvider.geoapify),
255+
],
256+
),
257+
const SizedBox(height: 24),
258+
233259
// Appearance Section
234260
_buildSectionHeader(
235261
context,
@@ -541,6 +567,59 @@ class _SettingsScreenState extends State<SettingsScreen>
541567
);
542568
}
543569

570+
Widget _buildGeocodingProviderTile(
571+
BuildContext context, GeocodingProvider provider) {
572+
final theme = Theme.of(context);
573+
final colorScheme = theme.colorScheme;
574+
final isSelected = _geocodingProvider == provider;
575+
final needsKey = provider.requiresApiKey;
576+
final keyMissing = (provider == GeocodingProvider.locationIQ &&
577+
AppConstants.locationIQApiKey == 'YOUR_LOCATIONIQ_API_KEY') ||
578+
(provider == GeocodingProvider.geoapify &&
579+
AppConstants.geoapifyApiKey == 'YOUR_GEOAPIFY_API_KEY');
580+
581+
return ListTile(
582+
leading: Icon(
583+
Icons.language_rounded,
584+
color: isSelected ? colorScheme.primary : colorScheme.onSurfaceVariant,
585+
),
586+
title: Text(
587+
provider.displayName,
588+
style: theme.textTheme.bodyLarge?.copyWith(
589+
fontWeight: isSelected ? FontWeight.w600 : FontWeight.normal,
590+
color: isSelected ? colorScheme.primary : colorScheme.onSurface,
591+
),
592+
),
593+
subtitle: Column(
594+
crossAxisAlignment: CrossAxisAlignment.start,
595+
children: [
596+
Text(
597+
provider.description,
598+
style: theme.textTheme.bodySmall
599+
?.copyWith(color: colorScheme.onSurfaceVariant),
600+
),
601+
if (needsKey && keyMissing)
602+
Text(
603+
'API key required — add to AppConstants',
604+
style: theme.textTheme.bodySmall?.copyWith(
605+
color: colorScheme.error,
606+
fontWeight: FontWeight.w500,
607+
),
608+
),
609+
],
610+
),
611+
trailing: isSelected
612+
? Icon(Icons.check_circle_rounded, color: colorScheme.primary)
613+
: null,
614+
onTap: needsKey && keyMissing
615+
? null
616+
: () async {
617+
setState(() => _geocodingProvider = provider);
618+
await _settingsService.setGeocodingProvider(provider);
619+
},
620+
);
621+
}
622+
544623
/// Builds a section header with icon and title.
545624
Widget _buildSectionHeader(
546625
BuildContext context, {

0 commit comments

Comments
 (0)