From 04e32be71f056b58900b1322940c31383137671a Mon Sep 17 00:00:00 2001 From: dammyololade Date: Sat, 20 Sep 2025 13:34:31 +0200 Subject: [PATCH 1/2] feat: Enhance Routes API integration and fix decoding issues This commit introduces several improvements and bug fixes related to the Google Routes API integration and polyline decoding. Key changes: - **ENHANCED**: Added validation for `bicycling` and `walking` travel modes to ensure they use `RoutingPreference.unspecified` as required by the Google Routes API. This prevents potential API errors and ensures correct route calculations. - **FIXED**: Resolved a polyline decoding issue that primarily affected web platforms. The decoding logic has been updated to correctly handle `BigInt` operations, ensuring accurate coordinate parsing. - **FIXED**: Corrected an issue with decoding intermediate locations in API responses. - **CHORE**: Updated the `http` package dependency to version `^1.5.0`. - **CHORE**: Set `publish_to: none` in the example app's `pubspec.yaml` to prevent accidental publishing. - **TEST**: Added a GitHub Actions workflow for automated testing, linting, formatting, and dry-run publishing on pull requests. - **TEST**: Introduced new unit tests for network provider functionality, specifically focusing on header handling in `RoutesApiRequest`. - **TEST**: Updated integration tests to reflect the routing preference change for walking mode. - **TEST**: Removed old `polyline_points_test.dart` and `widget_test.dart` from the example app as they were outdated or redundant. - **DOCS**: Updated CHANGELOG.md with details of these changes. - Incremented package version to 3.1.0. --- .github/workflows/test.yml | 35 ++ CHANGELOG.md | 11 +- example/pubspec.yaml | 14 +- example/test/widget_test.dart | 30 - lib/src/network/network_provider.dart | 2 + lib/src/utils/polyline_decoder.dart | 60 +- pubspec.yaml | 5 +- test/flutter_polyline_points_test.dart | 1 + .../polyline_integration_test.dart | 4 +- test/network/network_provider_test.dart | 218 ++++++++ test/polyline_points_test.dart | 521 ------------------ test/routes_api/routes_request_test.dart | 3 + 12 files changed, 307 insertions(+), 597 deletions(-) create mode 100644 .github/workflows/test.yml delete mode 100644 example/test/widget_test.dart create mode 100644 test/network/network_provider_test.dart delete mode 100644 test/polyline_points_test.dart diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..36650c5 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,35 @@ +name: Test + +on: + pull_request: + branches: [ main ] + workflow_dispatch: + +jobs: + test: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Flutter + uses: subosito/flutter-action@v2 + with: + flutter-version: '3.35.0' + channel: 'stable' + + - name: Get dependencies + run: flutter pub get + + - name: Analyze code + run: dart analyze + + - name: Run tests + run: flutter test + + - name: Check formatting + run: dart format --set-exit-if-changed . + + - name: Check pub publish + run: dart pub publish --dry-run \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 8544e55..7169d94 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,14 @@ -## [3.0.1] - 2024-07-23 +## [3.1.0] - 2025-09-20 +- **ENHANCED**: Added validation for bicycling and walking travel modes to ensure they use RoutingPreference.unspecified as required by Google Routes API. + - Special thanks to [@enricava](https://github.com/enricava) for the contribution. +- **Fixed**: Decoding issue on web. +- **Fixed**: Decoding intermediate locations [issue](https://github.com/Dammyololade/flutter_polyline_points/pull/122). + - Thanks to [@razosx](https://github.com/razosx) + +## [3.0.1] - 2025-07-23 - **NEW**: Add support for custom headers in Routes API requests. -## [3.0.0] - 2024-07-22 +## [3.0.0] - 2025-07-22 ### 🚀 Major Refactor - Simplified and Unified API diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 47273fe..1d49cf1 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -1,17 +1,7 @@ name: example -description: A new Flutter application to describe the use of polyline_point library - -# The following defines the version and build number for your application. -# A version number is three numbers separated by dots, like 1.2.43 -# followed by an optional build number separated by a +. -# Both the version and the builder number may be overridden in flutter -# build by specifying --build-name and --build-number, respectively. -# In Android, build-name is used as versionName while build-number used as versionCode. -# Read more about Android versioning at https://developer.android.com/studio/publish/versioning -# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. -# Read more about iOS versioning at -# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html +description: A new Flutter application to describe the use of polyline_point libraryi version: 1.0.0+1 +publish_to: none environment: sdk: '>=3.0.0 <4.0.0' diff --git a/example/test/widget_test.dart b/example/test/widget_test.dart deleted file mode 100644 index 747db1d..0000000 --- a/example/test/widget_test.dart +++ /dev/null @@ -1,30 +0,0 @@ -// This is a basic Flutter widget test. -// -// To perform an interaction with a widget in your test, use the WidgetTester -// utility that Flutter provides. For example, you can send tap and scroll -// gestures. You can also use WidgetTester to find child widgets in the widget -// tree, read text, and verify that the values of widget properties are correct. - -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; - -import 'package:example/main.dart'; - -void main() { - testWidgets('Counter increments smoke test', (WidgetTester tester) async { - // Build our app and trigger a frame. - await tester.pumpWidget(MyApp()); - - // Verify that our counter starts at 0. - expect(find.text('0'), findsOneWidget); - expect(find.text('1'), findsNothing); - - // Tap the '+' icon and trigger a frame. - await tester.tap(find.byIcon(Icons.add)); - await tester.pump(); - - // Verify that our counter has incremented. - expect(find.text('0'), findsNothing); - expect(find.text('1'), findsOneWidget); - }); -} diff --git a/lib/src/network/network_provider.dart b/lib/src/network/network_provider.dart index 0380799..a815f6b 100644 --- a/lib/src/network/network_provider.dart +++ b/lib/src/network/network_provider.dart @@ -80,6 +80,8 @@ class NetworkProvider { if (response.statusCode == 200) { final data = json.decode(response.body); + // v2 api does not include status so use response Phrase instead + data['status'] = response.reasonPhrase; return RoutesApiResponse.fromJson(data); } else { final errorData = json.decode(response.body); diff --git a/lib/src/utils/polyline_decoder.dart b/lib/src/utils/polyline_decoder.dart index 6c088e6..88d0c72 100644 --- a/lib/src/utils/polyline_decoder.dart +++ b/lib/src/utils/polyline_decoder.dart @@ -7,44 +7,48 @@ class PolylineDecoder { static List run(String encoded) { try { final points = []; - int index = 0; - int lat = 0; - int lng = 0; + int index = 0, len = encoded.length; + int lat = 0, lng = 0; + BigInt Big0 = BigInt.from(0); + BigInt Big0x1f = BigInt.from(0x1f); + BigInt Big0x20 = BigInt.from(0x20); - while (index < encoded.length) { + while (index < len) { int shift = 0; - int result = 0; - int byte; - + BigInt b, result; + result = Big0; do { - byte = encoded.codeUnitAt(index++) - 63; - result |= (byte & 0x1f) << shift; + b = BigInt.from(encoded.codeUnitAt(index++) - 63); + result |= (b & Big0x1f) << shift; shift += 5; - } while (byte >= 0x20); - - int deltaLat = ((result & 1) != 0 ? ~(result >> 1) : (result >> 1)); - lat += deltaLat; + } while (b >= Big0x20); + BigInt rShifted = result >> 1; + int dLat; + if (result.isOdd) + dLat = (~rShifted).toInt(); + else + dLat = rShifted.toInt(); + lat += dLat; shift = 0; - result = 0; - + result = Big0; do { - byte = encoded.codeUnitAt(index++) - 63; - result |= (byte & 0x1f) << shift; + b = BigInt.from(encoded.codeUnitAt(index++) - 63); + result |= (b & Big0x1f) << shift; shift += 5; - } while (byte >= 0x20); - - int deltaLng = ((result & 1) != 0 ? ~(result >> 1) : (result >> 1)); - lng += deltaLng; - - points.add(PointLatLng( - lat / 1E5, - lng / 1E5, - )); + } while (b >= Big0x20); + rShifted = result >> 1; + int dLng; + if (result.isOdd) + dLng = (~rShifted).toInt(); + else + dLng = rShifted.toInt(); + lng += dLng; + + points.add(PointLatLng((lat / 1E5).toDouble(), (lng / 1E5).toDouble())); } - return points; - } catch (e) { + } catch (e) { return []; } } diff --git a/pubspec.yaml b/pubspec.yaml index 5900274..20ebd0a 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: flutter_polyline_points description: A flutter package to get polyline points by either passing the coordinates or google encoded polyline string -version: 3.0.1 +version: 3.1.0 homepage: https://github.com/Dammyololade/flutter_polyline_points environment: @@ -9,11 +9,12 @@ environment: dependencies: flutter: sdk: flutter - http: ^1.2.1 + http: ^1.5.0 dev_dependencies: flutter_test: sdk: flutter + test: ^1.25.15 #dependency_overrides: # path: ^1.8.0 diff --git a/test/flutter_polyline_points_test.dart b/test/flutter_polyline_points_test.dart index 5e7c293..6fa78f4 100644 --- a/test/flutter_polyline_points_test.dart +++ b/test/flutter_polyline_points_test.dart @@ -31,6 +31,7 @@ void main() { ); expect(response.routes.isNotEmpty, isTrue); + expect(response.status, equals("OK")); expect(response.routes.first.polylinePoints!.isNotEmpty, isTrue); diff --git a/test/integration/polyline_integration_test.dart b/test/integration/polyline_integration_test.dart index 1d811dc..c9f751a 100644 --- a/test/integration/polyline_integration_test.dart +++ b/test/integration/polyline_integration_test.dart @@ -37,7 +37,7 @@ void main() { travelMode: TravelMode.walking, intermediates: intermediates, computeAlternativeRoutes: true, - routingPreference: RoutingPreference.trafficAwareOptimal, + routingPreference: RoutingPreference.unspecified, units: Units.imperial, polylineQuality: PolylineQuality.highQuality, languageCode: 'en', @@ -54,7 +54,7 @@ void main() { expect(json['travelMode'], equals('WALK')); expect(json['intermediates'], hasLength(2)); expect(json['computeAlternativeRoutes'], isTrue); - expect(json['routingPreference'], equals('TRAFFIC_AWARE_OPTIMAL')); + expect(json['routingPreference'], equals("ROUTING_PREFERENCE_UNSPECIFIED")); expect(json['units'], equals('IMPERIAL')); expect(json['polylineQuality'], equals('HIGH_QUALITY')); expect(json['languageCode'], equals('en')); diff --git a/test/network/network_provider_test.dart b/test/network/network_provider_test.dart new file mode 100644 index 0000000..08d33b9 --- /dev/null +++ b/test/network/network_provider_test.dart @@ -0,0 +1,218 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:flutter_polyline_points/src/routes_api/routes_request.dart'; +import 'package:flutter_polyline_points/src/commons/point_lat_lng.dart'; +import 'package:flutter_polyline_points/src/commons/travel_mode.dart'; + +void main() { + group('NetworkProvider', () { + late PointLatLng origin; + late PointLatLng destination; + + setUp(() { + origin = PointLatLng(37.7749, -122.4194); // San Francisco + destination = PointLatLng(34.0522, -118.2437); // Los Angeles + }); + + group('Headers functionality', () { + test('should generate basic headers without custom headers', () { + final request = RoutesApiRequest( + origin: origin, + destination: destination, + ); + + // Access the private method through reflection or create a test helper + // For now, we'll test the integration through the public API behavior + expect(request.headers, isNull); + }); + + test('should include custom headers in request', () { + final customHeaders = { + 'X-Android-Package': 'com.example.testapp', + 'X-Android-Cert': 'TEST_CERT_FINGERPRINT', + 'Custom-Header': 'custom-value', + }; + + final request = RoutesApiRequest( + origin: origin, + destination: destination, + headers: customHeaders, + ); + + expect(request.headers, isNotNull); + expect(request.headers!['X-Android-Package'], equals('com.example.testapp')); + expect(request.headers!['X-Android-Cert'], equals('TEST_CERT_FINGERPRINT')); + expect(request.headers!['Custom-Header'], equals('custom-value')); + }); + + test('should handle Android-specific headers for restricted API keys', () { + final androidHeaders = { + 'X-Android-Package': 'com.example.flutter_polyline_points', + 'X-Android-Cert': 'AA:BB:CC:DD:EE:FF:00:11:22:33:44:55:66:77:88:99:AA:BB:CC:DD', + }; + + final request = RoutesApiRequest( + origin: origin, + destination: destination, + travelMode: TravelMode.driving, + headers: androidHeaders, + ); + + expect(request.headers, isNotNull); + expect(request.headers!['X-Android-Package'], + equals('com.example.flutter_polyline_points')); + expect(request.headers!['X-Android-Cert'], + equals('AA:BB:CC:DD:EE:FF:00:11:22:33:44:55:66:77:88:99:AA:BB:CC:DD')); + }); + + test('should handle multiple custom headers', () { + final multipleHeaders = { + 'X-Android-Package': 'com.example.app', + 'X-Android-Cert': 'cert123', + 'Authorization': 'Bearer token123', + 'X-Custom-Header-1': 'value1', + 'X-Custom-Header-2': 'value2', + }; + + final request = RoutesApiRequest( + origin: origin, + destination: destination, + headers: multipleHeaders, + ); + + expect(request.headers, hasLength(5)); + multipleHeaders.forEach((key, value) { + expect(request.headers![key], equals(value)); + }); + }); + + test('should handle empty headers map', () { + final request = RoutesApiRequest( + origin: origin, + destination: destination, + headers: {}, + ); + + expect(request.headers, isNotNull); + expect(request.headers, isEmpty); + }); + + test('should preserve headers when copying request', () { + final originalHeaders = { + 'X-Android-Package': 'com.example.original', + 'X-Android-Cert': 'original_cert', + }; + + final newHeaders = { + 'X-Android-Package': 'com.example.updated', + 'X-Android-Cert': 'updated_cert', + 'New-Header': 'new_value', + }; + + final originalRequest = RoutesApiRequest( + origin: origin, + destination: destination, + headers: originalHeaders, + ); + + final copiedRequest = originalRequest.copyWith( + headers: newHeaders, + ); + + // Original request should remain unchanged + expect(originalRequest.headers, equals(originalHeaders)); + expect(originalRequest.headers!['X-Android-Package'], equals('com.example.original')); + + // Copied request should have new headers + expect(copiedRequest.headers, equals(newHeaders)); + expect(copiedRequest.headers!['X-Android-Package'], equals('com.example.updated')); + expect(copiedRequest.headers!['New-Header'], equals('new_value')); + }); + + test('should handle language code with custom headers', () { + final customHeaders = { + 'X-Android-Package': 'com.example.app', + 'X-Android-Cert': 'cert123', + }; + + final request = RoutesApiRequest( + origin: origin, + destination: destination, + languageCode: 'es', + headers: customHeaders, + ); + + expect(request.languageCode, equals('es')); + expect(request.headers, equals(customHeaders)); + expect(request.headers!['X-Android-Package'], equals('com.example.app')); + }); + + test('should handle request with both custom body parameters and headers', () { + final customHeaders = { + 'X-Android-Package': 'com.example.app', + 'X-Android-Cert': 'cert123', + }; + + final customBodyParams = { + 'extraComputations': ['TRAFFIC_ON_POLYLINE'], + 'customField': 'customValue', + }; + + final request = RoutesApiRequest( + origin: origin, + destination: destination, + headers: customHeaders, + customBodyParameters: customBodyParams, + ); + + expect(request.headers, equals(customHeaders)); + expect(request.customBodyParameters, equals(customBodyParams)); + + final json = request.toJson(); + expect(json['extraComputations'], contains('TRAFFIC_ON_POLYLINE')); + expect(json['customField'], equals('customValue')); + }); + }); + + group('Header validation', () { + test('should accept valid header names and values', () { + final validHeaders = { + 'X-Android-Package': 'com.example.app', + 'X-Android-Cert': 'AA:BB:CC:DD:EE:FF:00:11:22:33:44:55:66:77:88:99:AA:BB:CC:DD', + 'Authorization': 'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9', + 'Content-Type': 'application/json', + 'User-Agent': 'MyApp/1.0.0', + 'Accept-Language': 'en-US,en;q=0.9', + }; + + final request = RoutesApiRequest( + origin: origin, + destination: destination, + headers: validHeaders, + ); + + expect(request.headers, equals(validHeaders)); + validHeaders.forEach((key, value) { + expect(request.headers![key], equals(value)); + }); + }); + + test('should handle special characters in header values', () { + final headersWithSpecialChars = { + 'X-Android-Package': 'com.example.app_test-123', + 'X-Android-Cert': 'AA:BB:CC:DD:EE:FF:00:11:22:33:44:55:66:77:88:99:AA:BB:CC:DD', + 'Custom-Header': 'value with spaces and-dashes_underscores', + }; + + final request = RoutesApiRequest( + origin: origin, + destination: destination, + headers: headersWithSpecialChars, + ); + + expect(request.headers, equals(headersWithSpecialChars)); + expect(request.headers!['Custom-Header'], + equals('value with spaces and-dashes_underscores')); + }); + }); + }); +} \ No newline at end of file diff --git a/test/polyline_points_test.dart b/test/polyline_points_test.dart deleted file mode 100644 index cdba4a7..0000000 --- a/test/polyline_points_test.dart +++ /dev/null @@ -1,521 +0,0 @@ -import 'package:flutter_test/flutter_test.dart'; -import 'package:flutter_polyline_points/flutter_polyline_points.dart'; - -void main() { - group('PolylinePoints', () { - late PointLatLng origin; - late PointLatLng destination; - - setUp(() { - origin = PointLatLng(37.7749, -122.4194); // San Francisco - destination = PointLatLng(34.0522, -118.2437); // Los Angeles - }); - - group('Constructor', () { - test('should create instance with API key', () { - final instance = PolylinePoints(apiKey: 'test_key'); - expect(instance, isA()); - }); - - test('should create instance with custom timeout', () { - final instance = PolylinePoints( - apiKey: 'test_key', - defaultTimeout: Duration(seconds: 60), - ); - expect(instance, isA()); - }); - - test('should create instance with preferRoutesApi setting', () { - final instance = PolylinePoints( - apiKey: 'test_key', - preferRoutesApi: false, - ); - expect(instance, isA()); - }); - }); - - group('Static Methods', () { - test('decodePolyline should decode valid polyline', () { - const encodedPolyline = 'u{~vFvyys@fS]'; - - final decodedPoints = PolylinePoints.decodePolyline(encodedPolyline); - - expect(decodedPoints, isNotEmpty); - expect(decodedPoints, isA>()); - - for (final point in decodedPoints) { - expect(point, isA()); - expect(point.latitude, isA()); - expect(point.longitude, isA()); - } - }); - - test('decodePolyline should handle empty string', () { - final decodedPoints = PolylinePoints.decodePolyline(''); - expect(decodedPoints, isEmpty); - }); - - test('decodePolyline should handle malformed polyline', () { - const malformedPolyline = 'invalid_polyline'; - - expect( - () => PolylinePoints.decodePolyline(malformedPolyline), - returnsNormally, - ); - - final result = PolylinePoints.decodePolyline(malformedPolyline); - expect(result, isA>()); - }); - - test('decodePolyline should be consistent across calls', () { - const polyline = 'u{~vFvyys@fS]'; - - final firstDecode = PolylinePoints.decodePolyline(polyline); - final secondDecode = PolylinePoints.decodePolyline(polyline); - - expect(firstDecode.length, equals(secondDecode.length)); - - for (int i = 0; i < firstDecode.length; i++) { - expect(firstDecode[i].latitude, equals(secondDecode[i].latitude)); - expect(firstDecode[i].longitude, equals(secondDecode[i].longitude)); - } - }); - }); - - group('RoutesApiRequest Building', () { - test('should build basic request correctly', () { - final request = RoutesApiRequest( - origin: origin, - destination: destination, - ); - - expect(request.origin, equals(origin)); - expect(request.destination, equals(destination)); - expect(request.travelMode, equals(TravelMode.driving)); - expect(request.routingPreference, - equals(RoutingPreference.trafficUnaware)); - }); - - test('should build request with all options', () { - final request = RoutesApiRequest( - origin: origin, - destination: destination, - travelMode: TravelMode.walking, - routingPreference: RoutingPreference.trafficAware, - languageCode: 'en-US', - regionCode: 'US', - units: Units.metric, - departureTime: DateTime.now(), - arrivalTime: DateTime.now().add(Duration(hours: 2)), - customBodyParameters: {'custom': 'value'}, - ); - - expect(request.origin, equals(origin)); - expect(request.destination, equals(destination)); - expect(request.travelMode, equals(TravelMode.walking)); - expect(request.routingPreference, equals(RoutingPreference.trafficAware)); - expect(request.languageCode, equals('en-US')); - expect(request.regionCode, equals('US')); - expect(request.units, equals(Units.metric)); - expect(request.departureTime, isNotNull); - expect(request.arrivalTime, isNotNull); - expect(request.customBodyParameters, containsPair('custom', 'value')); - }); - - test('should convert request to JSON correctly', () { - final request = RoutesApiRequest( - origin: origin, - destination: destination, - travelMode: TravelMode.bicycling, - ); - - final json = request.toJson(); - - expect(json, isA>()); - expect(json['origin'], isNotNull); - expect(json['destination'], isNotNull); - expect(json['travelMode'], equals('BICYCLE')); - }); - - test('should handle coordinate locations in request', () { - final request = RoutesApiRequest( - origin: origin, - destination: destination, - ); - - expect(request.origin, equals(origin)); - expect(request.destination, equals(destination)); - - final json = request.toJson(); - expect(json['origin']['location']['latLng']['latitude'], equals(origin.latitude)); - expect(json['destination']['location']['latLng']['longitude'], equals(destination.longitude)); - }); - - test('should handle intermediate waypoints correctly', () { - // Note: RoutesApiRequest uses PolylineWayPoint objects for intermediates - // This test focuses on the basic structure - final request = RoutesApiRequest( - origin: origin, - destination: destination, - ); - - expect(request.intermediates, isNull); - - final json = request.toJson(); - expect(json['origin'], isNotNull); - expect(json['destination'], isNotNull); - }); - - test('should generate correct field mask', () { - final request = RoutesApiRequest( - origin: origin, - destination: destination, - ); - - final fieldMask = request.getFieldMask(); - - expect(fieldMask, contains('routes.duration')); - expect(fieldMask, contains('routes.staticDuration')); - expect(fieldMask, contains('routes.distanceMeters')); - expect(fieldMask, contains('routes.polyline.encodedPolyline')); - }); - - test('should generate field mask with extra computations', () { - final request = RoutesApiRequest( - origin: origin, - destination: destination, - ); - - final fieldMask = request.getFieldMask(); - - expect(fieldMask, contains('routes.duration')); - expect(fieldMask, contains('routes.staticDuration')); - expect(fieldMask, contains('routes.distanceMeters')); - expect(fieldMask, contains('routes.polyline.encodedPolyline')); - }); - - test('should handle copyWith correctly', () { - final originalRequest = RoutesApiRequest( - origin: origin, - destination: destination, - travelMode: TravelMode.driving, - ); - - final modifiedRequest = originalRequest.copyWith( - travelMode: TravelMode.walking, - ); - - expect(modifiedRequest.origin, equals(originalRequest.origin)); - expect(modifiedRequest.destination, equals(originalRequest.destination)); - expect(modifiedRequest.travelMode, equals(TravelMode.walking)); - expect(originalRequest.travelMode, equals(TravelMode.driving)); - }); - }); - - group('RoutesApiResponse Parsing', () { - test('should parse complete response correctly', () { - final responseJson = { - 'routes': [ - { - 'duration': '3600s', - 'staticDuration': '3500s', - 'distanceMeters': 100000, - 'polyline': { - 'encodedPolyline': 'u{~vFvyys@fS]', - }, - }, - ], - }; - - final response = RoutesApiResponse.fromJson(responseJson); - - expect(response.routes, isNotEmpty); - expect(response.routes.length, equals(1)); - - final route = response.routes.first; - expect(route.duration, equals(3600)); - expect(route.staticDuration, equals(3500)); - expect(route.distanceMeters, equals(100000)); - expect(route.polylineEncoded, equals('u{~vFvyys@fS]')); - expect(route.polylinePoints, isNotEmpty); - }); - - test('should handle empty routes response', () { - final responseJson = { - 'routes': [], - }; - - final response = RoutesApiResponse.fromJson(responseJson); - - expect(response.routes, isEmpty); - }); - - test('should handle missing routes field', () { - final responseJson = {}; - - final response = RoutesApiResponse.fromJson(responseJson); - - expect(response.routes, isEmpty); - }); - - test('should handle route with missing optional fields', () { - final responseJson = { - 'routes': [ - { - 'distanceMeters': 50000, - }, - ], - }; - - final response = RoutesApiResponse.fromJson(responseJson); - - expect(response.routes, isNotEmpty); - - final route = response.routes.first; - expect(route.duration, isNull); - expect(route.staticDuration, isNull); - expect(route.distanceMeters, equals(50000)); - expect(route.polylineEncoded, isNull); - expect(route.polylinePoints, isNull); - }); - - test('should preserve raw JSON data', () { - final responseJson = { - 'routes': [ - { - 'duration': '1800s', - 'distanceMeters': 25000, - 'customField': 'customValue', - }, - ], - 'status': 'OK', - 'geocodingResults': [], - }; - - final response = RoutesApiResponse.fromJson(responseJson); - - expect(response.rawJson, equals(responseJson)); - expect(response.rawJson['status'], equals('OK')); - expect(response.rawJson['geocodingResults'], isA()); - }); - - test('should handle route convenience getters', () { - final responseJson = { - 'routes': [ - { - 'duration': '7200s', // 2 hours - 'staticDuration': '6600s', // 1.83 hours - 'distanceMeters': 150000, // 150 km - }, - ], - }; - - final response = RoutesApiResponse.fromJson(responseJson); - final route = response.routes.first; - - expect(route.durationMinutes, equals(120)); // 7200 / 60 - expect(route.staticDurationMinutes, equals(110)); // 6600 / 60 - expect(route.distanceKm, equals(150.0)); // 150000 / 1000 - }); - - test('should handle null values in convenience getters', () { - final responseJson = { - 'routes': [ - { - 'distanceMeters': 50000, - // duration and staticDuration are missing - }, - ], - }; - - final response = RoutesApiResponse.fromJson(responseJson); - final route = response.routes.first; - - expect(route.durationMinutes, isNull); - expect(route.staticDurationMinutes, isNull); - expect(route.distanceKm, equals(50.0)); - }); - - test('should handle route equality correctly', () { - final route1 = Route( - duration: 3600, - staticDuration: 3500, - distanceMeters: 100000, - polylineEncoded: 'u{~vFvyys@fS]', - ); - - final route2 = Route( - duration: 3600, - staticDuration: 3500, - distanceMeters: 100000, - polylineEncoded: 'u{~vFvyys@fS]', - ); - - final route3 = Route( - duration: 1800, - staticDuration: 3500, - distanceMeters: 100000, - polylineEncoded: 'u{~vFvyys@fS]', - ); - - expect(route1, equals(route2)); - expect(route1, isNot(equals(route3))); - expect(route1.hashCode, equals(route2.hashCode)); - expect(route1.hashCode, isNot(equals(route3.hashCode))); - }); - }); - - group('Error Handling', () { - test('should handle invalid location formats gracefully', () { - // Note: RoutesApiRequest requires PointLatLng objects - // This test verifies the constructor works with valid inputs - expect( - () => RoutesApiRequest( - origin: origin, - destination: destination, - ), - returnsNormally, - ); - }); - - test('should handle malformed JSON response', () { - final malformedJson = { - 'routes': 'not_an_array', - }; - - expect( - () => RoutesApiResponse.fromJson(malformedJson), - returnsNormally, - ); - }); - - test('should handle null JSON response', () { - expect( - () => RoutesApiResponse.fromJson({}), - returnsNormally, - ); - }); - - test('should handle invalid duration format', () { - final responseJson = { - 'routes': [ - { - 'duration': 'invalid_duration', - 'distanceMeters': 50000, - }, - ], - }; - - final response = RoutesApiResponse.fromJson(responseJson); - final route = response.routes.first; - - expect(route.duration, isNull); - expect(route.distanceMeters, equals(50000)); - }); - }); - - group('Integration Scenarios', () { - test('should handle complete workflow simulation', () { - // 1. Create request - final request = RoutesApiRequest( - origin: origin, - destination: destination, - travelMode: TravelMode.driving, - ); - - expect(request, isA()); - - // 2. Convert to JSON (simulating API call preparation) - final requestJson = request.toJson(); - expect(requestJson, isA>()); - - // 3. Simulate API response - final responseJson = { - 'routes': [ - { - 'duration': '3600s', - 'staticDuration': '3500s', - 'distanceMeters': 100000, - 'polyline': { - 'encodedPolyline': 'u{~vFvyys@fS]', - }, - }, - ], - }; - - // 4. Parse response - final response = RoutesApiResponse.fromJson(responseJson); - expect(response.routes, isNotEmpty); - - // 5. Extract polyline points - final route = response.routes.first; - expect(route.polylinePoints, isNotEmpty); - - // 6. Verify decoded points - for (final point in route.polylinePoints!) { - expect(point, isA()); - expect(point.latitude, isA()); - expect(point.longitude, isA()); - } - }); - - test('should handle multiple routes response', () { - final responseJson = { - 'routes': [ - { - 'duration': '3600s', - 'distanceMeters': 100000, - 'polyline': {'encodedPolyline': 'u{~vFvyys@fS]'}, - }, - { - 'duration': '4200s', - 'distanceMeters': 120000, - 'polyline': {'encodedPolyline': '_p~iF~ps|U_ulLnnqC_mqNvxq`@'}, - }, - ], - }; - - final response = RoutesApiResponse.fromJson(responseJson); - - expect(response.routes, hasLength(2)); - - final firstRoute = response.routes[0]; - final secondRoute = response.routes[1]; - - expect(firstRoute.duration, equals(3600)); - expect(firstRoute.distanceMeters, equals(100000)); - expect(firstRoute.polylinePoints, isNotEmpty); - - expect(secondRoute.duration, equals(4200)); - expect(secondRoute.distanceMeters, equals(120000)); - expect(secondRoute.polylinePoints, isNotEmpty); - }); - - test('should handle request with custom body parameters', () { - final customParams = { - 'customField': 'customValue', - 'nestedObject': { - 'key': 'value', - }, - 'arrayField': [1, 2, 3], - }; - - final request = RoutesApiRequest( - origin: origin, - destination: destination, - customBodyParameters: customParams, - ); - - final json = request.toJson(); - - expect(json['customField'], equals('customValue')); - expect(json['nestedObject'], equals({'key': 'value'})); - expect(json['arrayField'], equals([1, 2, 3])); - - // Ensure standard fields are still present - expect(json['origin'], isNotNull); - expect(json['destination'], isNotNull); - }); - }); - }); -} diff --git a/test/routes_api/routes_request_test.dart b/test/routes_api/routes_request_test.dart index c419709..cdb9679 100644 --- a/test/routes_api/routes_request_test.dart +++ b/test/routes_api/routes_request_test.dart @@ -82,6 +82,7 @@ void main() { origin: origin, destination: destination, travelMode: TravelMode.walking, + routingPreference: RoutingPreference.unspecified, computeAlternativeRoutes: true, languageCode: 'es', customBodyParameters: {'testParam': 'testValue'}, @@ -160,6 +161,7 @@ void main() { final modifiedRequest = originalRequest.copyWith( destination: newDestination, travelMode: TravelMode.walking, + routingPreference: RoutingPreference.unspecified, computeAlternativeRoutes: true, ); @@ -292,6 +294,7 @@ void main() { origin: origin, destination: destination, travelMode: TravelMode.walking, + routingPreference: RoutingPreference.unspecified, computeAlternativeRoutes: true, headers: {'Original': 'header'}, ); From c654621a74e899699dfeb03bcc8a98c374de7f09 Mon Sep 17 00:00:00 2001 From: dammyololade Date: Sat, 20 Sep 2025 13:45:14 +0200 Subject: [PATCH 2/2] Refactor: Apply code formatting and remove redundant code This commit focuses on improving code style and consistency across the project by applying automated code formatting. Additionally, some redundant newlines and comments have been removed to enhance readability. Key changes: - Applied Dart formatter to various files including tests, library code, and example app. - Removed unnecessary empty lines in multiple files. - Removed a redundant test file `test/flutter_polyline_points_test.dart` from `.gitignore` as it was already covered by other tests or was a duplicate. - Minor cleanup of comments and spacing in classes like `PointLatLng`, `PolylineWayPoint`, and API model classes. --- .gitignore | 1 + example/lib/main.dart | 131 +++++++++++------- lib/src/commons/point_lat_lng.dart | 5 +- lib/src/network/network_provider.dart | 55 +++++--- lib/src/network/network_util.dart | 29 ++-- lib/src/polyline_points.dart | 7 +- .../routes_api/enums/polyline_quality.dart | 12 +- .../routes_api/enums/routing_preference.dart | 12 +- lib/src/routes_api/enums/units.dart | 12 +- lib/src/routes_api/routes_request.dart | 55 ++++---- lib/src/routes_api/routes_response.dart | 21 +-- lib/src/routes_api/toll_info.dart | 23 ++- lib/src/routes_api/traffic_info.dart | 56 ++++---- lib/src/utils/polyline_request.dart | 2 +- lib/src/utils/polyline_waypoint.dart | 1 - lib/src/utils/request_converter.dart | 50 ++++--- test/flutter_polyline_points_test.dart | 44 ------ .../polyline_integration_test.dart | 14 +- test/network/network_provider_test.dart | 45 +++--- test/routes_api/routes_request_test.dart | 53 ++++--- test/utils/polyline_decoder_test.dart | 76 +++++----- 21 files changed, 380 insertions(+), 324 deletions(-) delete mode 100644 test/flutter_polyline_points_test.dart diff --git a/.gitignore b/.gitignore index aa54549..2ab0df4 100644 --- a/.gitignore +++ b/.gitignore @@ -74,3 +74,4 @@ lib/src/Constants.dart lib/src/constants.dart example/lib/Constants.dart .fvm +test/flutter_polyline_points_test.dart diff --git a/example/lib/main.dart b/example/lib/main.dart index 11c286a..0f31e32 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -32,7 +32,8 @@ class MainScreen extends StatefulWidget { class MainScreenState extends State with TickerProviderStateMixin { late TabController _tabController; - String googleApiKey = "YOUR_GOOGLE_API_KEY_HERE"; // Replace with your actual API key + String googleApiKey = + "YOUR_GOOGLE_API_KEY_HERE"; // Replace with your actual API key @override void initState() { @@ -100,7 +101,8 @@ class LegacyMapScreenState extends State { super.initState(); polylinePoints = PolylinePoints(apiKey: widget.apiKey); - _addMarker(LatLng(_originLatitude, _originLongitude), "origin", BitmapDescriptor.defaultMarker); + _addMarker(LatLng(_originLatitude, _originLongitude), "origin", + BitmapDescriptor.defaultMarker); _addMarker(LatLng(_destLatitude, _destLongitude), "destination", BitmapDescriptor.defaultMarkerWithHue(90)); @@ -113,8 +115,8 @@ class LegacyMapScreenState extends State { body: Stack( children: [ GoogleMap( - initialCameraPosition: - CameraPosition(target: LatLng(_originLatitude, _originLongitude), zoom: 12), + initialCameraPosition: CameraPosition( + target: LatLng(_originLatitude, _originLongitude), zoom: 12), myLocationEnabled: true, tiltGesturesEnabled: true, compassEnabled: true, @@ -188,14 +190,18 @@ class LegacyMapScreenState extends State { _addMarker(LatLng position, String id, BitmapDescriptor descriptor) { MarkerId markerId = MarkerId(id); - Marker marker = Marker(markerId: markerId, icon: descriptor, position: position); + Marker marker = + Marker(markerId: markerId, icon: descriptor, position: position); markers[markerId] = marker; } _addPolyLine() { PolylineId id = const PolylineId("poly"); - Polyline polyline = - Polyline(polylineId: id, color: Colors.blue, points: polylineCoordinates, width: 5); + Polyline polyline = Polyline( + polylineId: id, + color: Colors.blue, + points: polylineCoordinates, + width: 5); polylines[id] = polyline; setState(() {}); } @@ -261,14 +267,16 @@ class CustomHeadersMapScreenState extends State { // Custom headers for Android-restricted API keys final TextEditingController _packageNameController = TextEditingController(); - final TextEditingController _certFingerprintController = TextEditingController(); + final TextEditingController _certFingerprintController = + TextEditingController(); bool _useCustomHeaders = false; @override void initState() { super.initState(); polylinePoints = PolylinePoints.enhanced(widget.apiKey); - _addMarker(LatLng(_originLatitude, _originLongitude), "origin", BitmapDescriptor.defaultMarker); + _addMarker(LatLng(_originLatitude, _originLongitude), "origin", + BitmapDescriptor.defaultMarker); _addMarker(LatLng(_destLatitude, _destLongitude), "destination", BitmapDescriptor.defaultMarkerWithHue(90)); @@ -290,8 +298,8 @@ class CustomHeadersMapScreenState extends State { body: Stack( children: [ GoogleMap( - initialCameraPosition: - CameraPosition(target: LatLng(_originLatitude, _originLongitude), zoom: 12), + initialCameraPosition: CameraPosition( + target: LatLng(_originLatitude, _originLongitude), zoom: 12), myLocationEnabled: true, tiltGesturesEnabled: true, compassEnabled: true, @@ -352,7 +360,8 @@ class CustomHeadersMapScreenState extends State { ), const SizedBox(height: 8), CheckboxListTile( - title: const Text('Use Custom Headers', style: TextStyle(fontSize: 14)), + title: const Text('Use Custom Headers', + style: TextStyle(fontSize: 14)), subtitle: const Text('For Android-restricted API keys', style: TextStyle(fontSize: 12)), value: _useCustomHeaders, @@ -415,14 +424,15 @@ class CustomHeadersMapScreenState extends State { _addMarker(LatLng position, String id, BitmapDescriptor descriptor) { MarkerId markerId = MarkerId(id); - Marker marker = Marker(markerId: markerId, icon: descriptor, position: position); + Marker marker = + Marker(markerId: markerId, icon: descriptor, position: position); markers[markerId] = marker; } _addPolyLine(List coordinates) { PolylineId id = const PolylineId("poly"); - Polyline polyline = - Polyline(polylineId: id, color: Colors.purple, points: coordinates, width: 5); + Polyline polyline = Polyline( + polylineId: id, color: Colors.purple, points: coordinates, width: 5); polylines[id] = polyline; setState(() {}); } @@ -445,7 +455,8 @@ class CustomHeadersMapScreenState extends State { }; } - RoutesApiResponse response = await polylinePoints.getRouteBetweenCoordinatesV2( + RoutesApiResponse response = + await polylinePoints.getRouteBetweenCoordinatesV2( request: RoutesApiRequest( origin: PointLatLng(_originLatitude, _originLongitude), destination: PointLatLng(_destLatitude, _destLongitude), @@ -462,8 +473,9 @@ class CustomHeadersMapScreenState extends State { final route = response.routes.first; if (route.polylinePoints != null) { final points = polylinePoints.convertToLegacyResult(response).points; - final coordinates = - points.map((point) => LatLng(point.latitude, point.longitude)).toList(); + final coordinates = points + .map((point) => LatLng(point.latitude, point.longitude)) + .toList(); _addPolyLine(coordinates); } } else { @@ -509,7 +521,8 @@ class RoutesApiMapScreenState extends State { void initState() { super.initState(); polylinePoints = PolylinePoints.enhanced(widget.apiKey); - _addMarker(LatLng(_originLatitude, _originLongitude), "origin", BitmapDescriptor.defaultMarker); + _addMarker(LatLng(_originLatitude, _originLongitude), "origin", + BitmapDescriptor.defaultMarker); _addMarker(LatLng(_destLatitude, _destLongitude), "destination", BitmapDescriptor.defaultMarkerWithHue(90)); _getEnhancedRoute(); @@ -521,8 +534,8 @@ class RoutesApiMapScreenState extends State { body: Stack( children: [ GoogleMap( - initialCameraPosition: - CameraPosition(target: LatLng(_originLatitude, _originLongitude), zoom: 12), + initialCameraPosition: CameraPosition( + target: LatLng(_originLatitude, _originLongitude), zoom: 12), myLocationEnabled: true, tiltGesturesEnabled: true, compassEnabled: true, @@ -575,7 +588,8 @@ class RoutesApiMapScreenState extends State { style: Theme.of(context).textTheme.bodySmall, textAlign: TextAlign.center, ), - if (currentResponse != null && currentResponse!.routes.isNotEmpty) + if (currentResponse != null && + currentResponse!.routes.isNotEmpty) ..._buildRouteInfo(), const SizedBox(height: 8), Row( @@ -610,13 +624,15 @@ class RoutesApiMapScreenState extends State { children: [ Column( children: [ - const Text('Duration', style: TextStyle(fontWeight: FontWeight.bold)), + const Text('Duration', + style: TextStyle(fontWeight: FontWeight.bold)), Text(route.duration?.toString() ?? 'N/A'), ], ), Column( children: [ - const Text('Distance', style: TextStyle(fontWeight: FontWeight.bold)), + const Text('Distance', + style: TextStyle(fontWeight: FontWeight.bold)), Text(route.distanceMeters != null ? '${(route.distanceMeters! / 1000).toStringAsFixed(1)} km' : 'N/A'), @@ -633,14 +649,16 @@ class RoutesApiMapScreenState extends State { _addMarker(LatLng position, String id, BitmapDescriptor descriptor) { MarkerId markerId = MarkerId(id); - Marker marker = Marker(markerId: markerId, icon: descriptor, position: position); + Marker marker = + Marker(markerId: markerId, icon: descriptor, position: position); markers[markerId] = marker; } - _addPolyLine(List coordinates, {Color color = Colors.green, String id = "poly"}) { + _addPolyLine(List coordinates, + {Color color = Colors.green, String id = "poly"}) { PolylineId polylineId = PolylineId(id); - Polyline polyline = - Polyline(polylineId: polylineId, color: color, points: coordinates, width: 5); + Polyline polyline = Polyline( + polylineId: polylineId, color: color, points: coordinates, width: 5); polylines[polylineId] = polyline; setState(() {}); } @@ -655,8 +673,9 @@ class RoutesApiMapScreenState extends State { }); try { - RoutesApiResponse response = await polylinePoints.getRouteBetweenCoordinatesV2( - request: RequestConverter.createEnhancedRequest( + RoutesApiResponse response = + await polylinePoints.getRouteBetweenCoordinatesV2( + request: RequestConverter.createEnhancedRequest( origin: PointLatLng(_originLatitude, _originLongitude), destination: PointLatLng(_destLatitude, _destLongitude), )); @@ -669,8 +688,9 @@ class RoutesApiMapScreenState extends State { final route = response.routes.first; if (route.polylinePoints != null) { final points = polylinePoints.convertToLegacyResult(response).points; - final coordinates = - points.map((point) => LatLng(point.latitude, point.longitude)).toList(); + final coordinates = points + .map((point) => LatLng(point.latitude, point.longitude)) + .toList(); _addPolyLine(coordinates, color: Colors.green); } } else { @@ -698,8 +718,9 @@ class RoutesApiMapScreenState extends State { }); try { - RoutesApiResponse response = await polylinePoints.getRouteBetweenCoordinatesV2( - request: RequestConverter.createEnhancedRequest( + RoutesApiResponse response = + await polylinePoints.getRouteBetweenCoordinatesV2( + request: RequestConverter.createEnhancedRequest( origin: PointLatLng(_originLatitude, _originLongitude), destination: PointLatLng(_destLatitude, _destLongitude), waypoints: [PolylineWayPoint(location: "Sabo, Yaba Lagos Nigeria")], @@ -766,7 +787,8 @@ class TwoWheelerMapScreenState extends State { void initState() { super.initState(); polylinePointsV2 = PolylinePoints.enhanced(widget.apiKey); - _addMarker(LatLng(_originLatitude, _originLongitude), "origin", BitmapDescriptor.defaultMarker); + _addMarker(LatLng(_originLatitude, _originLongitude), "origin", + BitmapDescriptor.defaultMarker); _addMarker(LatLng(_destLatitude, _destLongitude), "destination", BitmapDescriptor.defaultMarkerWithHue(90)); _getTwoWheelerRoute(); @@ -778,8 +800,8 @@ class TwoWheelerMapScreenState extends State { body: Stack( children: [ GoogleMap( - initialCameraPosition: - CameraPosition(target: LatLng(_originLatitude, _originLongitude), zoom: 12), + initialCameraPosition: CameraPosition( + target: LatLng(_originLatitude, _originLongitude), zoom: 12), myLocationEnabled: true, tiltGesturesEnabled: true, compassEnabled: true, @@ -843,7 +865,8 @@ class TwoWheelerMapScreenState extends State { children: [ Expanded( child: CheckboxListTile( - title: const Text('Avoid Highways', style: TextStyle(fontSize: 12)), + title: const Text('Avoid Highways', + style: TextStyle(fontSize: 12)), value: avoidHighways, onChanged: (value) { setState(() { @@ -856,7 +879,8 @@ class TwoWheelerMapScreenState extends State { ), Expanded( child: CheckboxListTile( - title: const Text('Avoid Tolls', style: TextStyle(fontSize: 12)), + title: const Text('Avoid Tolls', + style: TextStyle(fontSize: 12)), value: avoidTolls, onChanged: (value) { setState(() { @@ -869,7 +893,8 @@ class TwoWheelerMapScreenState extends State { ), ], ), - if (currentResponse != null && currentResponse!.routes.isNotEmpty) + if (currentResponse != null && + currentResponse!.routes.isNotEmpty) ..._buildRouteInfo(), const SizedBox(height: 8), ElevatedButton.icon( @@ -900,13 +925,15 @@ class TwoWheelerMapScreenState extends State { children: [ Column( children: [ - const Text('Duration', style: TextStyle(fontWeight: FontWeight.bold)), + const Text('Duration', + style: TextStyle(fontWeight: FontWeight.bold)), Text(route.duration?.toString() ?? 'N/A'), ], ), Column( children: [ - const Text('Distance', style: TextStyle(fontWeight: FontWeight.bold)), + const Text('Distance', + style: TextStyle(fontWeight: FontWeight.bold)), Text(route.distanceMeters != null ? '${(route.distanceMeters! / 1000).toStringAsFixed(1)} km' : 'N/A'), @@ -923,14 +950,15 @@ class TwoWheelerMapScreenState extends State { _addMarker(LatLng position, String id, BitmapDescriptor descriptor) { MarkerId markerId = MarkerId(id); - Marker marker = Marker(markerId: markerId, icon: descriptor, position: position); + Marker marker = + Marker(markerId: markerId, icon: descriptor, position: position); markers[markerId] = marker; } _addPolyLine(List coordinates) { PolylineId id = const PolylineId("poly"); - Polyline polyline = - Polyline(polylineId: id, color: Colors.orange, points: coordinates, width: 5); + Polyline polyline = Polyline( + polylineId: id, color: Colors.orange, points: coordinates, width: 5); polylines[id] = polyline; setState(() {}); } @@ -944,8 +972,9 @@ class TwoWheelerMapScreenState extends State { }); try { - RoutesApiResponse response = await polylinePointsV2.getRouteBetweenCoordinatesV2( - request: RequestConverter.createEnhancedRequest( + RoutesApiResponse response = + await polylinePointsV2.getRouteBetweenCoordinatesV2( + request: RequestConverter.createEnhancedRequest( origin: PointLatLng(_originLatitude, _originLongitude), destination: PointLatLng(_destLatitude, _destLongitude), travelMode: TravelMode.twoWheeler, @@ -958,9 +987,11 @@ class TwoWheelerMapScreenState extends State { if (response.routes.isNotEmpty) { final route = response.routes.first; if (route.polylinePoints != null) { - final points = polylinePointsV2.convertToLegacyResult(response).points; - final coordinates = - points.map((point) => LatLng(point.latitude, point.longitude)).toList(); + final points = + polylinePointsV2.convertToLegacyResult(response).points; + final coordinates = points + .map((point) => LatLng(point.latitude, point.longitude)) + .toList(); _addPolyLine(coordinates); } } else { diff --git a/lib/src/commons/point_lat_lng.dart b/lib/src/commons/point_lat_lng.dart index 2b0364e..0f805b8 100644 --- a/lib/src/commons/point_lat_lng.dart +++ b/lib/src/commons/point_lat_lng.dart @@ -1,8 +1,5 @@ - - /// A pair of latitude and longitude coordinates, stored as degrees. class PointLatLng { - /// Creates a geographical location specified in degrees [latitude] and /// [longitude]. /// @@ -20,4 +17,4 @@ class PointLatLng { String toString() { return "lat: $latitude / longitude: $longitude"; } -} \ No newline at end of file +} diff --git a/lib/src/network/network_provider.dart b/lib/src/network/network_provider.dart index a815f6b..ef38d07 100644 --- a/lib/src/network/network_provider.dart +++ b/lib/src/network/network_provider.dart @@ -15,8 +15,10 @@ import '../utils/request_converter.dart'; /// Provides backward compatibility while enabling access to new Routes API features // ignore_for_file: deprecated_member_use_from_same_package class NetworkProvider { - static const String _directionsBaseUrl = 'https://maps.googleapis.com/maps/api/directions/json'; - static const String _routesBaseUrl = 'https://routes.googleapis.com/directions/v2:computeRoutes'; + static const String _directionsBaseUrl = + 'https://maps.googleapis.com/maps/api/directions/json'; + static const String _routesBaseUrl = + 'https://routes.googleapis.com/directions/v2:computeRoutes'; /// Default timeout for HTTP requests static const Duration _defaultTimeout = Duration(seconds: 30); @@ -105,8 +107,9 @@ class NetworkProvider { // Add waypoints if present if (request.wayPoints.isNotEmpty) { - final waypoints = - request.wayPoints.map((wp) => '${wp.stopOver ? '' : 'via:'}${wp.location}').join('|'); + final waypoints = request.wayPoints + .map((wp) => '${wp.stopOver ? '' : 'via:'}${wp.location}') + .join('|'); queryParams['waypoints'] = waypoints; } @@ -120,7 +123,8 @@ class NetworkProvider { queryParams['avoid'] = _buildAvoidString(request); } if (request.departureTime != null) { - queryParams['departure_time'] = (request.departureTime! ~/ 1000).toString(); + queryParams['departure_time'] = + (request.departureTime! ~/ 1000).toString(); } if (request.arrivalTime != null) { queryParams['arrival_time'] = (request.arrivalTime! ~/ 1000).toString(); @@ -140,9 +144,12 @@ class NetworkProvider { /// Build avoid string for Directions API static String _buildAvoidString(PolylineRequest request) { final avoidList = []; - if (request.avoidFeatures.contains(AvoidFeature.tolls)) avoidList.add('tolls'); - if (request.avoidFeatures.contains(AvoidFeature.highways)) avoidList.add('highways'); - if (request.avoidFeatures.contains(AvoidFeature.ferries)) avoidList.add('ferries'); + if (request.avoidFeatures.contains(AvoidFeature.tolls)) + avoidList.add('tolls'); + if (request.avoidFeatures.contains(AvoidFeature.highways)) + avoidList.add('highways'); + if (request.avoidFeatures.contains(AvoidFeature.ferries)) + avoidList.add('ferries'); return avoidList.join('|'); } @@ -207,14 +214,24 @@ class NetworkProvider { points: PolylineDecoder.run(encodedPoints), status: status, overviewPolyline: route['overview_polyline']['points'], - totalDistanceValue: - route['legs'].map((leg) => leg['distance']['value']).reduce((v1, v2) => v1 + v2), - distanceTexts: [...route['legs'].map((leg) => leg['distance']['text'])], - distanceValues: [...route['legs'].map((leg) => leg['distance']['value'])], - totalDurationValue: - route['legs'].map((leg) => leg['duration']['value']).reduce((v1, v2) => v1 + v2), - durationTexts: [...route['legs'].map((leg) => leg['duration']['text'])], - durationValues: [...route['legs'].map((leg) => leg['duration']['value'])], + totalDistanceValue: route['legs'] + .map((leg) => leg['distance']['value']) + .reduce((v1, v2) => v1 + v2), + distanceTexts: [ + ...route['legs'].map((leg) => leg['distance']['text']) + ], + distanceValues: [ + ...route['legs'].map((leg) => leg['distance']['value']) + ], + totalDurationValue: route['legs'] + .map((leg) => leg['duration']['value']) + .reduce((v1, v2) => v1 + v2), + durationTexts: [ + ...route['legs'].map((leg) => leg['duration']['text']) + ], + durationValues: [ + ...route['legs'].map((leg) => leg['duration']['value']) + ], endAddress: route["legs"].last['end_address'], startAddress: route["legs"].first['start_address'], ); @@ -232,7 +249,8 @@ class NetworkProvider { mode: TravelMode.driving, ); - await getRouteBetweenCoordinates(apiKey, testRequest, timeout: Duration(seconds: 10)); + await getRouteBetweenCoordinates(apiKey, testRequest, + timeout: Duration(seconds: 10)); results['directions_api'] = true; } catch (e) { results['directions_api'] = false; @@ -245,7 +263,8 @@ class NetworkProvider { destination: PointLatLng(37.7849, -122.4094), ); - await getRouteBetweenCoordinatesV2(apiKey, testRequest, timeout: Duration(seconds: 10)); + await getRouteBetweenCoordinatesV2(apiKey, testRequest, + timeout: Duration(seconds: 10)); results['routes_api'] = true; } catch (e) { results['routes_api'] = false; diff --git a/lib/src/network/network_util.dart b/lib/src/network/network_util.dart index b500fe7..96dae49 100644 --- a/lib/src/network/network_util.dart +++ b/lib/src/network/network_util.dart @@ -24,7 +24,8 @@ class NetworkUtil { headers: request.headers, ) .timeout(request.timeoutDuration, - onTimeout: () => throw Exception("Request timed out after the specified duration.")); + onTimeout: () => throw Exception( + "Request timed out after the specified duration.")); if (response.statusCode == 200) { var parsedJson = json.decode(response.body); @@ -37,15 +38,25 @@ class NetworkUtil { points: PolylineDecoder.run(route["overview_polyline"]["points"]), errorMessage: "", status: parsedJson["status"], - totalDistanceValue: - route['legs'].map((leg) => leg['distance']['value']).reduce((v1, v2) => v1 + v2), - distanceTexts: [...route['legs'].map((leg) => leg['distance']['text'])], - distanceValues: [...route['legs'].map((leg) => leg['distance']['value'])], + totalDistanceValue: route['legs'] + .map((leg) => leg['distance']['value']) + .reduce((v1, v2) => v1 + v2), + distanceTexts: [ + ...route['legs'].map((leg) => leg['distance']['text']) + ], + distanceValues: [ + ...route['legs'].map((leg) => leg['distance']['value']) + ], overviewPolyline: route["overview_polyline"]["points"], - totalDurationValue: - route['legs'].map((leg) => leg['duration']['value']).reduce((v1, v2) => v1 + v2), - durationTexts: [...route['legs'].map((leg) => leg['duration']['text'])], - durationValues: [...route['legs'].map((leg) => leg['duration']['value'])], + totalDurationValue: route['legs'] + .map((leg) => leg['duration']['value']) + .reduce((v1, v2) => v1 + v2), + durationTexts: [ + ...route['legs'].map((leg) => leg['duration']['text']) + ], + durationValues: [ + ...route['legs'].map((leg) => leg['duration']['value']) + ], endAddress: route["legs"].last['end_address'], startAddress: route["legs"].first['start_address'], )); diff --git a/lib/src/polyline_points.dart b/lib/src/polyline_points.dart index 14978fc..c73337c 100644 --- a/lib/src/polyline_points.dart +++ b/lib/src/polyline_points.dart @@ -93,7 +93,6 @@ class PolylinePoints { required RoutesApiRequest request, Duration? timeout, }) async { - return NetworkProvider.getRouteBetweenCoordinatesV2( apiKey, request, @@ -116,8 +115,9 @@ class PolylinePoints { } final route = response.routes.first; - final points = - route.polylineEncoded != null ? PolylineDecoder.run(route.polylineEncoded!) : []; + final points = route.polylineEncoded != null + ? PolylineDecoder.run(route.polylineEncoded!) + : []; return PolylineResult( points: points, @@ -151,5 +151,4 @@ class PolylinePoints { static List decodePolyline(String encodedString) { return PolylineDecoder.run(encodedString); } - } diff --git a/lib/src/routes_api/enums/polyline_quality.dart b/lib/src/routes_api/enums/polyline_quality.dart index e0fa2a8..93870f3 100644 --- a/lib/src/routes_api/enums/polyline_quality.dart +++ b/lib/src/routes_api/enums/polyline_quality.dart @@ -4,16 +4,16 @@ enum PolylineQuality { /// High-quality polyline with detailed geometry /// Recommended for precise route visualization highQuality('HIGH_QUALITY'), - + /// Overview polyline with simplified geometry /// Suitable for route overviews and reduced data usage overview('OVERVIEW'); const PolylineQuality(this.value); - + /// The string value used in API requests final String value; - + /// Convert from string value to enum static PolylineQuality? fromString(String value) { for (PolylineQuality quality in PolylineQuality.values) { @@ -23,7 +23,7 @@ enum PolylineQuality { } return null; } - + /// Get a description of the polyline quality String get description { switch (this) { @@ -33,7 +33,7 @@ enum PolylineQuality { return 'Simplified polyline for route overview with reduced data usage'; } } - + /// Recommended use case for this quality level String get useCase { switch (this) { @@ -43,4 +43,4 @@ enum PolylineQuality { return 'Route overview, list views, reduced bandwidth scenarios'; } } -} \ No newline at end of file +} diff --git a/lib/src/routes_api/enums/routing_preference.dart b/lib/src/routes_api/enums/routing_preference.dart index efaa9e3..a336962 100644 --- a/lib/src/routes_api/enums/routing_preference.dart +++ b/lib/src/routes_api/enums/routing_preference.dart @@ -6,18 +6,18 @@ enum RoutingPreference { /// Prioritize routes with the shortest travel time trafficUnaware('TRAFFIC_UNAWARE'), - + /// Consider current traffic conditions for optimal time trafficAware('TRAFFIC_AWARE'), - + /// Prioritize routes that avoid traffic when possible trafficAwareOptimal('TRAFFIC_AWARE_OPTIMAL'); const RoutingPreference(this.value); - + /// The string value used in API requests final String value; - + /// Convert from string value to enum static RoutingPreference? fromString(String value) { for (RoutingPreference preference in RoutingPreference.values) { @@ -27,7 +27,7 @@ enum RoutingPreference { } return null; } - + /// Get a human-readable description of the routing preference String get description { switch (this) { @@ -41,4 +41,4 @@ enum RoutingPreference { return 'Best route avoiding heavy traffic when possible'; } } -} \ No newline at end of file +} diff --git a/lib/src/routes_api/enums/units.dart b/lib/src/routes_api/enums/units.dart index 61018b4..1b6689a 100644 --- a/lib/src/routes_api/enums/units.dart +++ b/lib/src/routes_api/enums/units.dart @@ -2,15 +2,15 @@ enum Units { /// Metric system (kilometers, meters) metric('METRIC'), - + /// Imperial system (miles, feet) imperial('IMPERIAL'); const Units(this.value); - + /// The string value used in API requests final String value; - + /// Convert from string value to enum static Units? fromString(String value) { for (Units unit in Units.values) { @@ -20,7 +20,7 @@ enum Units { } return null; } - + /// Get the distance unit for this system String get distanceUnit { switch (this) { @@ -30,7 +30,7 @@ enum Units { return 'mi'; } } - + /// Get the short distance unit for this system String get shortDistanceUnit { switch (this) { @@ -40,4 +40,4 @@ enum Units { return 'ft'; } } -} \ No newline at end of file +} diff --git a/lib/src/routes_api/routes_request.dart b/lib/src/routes_api/routes_request.dart index d05449a..686a7a5 100644 --- a/lib/src/routes_api/routes_request.dart +++ b/lib/src/routes_api/routes_request.dart @@ -11,46 +11,46 @@ import 'route_modifiers.dart'; class RoutesApiRequest { /// Starting point of the route final PointLatLng origin; - + /// Ending point of the route final PointLatLng destination; - + /// Travel mode for the route final TravelMode travelMode; - + /// Intermediate waypoints along the route final List? intermediates; - + /// Whether to compute alternative routes final bool computeAlternativeRoutes; - + /// Route calculation preferences final RoutingPreference routingPreference; - + /// Unit system for distances and durations final Units units; - + /// Quality level for the returned polyline final PolylineQuality polylineQuality; - + /// Route modifiers (avoidances, vehicle info, etc.) final RouteModifiers? routeModifiers; - + /// Language code for localized text (e.g., 'en', 'es', 'fr') final String? languageCode; - + /// Region code for localization (e.g., 'US', 'GB') final String? regionCode; - + /// Departure time for transit or traffic-aware routing final DateTime? departureTime; - + /// Arrival time for transit routing final DateTime? arrivalTime; - + /// Whether to optimize waypoint order final bool optimizeWaypointOrder; - + /// Extra computations to include in the response final List? extraComputations; @@ -114,7 +114,7 @@ class RoutesApiRequest { 'via': !waypoint.stopOver, }) .toList(); - + if (optimizeWaypointOrder) { json['optimizeWaypointOrder'] = true; } @@ -143,9 +143,8 @@ class RoutesApiRequest { // Add extra computations if (extraComputations != null && extraComputations!.isNotEmpty) { - json['extraComputations'] = extraComputations! - .map((computation) => computation.value) - .toList(); + json['extraComputations'] = + extraComputations!.map((computation) => computation.value).toList(); } // Add custom body parameters @@ -188,7 +187,7 @@ class RoutesApiRequest { final fields = [ // Essential route information only 'routes.duration', - 'routes.staticDuration', + 'routes.staticDuration', 'routes.distanceMeters', 'routes.polyline.encodedPolyline', ]; @@ -222,7 +221,8 @@ class RoutesApiRequest { destination: destination ?? this.destination, travelMode: travelMode ?? this.travelMode, intermediates: intermediates ?? this.intermediates, - computeAlternativeRoutes: computeAlternativeRoutes ?? this.computeAlternativeRoutes, + computeAlternativeRoutes: + computeAlternativeRoutes ?? this.computeAlternativeRoutes, routingPreference: routingPreference ?? this.routingPreference, units: units ?? this.units, polylineQuality: polylineQuality ?? this.polylineQuality, @@ -231,7 +231,8 @@ class RoutesApiRequest { regionCode: regionCode ?? this.regionCode, departureTime: departureTime ?? this.departureTime, arrivalTime: arrivalTime ?? this.arrivalTime, - optimizeWaypointOrder: optimizeWaypointOrder ?? this.optimizeWaypointOrder, + optimizeWaypointOrder: + optimizeWaypointOrder ?? this.optimizeWaypointOrder, extraComputations: extraComputations ?? this.extraComputations, responseFieldMask: responseFieldMask ?? this.responseFieldMask, customBodyParameters: customBodyParameters ?? this.customBodyParameters, @@ -295,20 +296,20 @@ class RoutesApiRequest { enum ExtraComputation { /// Include toll information in the response tolls('TOLLS'), - + /// Include fuel consumption estimates fuelConsumption('FUEL_CONSUMPTION'), - + /// Include traffic information along the polyline trafficOnPolyline('TRAFFIC_ON_POLYLINE'), - + /// Include HTML-formatted navigation instructions htmlFormattedNavigationInstructions('HTML_FORMATTED_NAVIGATION_INSTRUCTIONS'); const ExtraComputation(this.value); - + final String value; - + static ExtraComputation? fromString(String value) { for (ExtraComputation computation in ExtraComputation.values) { if (computation.value == value) { @@ -317,4 +318,4 @@ enum ExtraComputation { } return null; } -} \ No newline at end of file +} diff --git a/lib/src/routes_api/routes_response.dart b/lib/src/routes_api/routes_response.dart index 4a7629b..56de674 100644 --- a/lib/src/routes_api/routes_response.dart +++ b/lib/src/routes_api/routes_response.dart @@ -28,29 +28,32 @@ class RoutesApiResponse { try { return RoutesApiResponse( routes: json['routes'] != null - ? (json['routes'] as List).map((route) => Route.fromJson(route)).toList() + ? (json['routes'] as List) + .map((route) => Route.fromJson(route)) + .toList() : [], rawJson: json, status: json['status'], errorMessage: json['errorMessage'], ); - } catch (e) { + } catch (e) { return RoutesApiResponse.error('Error parsing JSON: $e'); } } factory RoutesApiResponse.error(String errorMessage) => RoutesApiResponse( - routes: [], - rawJson: {}, - status: null, - errorMessage: errorMessage, - ); + routes: [], + rawJson: {}, + status: null, + errorMessage: errorMessage, + ); /// Get the primary (first) route Route? get primaryRoute => routes.isNotEmpty ? routes.first : null; /// Get alternative routes (excluding the primary route) - List get alternativeRoutes => routes.length > 1 ? routes.skip(1).toList() : []; + List get alternativeRoutes => + routes.length > 1 ? routes.skip(1).toList() : []; /// Check if the response contains any routes bool get hasRoutes => routes.isNotEmpty; @@ -116,7 +119,7 @@ class Route { /// Create from JSON response factory Route.fromJson(Map json) { final polylineEncoded = json['polyline']?['encodedPolyline']; - + return Route( duration: json['duration'] != null ? int.tryParse(json['duration'].toString().replaceAll('s', '')) diff --git a/lib/src/routes_api/toll_info.dart b/lib/src/routes_api/toll_info.dart index 43e5a32..e094a6c 100644 --- a/lib/src/routes_api/toll_info.dart +++ b/lib/src/routes_api/toll_info.dart @@ -2,10 +2,10 @@ class TollInfo { /// Estimated toll costs for the route final List? estimatedPrice; - + /// Whether the route contains toll roads final bool hasTolls; - + /// Toll passes that can be used on this route final List? applicableTollPasses; @@ -44,7 +44,7 @@ class TollInfo { /// Get the total estimated toll cost in the specified currency double? getTotalCost([String? currencyCode]) { if (estimatedPrice == null || estimatedPrice!.isEmpty) return null; - + if (currencyCode != null) { final costs = estimatedPrice! .where((cost) => cost.currencyCode == currencyCode) @@ -52,7 +52,7 @@ class TollInfo { if (costs.isEmpty) return null; return costs.fold(0.0, (sum, cost) => sum + cost.amount); } - + // Return the first currency's total if no specific currency requested final firstCurrency = estimatedPrice!.first.currencyCode; return estimatedPrice! @@ -63,10 +63,7 @@ class TollInfo { /// Get all available currencies for toll costs List getAvailableCurrencies() { if (estimatedPrice == null) return []; - return estimatedPrice! - .map((cost) => cost.currencyCode) - .toSet() - .toList(); + return estimatedPrice!.map((cost) => cost.currencyCode).toSet().toList(); } @override @@ -101,13 +98,13 @@ class TollInfo { class TollCost { /// The monetary amount of the toll final double amount; - + /// The currency code (e.g., 'USD', 'EUR') final String currencyCode; - + /// Human-readable description of the toll final String? description; - + /// The toll plaza or section name final String? tollPlaza; @@ -148,7 +145,7 @@ class TollCost { 'CAD': r'C$', 'AUD': r'A$', }; - + final symbol = symbols[currencyCode] ?? currencyCode; return '$symbol${amount.toStringAsFixed(2)}'; } @@ -177,4 +174,4 @@ class TollCost { String toString() { return 'TollCost(${formatCurrency()}${description != null ? ' - $description' : ''})'; } -} \ No newline at end of file +} diff --git a/lib/src/routes_api/traffic_info.dart b/lib/src/routes_api/traffic_info.dart index fed6685..0044a4e 100644 --- a/lib/src/routes_api/traffic_info.dart +++ b/lib/src/routes_api/traffic_info.dart @@ -2,19 +2,19 @@ class TrafficInfo { /// Current traffic conditions along the route final TrafficCondition overallCondition; - + /// Detailed traffic segments with specific conditions final List? segments; - + /// Traffic-aware duration in seconds final int? durationInTraffic; - + /// Traffic-aware duration as human-readable text final String? durationInTrafficText; - + /// Delay caused by traffic in seconds final int? trafficDelay; - + /// Last updated timestamp for traffic data final DateTime? lastUpdated; @@ -30,9 +30,9 @@ class TrafficInfo { /// Create from JSON response factory TrafficInfo.fromJson(Map json) { return TrafficInfo( - overallCondition: TrafficCondition.fromString( - json['overallCondition'] ?? 'UNKNOWN' - ) ?? TrafficCondition.unknown, + overallCondition: + TrafficCondition.fromString(json['overallCondition'] ?? 'UNKNOWN') ?? + TrafficCondition.unknown, segments: json['segments'] != null ? (json['segments'] as List) .map((segment) => TrafficSegment.fromJson(segment)) @@ -54,7 +54,8 @@ class TrafficInfo { if (segments != null) 'segments': segments!.map((segment) => segment.toJson()).toList(), if (durationInTraffic != null) 'durationInTraffic': durationInTraffic, - if (durationInTrafficText != null) 'durationInTrafficText': durationInTrafficText, + if (durationInTrafficText != null) + 'durationInTrafficText': durationInTrafficText, if (trafficDelay != null) 'trafficDelay': trafficDelay, if (lastUpdated != null) 'lastUpdated': lastUpdated!.toIso8601String(), }; @@ -76,7 +77,7 @@ class TrafficInfo { /// Get the most severe traffic condition from segments TrafficCondition get worstCondition { if (segments == null || segments!.isEmpty) return overallCondition; - + TrafficCondition worst = TrafficCondition.light; for (final segment in segments!) { if (segment.condition.severity > worst.severity) { @@ -124,16 +125,16 @@ class TrafficInfo { class TrafficSegment { /// Traffic condition for this segment final TrafficCondition condition; - + /// Start index in the route polyline final int startPolylinePointIndex; - + /// End index in the route polyline final int endPolylinePointIndex; - + /// Length of this segment in meters final double? lengthMeters; - + /// Speed in this segment (km/h) final double? speedKmh; @@ -148,9 +149,8 @@ class TrafficSegment { /// Create from JSON response factory TrafficSegment.fromJson(Map json) { return TrafficSegment( - condition: TrafficCondition.fromString( - json['condition'] ?? 'UNKNOWN' - ) ?? TrafficCondition.unknown, + condition: TrafficCondition.fromString(json['condition'] ?? 'UNKNOWN') ?? + TrafficCondition.unknown, startPolylinePointIndex: json['startPolylinePointIndex'] ?? 0, endPolylinePointIndex: json['endPolylinePointIndex'] ?? 0, lengthMeters: json['lengthMeters']?.toDouble(), @@ -196,30 +196,30 @@ class TrafficSegment { enum TrafficCondition { /// Light traffic, normal flow light('LIGHT', 1, 'Light traffic'), - + /// Moderate traffic, some delays moderate('MODERATE', 2, 'Moderate traffic'), - + /// Heavy traffic, significant delays heavy('HEAVY', 3, 'Heavy traffic'), - + /// Severe traffic, major delays severe('SEVERE', 4, 'Severe traffic'), - + /// Unknown traffic condition unknown('UNKNOWN', 0, 'Unknown'); const TrafficCondition(this.value, this.severity, this.description); - + /// The string value used in API responses final String value; - + /// Numeric severity level (higher = worse) final int severity; - + /// Human-readable description final String description; - + /// Convert from string value to enum static TrafficCondition? fromString(String value) { for (TrafficCondition condition in TrafficCondition.values) { @@ -229,7 +229,7 @@ enum TrafficCondition { } return null; } - + /// Get color representation for UI display String get colorHex { switch (this) { @@ -245,9 +245,9 @@ enum TrafficCondition { return '#9E9E9E'; // Grey } } - + /// Check if this condition indicates congestion bool get isCongested { return severity >= 2; // Moderate or worse } -} \ No newline at end of file +} diff --git a/lib/src/utils/polyline_request.dart b/lib/src/utils/polyline_request.dart index f9951ca..49497de 100644 --- a/lib/src/utils/polyline_request.dart +++ b/lib/src/utils/polyline_request.dart @@ -97,7 +97,7 @@ class PolylineRequest { params.addAll({"waypoints": wayPointsString}); } - if(avoidFeatures.isNotEmpty) { + if (avoidFeatures.isNotEmpty) { params.addAll({ "avoid": avoidFeatures.map((feature) => feature.name).join('|'), }); diff --git a/lib/src/utils/polyline_waypoint.dart b/lib/src/utils/polyline_waypoint.dart index e3e2171..a59b3f1 100644 --- a/lib/src/utils/polyline_waypoint.dart +++ b/lib/src/utils/polyline_waypoint.dart @@ -1,4 +1,3 @@ - /// description: /// project: flutter_polyline_points /// @package: diff --git a/lib/src/utils/request_converter.dart b/lib/src/utils/request_converter.dart index 8952b4e..fb5b28e 100644 --- a/lib/src/utils/request_converter.dart +++ b/lib/src/utils/request_converter.dart @@ -22,13 +22,16 @@ class RequestConverter { computeAlternativeRoutes: legacyRequest.alternatives, routingPreference: _determineRoutingPreference(legacyRequest), units: _determineUnits(legacyRequest), - polylineQuality: PolylineQuality.overview, // Default for legacy compatibility + polylineQuality: + PolylineQuality.overview, // Default for legacy compatibility routeModifiers: _createRouteModifiers(legacyRequest), - departureTime: legacyRequest.departureTime != null - ? DateTime.fromMillisecondsSinceEpoch(legacyRequest.departureTime! * 1000) + departureTime: legacyRequest.departureTime != null + ? DateTime.fromMillisecondsSinceEpoch( + legacyRequest.departureTime! * 1000) : null, - arrivalTime: legacyRequest.arrivalTime != null - ? DateTime.fromMillisecondsSinceEpoch(legacyRequest.arrivalTime! * 1000) + arrivalTime: legacyRequest.arrivalTime != null + ? DateTime.fromMillisecondsSinceEpoch( + legacyRequest.arrivalTime! * 1000) : null, optimizeWaypointOrder: legacyRequest.optimizeWaypoints, ); @@ -42,11 +45,11 @@ class RequestConverter { mode: routesRequest.travelMode, wayPoints: routesRequest.intermediates ?? [], alternatives: routesRequest.computeAlternativeRoutes, - departureTime: routesRequest.departureTime?.millisecondsSinceEpoch != null - ? (routesRequest.departureTime!.millisecondsSinceEpoch ~/ 1000) + departureTime: routesRequest.departureTime?.millisecondsSinceEpoch != null + ? (routesRequest.departureTime!.millisecondsSinceEpoch ~/ 1000) : null, - arrivalTime: routesRequest.arrivalTime?.millisecondsSinceEpoch != null - ? (routesRequest.arrivalTime!.millisecondsSinceEpoch ~/ 1000) + arrivalTime: routesRequest.arrivalTime?.millisecondsSinceEpoch != null + ? (routesRequest.arrivalTime!.millisecondsSinceEpoch ~/ 1000) : null, optimizeWaypoints: routesRequest.optimizeWaypointOrder, avoidFeatures: _createAvoidFeatures(routesRequest.routeModifiers), @@ -55,7 +58,8 @@ class RequestConverter { } /// Determine routing preference based on legacy request parameters - static RoutingPreference _determineRoutingPreference(PolylineRequest legacyRequest) { + static RoutingPreference _determineRoutingPreference( + PolylineRequest legacyRequest) { // Legacy API doesn't have explicit routing preferences // Use traffic-unaware as default for compatibility return RoutingPreference.trafficUnaware; @@ -74,22 +78,25 @@ class RequestConverter { return RouteModifiers( avoidTolls: legacyRequest.avoidFeatures.contains(AvoidFeature.tolls), - avoidHighways: legacyRequest.avoidFeatures.contains(AvoidFeature.highways), + avoidHighways: + legacyRequest.avoidFeatures.contains(AvoidFeature.highways), avoidFerries: legacyRequest.avoidFeatures.contains(AvoidFeature.ferries), avoidIndoor: legacyRequest.avoidFeatures.contains(AvoidFeature.indoor), ); } /// Create avoid features list from route modifiers - static List _createAvoidFeatures(RouteModifiers? routeModifiers) { + static List _createAvoidFeatures( + RouteModifiers? routeModifiers) { if (routeModifiers == null) return []; - + final features = []; if (routeModifiers.avoidTolls == true) features.add(AvoidFeature.tolls); - if (routeModifiers.avoidHighways == true) features.add(AvoidFeature.highways); + if (routeModifiers.avoidHighways == true) + features.add(AvoidFeature.highways); if (routeModifiers.avoidFerries == true) features.add(AvoidFeature.ferries); if (routeModifiers.avoidIndoor == true) features.add(AvoidFeature.indoor); - + return features; } @@ -164,7 +171,7 @@ class RequestConverter { List? extraComputations, }) { final baseRequest = convertToRoutesApi(legacyRequest); - + return baseRequest.copyWith( routingPreference: routingPreference ?? baseRequest.routingPreference, units: units ?? baseRequest.units, @@ -218,12 +225,13 @@ class RequestConverter { units: units, polylineQuality: polylineQuality, routeModifiers: routeModifiers, - extraComputations: extraComputations ?? [ - ExtraComputation.tolls, - ExtraComputation.trafficOnPolyline, - ], + extraComputations: extraComputations ?? + [ + ExtraComputation.tolls, + ExtraComputation.trafficOnPolyline, + ], languageCode: languageCode, regionCode: regionCode, ); } -} \ No newline at end of file +} diff --git a/test/flutter_polyline_points_test.dart b/test/flutter_polyline_points_test.dart deleted file mode 100644 index 6fa78f4..0000000 --- a/test/flutter_polyline_points_test.dart +++ /dev/null @@ -1,44 +0,0 @@ -import 'package:flutter_polyline_points/src/constants.dart'; -import 'package:flutter_test/flutter_test.dart'; - -import 'package:flutter_polyline_points/flutter_polyline_points.dart'; - -//ignore_for_file: deprecated_member_use_from_same_package -void main() { - final key = Constants.API_KEY; - - test('get list of coordinates from two geographical positions', () async { - final result = await PolylinePoints(apiKey: key).getRouteBetweenCoordinates( - request: PolylineRequest( - origin: PointLatLng(6.5212402, 3.3679965), - destination: PointLatLng(6.595680, 3.337030), - mode: TravelMode.driving), - ); - expect(result.points.isNotEmpty, isTrue); - }); - - test('get routes with RoutesApi', () async { - final response = await PolylinePoints(apiKey: key).getRouteBetweenCoordinatesV2( - request: RoutesApiRequest( - origin: PointLatLng(6.5212402, 3.3679965), - destination: PointLatLng(6.595680, 3.337030), - travelMode: TravelMode.driving, - routingPreference: RoutingPreference.trafficAwareOptimal, - units: Units.imperial, - extraComputations: [ExtraComputation.tolls], - optimizeWaypointOrder: true, - ), - ); - - expect(response.routes.isNotEmpty, isTrue); - expect(response.status, equals("OK")); - - expect(response.routes.first.polylinePoints!.isNotEmpty, isTrue); - - }); - - test('get list of coordinates from an encoded String', () { - List points = PolylinePoints.decodePolyline("_p~iF~ps|U_ulLnnqC_mqNvxq`@"); - expect(points.isNotEmpty, isTrue); - }); -} diff --git a/test/integration/polyline_integration_test.dart b/test/integration/polyline_integration_test.dart index c9f751a..902c869 100644 --- a/test/integration/polyline_integration_test.dart +++ b/test/integration/polyline_integration_test.dart @@ -54,7 +54,8 @@ void main() { expect(json['travelMode'], equals('WALK')); expect(json['intermediates'], hasLength(2)); expect(json['computeAlternativeRoutes'], isTrue); - expect(json['routingPreference'], equals("ROUTING_PREFERENCE_UNSPECIFIED")); + expect(json['routingPreference'], + equals("ROUTING_PREFERENCE_UNSPECIFIED")); expect(json['units'], equals('IMPERIAL')); expect(json['polylineQuality'], equals('HIGH_QUALITY')); expect(json['languageCode'], equals('en')); @@ -330,7 +331,8 @@ void main() { // Step 1: Create request with custom headers for Android restricted API key final customHeaders = { 'X-Android-Package': 'com.example.flutter_polyline_points', - 'X-Android-Cert': 'AA:BB:CC:DD:EE:FF:00:11:22:33:44:55:66:77:88:99:AA:BB:CC:DD', + 'X-Android-Cert': + 'AA:BB:CC:DD:EE:FF:00:11:22:33:44:55:66:77:88:99:AA:BB:CC:DD', }; final request = RoutesApiRequest( @@ -344,10 +346,12 @@ void main() { // Step 2: Verify request has headers expect(request.headers, isNotNull); - expect(request.headers!['X-Android-Package'], + expect(request.headers!['X-Android-Package'], equals('com.example.flutter_polyline_points')); - expect(request.headers!['X-Android-Cert'], - equals('AA:BB:CC:DD:EE:FF:00:11:22:33:44:55:66:77:88:99:AA:BB:CC:DD')); + expect( + request.headers!['X-Android-Cert'], + equals( + 'AA:BB:CC:DD:EE:FF:00:11:22:33:44:55:66:77:88:99:AA:BB:CC:DD')); // Step 3: Verify request JSON structure (headers are not included in body) final requestJson = request.toJson(); diff --git a/test/network/network_provider_test.dart b/test/network/network_provider_test.dart index 08d33b9..72124b9 100644 --- a/test/network/network_provider_test.dart +++ b/test/network/network_provider_test.dart @@ -39,15 +39,19 @@ void main() { ); expect(request.headers, isNotNull); - expect(request.headers!['X-Android-Package'], equals('com.example.testapp')); - expect(request.headers!['X-Android-Cert'], equals('TEST_CERT_FINGERPRINT')); + expect(request.headers!['X-Android-Package'], + equals('com.example.testapp')); + expect(request.headers!['X-Android-Cert'], + equals('TEST_CERT_FINGERPRINT')); expect(request.headers!['Custom-Header'], equals('custom-value')); }); - test('should handle Android-specific headers for restricted API keys', () { + test('should handle Android-specific headers for restricted API keys', + () { final androidHeaders = { 'X-Android-Package': 'com.example.flutter_polyline_points', - 'X-Android-Cert': 'AA:BB:CC:DD:EE:FF:00:11:22:33:44:55:66:77:88:99:AA:BB:CC:DD', + 'X-Android-Cert': + 'AA:BB:CC:DD:EE:FF:00:11:22:33:44:55:66:77:88:99:AA:BB:CC:DD', }; final request = RoutesApiRequest( @@ -58,10 +62,12 @@ void main() { ); expect(request.headers, isNotNull); - expect(request.headers!['X-Android-Package'], + expect(request.headers!['X-Android-Package'], equals('com.example.flutter_polyline_points')); - expect(request.headers!['X-Android-Cert'], - equals('AA:BB:CC:DD:EE:FF:00:11:22:33:44:55:66:77:88:99:AA:BB:CC:DD')); + expect( + request.headers!['X-Android-Cert'], + equals( + 'AA:BB:CC:DD:EE:FF:00:11:22:33:44:55:66:77:88:99:AA:BB:CC:DD')); }); test('should handle multiple custom headers', () { @@ -120,11 +126,13 @@ void main() { // Original request should remain unchanged expect(originalRequest.headers, equals(originalHeaders)); - expect(originalRequest.headers!['X-Android-Package'], equals('com.example.original')); + expect(originalRequest.headers!['X-Android-Package'], + equals('com.example.original')); // Copied request should have new headers expect(copiedRequest.headers, equals(newHeaders)); - expect(copiedRequest.headers!['X-Android-Package'], equals('com.example.updated')); + expect(copiedRequest.headers!['X-Android-Package'], + equals('com.example.updated')); expect(copiedRequest.headers!['New-Header'], equals('new_value')); }); @@ -143,10 +151,12 @@ void main() { expect(request.languageCode, equals('es')); expect(request.headers, equals(customHeaders)); - expect(request.headers!['X-Android-Package'], equals('com.example.app')); + expect( + request.headers!['X-Android-Package'], equals('com.example.app')); }); - test('should handle request with both custom body parameters and headers', () { + test('should handle request with both custom body parameters and headers', + () { final customHeaders = { 'X-Android-Package': 'com.example.app', 'X-Android-Cert': 'cert123', @@ -166,7 +176,7 @@ void main() { expect(request.headers, equals(customHeaders)); expect(request.customBodyParameters, equals(customBodyParams)); - + final json = request.toJson(); expect(json['extraComputations'], contains('TRAFFIC_ON_POLYLINE')); expect(json['customField'], equals('customValue')); @@ -177,8 +187,8 @@ void main() { test('should accept valid header names and values', () { final validHeaders = { 'X-Android-Package': 'com.example.app', - 'X-Android-Cert': 'AA:BB:CC:DD:EE:FF:00:11:22:33:44:55:66:77:88:99:AA:BB:CC:DD', - 'Authorization': 'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9', + 'X-Android-Cert': + 'AA:BB:CC:DD:EE:FF:00:11:22:33:44:55:66:77:88:99:AA:BB:CC:DD', 'Content-Type': 'application/json', 'User-Agent': 'MyApp/1.0.0', 'Accept-Language': 'en-US,en;q=0.9', @@ -199,7 +209,8 @@ void main() { test('should handle special characters in header values', () { final headersWithSpecialChars = { 'X-Android-Package': 'com.example.app_test-123', - 'X-Android-Cert': 'AA:BB:CC:DD:EE:FF:00:11:22:33:44:55:66:77:88:99:AA:BB:CC:DD', + 'X-Android-Cert': + 'AA:BB:CC:DD:EE:FF:00:11:22:33:44:55:66:77:88:99:AA:BB:CC:DD', 'Custom-Header': 'value with spaces and-dashes_underscores', }; @@ -210,9 +221,9 @@ void main() { ); expect(request.headers, equals(headersWithSpecialChars)); - expect(request.headers!['Custom-Header'], + expect(request.headers!['Custom-Header'], equals('value with spaces and-dashes_underscores')); }); }); }); -} \ No newline at end of file +} diff --git a/test/routes_api/routes_request_test.dart b/test/routes_api/routes_request_test.dart index cdb9679..2693fb9 100644 --- a/test/routes_api/routes_request_test.dart +++ b/test/routes_api/routes_request_test.dart @@ -27,7 +27,8 @@ void main() { expect(request.destination, equals(destination)); expect(request.travelMode, equals(TravelMode.driving)); expect(request.computeAlternativeRoutes, isFalse); - expect(request.routingPreference, equals(RoutingPreference.trafficUnaware)); + expect( + request.routingPreference, equals(RoutingPreference.trafficUnaware)); expect(request.units, equals(Units.metric)); expect(request.polylineQuality, equals(PolylineQuality.overview)); expect(request.optimizeWaypointOrder, isFalse); @@ -65,14 +66,16 @@ void main() { expect(request.travelMode, equals(TravelMode.transit)); expect(request.intermediates, equals(intermediates)); expect(request.computeAlternativeRoutes, isTrue); - expect(request.routingPreference, equals(RoutingPreference.trafficAwareOptimal)); + expect(request.routingPreference, + equals(RoutingPreference.trafficAwareOptimal)); expect(request.units, equals(Units.imperial)); expect(request.polylineQuality, equals(PolylineQuality.highQuality)); expect(request.languageCode, equals('en')); expect(request.regionCode, equals('US')); expect(request.departureTime, equals(departureTime)); expect(request.optimizeWaypointOrder, isTrue); - expect(request.responseFieldMask, equals('routes.duration,routes.distanceMeters')); + expect(request.responseFieldMask, + equals('routes.duration,routes.distanceMeters')); expect(request.customBodyParameters, equals(customParams)); expect(request.headers, equals(customHeaders)); }); @@ -91,9 +94,12 @@ void main() { final json = request.toJson(); expect(json['origin']['location']['latLng']['latitude'], equals(37.7749)); - expect(json['origin']['location']['latLng']['longitude'], equals(-122.4194)); - expect(json['destination']['location']['latLng']['latitude'], equals(34.0522)); - expect(json['destination']['location']['latLng']['longitude'], equals(-118.2437)); + expect( + json['origin']['location']['latLng']['longitude'], equals(-122.4194)); + expect(json['destination']['location']['latLng']['latitude'], + equals(34.0522)); + expect(json['destination']['location']['latLng']['longitude'], + equals(-118.2437)); expect(json['travelMode'], equals('WALK')); expect(json['computeAlternativeRoutes'], isTrue); expect(json['languageCode'], equals('es')); @@ -117,8 +123,10 @@ void main() { expect(json['intermediates'], isNotNull); expect(json['intermediates'], hasLength(2)); - expect(json['intermediates'][0]['via'], isFalse); // stopOver: true means via: false - expect(json['intermediates'][1]['via'], isTrue); // stopOver: false means via: true + expect(json['intermediates'][0]['via'], + isFalse); // stopOver: true means via: false + expect(json['intermediates'][1]['via'], + isTrue); // stopOver: false means via: true expect(json['optimizeWaypointOrder'], isTrue); }); @@ -257,8 +265,10 @@ void main() { ); expect(request.headers, equals(customHeaders)); - expect(request.headers!['X-Android-Package'], equals('com.example.myapp')); - expect(request.headers!['X-Android-Cert'], equals('SHA1_FINGERPRINT_HERE')); + expect( + request.headers!['X-Android-Package'], equals('com.example.myapp')); + expect(request.headers!['X-Android-Cert'], + equals('SHA1_FINGERPRINT_HERE')); expect(request.headers!['Custom-Header'], equals('custom-value')); }); @@ -286,10 +296,12 @@ void main() { expect(originalRequest.headers, equals(originalHeaders)); expect(updatedRequest.headers, equals(newHeaders)); - expect(updatedRequest.headers!['Additional-Header'], equals('new-value')); + expect( + updatedRequest.headers!['Additional-Header'], equals('new-value')); }); - test('should preserve other fields when updating headers via copyWith', () { + test('should preserve other fields when updating headers via copyWith', + () { final originalRequest = RoutesApiRequest( origin: origin, destination: destination, @@ -310,10 +322,12 @@ void main() { expect(updatedRequest.headers, equals({'Updated': 'header'})); }); - test('should handle Android-specific headers for restricted API keys', () { + test('should handle Android-specific headers for restricted API keys', + () { final androidHeaders = { 'X-Android-Package': 'com.example.flutter_app', - 'X-Android-Cert': 'AA:BB:CC:DD:EE:FF:00:11:22:33:44:55:66:77:88:99:AA:BB:CC:DD', + 'X-Android-Cert': + 'AA:BB:CC:DD:EE:FF:00:11:22:33:44:55:66:77:88:99:AA:BB:CC:DD', }; final request = RoutesApiRequest( @@ -322,10 +336,13 @@ void main() { headers: androidHeaders, ); - expect(request.headers!['X-Android-Package'], equals('com.example.flutter_app')); - expect(request.headers!['X-Android-Cert'], - equals('AA:BB:CC:DD:EE:FF:00:11:22:33:44:55:66:77:88:99:AA:BB:CC:DD')); + expect(request.headers!['X-Android-Package'], + equals('com.example.flutter_app')); + expect( + request.headers!['X-Android-Cert'], + equals( + 'AA:BB:CC:DD:EE:FF:00:11:22:33:44:55:66:77:88:99:AA:BB:CC:DD')); }); }); }); -} \ No newline at end of file +} diff --git a/test/utils/polyline_decoder_test.dart b/test/utils/polyline_decoder_test.dart index 11b6868..4b65a70 100644 --- a/test/utils/polyline_decoder_test.dart +++ b/test/utils/polyline_decoder_test.dart @@ -7,12 +7,12 @@ void main() { test('should decode simple polyline correctly', () { // Simple polyline encoding for a few points const encodedPolyline = 'u{~vFvyys@fS]'; - + final decodedPoints = PolylineDecoder.run(encodedPolyline); - + expect(decodedPoints, isNotEmpty); expect(decodedPoints.length, greaterThan(0)); - + // Verify all points are valid PointLatLng objects for (final point in decodedPoints) { expect(point, isA()); @@ -39,12 +39,12 @@ void main() { test('should decode complex polyline with multiple points', () { // More complex polyline with multiple coordinate changes const complexPolyline = '_p~iF~ps|U_ulLnnqC_mqNvxq`@'; - + final decodedPoints = PolylineDecoder.run(complexPolyline); - + expect(decodedPoints, isNotEmpty); expect(decodedPoints.length, greaterThan(1)); - + // Verify coordinates are reasonable (within expected ranges) for (final point in decodedPoints) { expect(point.latitude, isA()); @@ -57,11 +57,11 @@ void main() { test('should decode polyline with precision', () { // Test polyline that should decode to specific known coordinates const precisePolyline = 'u{~vFvyys@fS]'; - + final decodedPoints = PolylineDecoder.run(precisePolyline); - + expect(decodedPoints, isNotEmpty); - + // Verify that decoded points have reasonable precision for (final point in decodedPoints) { // Coordinates should have decimal precision @@ -73,12 +73,12 @@ void main() { test('should handle single point polyline', () { // Polyline that represents a single point const singlePointPolyline = '?'; - + final decodedPoints = PolylineDecoder.run(singlePointPolyline); - + // Should handle gracefully, either empty or single point expect(decodedPoints.length, lessThanOrEqualTo(1)); - + if (decodedPoints.isNotEmpty) { final point = decodedPoints.first; expect(point, isA()); @@ -96,14 +96,14 @@ void main() { '!@#\$%', ' ', ]; - + for (final input in malformedInputs) { expect( () => PolylineDecoder.run(input), returnsNormally, reason: 'Should handle malformed input gracefully: \$input', ); - + final result = PolylineDecoder.run(input); expect(result, isA>()); } @@ -112,9 +112,9 @@ void main() { test('should decode real-world Google Maps polyline', () { // Real polyline from Google Maps API (San Francisco to Los Angeles) const realPolyline = 'u{~vFvyys@fS]'; - + final decodedPoints = PolylineDecoder.run(realPolyline); - + expect(decodedPoints, isNotEmpty); // Verify points are in reasonable geographic range for SF to LA route for (final point in decodedPoints) { @@ -127,24 +127,24 @@ void main() { test('should maintain coordinate precision', () { const polyline = 'u{~vFvyys@fS]'; - + final decodedPoints = PolylineDecoder.run(polyline); - + expect(decodedPoints, isNotEmpty); - + // Check that coordinates have appropriate precision (at least 4 decimal places) for (final point in decodedPoints) { final latStr = point.latitude.toString(); final lngStr = point.longitude.toString(); - + // Should have decimal points expect(latStr, contains('.')); expect(lngStr, contains('.')); - + // Should have reasonable precision final latDecimals = latStr.split('.')[1].length; final lngDecimals = lngStr.split('.')[1].length; - + expect(latDecimals, greaterThanOrEqualTo(1)); expect(lngDecimals, greaterThanOrEqualTo(1)); } @@ -152,12 +152,12 @@ void main() { test('should be consistent with multiple calls', () { const polyline = 'u{~vFvyys@fS]'; - + final firstDecode = PolylineDecoder.run(polyline); final secondDecode = PolylineDecoder.run(polyline); - + expect(firstDecode.length, equals(secondDecode.length)); - + for (int i = 0; i < firstDecode.length; i++) { expect(firstDecode[i].latitude, equals(secondDecode[i].latitude)); expect(firstDecode[i].longitude, equals(secondDecode[i].longitude)); @@ -168,13 +168,13 @@ void main() { // Simulate a very long polyline (repeated pattern) const basePolyline = 'u{~vFvyys@fS]'; final longPolyline = basePolyline * 10; // Repeat pattern - + expect( () => PolylineDecoder.run(longPolyline), returnsNormally, reason: 'Should handle long polylines without crashing', ); - + final decodedPoints = PolylineDecoder.run(longPolyline); expect(decodedPoints, isA>()); }); @@ -188,7 +188,7 @@ void main() { '@', '_', ]; - + for (final polyline in edgeCasePolylines) { expect( () => PolylineDecoder.run(polyline), @@ -201,16 +201,18 @@ void main() { test('should decode polyline with high precision coordinates', () { // Test with a polyline that should produce high-precision coordinates const highPrecisionPolyline = 'u{~vFvyys@fS]'; - + final decodedPoints = PolylineDecoder.run(highPrecisionPolyline); - + expect(decodedPoints, isNotEmpty); - + // Verify that we get reasonable precision for (final point in decodedPoints) { // Coordinates should not be rounded to whole numbers - expect(point.latitude, isNot(equals(point.latitude.round().toDouble()))); - expect(point.longitude, isNot(equals(point.longitude.round().toDouble()))); + expect( + point.latitude, isNot(equals(point.latitude.round().toDouble()))); + expect( + point.longitude, isNot(equals(point.longitude.round().toDouble()))); } }); @@ -220,12 +222,12 @@ void main() { 'u{~vFvyys@fS]', // US coordinates '_p~iF~ps|U_ulLnnqC_mqNvxq`@', // Complex international ]; - + for (final polyline in internationalPolylines) { final decodedPoints = PolylineDecoder.run(polyline); - + expect(decodedPoints, isA>()); - + for (final point in decodedPoints) { // Verify coordinates are within valid Earth bounds expect(point.latitude, greaterThanOrEqualTo(-90.0)); @@ -236,4 +238,4 @@ void main() { } }); }); -} \ No newline at end of file +}