Using Angular

This article describes how to use the HERE Maps API for JavaScript with Angular. The target is to demonstrate how to build an Angular component that displays the map and responds to the actions of the user, be it direct interaction with the map or the other components.

Setup

For the fast setup of the new Angular application we will use the Angular CLI. It provides a fast way to get started building a new single-page application. To start with Angular CLI first install it globally:

npm install -g @angular/cli

After that run the ng command to initialize the project's scaffoldings:

ng new jsapi-angular && cd jsapi-angular

The call above will launch the interactive shell, for the simplicity of the example select to install Angular routing and use CSS as a stylesheet format:

[? Would you like to add Angular routing? No
[? Which stylesheet format would you like to use? CSS

The directory structure in the jsapi-angular directory looks as follows. The Angular components reside in the src directory:

jsapi-angular
├── node_modules
├── package.json
├── .gitignore
├── .browserslistrc
├── .editorconfig
├── angular.json
├── karma.conf.js
├── README.md
├── tsconfig.app.json
├── tsconfig.json
├── tsconfig.spec.json
├── dist
│   ├── ...
└── src
    ├── app
    │   ├── app-routing.module.ts
    │   ├── app.component.css
    │   ├── app.component.html
    │   ├── app.component.spec.ts
    │   ├── app.component.ts
    │   └── app.module.ts
    ├── assets
    │   ├── ...
    ├── environments
    |   ├── ...
    ├── favicon.ico
    ├── index.html
    ├── main.ts
    ├── polyfills.ts
    ├── styles.css
    └── test.ts

The recommended way to use HERE Maps API for JavaScript within this environment is to install maps-api-for-javascript NPM package which is hosted at https://repo.platform.here.com/. Add a registry entry to the NPM configuration by executing the following command:

npm config set @here:registry https://repo.platform.here.com/artifactory/api/npm/maps-api-for-javascript/

After that the package from the @here namespace can be installed as usual:

npm install @here/maps-api-for-javascript --save

After that add the option "allowSyntheticDefaultImports": true, under angularCompilerOptions to tsconfig.json. At this step the environment setup is complete, all packages needed to build a sample application are installed, and it is possible to start the development server by executing:

ng serve

The command above launches the development server with the "hot reload" functionality. You can load the application by navigating to http://localhost:4200/ in the browser.

Add a static map component

It is possible to add a static map to the application by creating a component that contains an H.Map instance and renders it to the component's container. First generate a component files with the help of CLI:

ng generate component jsmap

The command adds a jsmap folder under the src/app. The folder contains all the files needed to build the component:

└── src
    ├── app
    │   ├── jsmap
    |   |   ├── jsmap.component.css
    |   |   ├── jsmap.component.html
    |   |   ├── jsmap.component.spec.ts
    |   |   └── jsmap.component.ts
    │   ├── app-routing.module.ts
    │   ├── app.component.css
    │   ├── app.component.html
    │   ├── app.component.spec.ts
    │   ├── app.component.ts
    │   └── app.module.ts
    ...

Add the following code to the jsmap.component.ts:

import { Component, ViewChild, ElementRef } from '@angular/core';
import H from '@here/maps-api-for-javascript';

@Component({
  selector: 'app-jsmap',
  templateUrl: './jsmap.component.html',
  styleUrls: ['./jsmap.component.css']
})
export class JsmapComponent {

  private map?: H.Map;

  @ViewChild('map') mapDiv?: ElementRef; 

  ngAfterViewInit(): void {
    if (!this.map && this.mapDiv) {
      // instantiate a platform, default layers and a map as usual
      const platform = new H.service.Platform({
        apikey: '{YOUR_API_KEY}'
      });
      const layers = platform.createDefaultLayers();
      const map = new H.Map(
        this.mapDiv.nativeElement,
        layers.vector.normal.map,
        {
          pixelRatio: window.devicePixelRatio,
          center: {lat: 0, lng: 0},
          zoom: 2,
        },
      );
      this.map = map;
    }
  }

}

The code above imports the HERE Maps API for JavaScript library and instantiates the map in the ngAfterViewInit hook. Additionally replace the content of the jsmap.component.html with

<div #map id="map"></div>

and add the following style rule to jsmap.component.css:

#map {
  width: 300px;
  height: 300px;
}

The component above now can be used within the root App component, replace the content of the app.component.html with the following code:

<app-jsmap></app-jsmap>

That will render the static map at the zoom level 2 and 0 latitude and longitude in the 300 by 300 pixels viewport: Static Map component

Resizing the map

In many cases it is desirable that the map occupies the full width and/or height of the component. The H.Map instance does not attempt to deduce when the parent container is resized and the map needs an explicit resize() method call in order to adjust to the new dimensions of the container. To demonstrate how it can be achieved within the component we will use the simple-element-resize-detector. Run the following command from the project's directory:

npm install simple-element-resize-detector --save
npm install @types/simple-element-resize-detector --save-dev

In the src/jsmap/jsmap.component.ts adjust the import statements by importing the simple-element-resize-detector library:

import { Component, ViewChild, ElementRef } from '@angular/core';
import H from '@here/maps-api-for-javascript';
import onResize from 'simple-element-resize-detector';

Update ngAfterViewInit method with the map.getViewPort().resize():

  ngAfterViewInit(): void {
    if (!this.map && this.mapDiv) {
      // instantiate a platform, default layers and a map as usual
      const platform = new H.service.Platform({
        apikey: '{YOUR_API_KEY}'
      });
      const layers = platform.createDefaultLayers();
      const map = new H.Map(
        this.mapDiv.nativeElement,
        layers.vector.normal.map,
        {
          pixelRatio: window.devicePixelRatio,
          center: {lat: 0, lng: 0},
          zoom: 2,
        },
      );
      onResize(this.mapDiv.nativeElement, () => {
        map.getViewPort().resize();
      });
      this.map = map;
    }
  }

Change the container style in jsmap.component.css to:

#map {
  width: 100%;
  height: 300px;
}

the component will assume the width of the enclosing container: Static Map component 100% width

Setting the zoom and center

We want another component to take a user's input and change the zoom level and the center of the map. Create the new component by running Angular CLI:

ng generate component mapposition

The mapposition component (src/app/mapposition/mapposition.component.html) has three input fields: zoom, latitude and longitude. The template below introduces a change event listener that redispatches events to the parent component:

<div>
  Zoom:
  <input
    (change)="notify.emit($event)"
    name="zoom"
    type="number"
    value="2"
  />
</div>
<div>
  Latitude:
  <input
    (change)="notify.emit($event)"
    name="lat"
    type="number"
    value="0"
  />
</div>
<div>
  Longitude:
  <input
    (change)="notify.emit($event)"
    name="lng"
    type="number"
    value="0"
  />
</div>

The TypeScript part of the component (src/app/mapposition/mapposition.component.ts) uses the @Output decorator and EventEmitter class to notify the parent component about the changes in the user input:

import { Component, Output, EventEmitter } from '@angular/core';

@Component({
  selector: 'app-mapposition',
  templateUrl: './mapposition.component.html',
  styleUrls: ['./mapposition.component.css']
})
export class MappositionComponent {

  @Output() notify = new EventEmitter();

}

We will use the parent src/app component to pass the values between the map and input fields. First adjust the template file src/app/app.component.html to include the "input" and "output" values:

<app-jsmap
  [zoom]="zoom"
  [lat]="lat"
  [lng]="lng"
></app-jsmap>
<app-mapposition
  (notify)="handleInputChange($event)"
></app-mapposition>

Add the change event handler to the src/app/app.component.ts that handler will update the values according to the user's input and Angular will take care of passing these values to the jsmap component:

import { Component } from '@angular/core';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css'],
})
export class AppComponent {
  title = 'jsapi-angular';

  constructor() {
    this.zoom = 2;
    this.lat = 0;
    this.lng = 0;
  }

  zoom: number;
  lat: number;
  lng: number;

  handleInputChange(event: Event) {
    const target = <HTMLInputElement> event.target;
    if (target) {
      if (target.name === 'zoom') {
        this.zoom = parseFloat(target.value);
      }
      if (target.name === 'lat') {
        this.lat = parseFloat(target.value);
      }
      if (target.name === 'lng') {
        this.lng = parseFloat(target.value);
      }
    }
  }
}

The final step is to adjust the jsmap component by adding the ngOnChanges hook and @Input decorator properties for the latitude, longitude and the zoom level:

  @Input() public zoom = 2;
  @Input() public lat = 0;
  @Input() public lng = 0;

  ngOnChanges(changes: SimpleChanges) {
    if (this.map) {
      if (changes.zoom !== undefined) {
        this.map.setZoom(changes.zoom.currentValue);
      }
      if (changes.lat !== undefined) {
        this.map.setCenter({lat: changes.lat.currentValue, lng: this.lng});
      }
      if (changes.lng !== undefined) {
        this.map.setCenter({lat: this.lat, lng: changes.lng.currentValue});
      }
    }
  }

And adjust the import statement:

import { Component, ViewChild, ElementRef, Input, SimpleChanges } from '@angular/core';

The resulting application can take the input from the user with the help of the mapposition component, store the state in the app and update the jsmap as per the user input: Map component with the user input

The interactive map

The application above takes only the input via the mapposition component, normally users expect the map itself to be interactive. The optimal solution enables the user to input the values directly and see the desired location on the map, as well as interact with the map and see the current coordinates. That can be achieved by adding the mapviewchange listener to the H.Map instance and updating the app state with the help of the EventEmitter. In order to achieve that add a method in app.component.ts that will be responsible to update the App state:

  handleMapChange(event: H.map.ChangeEvent) {
    if (event.newValue.lookAt) {
      const lookAt = event.newValue.lookAt;
      this.zoom = lookAt.zoom;
      this.lat = lookAt.position.lat;
      this.lng = lookAt.position.lng;
    }
  }

Reflect the changes in the app.component.html template by replaceing the current content with the following code:

<app-jsmap
  [zoom]="zoom"
  [lat]="lat"
  [lng]="lng"
  (notify)="handleMapChange($event)"
></app-jsmap>
<app-mapposition
  [zoom]="zoom"
  [lat]="lat"
  [lng]="lng"
  (notify)="handleInputChange($event)"
></app-mapposition>

Additionally in the jsmap component import Output and EventEmitter classes:

import { Component, ViewChild, ElementRef, Input, SimpleChanges, Output, EventEmitter } from '@angular/core';

Add the following lines to attach a listener and enable the interactive behaviour after the map instantiation in the ngAfterViewInit:

map.addEventListener('mapviewchange', (ev: H.map.ChangeEvent) => {
  this.notify.emit(ev)
});
new H.mapevents.Behavior(new H.mapevents.MapEvents(map));

Update the ngOnChanges method to throttle the the unnecessary map updates:

  private timeoutHandle: any;
  @Output() notify = new EventEmitter();

  ngOnChanges(changes: SimpleChanges) {
      clearTimeout(this.timeoutHandle);
      this.timeoutHandle = setTimeout(() => {
        if (this.map) {
          if (changes.zoom !== undefined) {
            this.map.setZoom(changes.zoom.currentValue);
          }
          if (changes.lat !== undefined) {
            this.map.setCenter({lat: changes.lat.currentValue, lng: this.lng});
          }
          if (changes.lng !== undefined) {
            this.map.setCenter({lat: this.lat, lng: changes.lng.currentValue});
          }
        }
      }, 100);
  }

The last step is to adjust mapposition component in such a way, that it reflects the current position of the map. In order to achieve that the Input decorator must be imported from the @angular/core in the mapposition.component.ts:

import { Component, Output, EventEmitter, Input } from '@angular/core';

Afterwards the three input parameters must be added in the class body:

  @Input() public zoom = 2;
  @Input() public lat = 0;
  @Input() public lng = 0;

The template mapposition.component.ts must be adjusted accordingly:

<div>
  Zoom:
  <input
    (change)="notify.emit($event)"
    name="zoom"
    type="number"
    [value]="zoom"
  />
</div>
<div>
  Latitude:
  <input
    (change)="notify.emit($event)"
    name="lat"
    type="number"
    [value]="lat"
  />
</div>
<div>
  Longitude:
  <input
    (change)="notify.emit($event)"
    name="lng"
    type="number"
    [value]="lng"
  />
</div>

The resulting application consist of the interactive map and input fields. When the user interacts with the map the input fields are updated and vice versa.

results matching ""

    No results matching ""