addRouteWithAlternatives method

Future<PowerMapRoutesResult?> addRouteWithAlternatives({
  1. required LatLng start,
  2. required LatLng destination,
  3. String profile = 'driving',
  4. String language = 'th',
  5. int maxAlternatives = 3,
  6. bool fitCamera = true,
})

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,
    );
  }
}