Google Maps con AngularDart

En este artículo integraremos Google Maps con una aplicación de AngularDart. La aplicación en sí será muy simple: calcula la gran…

En este artículo integraremos Google Maps con una aplicación AngularDart. La app en sí será muy simple: calcula la distancia de círculo máximo (la distancia más corta en la superficie de una esfera) entre dos marcadores seleccionados en el mapa.

En el camino harás lo siguiente:

  • Registrar tu propia API key de Google Maps.

  • Crear una aplicación web Angular básica.

  • Integrar Dart con el Google Maps JavaScript API, y manejar la interacción con los mapas.

  • Aprender algunos consejos para pulir el componente Angular.

Este artículo es aproximadamente cuatro veces más largo que el código. Si quieres, solo ve a ver el código fuente completo y la demo final.

Demo de Google Maps mostrando marcadores cerca de París y Roma con una distancia calculada de 1095 km.
Captura de pantalla de la demo

Google Maps JavaScript API

#

El Google Maps JavaScript API permite a los desarrolladores integrar Google Maps en su sitio. Puedes personalizar el contenido y el manejo del mapa mostrado con tus propias imágenes, datos y procesamiento.

Para empezar a desarrollar con el Google Maps API, debes registrar una API key gratuita que permite una cantidad razonable de uso. A medida que tu aplicación gane más tracción, puedes actualizarla a un plan de pago.

Visita la página del JavaScript API, y en la parte superior de la página, haz clic en GET A KEY. Crea un nombre para tu proyecto y haz clic en CREATE AND ENABLE API:

Un cuadro de diálogo con el título 'Enable Google Maps JavaScript API' y un campo de texto con 'My Dart Map Project' ingresado.

Tu key estará habilitada y lista para usar en un par de segundos:

Un mensaje de éxito 'You're all set!' en la consola del Google Maps API mostrando que la API key está lista para usar.

Anota tu API key. La usaremos cuando carguemos la librería JavaScript de Maps:

https://maps.googleapis.com/maps/api/js?key=KEY_GOES_HERE

En JavaScript, para crear una instancia del mapa y colocar un marcador en ella, usaríamos el siguiente código:

js
var hostElement = document.getElementById('map-id');
var map = new google.maps.Map(hostElement, {
  zoom: 2,
  center: {lat: 47.4979, lng: 19.0402}
});
var marker = new google.maps.Marker({
  position: {lat: 47.4979, lng: 19.0402},
  map: map,
  label: 'A'
});

Como verás más adelante, el código Dart será muy similar (con todos los beneficios añadidos de Dart).

La aplicación AngularDart

#

La forma más fácil de empezar con una aplicación AngularDart es seguir la guía de Get Started y crear un nuevo proyecto en WebStorm o en la Community Edition de IntelliJ IDEA.

En caso de que estés usando un editor diferente, o solo quieras seguir la estructura de nuestro código de ejemplo, aquí está lo mínimo indispensable.

En el archivo pubspec, pubspec.yaml:

name: google_maps_angular_dart
version: 0.0.1
description: Angular application with Google Maps integration

environment:
  sdk: '>=1.19.0 <2.0.0'

dependencies:
  angular2: ^2.2.0

dev_dependencies:
  dart_to_js_script_rewriter: ^1.0.1

transformers:
- angular2:
    platform_directives:
      - 'package:angular2/common.dart#COMMON_DIRECTIVES'
    platform_pipes:
      - 'package:angular2/common.dart#COMMON_PIPES'
    entry_points: web/main.dart
- dart_to_js_script_rewriter

En la página host, web/index.html:

<!DOCTYPE html>
<html>
<head>
    <!-- other headers -->
    <script defer src="main.dart" type="application/dart"></script>
</head>
<body>
   **<map-control></map-control>**
</body>
</html>

En el punto de entrada de la app, web/main.dart:

import 'package:angular2/platform/browser.dart';
import 'package:google_maps_angular_dart/component/map_control.dart';

void main() {
  bootstrap(MapControl);
}

En el archivo Dart para el componente <map-control>, lib/component/map_control.dart:

import 'package:angular2/core.dart';

@Component(
  selector: 'map-control',
  template: '{{distance}}',
)
class MapControl {
  String distance = 'no distance yet';
}

El código anterior no hará demasiado, pero es un comienzo y puedes ejecutarlo usando pub serve y luego abriendo la página en Dartium:

$ pub serve
Loading source assets...
Loading angular2 and dart_to_js_script_rewriter transformers...
Serving google_maps_angular_dart web on [http://localhost:8080](http://localhost:8080)
Build completed successfully

Integración de Google Maps

#

Para empezar con la integración de Google Maps, pon la siguiente etiqueta script en tu web/index.html. Ten en cuenta que necesitas establecer tu API key:

<script src="https://maps.googleapis.com/maps/api/js?key=**[YOUR_KEY_HERE]**"></script>

Afortunadamente hay un paquete de Google Maps para Dart listo para usar en pub. Añádelo a tu pubspec.yaml:

dependencies:
  angular2: ^2.2.0
  **google_maps: ^3.0.0**

Luego ejecuta pub get para descargar el paquete.

Necesitamos crear un elemento host para el área del mapa en la plantilla de nuestro componente. Empezaremos con un estilo básico, y usaremos el #mapArea para identificar el elemento en el siguiente paso:

<div style="width: 300px; height: 300px" #**mapArea**>[map]</div>

En el código del componente, podemos inyectar la referencia del elemento de la siguiente manera:

@ViewChild('mapArea')
ElementRef mapAreaRef;

La referencia del elemento no está disponible inmediatamente después de que la clase MapControl haya sido creada, así que necesitamos engancharnos en los callbacks del ciclo de vida de Angular:

class MapControl **implements AfterViewInit** {

  @override
  void ngAfterViewInit() {
    // mapAreaRef is available now
  }
}

Consejo: usa el IDE para que escriba los cuerpos de los métodos por ti. Por ejemplo, en IntelliJ, presiona CMD + N (o CTRL + N), y selecciona el elemento de menú "Implement Methods…". Te permitirá seleccionar los métodos que faltan y solo tendrás que preocuparte por el cuerpo del método:

Un menú contextual en IntelliJ IDEA mostrando opciones de 'Generate' como 'Constructor', 'Getter', e 'Implement Methods...'.

Como muestra el siguiente código, el API de Dart es muy similar al de JavaScript, con el beneficio añadido de que tiene verificación de tipos.

class MapControl implements AfterViewInit {
  // ...

@override
  void ngAfterViewInit() {
    GMap map = new GMap(
        mapAreaRef.nativeElement,
        new MapOptions()
          ..zoom = 2
          ..center = new LatLng(47.4979, 19.0402)
                             // ^ Budapest, Hungary
        );
    new Marker(new MarkerOptions()
      ..map = map
      ..position = new LatLng(47.4979, 19.0402)
      ..label = 'A');
  }
}

Un ejemplo de los beneficios de la verificación de tipos es que cuando escuchamos eventos, no necesitamos pensar en cómo acceder a las propiedades. Por ejemplo, el IDE puede ayudarnos a encontrar la propiedad MouseEvent (latLng) que contiene la ubicación del marcador.

Aquí está el código que reacciona al arrastre del marcador:

    marker.onDrag.listen((MouseEvent event) {
      print('New location while dragging: ${event.latLng}');
    });

Capturar eventos de clic en el mapa es similar:

    map.onClick.listen((MouseEvent event) {
      print('User clicked on position: ${event.latLng}');
    });

Juntando todo

#

Para medir la distancia entre dos coordenadas, rastrearemos dos marcadores en el mapa. El usuario deberá poder arrastrar los marcadores o colocarlos haciendo clic.

Necesitamos mantener el seguimiento del mapa y los marcadores como campos:

  GMap _map;
  Marker _aMarker;
  Marker _bMarker;

Actualiza la inicialización para almacenar la referencia del mapa y registrar un manejador de clic. El manejador de clic actualiza las posiciones de los marcadores y la distancia:

  @override
  void ngAfterViewInit() {
    **_map** = new GMap(
        mapAreaRef.nativeElement,
        new MapOptions()
          ..zoom = 2
          ..center = new LatLng(47.4979, 19.0402)
                             // ^ Budapest, Hungary
        );
    **_map.onClick**.listen((MouseEvent event) {
      **_updatePosition**(event.latLng);
      **_updateDistance**();
    });
  }

Aquí está el código para la primera parte del manejador de clic:

  void _updatePosition(LatLng position) {
    if (_aMarker == null) {
      _aMarker = _createMarker(_map, 'A', position);
    } else if (_bMarker == null) {
      _bMarker = _createMarker(_map, 'B', position);
    } else {
      _aMarker.position = _bMarker.position;
      _bMarker.position = position;
    }
  }

El código para la instanciación del marcador es similar al ejemplo anterior:

  Marker _createMarker(GMap map, String label, LatLng position) {
    final Marker marker = new Marker(new MarkerOptions()
      ..map = map
      ..draggable = true
      ..label = label
      ..position = position);
    marker.onDrag.listen((MouseEvent event) {
      _updateDistance();
    });
    return marker;
  }

Con la ayuda de funciones utilitarias de dart:math, somos capaces de resolver las matemáticas del cálculo de la distancia de círculo máximo, y establecer el valor en nuestro campo distance:

  /// Radius of the earth in km.
  const int radiusOfEarth = 6371;

double _toRadian(num degree) => degree * PI / 180.0;

void _updateDistance() {
    if (_aMarker == null || _bMarker == null) return;
    LatLng a = _aMarker.position;
    LatLng b = _bMarker.position;
    double dLat = _toRadian(b.lat - a.lat);
    double sLat = pow(sin(dLat / 2), 2);
    double dLng = _toRadian(b.lng - a.lng);
    double sLng = pow(sin(dLng / 2), 2);
    double cosALat = cos(_toRadian(a.lat));
    double cosBLat = cos(_toRadian(b.lat));
    double x = sLat + cosALat * cosBLat * sLng;
    double d = 2 * atan2(sqrt(x), sqrt(1 - x)) * radiusOfEarth;
    distance = '${d.round()} km';
  }

¿Alguna vez te has preguntado a qué distancia vives de un familiar, una ubicación famosa o un punto de referencia? Es hora de probar la aplicación y comprobarlo por ti mismo. Resulta que mi casa está a 9815 km de la sede de Google en Mountain View:

Mapa mostrando la distancia entre el Googleplex en Mountain View, California, y la casa del autor en Budapest, Hungría.
La distancia entre el Googleplex en Mountain View, California, y la casa del autor en Budapest, Hungría

Puliendo la aplicación

#

La integración de Google Maps en nuestra app Angular está completa en este punto. Estamos escuchando y reaccionando a los eventos del mapa y creando objetos en el mapa. En esta última sección implementaremos funciones que añaden un buen acabado a nuestra demo.

Separar plantilla y estilo

#

Es una buena práctica poner las plantillas de UI complejas en un archivo .html separado. Podemos hacer lo mismo con los estilos CSS:

@Component(
    selector: 'map-control',
    templateUrl: 'map_control.html',
    styleUrls: const <String>['map_control.css'])
class MapControl implements AfterViewInit {

El elemento del mapa tiene una clase CSS, map-area:

<div class="map-area" #mapArea>[map]</div>

Nuestro archivo CSS puede ser tan simple como esto:

.map-area {
  width: 500px;
  height: 400px;
  margin: 10px;
}

Manejando la unidad de distancia

#

Algunas personas dominan las conversiones de km ⟷ millas, pero el resto de nosotros preferiría un menú desplegable donde podamos seleccionar la unidad de distancia. En la plantilla HTML el menú desplegable puede ser un simple elemento <SELECT> con un binding del modelo al campo unit.

<label>Unit:</label>
<select [(ngModel)]="**unit**">
  <option value="km">km</option>
  <option value="miles">miles</option>
</select>

En el código Dart, queremos almacenar la unidad y actualizar la distancia siempre que la unidad se actualice:

String _unit = 'km';

String get unit => _unit;

set unit(String value) {
  _unit = value;
  _updateDistance();
}

Y no olvides actualizar el km que estaba previamente hardcoded en el cálculo de distancia:

    /// Const value to convert from km to miles.
    const double milesPerKm = 0.621371;

    // ... same code as earlier
    if (unit == 'miles') {
      d *= milesPerKm;
    }
    distance = '${d.round()} $unit';

Formateando las coordenadas

#

¿Qué pasa si estamos interesados en publicar la posición de nuestros marcadores? La forma más fácil sería exponer los valores de posición en Dart, para que podamos usar {{a}} y {{b}} en nuestra plantilla:

  // Expose the position values.
  LatLng get a => _aMarker?.position;
  LatLng get b => _bMarker?.position;

La plantilla puede ocultar la etiqueta cuando el marcador aún no está inicializado, previniendo posibles problemas con valores nulos:

  <div *ngIf="a != null">A: {{a}}</div>
  <div *ngIf="b != null">B: {{b}}</div>

Sin embargo, {{a}} se traduciría en llamar a LatLng.toString(), y nos daría dos valores double realmente largos, cuando con unos pocos dígitos sería suficiente. Una solución es usar pipes en la plantilla:

<div *ngIf="a != null">A: {{a.lat | number : '1.4-4'}}, {{a.lng | number : '1.4-4'}}</div>
<div *ngIf="b != null">B: {{b.lat | number : '1.4-4'}}, {{b.lng | number : '1.4-4'}}</div>

La guía de sintaxis de plantillas sugiere colocar esa lógica en la clase controladora, para facilitar las pruebas:

  /// Formatted position of the 'A' marker.
  String get **aPosition** => _formatPosition(a);

  /// Formatted position of the 'B' marker.
  String get bPosition => _formatPosition(b);

  String _formatPosition(LatLng pos) {
    if (pos == null) return null;
    return '${pos.lat.toStringAsFixed(4)}, '
        '${pos.lng.toStringAsFixed(4)}';
  }

Con eso, la plantilla puede ser mucho más simple:

<div *ngIf="a != null">A: {{**aPosition**}}</div>
<div *ngIf="b != null">B: {{**bPosition**}}</div>

Limpiar la plantilla

#

Como último paso, mueve todas las partes de código restantes de la plantilla al controlador:

  /// Whether the 'A' marker's positions should be shown
  bool get **showA** => a != null;

  /// Whether the 'B' marker's positions should be shown
  bool get showB => b != null;

  /// Whether the 'distance' label should be shown
  bool get showDistance => distance != null;

Al hacerlo, la plantilla solo referencia los getters:

<div *ngIf="**showA**">A: {{aPosition}}</div>
<div *ngIf="**showB**">B: {{bPosition}}</div>

<p *ngIf="showDistance">
  Distance: {{distance}}<br/>
</p>

Notas finales

#

Como puedes ver, implementar integración bidireccional con Google Maps es fácil: el código fuente es limpio y legible. Con el soporte completo de la cadena de herramientas de Dart, es fácilmente extensible, sin preocuparse nunca por lo que podríamos romper en otros lugares.

Como ejercicio para el lector, puedes añadir visualización de mapa de calor al mapa usando el mismo API en Dart. Revisa el paquete google_maps para ver todas las posibilidades.

Más de Dart