jueves, 2 de mayo de 2013

API Google Maps y Visual Basic.NET. Parte VIII. Calcular ruta.

En esta nueva entrada vamos a adentrarnos un poco más en el API de Google Maps y vamos a ver cómo calcular una ruta para que podamos extraer la información del tiempo de recorrido, indicaciones, etc. Lo primero que hay que tener claro es cómo trabaja el API de Google Maps y cómo se hacen las peticiones HTTP para poder extraer la información (en este caso en formato XML), si esto no lo tienes muy claro, te recomiendo echar un vistazo a alguna de las entradas en las que se ha tratado esto (recomiendo esta).
Como hemos visto, de lo que se trata es de hacer un petición mediante una URL y ésta nos mostrará la información en formato XML y será de ahí de donde se extraerán todos los datos. Sin más dilación, voy a mostrar un ejemplo de una petición para una ruta en coche (sin restricción en cuanto a carreteras) de Madrid a Barcelona:
Si hacemos clic sobre el anterior enlace, vamos a ver un archivo XML con distintas partes, y de ahí extraeremos toda la información con respecto a la ruta.

XML con la ruta

Como podemos ver en la imagen anterior hay muchísima información disponible. En este ejemplo no vamos a utilizar toda, pero por poner un ejemplo, hay información de las polilíneas que conforman los diferentes tramos, tiempo de cada tramo, etc.
Primeramente vamos a centrarnos en los diferentes parámetros que tenemos que incluir en la URL.

PARÁMETROS
  • Origin (obligatorio): indica el punto de partida de la ruta, pudiendo ser una dirección postal (Puerta del Sol, Madrid) o por latitud/longitud (40.4167522,-3.7033701). Un ejemplo de este parámetro sería, "origin=Madrid" o "origin=40.4167522,-3.7033701".
  • Destination (obligatorio): indica el punto de llegada de la ruta, pudiendo ser una dirección postal (La Rambla, Barcelona) o por latitud/longitud (41.3806280,2.1736394). Un ejemplo de este parámetro sería, "origin=Barcelona" o "origin=41.3806280,2.1736394".
  • Mode (opcional): indica el tipo de transporte que se va a utilizar. Este parámetro puede tomar 4 valores diferentes que son, driving (en coche), walking (a pie), bicycling (en bicicleta), transit (transporte público). Ejemplificando, vamos a indicar ruta en coche, "mode=driving".
  • Avoid (opcional): este parámetro se incluye cuando se quiere aplicar restricciones a los tipos de carreteras. Si este parámetro no se incluye, se comprobarán todas las carreteras disponibles. Cuando el parámetro se incluye puede tomar dos valores, tolls (evita peajes) y highways (evita autopistas/autovías). Un ejemplo sería, "avoid=tolls". Debe recordarse que si no se quiere incluir restricciones, este parámetro se debe obviar.
  • Waypoints (opcional): indica el número de hitos por los cuales se debe pasar, es decir, sitios de parada obligatoria entre el origen y el destino. La localización de estos hitos puede ser por dirección postal o por latitud/longitud, y deben estar separados por una barra vertical "|". Además, si se quiere que el orden de los hitos sea de tal forma que el recorrido sea mínimo, se puede añadir al principio de los waypoints el parámetro "optimize:true". Por ejemplo, vamos a añadir 2 hitos (sin optimización de ruta), y sería así, "waypoints=Sevilla|Valencia", y el mismo ejemplo con optimización de ruta, "waypoints=optimize:true|Sevilla|Valencia".
  • Region: este es un parámetro opcional y hace que al realizar la búsqueda dé prioridad a resultados de la región seleccionada, es decir, si buscamos la ciudad de León habiendo puesto como región España, el primer resultado será de León (España), en cambio, si ponemos como región México, nos aparece como primer resultado la ciudad de León (México). Para establecer la región hay que incluir la sentencia "region=" y la región que queremos, por ejemplo para España sería, "region=es". Entra aquí para más información sobre los códigos.
  • Language (idioma)es el idioma en el que se devuelven los resultados, aquí podemos ver los idiomas disponibles. Para seleccionar español la sentencia sería, "language=es".
  • Sensor (obligatorio): determina si la petición procede de un dispositivo con sensor (por ejemplo un receptor GNSS (GPS) de un teléfono móvil). Se puede seleccionar entre true o false. Un ejemplo sería, "sensor=false".
Además de los mencionados, hay algún parámetro más, pero únicamente vamos a utilizar estos.
Ahora vamos a crear una aplicación en Visual Basic que nos diga qué ruta seguir entre dos puntos, y nos dé algo más de información. Lo primero creamos un proyecto nuevo (Windows Forms) y le añadimos una clase denominada Ruta, y añadimos el siguiente código.

Imports System.Xml.XPath
Imports System.Reflection

Public Class Ruta
    Public Property Status As String
    Public Property Copyright As String
    Public Property OrdenHitos As New ArrayList
    Public Property TiempoTotal As New ArrayList
    Public Property IDruta As String
    Public Property DistanciaTotal As New ArrayList
    Public Property DuracionTotal As New ArrayList
    Public Property PeticionHTTP As String

    Sub New()
        'Borramos el contenido de todas las propiedades
        For Each Propiedad As PropertyInfo In Me.GetType.GetProperties()
            Propiedad = Nothing
        Next
    End Sub
    Public Enum TipoTransporte
        Coche = 0
        Andando = 1
        Bibicleta = 2
    End Enum

    Public Enum RestriccionesVias
        Sin_restricciones = 0
        Sin_Peajes = 1
        Sin_Autovias_Autopistas = 2
    End Enum

    Public Function CalcularRuta(ByVal DireccionOrigen As String, ByVal DireccionDestino As String,
                                 Optional TipoTransport As TipoTransporte = 0, Optional ByVal RestriccionesCarretera As RestriccionesVias = 0,
                                 Optional ByVal Hitos As ArrayList = Nothing, Optional ByVal optimizar As Boolean = False,
                                 Optional ByVal region As String = "es", Optional ByVal idioma As String = "es") As String()

        'Tipo de transporte
        Dim transporte As String = TipoTransport
        Select Case TipoTransport
            Case 0
                transporte = "&mode=driving"
            Case 1
                transporte = "&mode=walking"
            Case 2
                transporte = "&mode=bicycling"
        End Select

        'Dirección origen (sustituimos espacio en blanco por +)
        DireccionOrigen = DireccionOrigen.Replace(" ", "+")
        DireccionOrigen = "&origin=" & DireccionOrigen

        'Dirección destino (sustituimos espacio en blanco por +)
        DireccionDestino = DireccionDestino.Replace(" ", "+")
        DireccionDestino = "&destination=" & DireccionDestino

        'Hitos y optimización
        Dim todosHitos As String = "&waypoints="
        If optimizar = True Then
            todosHitos = "&waypoints=optimize:true|"
        Else
            todosHitos = "&waypoints="
        End If

        'Añadimos los hitos (en caso de haberlos)
        If Hitos IsNot Nothing Then
            For Each item As Object In Hitos
                item = item.ToString.Replace(" ", "+")
                todosHitos = todosHitos & item & "|"
            Next
        Else
            todosHitos = ""
        End If

        'Restricciones en carreteeras (peajes)
        Dim peajesFin As String = ""
        Select Case RestriccionesCarretera
            Case 0  'No evitamos peajes
                peajesFin = ""
            Case 1 'evitar los peajes de carretera y de puentes.
                peajesFin = "&avoid=tolls"
            Case 2 'evitar las autopistas y las autovías
                peajesFin = "&avoid=highways"
            Case Else
        End Select

        'Añadimos la región ("es" por defecto)
        region = "&region=" & region

        'Añadimos el idioma ("es" por defecto)
        idioma = "&language=" & idioma

        'Creamos la url con todos los datos'
        Dim url = "https://maps.googleapis.com/maps/api/directions/xml?" & DireccionOrigen & DireccionDestino & todosHitos & transporte & peajesFin & idioma & region & "&sensor=false"
        PeticionHTTP = url
        'Creamos una variable donde se almacenarán los datos de
        'Latitud, Longitud, Tiempo, Distancia, Indicaciones.
        Dim DatosRuta As New ArrayList()
        'Creamos la variable de salida (luego se redimensionará)
        Dim ValorSalida(0) As String
        'Preparamos la peticción HTTP
        Dim req As System.Net.HttpWebRequest = DirectCast(System.Net.WebRequest.Create(url), System.Net.HttpWebRequest)
        req.Timeout = 5000

        Try
            'Preparamos el archivo xml
            Dim res As System.Net.WebResponse = req.GetResponse()
            Dim responseStream As Stream = res.GetResponseStream()
            Dim NodeIter As XPathNodeIterator
            Dim docNav As New XPathDocument(responseStream)
            Dim nav = docNav.CreateNavigator

            'Creamos las variables que contendrán los paths del XML
            Dim Exruta, Extiempo, Exdistancia, Exindicaciones, Exlatitud, Exlongitud, Excopyrights, ExordenRuta, Exstatus, ExduracionTot, ExdistanciTot, Extiemposeg, ExPolilineas As String
            'Creamos los paths
            Exstatus = "DirectionsResponse/status"
            Exlatitud = "DirectionsResponse/route/leg/step/start_location/lat"
            Exlongitud = "DirectionsResponse/route/leg/step/start_location/lng"
            Extiempo = "DirectionsResponse/route/leg/step/duration/text"
            Exdistancia = "DirectionsResponse/route/leg/step/distance/text"
            Exindicaciones = "DirectionsResponse/route/leg/step/html_instructions"
            Exruta = "DirectionsResponse/route/summary"
            Excopyrights = "DirectionsResponse/route/copyrights"
            ExordenRuta = "DirectionsResponse/route/waypoint_index"
            ExduracionTot = "DirectionsResponse/route/leg/duration/value"
            ExdistanciTot = "DirectionsResponse/route/leg/distance/value"
            Extiemposeg = "DirectionsResponse/route/leg/step/duration/value"
            ExPolilineas = "DirectionsResponse/route/leg/step/polyline/points"

            '******************************************************
            'Recorremos el XML entero para cada datos buscado (path)

            'Propiedades----

            'Estatus de ruta
            NodeIter = nav.Select(Exstatus)
            While (NodeIter.MoveNext())
                Status = NodeIter.Current.Value
            End While

            'Copyright de los datos de la ruta
            NodeIter = nav.Select(Excopyrights)
            While (NodeIter.MoveNext())
                Copyright = NodeIter.Current.Value
            End While

            'Orden de los hitos (en caso de haberlos)
            NodeIter = nav.Select(ExordenRuta)
            While (NodeIter.MoveNext())
                OrdenHitos.Add(NodeIter.Current.Value)
            End While

            'ID de la ruta
            NodeIter = nav.Select(Exruta)
            While (NodeIter.MoveNext())
                IDruta = NodeIter.Current.Value
            End While

            'Duración total de la ruta (por tramos en caso de haber hitos)
            NodeIter = nav.Select(ExduracionTot)
            While (NodeIter.MoveNext())
                DuracionTotal.Add(NodeIter.Current.Value)
            End While

            'Distancia total del recorrido (por tramos en caso de haber hitos)
            NodeIter = nav.Select(ExdistanciTot)
            While (NodeIter.MoveNext())
                DistanciaTotal.Add(NodeIter.Current.Value)
            End While
            '-----

            'Datos que se devolverán en la función (los recorremos de una vez con un foreach----------
            Dim r As New ArrayList({(Exlatitud), (Exlongitud), (Extiempo), (Exdistancia), (Exindicaciones)})

            For Each item In r
                NodeIter = nav.Select(item)
                While (NodeIter.MoveNext())
                    DatosRuta.Add(NodeIter.Current.Value)
                End While
            Next

            '*****************************************************

            'Variable que almacenará los datos de Latitud, Longitud, Tiempo, Distancia, Indicaciones.
            ReDim ValorSalida(DatosRuta.Count - 1)

            'Ordenamos la variable de salida para que contenga la información de cada tramo
            Dim tamaño = CInt(DatosRuta.Count / 5)
            Dim contador As Integer = 0
            For i = 0 To tamaño - 1
                ValorSalida(contador) = DatosRuta(i)
                ValorSalida(contador + 1) = DatosRuta(i + tamaño)
                ValorSalida(contador + 2) = DatosRuta(i + tamaño + tamaño)
                ValorSalida(contador + 3) = DatosRuta(i + tamaño + tamaño + tamaño)
                ValorSalida(contador + 4) = DatosRuta(i + tamaño + tamaño + tamaño + tamaño)
                contador += 5
            Next
            'Cerramos 
            responseStream.Close()
        Catch
            Status = "UNKNOWN_ERROR"
        End Try

        Return ValorSalida

    End Function
End Class

El código es un poco largo y se podría hacer (mucho) mejor, pero se entiende bastante bien. Simplemente es una clase que agrupa una serie de propiedades y una función. El siguiente paso sería, desde el formulario principal, crear un objeto de la clase Ruta y llamar a la función CalcularRuta, y ésta nos devolverá un Arraylist con los datos de latitud1/longitud1/tiempo1/distancia1/indicaciones1/latitud2, etc. , ordenados por tramos. Una vez la función ha devuelto el valor, también se pueden comprobar el valor de las propiedades de la clase Ruta para saber, por ejemplo, el Copyright de la ruta, la distancia total recorrida, el orden de los hitos, etc.
Ahora vamos a crear un formulario para incluir los datos de la ruta en un DataGridView, y algún Textbox más con información. En este caso la opción de incluir hitos no se ha incluido.
El formulario quedaría así.

Formulario para ruta

Ahora, antes de poner el código fuente del formulario vamos a analizar unos pequeños detalles. Al calcular la ruta, uno de los datos que nos devuelve son las indicaciones, pero estas indicaciones tienen algunas etiquetas HTML que debemos eliminar antes de mostrarlas. Para ello, vamos a incluir una función que las quite:

Function QuitarEtiqueta(ByVal str As String) As String 'Eliminamos etiquetas HTML y ponemos en mayúsculas
    Dim RegExp As String = "]*>[^<]*"
    Dim RegExp2 As String = "
]*>[^<]*
" Dim R As New Regex(RegExp) Dim R2 As New Regex(RegExp2) Dim mc As MatchCollection = R.Matches(str) If mc.Count > 0 Then For Each m In mc Dim cadena = ((m.Result("$0").ToString)) str = str.Replace(cadena, cadena.ToString.ToUpper) Next End If Dim mc2 As MatchCollection = R2.Matches(str) If mc.Count > 0 Then For Each m In mc2 Dim cadena = ((m.Result("$0").ToString)) str = str.Replace(cadena, cadena.ToString.ToUpper) Next End If str = str.Replace("", "").Replace("", "").Replace("
", " ").Replace("
", "") Return str End Function

Ahora vamos a ver el código fuente del formulario.

Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
    DataGridView1.Rows.Clear()
    'Creamos un objeto de la clase ruta
    Dim objRuta As New Ruta
    'Variable que almacenará los datos de la ruta
    Dim DatosRuta() As String

    'Llamamos a la función para que nos devuelva los datos
    DatosRuta = objRuta.CalcularRuta(txtOrigen.Text, txtDestino.Text, comboTransporte.SelectedIndex, comboRestricciones.SelectedIndex)

    'Propiedadad para saber es estatus de la petición
    txtStatus.Text = objRuta.Status

    'Calculamos el tiempo total de la ruta (en segundos)
    Dim tiempoTotal As Integer = 0
    For Each item In objRuta.DuracionTotal
        tiempoTotal += item
    Next
   'Pasamos el tiempo a días, horas, minutos y segundos
    Dim t2 As TimeSpan = TimeSpan.FromSeconds(tiempoTotal)
    Dim TiempoT = t2.Days.ToString() & " días, " & t2.Hours.ToString() & " horas, " & t2.Minutes.ToString() & " minutos, " & t2.Seconds.ToString() & " segundos "
    txtTiempoTotal.Text = TiempoT

    'Calculamos la distancia total
    Dim DistanciaTotal As Integer = 0
    For Each item In objRuta.DistanciaTotal
        DistanciaTotal += item
    Next
    txtDistanciaTotal.Text = DistanciaTotal / 1000 & " km"

    'Valor de la petición HTTP
    txtURL.Text = objRuta.PeticionHTTP

    'Copyright e ID de la ruta
    lblCopyright.Text = objRuta.Copyright
    lblIDruta.Text = objRuta.IDruta

    'Rellenamos el Datagridview con los datos de la ruta
    Dim contadorT = 0
    For i = 0 To UBound(DatosRuta) - 1 Step 5
        DataGridView1.Rows.Add(DatosRuta(i + 2), DatosRuta(i + 3), QuitarEtiqueta(DatosRuta(i + 4)), DatosRuta(i), DatosRuta(i + 1))
    Next
End Sub

Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load
   comboRestricciones.SelectedIndex = 0
   comboTransporte.SelectedIndex = 0
End Sub

Lo único que hay que tener en cuenta es que algunas propiedades, como por ejemplo DistanciaTotal, es un arraylist porque si se añaden hitos a la ruta (no es el caso de este ejemplo), devolverá la distancia total por cada tramo entre punto de partida/salida con los hitos.
Como hemos visto, el proceso para adquirir la información es muy simple, sólo hay que hacer la URL para obtener el archivo XML, y a partir de ahí, es cuestión de adquirir lo que nos interese y mostrarlo. En nuestro caso el resultado sería así:

Resultado de ruta

Puedes descargar el código fuente aquí:

17 comentarios:

  1. hola, me a encantado tu trabajo, pero en este punto del calculo de ruta, como podemos graficarlo en el mapa utilizando el webbrowser al mas puro estilo de la web de google donde aparece una linea desde el punto al punto b.

    gracias.

    ResponderEliminar
    Respuestas
    1. Lo que se puede hacer es dibujarlo en un mapa estático a partir de la información que nos devuelve la ruta, en concreto, con el campo que se llama polilínea.
      Un saludo

      Eliminar
  2. Buen trabajo. ¿Sabes si es posible con API de Google saber cada tramo de ruta por qué carretera va y de que tipo es, si es de peaje, autovía, etc?
    Muchas gracias

    ResponderEliminar
    Respuestas
    1. En principio eso no se puede saber.
      Echa un vistazo al enlace de esta ruta (https://maps.googleapis.com/maps/api/directions/xml?&origin=madrid&destination=Barcelona&mode=driving&language=es&region=es&sensor=false) y ahí te muestra toda la información disponible.
      También puedes revisar la documentación (en castellano): https://developers.google.com/maps/documentation/webservices/?hl=es

      Eliminar
  3. Se podria agregar mas de un Origen Y destino, estoy trabajando en un proyecto que es de como corren los trenes y en base al tiempo de salida y transito saber donde se interceptan para una mejor administración de los encuentros y si se pueden plasmar en trazado en un mapa sea web o no,

    ResponderEliminar
  4. Se podria agregar mas de un Origen Y destino, estoy trabajando en un proyecto que es de como corren los trenes y en base al tiempo de salida y transito saber donde se interceptan para una mejor administración de los encuentros y si se pueden plasmar en trazado en un mapa sea web o no,

    ResponderEliminar
  5. Excelente proyecto, muchas gracias por compartir Saludos

    ResponderEliminar
  6. Excelente proyecto, como puedo descargar el archivo de ejemplo?

    ResponderEliminar
  7. Gracias por el aporte, probé el proyecto pero me mando en el text de estatus "UNKNOWN_ERROR" ya probe por aparte la URL y esta correcta¡¡
    Sabras si existe un error ya que no lo hacia y no le he cambiado nada al modulo de calculo apartir del XML

    ResponderEliminar
  8. Hola gracias por el aporte, esta excelente tu programa, estoy intentado hacer un programa para una linea de taxis, pero aun tengo problemas, que posibilidad hay de que me des una pequeña asesoria :)

    ResponderEliminar
  9. Saludos,
    Una pregunta: Estoy empleando el código para una aplicación, en relación con el tráfico, he colocado el tiempo de inicio en la URL (departure_time), pero al calcular la ruta me arroja siempre el mismo tiempo y he comparado con google maps, pero los resultados son diferentes en cuanto al tiempo total de la ruta. No si tiene en cuenta el trafico a la hora indicada y fecha o se debe contar con alguna Key (clave) en especifica para acceder a los servicios en relación con el trafico.

    Ejemplo:
    Resultados por el programa:
    Url: https://maps.googleapis.com/maps/api/directions/xml?&origin=2.437713,-76.604398&destination=2.442145,-76.602778&departure_time=1514829600&mode=driving&language=es&region=es&sensor=false
    Resultado: Tiempo total de ruta: 2 minutos, 13 segundos

    Resultados por google maps:
    Datos iniciales:
    1. Depart at: 6:00 PM 1 de Enero de 2018
    2. Origen: Cra. 3 #7-2, Popayán, Cauca(2.437713,-76.60439)
    3. Destino: Cra. 3 #3-1, Popayán, Cauca (2.442145,-76.602778)
    Resultados: Tiempo total de ruta: 4 min.

    Muchas gracias,

    Muy buen aporte.



    ResponderEliminar
    Respuestas
    1. El enlace que se encuentra en el Blog, para mas información de rutas, dice que no encuentra la pagina que busca.

      Enlace: Más información sobre rutas: https://developers.google.com/maps/documentation/webservices/?hl=es

      Eliminar
  10. HOLA BUENAS MUY BUEN APORTE PERO TENGO UN PROBLEMA NO ME CALCULA LA DISTANCIA TOTAL DE LA RUTA

    ResponderEliminar
  11. me sale mucho este problemas OVER_QUERY_LIMIT

    ResponderEliminar
  12. Felicidades por tu excelente trabajo,

    Me gustaria si fueses tan amable que me aclararas exactamente como dibujar una ruta optimizada en google maps si es posible en vba, el ejemplo que estoy usando es este
    https://maps.googleapis.com/maps/api/directions/json?origin=Madrid&destination=madrid&waypoints=optimize:true|sevilla|zaragoza|cordoba&key

    si no es posible me gustaria saber extraer el valor de "waypoint_order" : [ 0, 2, 1 ] para hacer un hipervinculo con https://www.google.es/maps/dir/Madrid/Barcelona/poniendo el orden optimo de los waypoints

    Muchas gracias

    ResponderEliminar
  13. Buenos dias, muy buen trabajo.
    Consulta, al intentar calcular, no me calcula y me tira UNKNOWN_ERROR.
    Podrias ayudarme?
    Desde ya muchas gracias

    ResponderEliminar