ISS-Tracking in Angular2

By | 2016-09-27

So a while back I created little app, while learning Angular 1.x, that tracked the International Space Station in near-real-time. I have since updated this to use Angular2. Due to Github now running HTTPS, in Google Chrome you may have to click the button in the address bar to ‘Load unsafe scripts’ because the API data endpoints are HTTP only. For those coming up to speed, Angular2 is the latest JavaScript framework from Google. The preference is to code in TypeScript for added type-checking (similar to how React users may use Babel and Flow to use ES6 syntax and have type-checking). This helps provide a complete developer experience from just a couple packages instead of having many libraries to handle transpilation, type-checking, etc. I do use Webpack for my build process and won’t be going into that here, but for those who wish to take look the repo can be found here.

Setting Up
First things first, in any web project you need an html file:
index.html

<!DOCTYPE html>
<html>
  <head>
    <base href="/ISS-Tracking/">
    <title>Tracking the International Space Station</title>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <script type="text/javascript" src="https://maps.googleapis.com/maps/api/js"></script>
    <link rel="stylesheet" type="text/css" href="./dist/bootstrap.min.css">
  </head>
  <body>
    <iis-app>Loading...</iis-app>
  </body>
</html>

In this html template I’m being a little lazy and loading the Google Maps API and Bootstrap CSS the old fashioned way. The import piece is in the body tag where we include:

<iis-app>Loading...</iis-app>

This tells Angular where to load our root component.

I already know how I want the application to look so we are going to approach building this a bit backwards by going ahead and setting out bootstrapping and routing to what I know we will want. Our folder structure will look like this:
-main.ts
-index.html
-app/app.component.html
-app/app.component.ts
-app/app.module.ts
-app/app.routes.ts
-app/Shared/formatDurationTime.pipe.ts
-app/Shared/formatPassTimes.pipe.ts
-app/Shared/iss.service.ts
-app/PassTimes/passTimes.component.html
-app/PassTimes/passTimes.component.ts
-app/Map/map.component.html
-app/Map/map.component.ts
-app/Home/home.component.html
-app/Home/home.component.ts
-app/Astronauts/astronauts.component.html
-app/Astronauts/astronauts.component.ts

The main.ts file is where we will bootstrap our application, our app.** files contain our root module definition, our routes, and our root component. Shared files are put in the Shared folder, and beyond that each component or page/route of our application has it’s own folder containing the component logic and template.

Bootstrapping Angular
To bootstrap our application we import platformBrowserDynamic from the appropriate Angular library, also import enableProdMode to enable optimizations, and import our AppModule (which we haven’t created yet). Then we call bootstrapModule on our AppModule.
main.ts

import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { enableProdMode } from '@angular/core';
import { AppModule } from './app/app.module';

enableProdMode();

platformBrowserDynamic().bootstrapModule(AppModule);

What does the AppModule look like?
The AppModule is where we combine all references to any Angular components we will use from the Angular library with any components and providers/service we build in our application. For instance, if we plan on using Angular’s HTTP library/Module or the Forms Module we need to reference that here. In the @NgModule decorator we list our imports, declarations, providers, and bootstrap the root component. In short, ‘imports’ include an Angular library modules, any sub-modules you create, and the routing for our single-page-application. Declarations include any components you build. Providers include any ‘injectables’ such as services. And lastly bootstrap your root component.
app/app.module.ts

import { NgModule } from '@angular/core';
import { BrowserModule }  from '@angular/platform-browser';
import { JsonpModule, HttpModule } from '@angular/http';
import { FormsModule } from '@angular/forms';

import {routing} from './app.routes';
import {AppComponent} from './app.component';
import {HomeComponent} from './Home/home.component';
import {MapComponent} from './Map/map.component';
import {AstronautsComponent} from './Astronauts/astronauts.component';
import {PassTimesComponent} from './PassTimes/passTimes.component';
import {PageNotFoundComponent} from './PageNotFound/pageNotFound.component';
import {ISSService} from './Shared/iss.service';
import {FormatDurationTimePipe} from './Shared/formatDurationTime.pipe';
import {FormatPassTimePipe} from './Shared/formatPassTime.pipe';

@NgModule({
  imports: [
    BrowserModule,
    JsonpModule,
    HttpModule,
    FormsModule,
    routing
  ],
  declarations: [
    AppComponent,
    HomeComponent,
    MapComponent,
    AstronautsComponent,
    PassTimesComponent,
    PageNotFoundComponent,
    FormatDurationTimePipe,
    FormatPassTimePipe
  ],
  bootstrap: [ AppComponent ],
  providers: [ISSService]
})
export class AppModule { }

Routing
To setup our routing we need to import Routes and RouterModule from @angular/router, then import a reference to each component you want to set routing to. Each route contains at minimum a ‘path’ and generally a ‘component’, though you can do other things such as redirect to other routes, etc. Optionally, you can set ‘canActivate’ guards here to make conditions that have to be met in order to allow user to navigate certain routes. The ‘**’ path is a catch all in case a route is not found.
app/app.routes.ts

import {Routes, RouterModule} from '@angular/router';

import {HomeComponent} from './Home/home.component';
import {MapComponent} from './Map/map.component';
import {AstronautsComponent} from './Astronauts/astronauts.component';
import {PassTimesComponent} from './PassTimes/passTimes.component';

export const routes: Routes = [
  {path: '', component: HomeComponent},
  {path: 'map', component: MapComponent},
  {path: 'astronauts', component: AstronautsComponent},
  {path: 'passtimes', component: PassTimesComponent},
  {path: '**', component: HomeComponent}
];

export const routing = RouterModule.forRoot(routes);

Root App Component
Our root App component is pretty sparse, mainly setting up the template on the main page with navigation bar and links and specifying a place holder where our routes will be injected.
app/app.component.ts

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

@Component({
  selector: 'iis-app',
  templateUrl: './app.component.html'
})
export class AppComponent { }

app/app.component.html

<nav class="navbar navbar-inverse navbar-fixed-top">
    <div class="container">
        <div class="navbar-header">
            <button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#navbar" aria-expanded="false" aria-controls="navbar">
                <span class="sr-only">Toggle navigation</span>
                <span class="icon-bar"></span>
                <span class="icon-bar"></span>
                <span class="icon-bar"></span>
            </button>
            <a class="navbar-brand">IIS Tracker</a>
        </div>
        <div id="navbar" class="collapse navbar-collapse">
            <ul class="nav navbar-nav">
                <li><a href="#" [routerLink]="['/']">Home</a></li>
                <li><a href="#" [routerLink]="['/astronauts']">Astronauts</a></li>
                <li><a href="#" [routerLink]="['/map']">Map</a></li>
                <li><a href="#" [routerLink]="['/passtimes']">Pass Times</a></li>
            </ul>
        </div>
        <!--/.nav-collapse -->
    </div>
</nav>

<div class="container" style="margin-top:40px">
    <router-outlet></router-outlet>
</div>

Shared Items
Before we go any futher, in our Shared folder we contain two Pipes. Pipes are functions that can be used on string interpolation in Angular… ie {{variable | pipeFunction}} In this example, the variable value is passed into the pipeFunction and the result is what is displayed in the data binding. Our two pipes look like the following:
app/Shared/formatdurationTime.pipe.ts

import { Pipe, PipeTransform } from '@angular/core';

@Pipe({ name: 'formatDurationTime' })
export class FormatDurationTimePipe implements PipeTransform {
    transform(value: number): string {
        let visible = Math.round(value / 60);
        return Math.round(visible / 2).toString() + "min";
    }
}

app/Shared/formatPassTime.pipe.ts

import { Pipe, PipeTransform } from '@angular/core';

@Pipe({name: 'formatPassTime'})
export class FormatPassTimePipe implements PipeTransform {
  transform(value: number): string {
    return (new Date(value * 1000)).toString();
  }
}

Next, in our Shared folder is going to be the iis.service.ts. This file is responsible for handling our HTTP calls to the API service. This service will be injected into our components to enable them to easily get data without having to know about the underlying HTTP service. In a full production app, you’d want to have an Error Handler service that can catch the errors off your HTTP calls, log them, and handle them appropriately. For more information on that check here.
app/Shared/iss.service.ts

import {Injectable} from '@angular/core';
import {Jsonp, Http, Response} from '@angular/http';
import {Observable} from 'rxjs/Observable';
import 'rxjs/Rx';
import 'rxjs/add/operator/map';

@Injectable()
export class ISSService {

    constructor(private jsonp: Jsonp, private http: Http) {

    }

    getAstros() {
        return this.jsonp.request('http://api.open-notify.org/astros.json?callback=JSONP_CALLBACK', { method: 'Get' })
          .map(res => res.json());
    };

    moveISS() {
        return this.jsonp.request('http://api.open-notify.org/iss-now.json?callback=JSONP_CALLBACK', { method: 'Get' })
            .map(res => res.json());
    };

    futurePasses(myLat, myLng) {
        return this.jsonp.request('http://api.open-notify.org/iss-pass.json?lat=' + myLat + '&lon=' + myLng + '&alt=300&n=5&callback=JSONP_CALLBACK', { method: 'Get' })
          .map(res => res.json());
    };

    getLatLong(loc:string){
      return this.http.get('https://maps.googleapis.com/maps/api/geocode/json?address=' + loc)
        .map(res => res.json());
    }

}

Home Component
Our home component is pretty slim. All we are doing here is displaying a message on the home page that explains a little about the app.
app/Home/home.component.ts

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

@Component({
  templateUrl: './home.component.html'
})
export class HomeComponent { }

app/Home/home.component.html

<h2>Home</h2>
<p>This application serves as a more sophisticated 'Todo' app for learning new languages/frameworks while working on something that is fun and exciting. This simple app explores many of the functions/features that one would encounter in a larger app -- routing, api calls, build processes.  To enable the <span style="font-style:italics">fun</span> part of the application, I am using the <a href="http://open-notify.org/">Open Notify</a> to retrieve information about the International Space Station.  Using the links in the menu bar above, you can see the names of the current astronauts on the station, view the location on Google Maps, and check the next times the station will pass over a city near you.</p>

Pass Times Component
Our Pass Times Component allows a user to input a City, State and determine when the Space Station will next pass over that region and approximately how long it will be visible. We aren’t doing any checks on daylight vs nighttime or weather. We are just displaying the data the API gives us. In our constructor we inject the IISService instance. In our html form we tell Angular to handle the submit by calling ‘getPasses()’. Notice our template will use the Pipes we created earlier, but they are not injected anywhere into this component. Our AppModule is handling that for us.
app/PassTimes/passTimes.component.ts

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

declare var google: any;

import {ISSService} from '../Shared/iss.service';

@Component({
    templateUrl: './passTimes.component.html'
})
export class PassTimesComponent {
    private searchLocation: string;
    private nextPasses: any;
    private myLat: any;
    private myLng: any;
    constructor(private issSvc: ISSService) {

    }

    getPasses() {

        this.issSvc.getLatLong(this.searchLocation.replace(' ', '+')).subscribe(
            data1 => {
                let lat = data1.results[0].geometry.location.lat;
                let long = data1.results[0].geometry.location.lng;
                this.issSvc.futurePasses(lat, long).subscribe(
                    data2 => { this.nextPasses = data2.response; console.log(data2.response) }
                );
            }
        );

    }

}

app/PassTimes/passTimes.component.html

<h2>Pass Times</h2>

<h4>Search Next Pass Times</h4>
<form (ngSubmit)="getPasses()" #passForm="ngForm">
  <input type="text" placeholder="City, State" [(ngModel)]="searchLocation" required name="searchLocation">
  <button type="submit">Get Pass Times</button>
</form>

<table *ngIf="nextPasses" class="table table-striped">
    <tr>
        <th>Time</th>
        <th>Duration</th>
    </tr>
    <tr *ngFor="let pass of nextPasses">
        <td>{{pass.risetime | formatPassTime}}</td>
        <td>{{pass.duration | formatDurationTime}}</td>
    </tr>
</table>

Map Component
In our Map Component we again inject our IISService and then in the constructor we get the initial location and instantiate our Google Map. Then we set off an interval to update that location of the station every 5 seconds. Our template simply contains interpolations for the Latitude and Longitude and a place holder div for the Google Map.
app/Map/map.component.ts

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

declare var google: any;

import {ISSService} from '../Shared/iss.service';

@Component({
    templateUrl: './map.component.html'
})
export class MapComponent {
    private lat: number;
    private lon: number;
    private map: any;
    private marker: any;

    constructor(private issSvc: ISSService) {
        this.issSvc.moveISS().subscribe(
            data => {
                this.lat = data.iss_position.latitude;
                this.lon = data.iss_position.longitude;
                let mapOptions = {
                    center: { lat: this.lat, lng: this.lon },
                    zoom: 4,
                    mapTypeId: google.maps.MapTypeId.HYBRID
                };
                this.map = new google.maps.Map(document.getElementById('map-canvas'), mapOptions);
                this.marker = new google.maps.Marker({
                    position: { lat: this.lat, lng: this.lon },
                    map: this.map,
                    title: 'ISS'
                });
            });

        setInterval(this.getNewLocation.bind(this), 5000);
    }


    getNewLocation() {
        this.issSvc.moveISS().subscribe(
            data => {
                this.lat = data.iss_position.latitude;
                this.lon = data.iss_position.longitude;
                var LatLng = new google.maps.LatLng(this.lat, this.lon);
                this.marker.setPosition(LatLng);
                this.map.setCenter(LatLng);
            });

    };

}

app/Map/map.component.html

<h2>Map</h2>

<h4>Current Location</h4>
<div>Latitude: {{lat}}</div>
<div>Longitude: {{lon}}</div>
<div id="map-canvas" class="col-md-12" style="height:500px;"></div>

Astronauts Component
Our last component displays the current astronauts onboard the station.
app/Astronauts/astronauts.component.ts

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

import {ISSService} from '../Shared/iss.service';

@Component({
    templateUrl: './astronauts.component.html'
})
export class AstronautsComponent {
    private astronauts: any;
    private test: any;
    constructor(private issSvc: ISSService) {
        this.issSvc.getAstros().subscribe(
          data => this.astronauts = data
        );
    }
}

app/Astronauts/astronauts.component.html

<h2>Astronauts</h2>
<h4>Current Astronauts Onboard(<span *ngIf="astronauts">{{astronauts.number}}</span>)</h4>
<ul *ngIf="astronauts">
    <li *ngFor="let astro of astronauts.people">{{astro.name}}</li>
</ul>

All in all, Angular2 is not all that different from Angular1. You still have a module system, one way binding is the same, pipes and services are the same. There are a few syntax changes, such as *ngFor for iterating over collections and routing. A real production app would include better error handling and catches on the HTTP calls, and logging, but I hope this is a fun easy example to try out basics such as modules, components, routing, services, etc.