addRouteWithAlternatives method
Calculates multiple routes between start and destination using the Alternative Route API.
Falls back to standard single routing if alternative route API is unavailable or returns an error.
Implementation
Future<PowerMapRoutesResult?> addRouteWithAlternatives({
required LatLng start,
required LatLng destination,
String profile = 'driving',
String language = 'th',
int maxAlternatives = 3,
bool fitCamera = true,
}) async {
try {
final String baseUrl = PowerMapSDK.baseUrl;
final Map<String, String> headers = await PowerMapSDK.getServiceHeaders();
// Profile mapping for PowerMap GraphHopper API
String mappedProfile = profile;
if (profile == 'driving') {
mappedProfile = 'car';
} else if (profile == 'cycling') {
mappedProfile = 'bike';
} else if (profile == 'foot') {
mappedProfile = 'foot';
} else if (profile == 'motorcycling') {
mappedProfile = 'motorcycle';
}
final body = {
'points': [
[start.longitude, start.latitude],
[destination.longitude, destination.latitude],
],
'profile': mappedProfile,
'elevation': false,
'debug': false,
'instructions': true,
'locale': language,
'optimize': 'false',
'points_encoded': true,
'snap_preventions': ['ferry'],
'details': [
'road_class',
'road_environment',
'max_speed',
'average_speed',
],
'alternative_route.max_paths': maxAlternatives,
'algorithm': 'alternative_route',
};
final response = await http.post(
Uri.parse('$baseUrl/api/v2/map/route/'),
headers: {...headers, 'Content-Type': 'application/json'},
body: json.encode(body),
);
if (response.statusCode != 200) {
debugPrint(
'Alternative route API error: ${response.statusCode}. Falling back to default route.',
);
return _fallbackToSingleRoute(
start: start,
destination: destination,
profile: profile,
language: language,
fitCamera: fitCamera,
);
}
final data = json.decode(response.body);
if (data == null ||
data['paths'] == null ||
(data['paths'] as List).isEmpty) {
return _fallbackToSingleRoute(
start: start,
destination: destination,
profile: profile,
language: language,
fitCamera: fitCamera,
);
}
final paths = data['paths'] as List;
final List<PowerMapRouteResult> routes = [];
for (var path in paths) {
List<LatLng> geometry = [];
if (path['points'] is String) {
final encoded = path['points'] as String;
geometry = _decodePolyline(encoded, precision: 5, is3D: false);
} else if (path['points'] is Map &&
path['points']['coordinates'] != null) {
final coords = path['points']['coordinates'] as List;
geometry = coords
.map(
(c) =>
LatLng((c[1] as num).toDouble(), (c[0] as num).toDouble()),
)
.toList();
}
final instructions = (path['instructions'] as List?) ?? [];
final details = (path['details'] as Map<String, dynamic>?) ?? {};
final maxSpeedDetails = (details['max_speed'] as List?) ?? [];
final roadClassDetails = (details['road_class'] as List?) ?? [];
double? getMaxSpeedForIndex(int idx) {
for (var item in maxSpeedDetails) {
if (item is List && item.length >= 3) {
final start = (item[0] as num).toInt();
final end = (item[1] as num).toInt();
if (idx >= start && idx <= end) {
return (item[2] as num?)?.toDouble();
}
}
}
return null;
}
String? getRoadClassForIndex(int idx) {
for (var item in roadClassDetails) {
if (item is List && item.length >= 3) {
final start = (item[0] as num).toInt();
final end = (item[1] as num).toInt();
if (idx >= start && idx <= end) {
return item[2]?.toString();
}
}
}
return null;
}
// Pass 1: Parse all instructions and apply NavigationTextFormatter
final List<PowerRouteStep> rawSteps = [];
PowerRouteStep? previousStep;
for (var inst in instructions) {
final interval = (inst['interval'] as List?) ?? [];
final int startIdx = (interval.isNotEmpty
? (interval[0] as num).toInt()
: 0);
final stepLoc = (startIdx >= 0 && startIdx < geometry.length)
? geometry[startIdx]
: (geometry.isNotEmpty ? geometry.first : start);
final rawText = inst['text'] as String? ?? '';
final dist = (inst['distance'] as num?)?.toDouble() ?? 0.0;
final durationSec =
((inst['time'] as num?)?.toDouble() ?? 0.0) / 1000.0;
final sign = (inst['sign'] as num?)?.toInt();
String? streetName = inst['street_name'] as String?;
String? streetRef = inst['street_ref'] as String?;
// --- GraphHopper Ref Extraction ---
if ((streetRef == null || streetRef.isEmpty) &&
streetName != null &&
streetName.isNotEmpty) {
final hwMatch = RegExp(
r'(?:ทางหลวง(?:แผ่นดิน|พิเศษ|ชนบท)?หมายเลข|ถนนหมายเลข|AH)\s*([a-zA-Z0-9\.]+)',
).firstMatch(streetName);
if (hwMatch != null) {
streetRef = hwMatch.group(1);
} else {
final parts = streetName.split(RegExp(r'[,;/\s]+'));
for (var part in parts) {
final p = part.trim();
if (RegExp(
r'^((M|S|AH)\d+|\d{1,4}|[\u0E01-\u0E2E]{1,3}\.\d{1,4})$',
).hasMatch(p)) {
// Guard: pure-digit + Thai name → number is part of
// road name or soi, not a highway ref.
// e.g. "พระราม 4" → 4, "ประชาราษฎร์สาย 2" → 2
final isPureDigit = RegExp(r'^\d+$').hasMatch(p);
final nameHasThai =
RegExp(r'[\u0E01-\u0E4F]{2,}').hasMatch(streetName);
if (!streetName.contains('ซอย') &&
!streetName.contains('ซ.') &&
!streetName.contains('หมู่') &&
!streetName.contains('บ้าน') &&
!(isPureDigit && nameHasThai)) {
streetRef = p;
break;
}
}
}
}
}
ManeuverType maneuverType;
ManeuverModifier modifier;
switch (sign) {
case -7:
maneuverType = ManeuverType.fork;
modifier = ManeuverModifier.slightLeft;
break;
case 7:
maneuverType = ManeuverType.fork;
modifier = ManeuverModifier.slightRight;
break;
case -1:
maneuverType = ManeuverType.turn;
modifier = ManeuverModifier.slightLeft;
break;
case 1:
maneuverType = ManeuverType.turn;
modifier = ManeuverModifier.slightRight;
break;
case -2:
maneuverType = ManeuverType.turn;
modifier = ManeuverModifier.left;
break;
case 2:
maneuverType = ManeuverType.turn;
modifier = ManeuverModifier.right;
break;
case -3:
maneuverType = ManeuverType.turn;
modifier = ManeuverModifier.sharpLeft;
break;
case 3:
maneuverType = ManeuverType.turn;
modifier = ManeuverModifier.sharpRight;
break;
case 0:
maneuverType = ManeuverType.continueStraight;
modifier = ManeuverModifier.straight;
break;
case 4:
maneuverType = ManeuverType.arrive;
modifier = ManeuverModifier.straight;
break;
case 6:
maneuverType = ManeuverType.roundabout;
modifier = ManeuverModifier.straight;
break;
case 8:
case -9:
case -98:
maneuverType = ManeuverType.uTurn;
modifier = ManeuverModifier.uTurnRight;
break;
default:
maneuverType = NavigationTextFormatter.parseType(rawText);
modifier = NavigationTextFormatter.parseModifier(rawText);
}
final tempStep = PowerRouteStep(
text: rawText,
distance: dist,
duration: durationSec,
location: stepLoc,
streetName: streetName,
streetRef: streetRef,
maneuverType: maneuverType,
modifier: modifier,
sign: sign,
);
final formatted = NavigationTextFormatter.format(
currentStep: tempStep,
previousStep: previousStep,
languageCode: language,
);
final finalStep = PowerRouteStep(
text: rawText,
naturalText: formatted.bannerText,
voiceAnnouncement: formatted.voiceText,
distance: dist,
duration: durationSec,
location: stepLoc,
sign: sign,
streetName: streetName,
streetRef: streetRef,
maneuverType: maneuverType,
modifier: modifier,
);
rawSteps.add(finalStep);
previousStep = finalStep;
}
// Pass 2: Generate Synthetic Voice Instructions for upcoming maneuvers
final List<PowerRouteStep> steps = [];
for (int i = 0; i < rawSteps.length; i++) {
final curr = rawSteps[i];
final voiceList = <PowerRouteVoiceInstruction>[];
if (i + 1 < rawSteps.length) {
final next = rawSteps[i + 1];
String nextAnnouncement = next.voiceAnnouncement.isNotEmpty
? next.voiceAnnouncement
: next.text.replaceAll(RegExp(r'<[^>]*>'), ' ').trim();
// Chained maneuver check: if step i + 2 occurs within 150m of next
if (i + 2 < rawSteps.length &&
next.distance < 150.0 &&
next.distance > 0) {
final nextNext = rawSteps[i + 2];
final nextNextText = nextNext.voiceAnnouncement.isNotEmpty
? nextNext.voiceAnnouncement
: nextNext.text.replaceAll(RegExp(r'<[^>]*>'), ' ').trim();
if (nextNextText.isNotEmpty) {
nextAnnouncement = '$nextAnnouncement, แล้ว $nextNextText';
}
}
final interval = (instructions[i]['interval'] as List?) ?? [];
final int startIdx = interval.isNotEmpty
? (interval[0] as num).toInt()
: 0;
final roadClass = getRoadClassForIndex(startIdx);
final maxSpd = getMaxSpeedForIndex(startIdx);
final isHighway =
(maxSpd != null && maxSpd >= 80.0) ||
roadClass == 'primary' ||
roadClass == 'motorway' ||
roadClass == 'trunk';
if (isHighway) {
// High-speed / Highway tiers
if (curr.distance >= 1800.0) {
voiceList.add(
PowerRouteVoiceInstruction(
distanceAlongGeometry: 1500.0,
announcement: 'อีก 1.5 กิโลเมตร $nextAnnouncement',
),
);
}
if (curr.distance >= 450.0) {
voiceList.add(
PowerRouteVoiceInstruction(
distanceAlongGeometry: 500.0,
announcement: 'อีก 500 เมตร $nextAnnouncement',
),
);
}
voiceList.add(
PowerRouteVoiceInstruction(
distanceAlongGeometry: 80.0,
announcement: nextAnnouncement,
),
);
} else {
// Urban / Local road tiers
if (curr.distance >= 500.0) {
voiceList.add(
PowerRouteVoiceInstruction(
distanceAlongGeometry: 400.0,
announcement: 'อีก 400 เมตร $nextAnnouncement',
),
);
}
if (curr.distance >= 180.0) {
voiceList.add(
PowerRouteVoiceInstruction(
distanceAlongGeometry: 150.0,
announcement: 'อีก 150 เมตร $nextAnnouncement',
),
);
}
voiceList.add(
PowerRouteVoiceInstruction(
distanceAlongGeometry: 40.0,
announcement: nextAnnouncement,
),
);
}
} else {
// Arrival step
voiceList.add(
PowerRouteVoiceInstruction(
distanceAlongGeometry: 0.0,
announcement: curr.voiceAnnouncement.isNotEmpty
? curr.voiceAnnouncement
: 'ถึงจุดหมายปลายทางแล้ว',
),
);
}
steps.add(
PowerRouteStep(
text: curr.text,
naturalText: curr.naturalText,
voiceAnnouncement: curr.voiceAnnouncement,
distance: curr.distance,
duration: curr.duration,
location: curr.location,
voiceInstructions: voiceList,
sign: curr.sign,
streetName: curr.streetName,
streetRef: curr.streetRef,
maneuverType: curr.maneuverType,
modifier: curr.modifier,
),
);
}
final summary = _extractMajorRoadSummary(instructions);
final bboxList = path['bbox'] != null
? (path['bbox'] as List).map((n) => (n as num).toDouble()).toList()
: null;
routes.add(
PowerMapRouteResult(
distance: (path['distance'] as num).toDouble(),
duration: (path['time'] as num).toDouble() / 1000.0,
geometry: geometry,
steps: steps,
raw: path as Map<String, dynamic>,
profile: profile,
summary: summary,
timeMs: (path['time'] as num).toDouble(),
bbox: bboxList,
ascend: (path['ascend'] as num?)?.toDouble(),
descend: (path['descend'] as num?)?.toDouble(),
),
);
}
if (routes.isEmpty) {
return _fallbackToSingleRoute(
start: start,
destination: destination,
profile: profile,
language: language,
fitCamera: fitCamera,
);
}
// Sort routes so the fastest (shortest duration, then shortest distance) is first (index 0)
routes.sort((a, b) {
final durComp = a.duration.compareTo(b.duration);
if (durComp != 0) return durComp;
return a.distance.compareTo(b.distance);
});
final result = PowerMapRoutesResult(routes: routes, selectedIndex: 0);
await drawRoutes(routes, selectedIndex: 0, fitCamera: fitCamera);
return result;
} catch (e) {
debugPrint(
'Alternative routes request error: $e. Falling back to default route.',
);
return _fallbackToSingleRoute(
start: start,
destination: destination,
profile: profile,
language: language,
fitCamera: fitCamera,
);
}
}