Implementación de polilíneas para trazar rutas
Ahora que has añadido un mapa de Google Maps a tu aplicación Flutter y puedes agregar marcadores, el siguiente paso será trazar una ruta entre ellos.
El primer paso será configurar tu clave API en Google Cloud, agregar los permisos necesarios para Android e iOS, e integrar el paquete flutter_polyline_points
en tu aplicación Flutter para generar la ruta.
Configurar tu clave API en Google Cloud
Antes de poder interactuar con la API de Directions y generar una ruta, debes configurar correctamente tu clave API en la Consola de Google Cloud. Para comenzar, agrega los servicios Directions API y Routes API.
Luego, restringe tu clave API a esta lista de servicios y cópiala:
- Maps SDK for Android
- Places API
- Maps JavaScript API
- Maps Embed API
- Maps SDK for iOS
- Directions API
- Routes API
- Roads API
- Geocoding API
- Geolocation API
- Street View Static API
- Distance Matrix API
Estas API son esenciales para permitir que tu aplicación acceda a funcionalidades de mapeo, geolocalización y creación de rutas.
Configurar Android
Para Android, deberás agregar algunos permisos para permitir el acceso a la ubicación del usuario. Lo único que tendrás que hacer es actualizar el archivo AndroidManifest.xml
agregando estas líneas si no están presentes:
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION"/>
Configurar iOS
Para iOS, no necesitarás solicitar permisos, pero deberás especificar las razones por las cuales tu aplicación necesita acceso a la ubicación.
Para ello, ve al archivo Info.plist
de tu aplicación y agrega los siguientes permisos:
<key>NSLocationWhenInUseUsageDescription</key>
<string>Esta app necesita acceso a la ubicación cuando la aplicación está abierta.</string>
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
<string>Esta app necesita acceso a la ubicación en todo momento y cuando la aplicación está abierta.</string>
<key>NSLocationUsageDescription</key>
<string>Los dispositivos antiguos requieren acceso a la ubicación.</string>
<key>NSLocationAlwaysUsageDescription</key>
<string>Esta app necesita acceso a la ubicación en segundo plano.</string>
Luego, asegúrate de configurar correctamente Google Maps en el archivo AppDelegate.swift
agregando la importación y la configuración de la clave API como sigue:
import GoogleMaps
@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
GMSServices.provideAPIKey("TU_CLAVE_API") // Reemplaza esta clave con tu clave
GeneratedPluginRegistrant.register(with: self)
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
}
Integración del package flutter_polyline_points
Finalmente, el último paso será instalar y configurar el package flutter_polyline_points
para generar la ruta entre dos marcadores.
En tu archivo pubspec.yaml
, agrega la última versión (2.1.0) del paquete:
dependencies:
flutter:
sdk: flutter
google_maps_flutter: ^2.1.0
flutter_polyline_points: ^2.1.0
O usa este comando en la terminal:
flutter pub add flutter_polyline_points
Luego importa el paquete en tu archivo Dart donde deseas gestionar la ruta:
import 'package:flutter_polyline_points/flutter_polyline_points.dart';
Crear una ruta con Google Maps en Flutter
Ahora que la configuración está lista, podrás crear rutas colocando marcadores en tu mapa. Aquí tienes un código de ejemplo que incluye las funcionalidades básicas, es decir:
- Colocar y eliminar marcadores en el mapa.
- Encontrar la ruta más rápida entre esos marcadores.
- Adaptar la ruta según los marcadores agregados o eliminados.
- Cambiar el modo de transporte (Coche, caminar o transporte público).
- Estimar el tiempo de viaje.
- Recambiar el mapa alrededor de la ruta.
import 'package:flutter/material.dart';
import 'package:google_maps_flutter/google_maps_flutter.dart';
import 'package:flutter_polyline_points/flutter_polyline_points.dart';
import 'package:permission_handler/permission_handler.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return const MaterialApp(
debugShowCheckedModeBanner: false,
home: MapScreen(),
);
}
}
class MapScreen extends StatefulWidget {
const MapScreen({Key? key}) : super(key: key);
@override
_MapScreenState createState() => _MapScreenState();
}
class _MapScreenState extends State<MapScreen> {
late GoogleMapController mapController;
Set<Marker> _markers = {};
Set<Polyline> _polylines = {};
List<LatLng> _routePoints = [];
PolylinePoints polylinePoints = PolylinePoints();
String googleApiKey = "TU_CLAVE_API";
String _travelTime = "Calculando...";
TravelMode _selectedMode = TravelMode.driving;
@override
void initState() {
super.initState();
checkAndRequestPermission();
}
Future<void> checkAndRequestPermission() async {
PermissionStatus status = await Permission.location.status;
if (!status.isGranted) {
await Permission.location.request();
}
}
void _onMapTap(LatLng position) {
setState(() {
String markerId = "marker_${_markers.length}";
_markers.add(
Marker(
markerId: MarkerId(markerId),
position: position,
infoWindow: InfoWindow(title: "Punto ${_markers.length + 1}"),
onTap: () {
setState(() {
_markers
.removeWhere((marker) => marker.markerId.value == markerId);
_updateRoute();
});
},
),
);
_updateRoute();
});
}
void _updateRoute() async {
if (_markers.length < 2) return;
_routePoints.clear();
List<Marker> markerList = _markers.toList();
for (int i = 0; i < markerList.length - 1; i++) {
PolylineResult result = await polylinePoints.getRouteBetweenCoordinates(
googleApiKey: googleApiKey,
request: PolylineRequest(
origin: PointLatLng(
markerList[i].position.latitude,
markerList[i].position.longitude,
),
destination: PointLatLng(
markerList[i + 1].position.latitude,
markerList[i + 1].position.longitude,
),
mode: _selectedMode,
),
);
if (result.points.isNotEmpty) {
_routePoints
.addAll(result.points.map((p) => LatLng(p.latitude, p.longitude)));
}
}
setState(() {
_polylines.clear();
_polylines.add(
Polyline(
polylineId: const PolylineId("route"),
color: Colors.blue,
width: 5,
points: _routePoints,
),
);
_calculateTravelTime();
_recenterMap(); // Recentrar el mapa
});
}
void _calculateTravelTime() {
setState(() {
_travelTime = "~ ${(_routePoints.length / 5).toStringAsFixed(1)} min";
});
}
// Recensar el mapa para mostrar todos los puntos
void _recenterMap() {
if (_markers.isEmpty) return;
double minLat = _routePoints.first.latitude;
double maxLat = _routePoints.first.latitude;
double minLon = _routePoints.first.longitude;
double maxLon = _routePoints.first.longitude;
for (LatLng point in _routePoints) {
if (point.latitude < minLat) minLat = point.latitude;
if (point.latitude > maxLat) maxLat = point.latitude;
if (point.longitude < minLon) minLon = point.longitude;
if (point.longitude > maxLon) maxLon = point.longitude;
}
LatLngBounds bounds = LatLngBounds(
southwest: LatLng(minLat, minLon),
northeast: LatLng(maxLat, maxLon),
);
mapController.animateCamera(CameraUpdate.newLatLngBounds(bounds, 50));
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text("Planificador de rutas"),
actions: [
DropdownButton<TravelMode>(
value: _selectedMode,
onChanged: (TravelMode? mode) {
setState(() {
_selectedMode = mode!;
_updateRoute();
});
},
items: const [
DropdownMenuItem(value: TravelMode.driving, child: Text("Coche")),
DropdownMenuItem(value: TravelMode.walking, child: Text("Caminar")),
DropdownMenuItem(value: TravelMode.transit, child: Text("Transporte")),
DropdownMenuItem(value: TravelMode.bicycling, child: Text("Bicicleta")),
],
),
],
),
body: Stack(
children: [
GoogleMap(
onMapCreated: (controller) => mapController = controller,
initialCameraPosition: const CameraPosition(
target: LatLng(48.8566, 2.3522),
zoom: 12.0,
),
markers: _markers,
polylines: _polylines,
onTap: _onMapTap,
),
Positioned(
top: 20,
left: 20,
child: Container(
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(8),
boxShadow: [BoxShadow(color: Colors.black26, blurRadius: 5)],
),
child: Text(
"Tiempo estimado: $_travelTime",
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
),
),
),
],
),
floatingActionButton: FloatingActionButton(
onPressed: () {
setState(() {
_markers.clear();
_polylines.clear();
_routePoints.clear();
_travelTime = "Calculando...";
});
},
child: const Icon(Icons.delete),
),
);
}
}
Con este código de ejemplo que puedes adaptar según tus necesidades, puedes crear fácilmente rutas y estimar el tiempo de viaje según el modo de transporte.
Añadir y eliminar marcadores
Añadir marcadores en el mapa es el primer paso para crear una ruta. Para agregar un marcador, se utiliza el método onTap
de GoogleMap
, que permite obtener la posición clicada y añadir un widget de tipo Marker
en ese lugar:
// Función lanzada cuando el usuario hace clic en algún lugar del mapa
void _onMapTap(LatLng position) {
setState(() {
// Genera un identificador único para el marcador
String markerId = "marker_\${_markers.length}";
// Añade un marcador en el lugar donde se hizo clic
_markers.add(
Marker(
markerId: MarkerId(markerId),
position: position,
infoWindow: InfoWindow(title: "Punto \${_markers.length + 1}"),
// Añade una acción de eliminación al hacer clic sobre el marcador
onTap: () {
setState(() {
_markers.removeWhere((marker) => marker.markerId.value == markerId);
_updateRoute(); // Actualiza la ruta
});
},
),
);
_updateRoute(); // Llama a la función que actualiza la ruta después de añadir el marcador
});
}
Te invito a leer mi guía si deseas aprender más sobre cómo añadir marcadores y personalizarlos según tus necesidades.
Recuperar los marcadores y trazar una ruta
Una vez que se han añadido varios marcadores, la ruta entre ellos puede trazarse utilizando el paquete flutter_polyline_points
, que usa la API de Google Directions para calcular el mejor camino.
void _updateRoute() async {
if (_markers.length < 2) return; // No hay suficientes puntos para trazar una ruta
_routePoints.clear(); // Borra la ruta actual
List<Marker> markerList = _markers.toList();
for (int i = 0; i < markerList.length - 1; i++) {
// Llamada a la API de Google Directions para obtener la ruta entre dos puntos. Este paso se repite hasta que todos los puntos estén conectados.
PolylineResult result = await polylinePoints.getRouteBetweenCoordinates(
googleApiKey: googleApiKey,
request: PolylineRequest(
origin: PointLatLng(
markerList[i].position.latitude,
markerList[i].position.longitude,
),
destination: PointLatLng(
markerList[i + 1].position.latitude,
markerList[i + 1].position.longitude,
),
mode: _selectedMode, // Modo de transporte seleccionado
),
);
// Añade los puntos de la ruta a la lista
if (result.points.isNotEmpty) {
_routePoints.addAll(result.points.map((p) => LatLng(p.latitude, p.longitude)));
}
}
setState(() {
_polylines.clear(); // Borra las líneas anteriores
_polylines.add(
Polyline(
polylineId: const PolylineId("route"),
color: Colors.blue,
width: 5,
points: _routePoints,
),
);
_calculateTravelTime(); // Calcula el tiempo de viaje según el modo de transporte seleccionado
});
}
La función consta de tres etapas:
- Recuperar los marcadores: La aplicación mantiene una lista de los marcadores añadidos, que se guarda en la variable
_markers
. Cada marcador representa una etapa de la ruta. - Llamada a la API de Google Directions: Para cada par de puntos adyacentes (es decir, dos marcadores consecutivos en la lista
_markers
), la aplicación llama a la API de Google Directions a través del paqueteflutter_polyline_points
para obtener la ruta entre esos dos puntos. La API devuelve una serie de puntos que describen la trayectoria de la ruta entre esos dos puntos. Este proceso se repite hasta que todos los marcadores están conectados por una ruta. - Trazar la ruta en el mapa: Los puntos obtenidos en cada llamada se añaden a una lista (
_routePoints
) y se utilizan para dibujar una polyline en el mapa, que representa visualmente la ruta. - La antigua polyline se borra y, una vez que todos los pares de puntos han sido trazados, se actualiza con los nuevos puntos.
De este modo, la ruta se traza entre los marcadores y el mapa se actualiza para reflejar visualmente el trayecto entre cada punto.
Adaptar el trazado cuando se eliminan marcadores
Cuando el usuario elimina un marcador, la ruta debe recalcularse. Esto se hace directamente en la función _onMapTap
, que se vuelve a ejecutar cuando se elimina un punto del trazado y utiliza la función _updateRoute()
:
onTap: () {
setState(() {
_markers.removeWhere((marker) => marker.markerId.value == markerId); // Elimina el marcador
_updateRoute(); // Recalcula la ruta
});
}
El trazado se elimina y luego se redibuja a partir de los puntos restantes.
Cambiar el modo de transporte
Google Maps permite calcular rutas según diferentes modos de transporte. Así, podemos usar la propiedad TravelMode
para especificar la opción deseada.
En mi ejemplo, utilizo un DropdownButton
para cambiar dinámicamente el modo de transporte y recalcular la ruta:
DropdownButton<TravelMode>(
value: _selectedMode, // Modo de transporte actual
onChanged: (TravelMode? mode) {
setState(() {
_selectedMode = mode!; // Actualiza el modo de transporte
_updateRoute(); // Recalcula la ruta con el nuevo modo
});
},
items: const [
DropdownMenuItem(value: TravelMode.driving, child: Text("Coche")),
DropdownMenuItem(value: TravelMode.walking, child: Text("Caminando")),
DropdownMenuItem(value: TravelMode.transit, child: Text("Transporte público")),
],
)
Calcular el tiempo de trayecto
Una estimación del tiempo de trayecto puede calcularse en función del número de puntos en la ruta y el modo de transporte.
void _calculateTravelTime() {
setState(() {
_travelTime = "~ ${(_routePoints.length / 5).toStringAsFixed(1)} min";
});
}
Aquí, la estimación del tiempo de trayecto se basa en el número de puntos que definen la ruta. Estos puntos son generados por la API de Google Maps cuando calculas la ruta entre dos lugares (marcadores).
Cada vez que añades marcadores en el mapa, la aplicación calcula la ruta entre cada par de marcadores y genera una serie de puntos (coordenadas GPS) que representan esta ruta. Estos puntos se almacenan en la variable _routePoints
.
Para estimar el tiempo de trayecto, la aplicación simplemente divide el número de puntos por 5. Esta división es una aproximación, ya que la idea es que aproximadamente 5 puntos equivalen a 1 minuto de trayecto, dependiendo de la densidad de la ruta.
El número de puntos generados por la API varía según el modo de transporte elegido (coche, caminando, bicicleta, etc.). Por ejemplo, para un trayecto en coche, la ruta será más directa, con menos puntos, mientras que para caminar, la ruta será más sinuosa y tendrá más puntos.
Recintar el mapa alrededor de la ruta
Cuando se añaden o eliminan puntos en el mapa y se recalcula la ruta, es necesario asegurarse de que toda la ruta sea visible para el usuario.
void _recenterMap() {
if (_markers.isEmpty) return;
double minLat = _routePoints.first.latitude;
double maxLat = _routePoints.first.latitude;
double minLon = _routePoints.first.longitude;
double maxLon = _routePoints.first.longitude;
for (LatLng point in _routePoints) {
if (point.latitude < minLat) minLat = point.latitude;
if (point.latitude > maxLat) maxLat = point.latitude;
if (point.longitude < minLon) minLon = point.longitude;
if (point.longitude > maxLon) maxLon = point.longitude;
}
LatLngBounds bounds = LatLngBounds(
southwest: LatLng(minLat, minLon),
northeast: LatLng(maxLat, maxLon),
);
mapController.animateCamera(CameraUpdate.newLatLngBounds(bounds, 50));
}
Así es como funciona esta funcionalidad:
- Actualización de los puntos de la ruta: En cuanto un usuario añade o elimina un marcador en el mapa, esto desencadena la actualización de la ruta. El cálculo de la ruta se realiza con el método
_updateRoute()
. - Cálculo de las coordenadas extremas: Una vez calculados los puntos de la ruta, se recorren todas las coordenadas de los puntos para determinar los límites norte, sur, este y oeste (latitud y longitud mínima y máxima).
- Creación de los límites del mapa: Con estas coordenadas extremas, podemos crear un
LatLngBounds
que delimita la zona geográfica a mostrar en el mapa. - Recintado del mapa: Finalmente, el método
animateCamera
permite aplicar un zoom y centrar el mapa para que muestre toda la ruta en la vista.
Esto permite al usuario ver siempre la ruta completa en el mapa, incluso si añade o elimina puntos.
Personalizar la apariencia de la ruta
Varias opciones te permiten personalizar la apariencia de la ruta en el mapa. Aquí están las opciones principales que se te ofrecen.
Cambiar el color de la polyline
El color de la polyline es uno de los aspectos más simples de personalizar. Puedes definir el color de tu polyline (el camino que conecta los puntos) para que coincida con la identidad visual de tu aplicación o simplemente para hacerla más visible.
_polylines.add(
Polyline(
polylineId: PolylineId("route"),
color: Colors.blue, // Definir el color de la polyline
width: 5,
points: _routePoints,
),
);
Cambiar el grosor de la Polyline
El grosor de la polyline puede modificarse para hacerla más o menos visible. Puedes ajustar el grosor modificando la propiedad width
.
_polylines.add(
Polyline(
polylineId: PolylineId("route"),
color: Colors.blue,
width: 10, // Modificar el grosor de la polyline
points: _routePoints,
),
);
Cambiar el estilo de la Polyline (Punteada o Dashada)
Google Maps te permite modificar el estilo de las polylines, añadiendo segmentos o puntos en la ruta.
_polylines.add(
Polyline(
polylineId: PolylineId("route"),
color: Colors.blue,
width: 5,
patterns: [PatternItem.dash(30.0), PatternItem.gap(20.0)], // Estilo dashado
points: _routePoints,
),
);
Precio de la API de Google Directions
Antes de empezar a usar la API de Google Directions, ten en cuenta que sigue un modelo de facturación por uso. Por lo tanto, es importante entender cuánto cuestan y las opciones disponibles.
Modelo de facturación
La API Directions utiliza un modelo de facturación basado en el número de solicitudes realizadas. Cada solicitud enviada al servicio Directions (vía API o SDK) genera un costo, y ese costo depende del tipo de solicitud realizada. Existen dos tipos principales de SKU para esta API: Directions y Directions Advanced.
Tarifas para solicitudes estándar (Directions)
Para solicitudes estándar que no requieren información adicional (como el tráfico en tiempo real o más de 10 puntos de ruta), la tarifa es la siguiente:
- De 0 a 100,000 solicitudes por mes: 0.005 USD por solicitud.
- De 100,001 a 500,000 solicitudes por mes: 0.004 USD por solicitud.
- Más de 500,000 solicitudes por mes: Contacta con Google para obtener un precio personalizado.
Tarifas para solicitudes avanzadas (Directions Advanced)
Las solicitudes avanzadas incluyen funcionalidades adicionales como la información del tráfico en tiempo real. La facturación de este servicio es la siguiente:
- De 0 a 100,000 solicitudes por mes: 0.01 USD por solicitud.
- De 100,001 a 500,000 solicitudes por mes: 0.008 USD por solicitud.
- Más de 500,000 solicitudes por mes: Contacta con Google para obtener un precio personalizado.