В этой статье мы интегрируем Google Maps с приложением AngularDart. Само приложение будет очень простым: оно рассчитывает расстояние по большому кругу (кратчайшее расстояние на поверхности сферы) между двумя выбранными маркерами на карте.

По пути вы:

  • Зарегистрируйте свой собственный ключ API Карт Google.
  • Создайте базовое веб-приложение на Angular.
  • Интегрируйте Dart с Google Maps JavaScript API и управляйте взаимодействием с картами.
  • Изучите несколько советов по полировке компонента Angular.

Эта статья примерно в четыре раза длиннее кода. Если хотите, просто посмотрите полный исходный код и последнюю демонстрацию.

Google Maps JavaScript API

Google Maps JavaScript API позволяет разработчикам встраивать и интегрировать Google Maps на свои сайты. Вы можете настроить отображение отображаемой карты и обработку ваших собственных изображений, данных и обработки.

Чтобы начать разработку с помощью Google Maps API, вы должны зарегистрировать бесплатный ключ API, который позволяет использовать его в разумных пределах. По мере того, как ваше приложение набирает обороты, вы можете перейти на платный план.

Посетите страницу JavaScript API и вверху страницы нажмите GET A KEY. Придумайте название для своего проекта и нажмите CREATE AND ENABLE API:

Ваш ключ будет включен и готов к использованию через пару секунд:

Запишите свой ключ API. Мы будем использовать его при загрузке библиотеки JavaScript Карт:

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

В JavaScript, чтобы создать экземпляр карты и разместить на нем маркер, мы будем использовать следующий код:

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'
});

Как вы увидите позже, код Dart будет очень похож (со всеми дополнительными преимуществами Dart).

Приложение AngularDart

Самый простой способ начать работу с приложением AngularDart - следовать руководству Начало работы и создать новый проект в WebStorm или в Community Edition IntelliJ IDEA.

Если вы используете другой редактор или просто хотите следовать структуре нашего примера кода, вот минимум.

В файле 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

На главной странице 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>

В точке входа приложения web/main.dart:

import 'package:angular2/platform/browser.dart';
import 'package:google_maps_angular_dart/component/map_control.dart';
void main() {
  bootstrap(MapControl);
}

В файле Dart для компонента <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';
}

Приведенный выше код мало что делает, но это только начало, и вы можете запустить его, используя pub serve, а затем открыв страницу в 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
Build completed successfully

Интеграция с Google Maps

Чтобы начать интеграцию с Google Maps, поместите следующий тег скрипта в свой web/index.html. Обратите внимание, что вам нужно установить свой ключ API:

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

К счастью, в pub есть готовый к использованию пакет Google Maps Dart. Добавьте его в свой pubspec.yaml:

dependencies:
  angular2: ^2.2.0
  google_maps: ^3.0.0

Затем запустите pub get, чтобы загрузить пакет.

Нам нужно создать основной элемент для области карты в нашем шаблоне компонента. Мы начнем с базового стиля и на следующем шаге будем использовать привязку #mapArea для идентификации элемента:

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

В код компонента мы можем вставить ссылку на элемент следующим образом:

@ViewChild('mapArea')
ElementRef mapAreaRef;

Ссылка на элемент недоступна сразу после создания класса MapControl, поэтому нам нужно подключиться к обратным вызовам жизненного цикла Angular:

class MapControl implements AfterViewInit {
  @override
  void ngAfterViewInit() {
    // mapAreaRef is available now
  }
}

Совет: используйте IDE, чтобы написать за вас тела методов. Например, в IntelliJ нажмите CMD + N (или CTRL + N) и выберите пункт меню «Реализовать методы…». Это позволит вам выбрать недостающие методы, и вам нужно будет беспокоиться только о теле метода:

Как показано в следующем коде, Dart API очень похож на API JavaScript, с дополнительным преимуществом, заключающимся в проверке типа.

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');
  }
}

Одним из примеров преимуществ проверки типов является то, что, когда мы отслеживаем события, нам не нужно думать о том, как получить доступ к свойствам. Например, IDE может помочь нам найти свойство MouseEvent (latLng), которое содержит местоположение маркера.

Вот код, который реагирует на перетаскивание маркера:

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

Захват событий щелчка на карте аналогичен:

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

Собираем все вместе

Чтобы измерить расстояние между двумя координатами, мы будем отслеживать два маркера на карте. Пользователь может перетаскивать маркеры или размещать их, щелкая.

Нам нужно отслеживать карту и маркеры как поля:

  GMap _map;
  Marker _aMarker;
  Marker _bMarker;

Обновите инициализацию, чтобы сохранить ссылку на карту и зарегистрировать обработчик кликов. Обработчик кликов обновляет позиции маркеров и расстояние:

  @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();
    });
  }

Вот код первой части обработчика кликов:

  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;
    }
  }

Код для создания экземпляра маркера аналогичен предыдущему примеру:

  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;
  }

С помощью вспомогательных функций из dart:math мы можем разобраться в математике вычисления расстояния большого круга и установить значение в нашем поле 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';
  }

Вы когда-нибудь задумывались, как далеко вы живете от родственника, известного места или достопримечательности? Пришло время опробовать приложение и проверить его на себе. Оказывается, мой дом находится в 9815 км от штаб-квартиры Google в Маунтин-Вью:

Полировка приложения

На этом интеграция Google Maps в наше приложение Angular завершена. Мы слушаем и реагируем на события карты и создаем объекты на карте. В этом последнем разделе мы реализуем функции, которые добавят красивую отделку нашей демонстрации.

Отдельный шаблон и стиль

Рекомендуется помещать сложный шаблон пользовательского интерфейса в отдельный файл .html. То же самое можно сделать и со стилями CSS:

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

Элемент карты имеет класс CSS map-area:

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

Наш файл CSS может быть таким простым:

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

Обработка единицы расстояния

Некоторые люди свободно конвертируют км ⟷ мили, но остальные хотели бы раскрывающийся список, в котором мы могли бы выбрать единицу измерения расстояния. В шаблоне HTML раскрывающийся список может быть простым элементом <SELECT> с привязкой модели к полю unit.

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

В коде Dart мы хотим сохранить единицу и обновлять расстояние при каждом обновлении единицы:

String _unit = 'km';
String get unit => _unit;
set unit(String value) {
  _unit = value;
  _updateDistance();
}

И не забудьте обновить ранее жестко запрограммированный km при расчете расстояния:

    /// 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';

Форматирование координат

Что, если мы заинтересованы в публикации положения наших маркеров? Самый простой способ - показать значения позиции в Dart, чтобы мы могли использовать {{a}} и {{b}} в нашем шаблоне:

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

Шаблон может скрыть метку, когда маркер еще не инициализирован, что предотвращает потенциальные проблемы с нулевыми значениями:

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

Однако {{a}} будет преобразовываться в вызов LatLng.toString() и даст нам два действительно длинных двойных значения, тогда как несколько цифр вполне подойдут. Одно из решений - использовать в шаблоне трубы:

<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>

Руководство по синтаксису шаблонов предлагает разместить эту логику в классе контроллера для лучшего тестирования:

  /// 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)}';
  }

При этом шаблон может быть намного проще:

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

Очистите шаблон

В качестве последнего шага переместите все оставшиеся части кода из шаблона в контроллер:

  /// 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;

При этом шаблон ссылается только на геттеры:

<div *ngIf="showA">A: {{aPosition}}</div>
<div *ngIf="showB">B: {{bPosition}}</div>
<p *ngIf="showDistance">
  Distance: {{distance}}<br/>
</p>

Заключительные примечания

Как видите, реализовать двустороннюю интеграцию с Google Maps просто: исходный код чистый и читаемый. Благодаря полной поддержке набора инструментов Dart его легко расширять, не беспокоясь о том, что мы можем сломать в других местах.

В качестве упражнения для читателя вы можете добавить на карту визуализацию тепловой карты, используя тот же API в Dart. Ознакомьтесь со всеми возможностями пакета google_maps.