feat: calculateur de frais de déplacement - backend et modèles Flutter
- Cloud Function travel.js : autocomplete Google Places + calcul itinéraires via Google Routes API avec péages Ulys /legs (precision=6) + /rate - Modèles : VehicleModel, DepotModel, RouteResultModel + FuelPrices - Services : VehicleService, TravelService (Firestore CRUD + API calls) - Gestion des données : 3 nouveaux onglets (Dépôts, Véhicules, Prix carburants) - Autocomplétion adresse dans le formulaire événement - Dialog calcul frais : config + carte flutter_map OSM + sélection itinéraire - Injection option FRAIS_KM dans l'événement à la sélection - flutter_map 7.0.2 + latlong2 0.9.1 ajoutés - npm: csv-parser + @mapbox/polyline installés dans functions
This commit is contained in:
@@ -0,0 +1,146 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_map/flutter_map.dart';
|
||||
import 'package:latlong2/latlong.dart';
|
||||
import 'package:em2rp/models/route_result_model.dart';
|
||||
|
||||
/// Affiche 1 ou 2 itinéraires sur une carte OpenStreetMap.
|
||||
/// Route TOLL = bleu, Route TOLL_FREE = vert.
|
||||
class RouteMapWidget extends StatelessWidget {
|
||||
final List<RouteResult> routes;
|
||||
final RouteResult? selectedRoute;
|
||||
|
||||
const RouteMapWidget({
|
||||
super.key,
|
||||
required this.routes,
|
||||
this.selectedRoute,
|
||||
});
|
||||
|
||||
/// Décode une polyline Google encodée en liste de LatLng.
|
||||
List<LatLng> _decode(String encoded) {
|
||||
if (encoded.isEmpty) return [];
|
||||
try {
|
||||
final result = <LatLng>[];
|
||||
int index = 0, lat = 0, lng = 0;
|
||||
final len = encoded.length;
|
||||
|
||||
while (index < len) {
|
||||
int shift = 0, result0 = 0;
|
||||
int b;
|
||||
do {
|
||||
b = encoded.codeUnitAt(index++) - 63;
|
||||
result0 |= (b & 0x1f) << shift;
|
||||
shift += 5;
|
||||
} while (b >= 0x20);
|
||||
final dlat = (result0 & 1) != 0 ? ~(result0 >> 1) : (result0 >> 1);
|
||||
lat += dlat;
|
||||
|
||||
shift = 0;
|
||||
result0 = 0;
|
||||
do {
|
||||
b = encoded.codeUnitAt(index++) - 63;
|
||||
result0 |= (b & 0x1f) << shift;
|
||||
shift += 5;
|
||||
} while (b >= 0x20);
|
||||
final dlng = (result0 & 1) != 0 ? ~(result0 >> 1) : (result0 >> 1);
|
||||
lng += dlng;
|
||||
|
||||
result.add(LatLng(lat / 1e5, lng / 1e5));
|
||||
}
|
||||
return result;
|
||||
} catch (e) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
LatLngBounds? _computeBounds(List<List<LatLng>> allPoints) {
|
||||
double? minLat, maxLat, minLng, maxLng;
|
||||
for (final pts in allPoints) {
|
||||
for (final p in pts) {
|
||||
minLat = minLat == null ? p.latitude : p.latitude < minLat ? p.latitude : minLat;
|
||||
maxLat = maxLat == null ? p.latitude : p.latitude > maxLat ? p.latitude : maxLat;
|
||||
minLng = minLng == null ? p.longitude : p.longitude < minLng ? p.longitude : minLng;
|
||||
maxLng = maxLng == null ? p.longitude : p.longitude > maxLng ? p.longitude : maxLng;
|
||||
}
|
||||
}
|
||||
if (minLat == null) return null;
|
||||
return LatLngBounds(
|
||||
LatLng(minLat - 0.02, minLng! - 0.02),
|
||||
LatLng(maxLat! + 0.02, maxLng! + 0.02),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final allPolylines = <Polyline>[];
|
||||
final allPointGroups = <List<LatLng>>[];
|
||||
|
||||
for (final route in routes) {
|
||||
final pts = _decode(route.encodedPolyline);
|
||||
if (pts.isEmpty) continue;
|
||||
allPointGroups.add(pts);
|
||||
|
||||
final isSelected =
|
||||
selectedRoute == null || selectedRoute!.routeType == route.routeType;
|
||||
final isToll = route.routeType == 'TOLL';
|
||||
|
||||
allPolylines.add(Polyline(
|
||||
points: pts,
|
||||
strokeWidth: isSelected ? 5.0 : 3.0,
|
||||
color: isToll
|
||||
? (isSelected
|
||||
? const Color(0xFF1565C0)
|
||||
: const Color(0xFF1565C0).withValues(alpha: 0.4))
|
||||
: (isSelected
|
||||
? const Color(0xFF2E7D32)
|
||||
: const Color(0xFF2E7D32).withValues(alpha: 0.4)),
|
||||
));
|
||||
}
|
||||
|
||||
final bounds = _computeBounds(allPointGroups);
|
||||
final mapController = MapController();
|
||||
|
||||
// Marqueurs de départ / arrivée
|
||||
final markers = <Marker>[];
|
||||
for (final group in allPointGroups) {
|
||||
if (group.isEmpty) continue;
|
||||
// Départ
|
||||
markers.add(Marker(
|
||||
point: group.first,
|
||||
width: 32,
|
||||
height: 32,
|
||||
child: const Icon(Icons.circle, color: Colors.green, size: 20),
|
||||
));
|
||||
// Arrivée
|
||||
markers.add(Marker(
|
||||
point: group.last,
|
||||
width: 32,
|
||||
height: 32,
|
||||
child: const Icon(Icons.location_pin, color: Colors.red, size: 32),
|
||||
));
|
||||
}
|
||||
|
||||
return FlutterMap(
|
||||
mapController: mapController,
|
||||
options: MapOptions(
|
||||
initialCameraFit: bounds != null
|
||||
? CameraFit.bounds(
|
||||
bounds: bounds,
|
||||
padding: const EdgeInsets.all(32),
|
||||
)
|
||||
: const CameraFit.coordinates(
|
||||
coordinates: [LatLng(46.2276, 2.2137)], padding: EdgeInsets.all(32)),
|
||||
interactionOptions: const InteractionOptions(
|
||||
flags: InteractiveFlag.all,
|
||||
),
|
||||
),
|
||||
children: [
|
||||
TileLayer(
|
||||
urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
|
||||
userAgentPackageName: 'com.em2events.em2rp',
|
||||
),
|
||||
PolylineLayer(polylines: allPolylines),
|
||||
MarkerLayer(markers: markers),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user