Commit 685d7340 authored by Ashmini Jaisingh's avatar Ashmini Jaisingh

Implemented graph structures and digraph

parent 20408a09
# Miscellaneous # Miscellaneous
.env
*.class *.class
*.log *.log
*.pyc *.pyc
......
...@@ -11,9 +11,11 @@ import 'screens/profile_screen.dart'; ...@@ -11,9 +11,11 @@ import 'screens/profile_screen.dart';
import 'providers/delivery_provider.dart'; import 'providers/delivery_provider.dart';
import 'login.dart'; import 'login.dart';
import 'signup.dart'; import 'signup.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
void main() async { void main() async {
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
await dotenv.load(fileName: ".env");
await Firebase.initializeApp( await Firebase.initializeApp(
options: DefaultFirebaseOptions.currentPlatform, options: DefaultFirebaseOptions.currentPlatform,
); );
......
...@@ -6,8 +6,8 @@ class DeliveryAddress { ...@@ -6,8 +6,8 @@ class DeliveryAddress {
final String city; final String city;
final String state; final String state;
final String zipCode; final String zipCode;
final double? latitude; double? latitude;
final double? longitude; double? longitude;
final String? notes; final String? notes;
final DateTime createdAt; final DateTime createdAt;
......
// lib/models/digraph.dart
class Edge {
final int to;
final int weight; // seconds
Edge(this.to, this.weight);
}
class Digraph {
final int n; // number of nodes
final List<List<Edge>> adj;
Digraph(this.n) : adj = List.generate(n, (_) => []);
void addEdge(int u, int v, int w) {
adj[u].add(Edge(v, w));
}
/// Basic Dijkstra returning predecessor and distance arrays
Map<String, dynamic> dijkstra(int source) {
final dist = List<int>.filled(n, 1 << 30);
final prev = List<int?>.filled(n, null);
dist[source] = 0;
final visited = List<bool>.filled(n, false);
for (int i = 0; i < n; i++) {
int u = -1;
int best = 1 << 30;
for (int v = 0; v < n; v++) {
if (!visited[v] && dist[v] < best) {
best = dist[v];
u = v;
}
}
if (u == -1) break;
visited[u] = true;
for (final e in adj[u]) {
final alt = dist[u] + e.weight;
if (alt < dist[e.to]) {
dist[e.to] = alt;
prev[e.to] = u;
}
}
}
return {'dist': dist, 'prev': prev};
}
List<int> reconstructPath(int source, int target, List<int?> prev) {
final path = <int>[];
for (int? v = target; v != null; v = prev[v]) {
path.insert(0, v);
if (v == source) break;
}
if (path.isEmpty || path.first != source) return [];
return path;
}
}
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '../models/delivery_address.dart';
import '../models/digraph.dart';
import '../services/google_api_service.dart';
class GraphNode { class GraphNode {
final String id; final String id;
...@@ -81,4 +84,75 @@ class GraphProvider extends ChangeNotifier { ...@@ -81,4 +84,75 @@ class GraphProvider extends ChangeNotifier {
notifyListeners(); notifyListeners();
} }
} }
// ===== T7 / Routing Graph Fields =====
final GoogleApiService apiService;
GraphProvider({GoogleApiService? apiService})
: apiService = apiService ?? GoogleApiService();
// _api = GoogleApiService();
List<DeliveryAddress> addresses = [];
Digraph? graph;
List<List<int>>? matrix;
// ===== T7 Methods =====
/// Build the routing graph from delivery addresses
Future<void> buildGraphFromAddresses(List<DeliveryAddress> input) async {
addresses = input;
// 1) Geocode addresses using fullAddress getter
final fullAddresses = addresses.map((a) => a.fullAddress).toList();
final geocodeMap = await apiService.geocodeAddresses(fullAddresses);
// Update latitude / longitude in DeliveryAddress objects
for (int i = 0; i < addresses.length; i++) {
final loc = geocodeMap[fullAddresses[i]];
if (loc != null) {
addresses[i].latitude = loc.lat;
addresses[i].longitude = loc.lng;
} else {
addresses[i].latitude = null;
addresses[i].longitude = null;
}
}
// Filter valid addresses
final valid = addresses.where((a) => a.hasCoordinates).toList();
final origins = valid.map((a) => LatLng(a.latitude!, a.longitude!)).toList();
// 2) Distance matrix
matrix = await apiService.getDistanceMatrix(origins);
// 3) Build digraph for routing
graph = Digraph(origins.length);
for (int i = 0; i < origins.length; i++) {
for (int j = 0; j < origins.length; j++) {
if (i == j) continue;
final w = matrix![i][j];
if (w < (1 << 29)) {
graph!.addEdge(i, j, w);
}
}
}
notifyListeners();
}
/// Compute shortest route between two nodes in the routing graph
Map<String, dynamic> shortestRoute(int startIndex, int endIndex) {
if (graph == null) return {'distanceSeconds': 0, 'path': []};
final res = graph!.dijkstra(startIndex);
final prev = res['prev'] as List<int?>;
final dist = res['dist'] as List<int>;
final path = graph!.reconstructPath(startIndex, endIndex, prev);
return {'distanceSeconds': dist[endIndex], 'path': path};
}
} }
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import '../providers/graph_provider.dart'; import '../providers/graph_provider.dart';
import '../providers/delivery_provider.dart';
import 'package:google_maps_flutter/google_maps_flutter.dart';
// import '../models/delivery_address.dart';
class GraphScreen extends StatelessWidget { class GraphScreen extends StatefulWidget {
const GraphScreen({super.key}); const GraphScreen({super.key});
@override
State<GraphScreen> createState() => _GraphScreenState();
}
class _GraphScreenState extends State<GraphScreen> {
bool _isLoading = false;
String _statusMessage = "";
List<int> _samplePath = [];
int _totalNodes = 0;
GoogleMapController? _mapController;
final Set<Marker> _markers = {};
final Set<Polyline> _polylines = {};
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final deliveryProvider = Provider.of<DeliveryProvider>(context);
final graphProvider = Provider.of<GraphProvider>(context);
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
backgroundColor: Theme.of(context).colorScheme.inversePrimary, backgroundColor: Theme.of(context).colorScheme.inversePrimary,
...@@ -18,27 +38,131 @@ class GraphScreen extends StatelessWidget { ...@@ -18,27 +38,131 @@ class GraphScreen extends StatelessWidget {
actions: [ actions: [
IconButton( IconButton(
icon: const Icon(Icons.add), icon: const Icon(Icons.add),
onPressed: () { onPressed: () => _showAddNodeDialog(context),
// Add node functionality
_showAddNodeDialog(context);
},
), ),
IconButton( IconButton(
icon: const Icon(Icons.refresh), icon: const Icon(Icons.refresh),
onPressed: () { onPressed: () {
Provider.of<GraphProvider>(context, listen: false).clearGraph(); graphProvider.clearGraph();
}, },
), ),
], ],
), ),
body: Consumer<GraphProvider>( body: Column(
children: [
Padding(
padding: const EdgeInsets.all(16.0),
child: ElevatedButton(
onPressed: _isLoading
? null
: () async {
setState(() {
_isLoading = true;
_statusMessage = "Building graph...";
_samplePath = [];
_totalNodes = 0;
_markers.clear();
_polylines.clear();
});
try {
// Build graph from addresses
await graphProvider.buildGraphFromAddresses(
deliveryProvider.addresses);
final graph = graphProvider.graph;
if (graph != null) {
setState(() {
_totalNodes = graph.n;
if (graph.n >= 2) {
final res =
graphProvider.shortestRoute(0, graph.n - 1);
_samplePath = List<int>.from(res['path']);
_statusMessage =
"Graph built! Nodes: ${graph.n}";
} else {
_statusMessage = "Graph built, but not enough nodes";
}
// Optional: markers for all addresses
for (int i = 0; i < deliveryProvider.addresses.length; i++) {
final addr = deliveryProvider.addresses[i];
if (addr.hasCoordinates) {
_markers.add(Marker(
markerId: MarkerId(addr.id),
position: LatLng(addr.latitude!, addr.longitude!),
infoWindow: InfoWindow(title: addr.fullAddress),
));
}
}
// Optional: polyline for sample path
if (_samplePath.isNotEmpty) {
final points = _samplePath
.map((idx) => LatLng(
deliveryProvider.addresses[idx].latitude!,
deliveryProvider.addresses[idx].longitude!))
.toList();
_polylines.add(Polyline(
polylineId: const PolylineId("sample_path"),
points: points,
color: Colors.blue,
width: 4));
}
});
} else {
setState(() {
_statusMessage = "Graph could not be built";
});
}
} catch (e) {
setState(() {
_statusMessage = "Error building graph: $e";
});
} finally {
setState(() {
_isLoading = false;
});
}
},
child: _isLoading
? const CircularProgressIndicator(color: Colors.white)
: const Text("Build Routes"),
),
),
Text(_statusMessage),
if (_samplePath.isNotEmpty)
Text("Sample shortest path: $_samplePath"),
if (_totalNodes > 0) Text("Total nodes: $_totalNodes"),
const SizedBox(height: 16),
Expanded(
child: Consumer<GraphProvider>(
builder: (context, graphProvider, child) { builder: (context, graphProvider, child) {
return graphProvider.nodes.isEmpty
? _buildEmptyGraphPlaceholder()
: CustomPaint(
painter: GraphPainter(
graphProvider.nodes, graphProvider.edges),
child: Container(),
);
},
),
),
],
),
floatingActionButton: FloatingActionButton(
onPressed: () => _showAddNodeDialog(context),
tooltip: 'Add Node',
child: const Icon(Icons.add),
),
);
}
Widget _buildEmptyGraphPlaceholder() {
return Center( return Center(
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [
if (graphProvider.nodes.isEmpty)
Column(
children: [ children: [
Icon( Icon(
Icons.account_tree_outlined, Icons.account_tree_outlined,
...@@ -48,46 +172,22 @@ class GraphScreen extends StatelessWidget { ...@@ -48,46 +172,22 @@ class GraphScreen extends StatelessWidget {
const SizedBox(height: 16), const SizedBox(height: 16),
Text( Text(
'No graph data yet', 'No graph data yet',
style: Theme.of(context).textTheme.headlineSmall?.copyWith( style: Theme.of(context)
color: Colors.grey[600], .textTheme
), .headlineSmall
?.copyWith(color: Colors.grey[600]),
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
Text( Text(
'Add nodes to start building your graph', 'Add nodes to start building your graph or use "Build Routes"',
style: Theme.of(context).textTheme.bodyMedium?.copyWith( style: Theme.of(context)
color: Colors.grey[500], .textTheme
), .bodyMedium
), ?.copyWith(color: Colors.grey[500]),
],
)
else
Expanded(
child: CustomPaint(
painter: GraphPainter(graphProvider.nodes, graphProvider.edges),
child: Container(),
),
),
const SizedBox(height: 20),
Text(
'Nodes: ${graphProvider.nodes.length}',
style: Theme.of(context).textTheme.bodyLarge,
),
Text(
'Edges: ${graphProvider.edges.length}',
style: Theme.of(context).textTheme.bodyLarge,
), ),
], ],
), ),
); );
},
),
floatingActionButton: FloatingActionButton(
onPressed: () => _showAddNodeDialog(context),
tooltip: 'Add Node',
child: const Icon(Icons.add),
),
);
} }
void _showAddNodeDialog(BuildContext context) { void _showAddNodeDialog(BuildContext context) {
...@@ -144,8 +244,6 @@ class GraphPainter extends CustomPainter { ...@@ -144,8 +244,6 @@ class GraphPainter extends CustomPainter {
..strokeWidth = 2.0 ..strokeWidth = 2.0
..style = PaintingStyle.stroke; ..style = PaintingStyle.stroke;
// Draw edges first
for (final edge in edges) { for (final edge in edges) {
final startNode = nodes.firstWhere((n) => n.id == edge.fromId); final startNode = nodes.firstWhere((n) => n.id == edge.fromId);
final endNode = nodes.firstWhere((n) => n.id == edge.toId); final endNode = nodes.firstWhere((n) => n.id == edge.toId);
...@@ -157,7 +255,6 @@ class GraphPainter extends CustomPainter { ...@@ -157,7 +255,6 @@ class GraphPainter extends CustomPainter {
); );
} }
// Draw nodes
for (final node in nodes) { for (final node in nodes) {
canvas.drawCircle( canvas.drawCircle(
Offset(node.x, node.y), Offset(node.x, node.y),
...@@ -165,7 +262,6 @@ class GraphPainter extends CustomPainter { ...@@ -165,7 +262,6 @@ class GraphPainter extends CustomPainter {
nodePaint, nodePaint,
); );
// Draw node label
final textPainter = TextPainter( final textPainter = TextPainter(
text: TextSpan( text: TextSpan(
text: node.label, text: node.label,
......
// lib/services/google_api_service.dart
import 'dart:convert';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:http/http.dart' as http;
class LatLng {
final double lat;
final double lng;
LatLng(this.lat, this.lng);
}
class GoogleApiService {
final String key = dotenv.env['GOOGLE_MAPS_API_KEY'] ?? '';
Future<LatLng?> geocodeAddress(String address) async {
final encoded = Uri.encodeComponent(address);
final url = Uri.parse(
'https://maps.googleapis.com/maps/api/geocode/json?address=$encoded&key=$key',
);
final res = await http.get(url);
if (res.statusCode != 200) return null;
final data = json.decode(res.body);
if (data['status'] != 'OK' || (data['results'] as List).isEmpty) return null;
final loc = data['results'][0]['geometry']['location'];
return LatLng(loc['lat'], loc['lng']);
}
/// Geocode many addresses in parallel (with small concurrency)
Future<Map<String, LatLng?>> geocodeAddresses(List<String> addresses) async {
final Map<String, LatLng?> out = {};
// Simple sequential approach (easier to respect rate limits). You can parallelize (Future.wait) cautiously.
for (final a in addresses) {
out[a] = await geocodeAddress(a);
}
return out;
}
/// Build a distance matrix (durations in seconds). Returns matrix[i][j] = duration (int), or large = unreachable
Future<List<List<int>>> getDistanceMatrix(List<LatLng> origins, {String travelMode = 'driving'}) async {
final originStr = origins.map((o) => '${o.lat},${o.lng}').join('|');
final destinationStr = originStr; // origins==destinations for full matrix
final url = Uri.parse(
'https://maps.googleapis.com/maps/api/distancematrix/json'
'?origins=$originStr'
'&destinations=$destinationStr'
'&mode=$travelMode'
'&units=metric'
'&key=$key',
);
final res = await http.get(url);
if (res.statusCode != 200) {
throw Exception('DistanceMatrix failed: ${res.statusCode}');
}
final data = json.decode(res.body);
if (data['status'] != 'OK') {
throw Exception('DistanceMatrix response: ${data['status']}');
}
final rows = data['rows'] as List;
final n = rows.length;
final List<List<int>> matrix = List.generate(n, (_) => List.filled(n, 1 << 30));
for (int i = 0; i < n; i++) {
final elements = rows[i]['elements'] as List;
for (int j = 0; j < elements.length; j++) {
final el = elements[j];
if (el['status'] == 'OK' && el['duration'] != null) {
matrix[i][j] = el['duration']['value']; // seconds
} else {
matrix[i][j] = 1 << 30; // unreachable sentinel
}
}
}
return matrix;
}
}
...@@ -342,6 +342,14 @@ packages: ...@@ -342,6 +342,14 @@ packages:
description: flutter description: flutter
source: sdk source: sdk
version: "0.0.0" version: "0.0.0"
flutter_dotenv:
dependency: "direct main"
description:
name: flutter_dotenv
sha256: b7c7be5cd9f6ef7a78429cabd2774d3c4af50e79cb2b7593e3d5d763ef95c61b
url: "https://pub.dev"
source: hosted
version: "5.2.1"
flutter_driver: flutter_driver:
dependency: transitive dependency: transitive
description: flutter description: flutter
......
...@@ -25,6 +25,7 @@ dependencies: ...@@ -25,6 +25,7 @@ dependencies:
google_sign_in: ^6.2.1 google_sign_in: ^6.2.1
image_picker: ^1.0.4 image_picker: ^1.0.4
firebase_storage: ^11.5.6 firebase_storage: ^11.5.6
flutter_dotenv: ^5.0.2
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:
...@@ -39,5 +40,5 @@ flutter: ...@@ -39,5 +40,5 @@ flutter:
uses-material-design: true uses-material-design: true
assets: assets:
- assets/images/ # - assets/images/
- assets/icons/ - assets/icons/
import 'package:graph_go/services/google_api_service.dart';
import 'package:mockito/annotations.dart';
@GenerateMocks([GoogleApiService])
void main() {}
// Mocks generated by Mockito 5.4.6 from annotations
// in graph_go/test/mocks/mock_services.dart.
// Do not manually edit this file.
// ignore_for_file: no_leading_underscores_for_library_prefixes
import 'dart:async' as _i4;
import 'package:graph_go/services/google_api_service.dart' as _i2;
import 'package:mockito/mockito.dart' as _i1;
import 'package:mockito/src/dummies.dart' as _i3;
// ignore_for_file: type=lint
// ignore_for_file: avoid_redundant_argument_values
// ignore_for_file: avoid_setters_without_getters
// ignore_for_file: comment_references
// ignore_for_file: deprecated_member_use
// ignore_for_file: deprecated_member_use_from_same_package
// ignore_for_file: implementation_imports
// ignore_for_file: invalid_use_of_visible_for_testing_member
// ignore_for_file: must_be_immutable
// ignore_for_file: prefer_const_constructors
// ignore_for_file: unnecessary_parenthesis
// ignore_for_file: camel_case_types
// ignore_for_file: subtype_of_sealed_class
// ignore_for_file: invalid_use_of_internal_member
/// A class which mocks [GoogleApiService].
///
/// See the documentation for Mockito's code generation for more information.
class MockGoogleApiService extends _i1.Mock implements _i2.GoogleApiService {
MockGoogleApiService() {
_i1.throwOnMissingStub(this);
}
@override
String get key => (super.noSuchMethod(
Invocation.getter(#key),
returnValue: _i3.dummyValue<String>(
this,
Invocation.getter(#key),
),
) as String);
@override
_i4.Future<_i2.LatLng?> geocodeAddress(String? address) =>
(super.noSuchMethod(
Invocation.method(
#geocodeAddress,
[address],
),
returnValue: _i4.Future<_i2.LatLng?>.value(),
) as _i4.Future<_i2.LatLng?>);
@override
_i4.Future<Map<String, _i2.LatLng?>> geocodeAddresses(
List<String>? addresses) =>
(super.noSuchMethod(
Invocation.method(
#geocodeAddresses,
[addresses],
),
returnValue:
_i4.Future<Map<String, _i2.LatLng?>>.value(<String, _i2.LatLng?>{}),
) as _i4.Future<Map<String, _i2.LatLng?>>);
@override
_i4.Future<List<List<int>>> getDistanceMatrix(
List<_i2.LatLng>? origins, {
String? travelMode = 'driving',
}) =>
(super.noSuchMethod(
Invocation.method(
#getDistanceMatrix,
[origins],
{#travelMode: travelMode},
),
returnValue: _i4.Future<List<List<int>>>.value(<List<int>>[]),
) as _i4.Future<List<List<int>>>);
}
...@@ -53,7 +53,11 @@ void main() { ...@@ -53,7 +53,11 @@ void main() {
// Arrange // Arrange
final address = DeliveryAddress( final address = DeliveryAddress(
id: 'test-id', id: 'test-id',
fullAddress: '123 Test St, Test City', // fullAddress: '123 Test St, Test City',
streetAddress: '123 Test St',
city: 'Test City',
state: 'NY',
zipCode: '10001',
latitude: 40.7128, latitude: 40.7128,
longitude: -74.0060, longitude: -74.0060,
); );
...@@ -72,7 +76,11 @@ void main() { ...@@ -72,7 +76,11 @@ void main() {
for (int i = 0; i < 100; i++) { for (int i = 0; i < 100; i++) {
final address = DeliveryAddress( final address = DeliveryAddress(
id: 'test-id-$i', id: 'test-id-$i',
fullAddress: '123 Test St $i, Test City', // fullAddress: '123 Test St $i, Test City',
streetAddress: '123 Test St $i',
city: 'Test City',
state: 'NY',
zipCode: '10001',
latitude: 40.7128, latitude: 40.7128,
longitude: -74.0060, longitude: -74.0060,
); );
...@@ -81,7 +89,11 @@ void main() { ...@@ -81,7 +89,11 @@ void main() {
final extraAddress = DeliveryAddress( final extraAddress = DeliveryAddress(
id: 'extra-id', id: 'extra-id',
fullAddress: '123 Extra St, Test City', // fullAddress: '123 Extra St, Test City',
streetAddress: '123 Extra St',
city: 'Test City',
state: 'NY',
zipCode: '10001',
latitude: 40.7128, latitude: 40.7128,
longitude: -74.0060, longitude: -74.0060,
); );
...@@ -99,7 +111,11 @@ void main() { ...@@ -99,7 +111,11 @@ void main() {
// Arrange // Arrange
final originalAddress = DeliveryAddress( final originalAddress = DeliveryAddress(
id: 'test-id', id: 'test-id',
fullAddress: '123 Test St, Test City', // fullAddress: '123 Test St, Test City',
streetAddress: '123 Test St',
city: 'Test City',
state: 'NY',
zipCode: '10001',
latitude: 40.7128, latitude: 40.7128,
longitude: -74.0060, longitude: -74.0060,
); );
...@@ -107,7 +123,11 @@ void main() { ...@@ -107,7 +123,11 @@ void main() {
final updatedAddress = DeliveryAddress( final updatedAddress = DeliveryAddress(
id: 'test-id', id: 'test-id',
fullAddress: '456 Updated St, Updated City', // fullAddress: '456 Updated St, Updated City',
streetAddress: '456 Updated St',
city: 'Updated City',
state: 'NY',
zipCode: '10008',
latitude: 41.7128, latitude: 41.7128,
longitude: -75.0060, longitude: -75.0060,
); );
...@@ -125,7 +145,11 @@ void main() { ...@@ -125,7 +145,11 @@ void main() {
// Arrange // Arrange
final address = DeliveryAddress( final address = DeliveryAddress(
id: 'test-id', id: 'test-id',
fullAddress: '123 Test St, Test City', // fullAddress: '123 Test St, Test City',
streetAddress: '123 Test St',
city: 'Test City',
state: 'NY',
zipCode: '10001',
latitude: 40.7128, latitude: 40.7128,
longitude: -74.0060, longitude: -74.0060,
); );
...@@ -145,19 +169,31 @@ void main() { ...@@ -145,19 +169,31 @@ void main() {
// Add test addresses // Add test addresses
final address1 = DeliveryAddress( final address1 = DeliveryAddress(
id: 'addr1', id: 'addr1',
fullAddress: '123 First St, Test City', // fullAddress: '123 First St, Test City',
streetAddress: '123 First St',
city: 'Test City',
state: 'NY',
zipCode: '10001',
latitude: 40.7128, latitude: 40.7128,
longitude: -74.0060, longitude: -74.0060,
); );
final address2 = DeliveryAddress( final address2 = DeliveryAddress(
id: 'addr2', id: 'addr2',
fullAddress: '456 Second St, Test City', // fullAddress: '456 Second St, Test City',
streetAddress: '456 Second St',
city: 'Test City',
state: 'NY',
zipCode: '10001',
latitude: 40.7589, latitude: 40.7589,
longitude: -73.9851, longitude: -73.9851,
); );
final address3 = DeliveryAddress( final address3 = DeliveryAddress(
id: 'addr3', id: 'addr3',
fullAddress: '789 Third St, Test City', // fullAddress: '789 Third St, Test City',
streetAddress: '789 Third St',
city: 'Test City',
state: 'NY',
zipCode: '10001',
latitude: 40.7505, latitude: 40.7505,
longitude: -73.9934, longitude: -73.9934,
); );
...@@ -178,9 +214,9 @@ void main() { ...@@ -178,9 +214,9 @@ void main() {
expect(result.name, 'Test Route'); expect(result.name, 'Test Route');
expect(result.algorithm, RouteAlgorithm.dijkstra); expect(result.algorithm, RouteAlgorithm.dijkstra);
expect(result.addresses.length, 3); expect(result.addresses.length, 3);
expect(result.optimizedRoute.length, 3); expect(result.optimizedRoute?.length, 3);
expect(result.totalDistance, greaterThan(0)); expect(result.totalDistance, greaterThan(0));
expect(result.estimatedTime.inMinutes, greaterThan(0)); expect(result.estimatedTime?.inMinutes, greaterThan(0));
}); });
test('should optimize route with Nearest Neighbor algorithm', () async { test('should optimize route with Nearest Neighbor algorithm', () async {
...@@ -194,7 +230,7 @@ void main() { ...@@ -194,7 +230,7 @@ void main() {
expect(result.name, 'NN Route'); expect(result.name, 'NN Route');
expect(result.algorithm, RouteAlgorithm.nearestNeighbor); expect(result.algorithm, RouteAlgorithm.nearestNeighbor);
expect(result.addresses.length, 3); expect(result.addresses.length, 3);
expect(result.optimizedRoute.length, 3); expect(result.optimizedRoute?.length, 3);
}); });
test('should throw exception when no addresses available', () async { test('should throw exception when no addresses available', () async {
...@@ -233,7 +269,11 @@ void main() { ...@@ -233,7 +269,11 @@ void main() {
// Arrange // Arrange
final invalidAddress = DeliveryAddress( final invalidAddress = DeliveryAddress(
id: 'invalid-id', id: 'invalid-id',
fullAddress: 'Invalid Address That Cannot Be Geocoded', // fullAddress: 'Invalid Address That Cannot Be Geocoded',
streetAddress: 'Invalid Address That Cannot Be Geocoded',
city: 'Invalid',
state: '',
zipCode: '',
); );
// Act // Act
...@@ -250,7 +290,11 @@ void main() { ...@@ -250,7 +290,11 @@ void main() {
// Arrange // Arrange
final address = DeliveryAddress( final address = DeliveryAddress(
id: 'test-id', id: 'test-id',
fullAddress: '123 Test St, Test City', // fullAddress: '123 Test St, Test City',
streetAddress: '123 Test St',
city: 'Test City',
state: 'NY',
zipCode: '10001',
latitude: 40.7128, latitude: 40.7128,
longitude: -74.0060, longitude: -74.0060,
); );
......
This diff is collapsed.
import 'package:mockito/annotations.dart';
import 'package:graph_go/services/google_api_service.dart';
@GenerateMocks([GoogleApiService])
void main() {}
// Mocks generated by Mockito 5.4.6 from annotations
// in graph_go/test/providers/google_api_service_test.dart.
// Do not manually edit this file.
// ignore_for_file: no_leading_underscores_for_library_prefixes
import 'dart:async' as _i4;
import 'package:graph_go/services/google_api_service.dart' as _i2;
import 'package:mockito/mockito.dart' as _i1;
import 'package:mockito/src/dummies.dart' as _i3;
// ignore_for_file: type=lint
// ignore_for_file: avoid_redundant_argument_values
// ignore_for_file: avoid_setters_without_getters
// ignore_for_file: comment_references
// ignore_for_file: deprecated_member_use
// ignore_for_file: deprecated_member_use_from_same_package
// ignore_for_file: implementation_imports
// ignore_for_file: invalid_use_of_visible_for_testing_member
// ignore_for_file: must_be_immutable
// ignore_for_file: prefer_const_constructors
// ignore_for_file: unnecessary_parenthesis
// ignore_for_file: camel_case_types
// ignore_for_file: subtype_of_sealed_class
// ignore_for_file: invalid_use_of_internal_member
/// A class which mocks [GoogleApiService].
///
/// See the documentation for Mockito's code generation for more information.
class MockGoogleApiService extends _i1.Mock implements _i2.GoogleApiService {
MockGoogleApiService() {
_i1.throwOnMissingStub(this);
}
@override
String get key => (super.noSuchMethod(
Invocation.getter(#key),
returnValue: _i3.dummyValue<String>(
this,
Invocation.getter(#key),
),
) as String);
@override
_i4.Future<_i2.LatLng?> geocodeAddress(String? address) =>
(super.noSuchMethod(
Invocation.method(
#geocodeAddress,
[address],
),
returnValue: _i4.Future<_i2.LatLng?>.value(),
) as _i4.Future<_i2.LatLng?>);
@override
_i4.Future<Map<String, _i2.LatLng?>> geocodeAddresses(
List<String>? addresses) =>
(super.noSuchMethod(
Invocation.method(
#geocodeAddresses,
[addresses],
),
returnValue:
_i4.Future<Map<String, _i2.LatLng?>>.value(<String, _i2.LatLng?>{}),
) as _i4.Future<Map<String, _i2.LatLng?>>);
@override
_i4.Future<List<List<int>>> getDistanceMatrix(
List<_i2.LatLng>? origins, {
String? travelMode = 'driving',
}) =>
(super.noSuchMethod(
Invocation.method(
#getDistanceMatrix,
[origins],
{#travelMode: travelMode},
),
returnValue: _i4.Future<List<List<int>>>.value(<List<int>>[]),
) as _i4.Future<List<List<int>>>);
}
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/mockito.dart';
import 'package:graph_go/providers/graph_provider.dart';
import 'package:graph_go/models/delivery_address.dart';
import 'google_api_service_test.mocks.dart'; // import the generated mock
import 'package:graph_go/services/google_api_service.dart';
void main() {
late GraphProvider graphProvider;
late MockGoogleApiService mockApi;
setUp(() {
mockApi = MockGoogleApiService();
// Make sure GraphProvider accepts GoogleApiService via constructor
graphProvider = GraphProvider(apiService: mockApi);
});
test('buildGraphFromAddresses populates graph correctly', () async {
// Step 1: Create test addresses
final testAddresses = [
DeliveryAddress(
streetAddress: '1600 Amphitheatre Parkway',
city: 'Mountain View',
state: 'CA',
zipCode: '94043',
),
DeliveryAddress(
streetAddress: '1 Infinite Loop',
city: 'Cupertino',
state: 'CA',
zipCode: '95014',
),
DeliveryAddress(
streetAddress: '500 Terry A Francois Blvd',
city: 'San Francisco',
state: 'CA',
zipCode: '94158',
),
];
// Step 2: Mock GoogleApiService responses
when(mockApi.geocodeAddresses(any)).thenAnswer((_) async => {
testAddresses[0].fullAddress: LatLng(37.422, -122.084),
testAddresses[1].fullAddress: LatLng(37.331, -122.030),
testAddresses[2].fullAddress: LatLng(37.770, -122.387),
});
when(mockApi.getDistanceMatrix(any)).thenAnswer((_) async => [
[0, 1000, 2000],
[1000, 0, 1500],
[2000, 1500, 0],
]);
// Step 3: Run the method under test
await graphProvider.buildGraphFromAddresses(testAddresses);
// Step 4: Verify coordinates were populated
for (var addr in testAddresses) {
expect(addr.hasCoordinates, true);
}
// Step 5: Verify distance matrix
expect(graphProvider.matrix!.length, 3);
expect(graphProvider.matrix![0][1], 1000);
// Step 6: Verify digraph adjacency
expect(graphProvider.graph!.adj[0].length, 2); // edges to node 1 & 2
// Step 7: Test shortest path computation
final result = graphProvider.shortestRoute(0, 2);
expect(result['path'], isNotEmpty);
expect(result['distanceSeconds'], 2000);
});
}
...@@ -6,6 +6,8 @@ import 'package:mockito/mockito.dart'; ...@@ -6,6 +6,8 @@ import 'package:mockito/mockito.dart';
import 'package:mockito/annotations.dart'; import 'package:mockito/annotations.dart';
import 'package:graph_go/screens/home_screen.dart'; import 'package:graph_go/screens/home_screen.dart';
import 'package:graph_go/providers/delivery_provider.dart'; import 'package:graph_go/providers/delivery_provider.dart';
import 'package:google_maps_flutter/google_maps_flutter.dart';
// Generate mocks // Generate mocks
@GenerateMocks([FirebaseAuth, User, DeliveryProvider]) @GenerateMocks([FirebaseAuth, User, DeliveryProvider])
......
This diff is collapsed.
...@@ -8,6 +8,8 @@ import 'package:mockito/annotations.dart'; ...@@ -8,6 +8,8 @@ import 'package:mockito/annotations.dart';
import 'package:graph_go/main.dart'; import 'package:graph_go/main.dart';
import 'package:graph_go/screens/home_screen.dart'; import 'package:graph_go/screens/home_screen.dart';
import 'package:graph_go/providers/delivery_provider.dart'; import 'package:graph_go/providers/delivery_provider.dart';
import 'package:google_maps_flutter/google_maps_flutter.dart';
import 'home_screen_test.mocks.dart'; import 'home_screen_test.mocks.dart';
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment