e14b333a67
- 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
147 lines
4.4 KiB
Dart
147 lines
4.4 KiB
Dart
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),
|
|
],
|
|
);
|
|
}
|
|
}
|