import 'package:flutter/foundation.dart';
import 'package:url_launcher/url_launcher.dart';
enum ExternalNavigationPlatform { ios, android, web, other }
class NavigationTarget {
const NavigationTarget({
required this.latitude,
required this.longitude,
this.address = '',
});
final double latitude;
final double longitude;
final String address;
}
typedef NavigationUriLauncher = Future<bool> Function(Uri uri);
/// Opens a destination through an external navigation application.
///
/// The provider choice is supplied by the WordPress/API geo contract. For the
/// Google Maps profile the adapter first tries the native Google Maps hand-off
/// and then the platform's local navigation scheme. The final fallback is the
/// official Google Maps URL, which opens the installed app through the
/// platform's universal-link handling where available and otherwise opens the
/// browser. The URL does not require a Google API key.
class ExternalNavigationAdapter {
const ExternalNavigationAdapter({
required this.platform,
required this.launch,
});
final ExternalNavigationPlatform platform;
final NavigationUriLauncher launch;
factory ExternalNavigationAdapter.system() {
final platform = kIsWeb
? ExternalNavigationPlatform.web
: switch (defaultTargetPlatform) {
TargetPlatform.iOS => ExternalNavigationPlatform.ios,
TargetPlatform.android => ExternalNavigationPlatform.android,
_ => ExternalNavigationPlatform.other,
};
return ExternalNavigationAdapter(
platform: platform,
launch: (uri) => launchUrl(uri, mode: LaunchMode.externalApplication),
);
}
Future<bool> open(
NavigationTarget target, {
String webUrlTemplate = '',
String navigationProvider = 'google_maps',
}) async {
if (!_validCoordinate(target.latitude, target.longitude)) return false;
for (final uri in candidateUris(
target,
webUrlTemplate: webUrlTemplate,
navigationProvider: navigationProvider,
)) {
try {
if (await launch(uri)) return true;
} catch (_) {
// A missing native maps handler must not prevent the centrally
// configured browser fallback from being attempted.
}
}
return false;
}
/// Opens a map centered on a geographic target without requiring a Maps SDK
/// or a tile API key. This is the safe fallback when the internal map tile
/// source is not configured yet.
Future<bool> openMap(
NavigationTarget target, {
int zoom = 13,
}) async {
if (!_validCoordinate(target.latitude, target.longitude)) return false;
for (final uri in mapCandidateUris(target, zoom: zoom)) {
try {
if (await launch(uri)) return true;
} catch (_) {
// Continue through the local/browser fallback chain.
}
}
return false;
}
List<Uri> candidateUris(
NavigationTarget target, {
String webUrlTemplate = '',
String navigationProvider = 'google_maps',
}) {
final candidates = <Uri>[];
if (navigationProvider == 'google_maps') {
final googleNative = switch (platform) {
ExternalNavigationPlatform.ios => _googleIosUri(target),
ExternalNavigationPlatform.android => _googleAndroidUri(target),
_ => null,
};
if (googleNative != null) candidates.add(googleNative);
}
final localNative = switch (platform) {
ExternalNavigationPlatform.ios => _iosUri(target),
ExternalNavigationPlatform.android => _androidUri(target),
_ => null,
};
if (localNative != null) candidates.add(localNative);
final web = _webUri(
target,
webUrlTemplate,
navigationProvider: navigationProvider,
);
if (web != null) candidates.add(web);
return candidates;
}
List<Uri> mapCandidateUris(
NavigationTarget target, {
int zoom = 13,
}) {
final coordinates = '${target.latitude},${target.longitude}';
final candidates = <Uri>[];
if (platform == ExternalNavigationPlatform.ios) {
candidates.add(
Uri.parse('comgooglemaps://?center=$coordinates&zoom=$zoom'),
);
candidates.add(Uri.parse('maps://?ll=$coordinates&z=$zoom'));
} else if (platform == ExternalNavigationPlatform.android) {
candidates.add(Uri.parse('geo:$coordinates?z=$zoom'));
}
candidates.add(
Uri.https('www.google.com', '/maps/search/', {
'api': '1',
'query': coordinates,
}),
);
return candidates;
}
Uri _googleIosUri(NavigationTarget target) => Uri.parse(
'comgooglemaps://?daddr=${target.latitude},${target.longitude}&directionsmode=driving',
);
Uri _googleAndroidUri(NavigationTarget target) => Uri.parse(
'google.navigation:q=${target.latitude},${target.longitude}&mode=d',
);
Uri? _iosUri(NavigationTarget target) =>
Uri.parse('maps://?daddr=${target.latitude},${target.longitude}');
Uri? _androidUri(NavigationTarget target) {
final query = target.address.trim().isEmpty
? '${target.latitude},${target.longitude}'
: target.address.trim();
return Uri.parse(
'geo:${target.latitude},${target.longitude}?q=${Uri.encodeComponent(query)}',
);
}
Uri? _webUri(
NavigationTarget target,
String template, {
required String navigationProvider,
}) {
final raw = template.trim();
if (raw.isEmpty) {
if (navigationProvider != 'google_maps') return null;
return Uri.https('www.google.com', '/maps/dir/', {
'api': '1',
'destination': '${target.latitude},${target.longitude}',
'travelmode': 'driving',
'dir_action': 'navigate',
});
}
final resolved = raw
.replaceAll('{lat}', target.latitude.toString())
.replaceAll('{lng}', target.longitude.toString())
.replaceAll('{coordinates}', '${target.latitude},${target.longitude}')
.replaceAll('{destination}', '${target.latitude},${target.longitude}')
.replaceAll('{address}', Uri.encodeComponent(target.address.trim()));
if (resolved.contains('{')) return null;
final uri = Uri.tryParse(resolved);
if (uri == null || !{'http', 'https'}.contains(uri.scheme.toLowerCase())) {
return null;
}
return uri;
}
bool _validCoordinate(double latitude, double longitude) =>
latitude.isFinite &&
longitude.isFinite &&
latitude >= -90 &&
latitude <= 90 &&
longitude >= -180 &&
longitude <= 180;
}
