├── .editorconfig ├── .gitignore ├── README.md ├── angular-cli.json ├── e2e ├── app.e2e-spec.ts ├── app.po.ts └── tsconfig.json ├── karma.conf.js ├── package.json ├── protractor.conf.js ├── src ├── app │ ├── LeafletMap.component.ts │ ├── app.component.css │ ├── app.component.html │ ├── app.component.spec.ts │ ├── app.component.ts │ ├── app.module.ts │ ├── loading │ │ ├── loading.component.css │ │ ├── loading.component.html │ │ └── loading.component.ts │ ├── navigator │ │ ├── navigator.component.css │ │ ├── navigator.component.html │ │ └── navigator.component.ts │ ├── shared │ │ ├── FluxDispatcher.ts │ │ ├── Location.ts │ │ ├── actions │ │ │ └── BasicActions.ts │ │ ├── flux.component.ts │ │ ├── interfaces │ │ │ └── IReduxModel.ts │ │ ├── model │ │ │ └── LeafletModel.ts │ │ └── services │ │ │ ├── Geocode.ts │ │ │ └── location.service.ts │ ├── sharedDispatcher.module.ts │ └── sharedModel.module.ts ├── assets │ ├── .gitkeep │ ├── eyes.png │ └── preloader.gif ├── environments │ ├── environment.prod.ts │ └── environment.ts ├── favicon.ico ├── index.html ├── main.ts ├── polyfills.ts ├── styles.css ├── test.ts └── tsconfig.json └── tslint.json /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | max_line_length = off 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist 5 | /tmp 6 | 7 | # dependencies 8 | /node_modules 9 | 10 | # IDEs and editors 11 | /.idea 12 | .project 13 | .classpath 14 | .c9/ 15 | *.launch 16 | .settings/ 17 | 18 | # IDE - VSCode 19 | .vscode/ 20 | !.vscode/settings.json 21 | !.vscode/tasks.json 22 | !.vscode/launch.json 23 | !.vscode/extensions.json 24 | 25 | # misc 26 | /.sass-cache 27 | /connect.lock 28 | /coverage/* 29 | /libpeerconnection.log 30 | npm-debug.log 31 | testem.log 32 | /typings 33 | 34 | # e2e 35 | /e2e/*.js 36 | /e2e/*.map 37 | 38 | #System Files 39 | .DS_Store 40 | Thumbs.db 41 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Angular 2 and Leaflet 2 | 3 | This example is inspired by the Angular 2 Leaflet starter at [https://github.com/haoliangyu/angular2-leaflet-starter] . That project was correctly described as a 'soup'. I wanted to take an Angular 2 and Leaflet starter in a different direction, so this example was created as a completely separate project. Goals for this demo include: 4 | 5 | 6 | ```sh 7 | - Use the Angular2 CLI as the build tool 8 | - Create a complete micro-application as the demonstration environment 9 | - Adhere to concepts from Flux and Redux w/o 3rd party software 10 | - Classes adhere to principles such as single responsibility 11 | - Provide progress and error indication for services 12 | - Illustrate production-quality features such as preventing side effects from repeatedly clicking the same button 13 | - Provide an example of working with the component change detector 14 | - Use the Typescript Math Toolkit Location class for location data 15 | - Only minimal understanding of Leaflet is required 16 | ``` 17 | 18 | Author: Jim Armstrong - [The Algorithmist] 19 | 20 | @algorithmist 21 | 22 | theAlgorithmist [at] gmail [dot] com 23 | 24 | Angular: 2.3.1 25 | 26 | Angular CLI: 1.0.0-beta.25.5 27 | 28 | ## Installation 29 | 30 | Installation involves all the usual suspects 31 | 32 | - npm and Angular CLI installed globally 33 | - Clone the repository 34 | - npm install 35 | - get coffee (this is the most important step) 36 | 37 | 38 | ### Version 39 | 40 | 1.0.0 41 | 42 | ### Building and Running the demo 43 | 44 | After installation, _ng-build_ and _ng-serve_ are your friends. Build production or dev. as you see fit. localhost:4200 to run the demo, at which point you should see 45 | 46 | ![Image of Leaflet Demo] 47 | (http://algorithmist.net/image/leafletmap.jpg) 48 | 49 | 50 | The application provides two means for moving the map. You may use the current IP address or enter a physical address. Examples of the latter include complete street, city, state, zip or simply 'Austin, TX'. Notification of a request in progress is provided after clicking on the 'current location' button or entering an address followed by pressing 'Enter' or clicking the arrow button. Another notification is provided after the request is complete and the map is panned to the requested location. Errors are indicated as shown below. 51 | 52 | ![Image of Map Error] 53 | (http://algorithmist.net/image/maperror.jpg) 54 | 55 | 56 | The demo has been tested in late-model Chrome on a Mac. 57 | 58 | When it comes to deconstructing the code, don't worry if you have little experience with Leaflet. If you have made it through the 'Getting Started' guide, then you know enough about Leaflet to work with and expand upon this demo. 59 | 60 | 61 | ## Further help 62 | 63 | To get more help on the `angular-cli` use `ng help` or go check out the [Angular-CLI README](https://github.com/angular/angular-cli/blob/master/README.md). 64 | 65 | 66 | License 67 | ---- 68 | 69 | Apache 2.0 70 | 71 | **Free Software? Yeah, Homey plays that** 72 | 73 | [//]: # (kudos http://stackoverflow.com/questions/4823468/store-comments-in-markdown-syntax) 74 | 75 | [The Algorithmist]: 76 | [https://github.com/haoliangyu/angular2-leaflet-starter]: 77 | 78 | 79 | -------------------------------------------------------------------------------- /angular-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "project": { 3 | "version": "1.0.0", 4 | "name": "leaflet" 5 | }, 6 | "apps": [ 7 | { 8 | "root": "src", 9 | "outDir": "dist", 10 | "assets": [ 11 | "assets", 12 | "favicon.ico" 13 | ], 14 | "index": "index.html", 15 | "main": "main.ts", 16 | "test": "test.ts", 17 | "tsconfig": "tsconfig.json", 18 | "prefix": "app", 19 | "mobile": false, 20 | "styles": [ 21 | "../node_modules/bootstrap/dist/css/bootstrap.css", 22 | "../node_modules/leaflet/dist/leaflet.css", 23 | "../node_modules/font-awesome/css/font-awesome.css", 24 | "styles.css" 25 | ], 26 | "scripts": [ "../node_modules/leaflet/dist/leaflet.js" 27 | ], 28 | "environmentSource": "environments/environment.ts", 29 | "environments": { 30 | "dev": "environments/environment.ts", 31 | "prod": "environments/environment.prod.ts" 32 | } 33 | } 34 | ], 35 | "addons": [], 36 | "packages": [], 37 | "e2e": { 38 | "protractor": { 39 | "config": "./protractor.conf.js" 40 | } 41 | }, 42 | "test": { 43 | "karma": { 44 | "config": "./karma.conf.js" 45 | } 46 | }, 47 | "defaults": { 48 | "styleExt": "css", 49 | "prefixInterfaces": false, 50 | "inline": { 51 | "style": false, 52 | "template": false 53 | }, 54 | "spec": { 55 | "class": false, 56 | "component": true, 57 | "directive": true, 58 | "module": false, 59 | "pipe": true, 60 | "service": true 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /e2e/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { LeafletPage } from './app.po'; 2 | 3 | describe('leaflet App', function() { 4 | let page: LeafletPage; 5 | 6 | beforeEach(() => { 7 | page = new LeafletPage(); 8 | }); 9 | 10 | it('should display message saying app works', () => { 11 | page.navigateTo(); 12 | expect(page.getParagraphText()).toEqual('app works!'); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /e2e/app.po.ts: -------------------------------------------------------------------------------- 1 | import { browser, element, by } from 'protractor'; 2 | 3 | export class LeafletPage { 4 | navigateTo() { 5 | return browser.get('/'); 6 | } 7 | 8 | getParagraphText() { 9 | return element(by.css('app-root h1')).getText(); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /e2e/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "declaration": false, 5 | "emitDecoratorMetadata": true, 6 | "experimentalDecorators": true, 7 | "module": "commonjs", 8 | "moduleResolution": "node", 9 | "outDir": "../dist/out-tsc-e2e", 10 | "sourceMap": true, 11 | "target": "es5", 12 | "typeRoots": [ 13 | "../node_modules/@types" 14 | ] 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration file, see link for more information 2 | // https://karma-runner.github.io/0.13/config/configuration-file.html 3 | 4 | module.exports = function (config) { 5 | config.set({ 6 | basePath: '', 7 | frameworks: ['jasmine', 'angular-cli'], 8 | plugins: [ 9 | require('karma-jasmine'), 10 | require('karma-chrome-launcher'), 11 | require('karma-remap-istanbul'), 12 | require('angular-cli/plugins/karma') 13 | ], 14 | files: [ 15 | { pattern: './src/test.ts', watched: false } 16 | ], 17 | preprocessors: { 18 | './src/test.ts': ['angular-cli'] 19 | }, 20 | mime: { 21 | 'text/x-typescript': ['ts','tsx'] 22 | }, 23 | remapIstanbulReporter: { 24 | reports: { 25 | html: 'coverage', 26 | lcovonly: './coverage/coverage.lcov' 27 | } 28 | }, 29 | angularCli: { 30 | config: './angular-cli.json', 31 | environment: 'dev' 32 | }, 33 | reporters: config.angularCli && config.angularCli.codeCoverage 34 | ? ['progress', 'karma-remap-istanbul'] 35 | : ['progress'], 36 | port: 9876, 37 | colors: true, 38 | logLevel: config.LOG_INFO, 39 | autoWatch: true, 40 | browsers: ['Chrome'], 41 | singleRun: false 42 | }); 43 | }; 44 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Angular2CLI-Leaflet", 3 | "description": "MVP for Angular2, Angular2 CLI and Leaflet, based on A2 leaflet starter", 4 | "keywords": [ 5 | "angular2", 6 | "leaflet" 7 | ], 8 | "author": "Jim Armstrong (www.algorithmist.net)", 9 | "version": "1.0.0", 10 | "license": "Apache-2.0", 11 | "angular-cli": {}, 12 | "scripts": { 13 | "ng": "ng", 14 | "start": "ng serve", 15 | "lint": "tslint \"src/**/*.ts\" --project src/tsconfig.json --type-check && tslint \"e2e/**/*.ts\" --project e2e/tsconfig.json --type-check", 16 | "test": "ng test", 17 | "pree2e": "webdriver-manager update --standalone false --gecko false", 18 | "e2e": "protractor" 19 | }, 20 | "private": true, 21 | "dependencies": { 22 | "@angular/common": "^2.3.1", 23 | "@angular/compiler": "^2.3.1", 24 | "@angular/core": "^2.3.1", 25 | "@angular/forms": "^2.3.1", 26 | "@angular/http": "^2.3.1", 27 | "@angular/platform-browser": "^2.3.1", 28 | "@angular/platform-browser-dynamic": "^2.3.1", 29 | "@angular/router": "^3.3.1", 30 | "core-js": "^2.4.1", 31 | "rxjs": "^5.0.1", 32 | "ts-helpers": "^1.1.1", 33 | "zone.js": "^0.7.2", 34 | "bootstrap": "^4.0.0-alpha.5", 35 | "font-awesome": "4.7.0", 36 | "leaflet": "^1.0.2" 37 | }, 38 | "devDependencies": { 39 | "@angular/cli": "^1.0.0", 40 | "@angular/compiler-cli": "^2.3.1", 41 | "@types/geojson": "0.0.31", 42 | "@types/jasmine": "2.5.38", 43 | "@types/leaflet": "^1.0.40", 44 | "@types/node": "^6.0.42", 45 | "codelyzer": "~2.0.0-beta.1", 46 | "jasmine-core": "2.5.2", 47 | "jasmine-spec-reporter": "2.5.0", 48 | "karma": "1.2.0", 49 | "karma-chrome-launcher": "^2.0.0", 50 | "karma-cli": "^1.0.1", 51 | "karma-jasmine": "^1.0.2", 52 | "karma-remap-istanbul": "^0.2.1", 53 | "protractor": "~4.0.13", 54 | "ts-node": "1.2.1", 55 | "tslint": "^4.3.0", 56 | "typescript": "~2.0.3" 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /protractor.conf.js: -------------------------------------------------------------------------------- 1 | // Protractor configuration file, see link for more information 2 | // https://github.com/angular/protractor/blob/master/lib/config.ts 3 | 4 | /*global jasmine */ 5 | var SpecReporter = require('jasmine-spec-reporter'); 6 | 7 | exports.config = { 8 | allScriptsTimeout: 11000, 9 | specs: [ 10 | './e2e/**/*.e2e-spec.ts' 11 | ], 12 | capabilities: { 13 | 'browserName': 'chrome' 14 | }, 15 | directConnect: true, 16 | baseUrl: 'http://localhost:4200/', 17 | framework: 'jasmine', 18 | jasmineNodeOpts: { 19 | showColors: true, 20 | defaultTimeoutInterval: 30000, 21 | print: function() {} 22 | }, 23 | useAllAngular2AppRoots: true, 24 | beforeLaunch: function() { 25 | require('ts-node').register({ 26 | project: 'e2e' 27 | }); 28 | }, 29 | onPrepare: function() { 30 | jasmine.getEnv().addReporter(new SpecReporter()); 31 | } 32 | }; 33 | -------------------------------------------------------------------------------- /src/app/LeafletMap.component.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2016 Jim Armstrong (www.algorithmist.net) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | /** 18 | * LeafletMap - A simple leaflet map component 19 | * 20 | * @author Jim Armstrong (www.algorithmist.net) 21 | * 22 | * @version 1.0 23 | */ 24 | 25 | import { Component 26 | , EventEmitter 27 | , Output 28 | } from '@angular/core'; 29 | 30 | import * as L from 'leaflet'; 31 | import { Map } from 'leaflet'; 32 | 33 | @Component({ 34 | selector: 'leaflet-map', 35 | 36 | template: '
', 37 | 38 | styles: [`.leafletMapComponent { 39 | width: 600px; 40 | height: 400px; 41 | }`] 42 | }) 43 | 44 | export class LeafletMap 45 | { 46 | protected _map: Map; // leaflet map 47 | 48 | // Outputs 49 | @Output() layerAdded : EventEmitter = new EventEmitter(); 50 | @Output() layerRemoved: EventEmitter = new EventEmitter(); 51 | 52 | /** 53 | * Construct a new Leaflet Map component 54 | * 55 | * @return nothing 56 | */ 57 | constructor() 58 | { 59 | // empty 60 | } 61 | 62 | /** 63 | * Initialize the map 64 | * 65 | * @param params: Object Map params recognized by Leaflet 66 | * 67 | * @param tileData: Object containing 'url' and 'attribution' data for the tile layer 68 | * 69 | * @return nothing The leaflet map is created, intialized with the supplied parameters, and assigned to the DIV created in the component template. A single 70 | * tile layer is addes 71 | */ 72 | public initialize(params: Object, tileData: Object): void 73 | { 74 | // the div id is hardcoded in this example - a future example will show how to make this component more general 75 | this._map = L.map('leaflet-map-component', params); 76 | 77 | // events supported in this demo 78 | this._map.on('layeradd' , () => {this.__onLayerAdded()} ); 79 | this._map.on('layerremove', () => {this.__onLayerRemoved()} ); 80 | 81 | // add a single tile layer 82 | L.tileLayer(tileData['url'], { attribution: tileData['attribution'] }).addTo(this._map); 83 | } 84 | 85 | /** 86 | * Move the map to the input location 87 | * 88 | * @param lat: number Location latitude in degrees 89 | * 90 | * @param long: number Location longitude in degrees 91 | */ 92 | public toLocation(lat: number, long: number): void 93 | { 94 | this._map.panTo( [lat, long]); 95 | } 96 | 97 | protected __onLayerAdded(): void 98 | { 99 | // perform additional logic on layer added here 100 | this.layerAdded.emit(); 101 | } 102 | 103 | protected __onLayerRemoved(): void 104 | { 105 | // perform additional logic on layer removed here 106 | this.layerRemoved.emit(); 107 | } 108 | } -------------------------------------------------------------------------------- /src/app/app.component.css: -------------------------------------------------------------------------------- 1 | .rounded 2 | { 3 | max-width: 600px; 4 | padding: 8px; 5 | border: 2px solid #dddddd; 6 | -moz-border-radius: 4px; 7 | -webkit-border-radius: 4px; 8 | -o-border-radius: 4px; 9 | border-radius: 4px; 10 | } 11 | 12 | .top-margin 13 | { 14 | margin-top: 20px; 15 | } 16 | 17 | .paddedDiv 18 | { 19 | padding: 8px; 20 | } 21 | 22 | .fullWidth 23 | { 24 | width: 100%; 25 | } 26 | 27 | .logo-img 28 | { 29 | float:left; 30 | margin-left: 6px; 31 | margin-right:6px; 32 | } 33 | 34 | a 35 | { 36 | color: #ffffff ! important; 37 | margin-right: 10px ! important; 38 | margin-top: 8px ! important; 39 | } 40 | 41 | a:hover 42 | { 43 | color: #cccccc ! important; 44 | } 45 | 46 | .disabled 47 | { 48 | pointer-events: none; 49 | color: #333333; 50 | } 51 | 52 | .bordered 53 | { 54 | border: 1px solid #333333; 55 | } -------------------------------------------------------------------------------- /src/app/app.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 | The Algorithmist Leaflet Demo 5 |
6 | 7 |
8 |
9 |
10 | 11 | 12 |
13 | 14 | 15 |
16 |
17 | 18 |
19 |
20 | 21 | 22 |
23 |
24 |
25 | -------------------------------------------------------------------------------- /src/app/app.component.spec.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable:no-unused-variable */ 2 | 3 | import { TestBed, async } from '@angular/core/testing'; 4 | import { AppComponent } from './app.component'; 5 | 6 | describe('AppComponent', () => { 7 | beforeEach(() => { 8 | TestBed.configureTestingModule({ 9 | declarations: [ 10 | AppComponent 11 | ], 12 | }); 13 | TestBed.compileComponents(); 14 | }); 15 | 16 | it('should create the app', async(() => { 17 | const fixture = TestBed.createComponent(AppComponent); 18 | const app = fixture.debugElement.componentInstance; 19 | expect(app).toBeTruthy(); 20 | })); 21 | 22 | it(`should have as title 'app works!'`, async(() => { 23 | const fixture = TestBed.createComponent(AppComponent); 24 | const app = fixture.debugElement.componentInstance; 25 | expect(app.title).toEqual('app works!'); 26 | })); 27 | 28 | it('should render title in a h1 tag', async(() => { 29 | const fixture = TestBed.createComponent(AppComponent); 30 | fixture.detectChanges(); 31 | const compiled = fixture.debugElement.nativeElement; 32 | expect(compiled.querySelector('h1').textContent).toContain('app works!'); 33 | })); 34 | }); 35 | -------------------------------------------------------------------------------- /src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2016 Jim Armstrong (www.algorithmist.net) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | // platform imports 18 | import { Component 19 | , OnInit 20 | , AfterViewInit 21 | , ViewChild 22 | , ChangeDetectorRef 23 | } from '@angular/core'; 24 | 25 | // leaflet map and loading components 26 | import { LeafletMap } from './LeafletMap.component'; 27 | import { LoadingComponent } from './loading/loading.component'; 28 | 29 | // base Flux component 30 | import { FluxComponent } from './shared/flux.component'; 31 | 32 | // Global store and dispatcher 33 | import { LeafletModel } from './shared/model/LeafletModel'; 34 | import { FluxDispatcher } from './shared/FluxDispatcher'; 35 | 36 | // actions 37 | import { BasicActions } from './shared/actions/BasicActions'; 38 | 39 | // Typescript Math Toolkit Location 40 | import { TSMT$Location } from './shared/Location'; 41 | 42 | // rxjs imports 43 | import { Subject } from 'rxjs/Subject'; 44 | import { Subscription } from 'rxjs/Subscription'; 45 | 46 | @Component({ 47 | selector: 'app-root', 48 | 49 | templateUrl: 'app.component.html', 50 | 51 | styleUrls: ['app.component.css'] 52 | }) 53 | 54 | /** 55 | * Root component for Leaflet demo 56 | * 57 | * @author Jim Armstrong (www.algorithmist.net) 58 | * 59 | * @version 1.0 60 | */ 61 | export class AppComponent extends FluxComponent implements OnInit, AfterViewInit 62 | { 63 | protected _loading: boolean = true; // true if content is being loaded 64 | 65 | // access the leaflet map 66 | @ViewChild(LeafletMap) _leafletMap: LeafletMap; 67 | 68 | /** 69 | * Construct the main app component 70 | * 71 | * @return Nothing 72 | */ 73 | constructor(private _m: LeafletModel, private _d: FluxDispatcher, private _chgDetector: ChangeDetectorRef) 74 | { 75 | super(_d); 76 | 77 | // since there is no formal framework that ties the dispatcher and global store together, this step is done manually. you can start to appreciate what 78 | // ngrx/store does under the hood :) 79 | _d.model = _m; 80 | } 81 | 82 | /** 83 | * Component lifecycle - on init 84 | * 85 | * @return nothing - reserved for future use 86 | */ 87 | public ngOnInit() 88 | { 89 | // for future use 90 | } 91 | 92 | /** 93 | * Component lifecycle - after view init 94 | * 95 | * @return nothing - dispatch action to request map paraams 96 | */ 97 | public ngAfterViewInit() 98 | { 99 | this._d.dispatchAction(BasicActions.GET_MAP_PARAMS, null); 100 | } 101 | 102 | // update the component based on a new state of the global store 103 | protected __onModelUpdate(data: Object): void 104 | { 105 | let location: TSMT$Location; 106 | 107 | switch (data['action']) 108 | { 109 | case BasicActions.GET_MAP_PARAMS: 110 | this._leafletMap.initialize( data['mapParams'], data['tileData'] ); 111 | break; 112 | 113 | case BasicActions.CURRENT_LOCATION: 114 | case BasicActions.ADDRESS: 115 | location = data['location']; 116 | 117 | this._leafletMap.toLocation(location.latitude, location.longitude); 118 | break; 119 | } 120 | } 121 | 122 | protected __onLayerAdded(payload: any): void 123 | { 124 | // if layer was added while loading in progress, this is an indication of the initial (application) load 125 | if (this._loading) 126 | { 127 | this._loading = false; 128 | 129 | // force change detection since we changed a bound property after the normal check cycle and outside anything that would trigger a 130 | // CD cycle - this will eliminate the error you get when running in dev mode and provide another example of how this process works. 131 | this._chgDetector.detectChanges(); 132 | } 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2016 Jim Armstrong (www.algorithmist.net) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | /** 18 | * This is the root application module for the Leaflet application 19 | * 20 | * @author Jim Armstrong (www.algorithmist.net) 21 | * 22 | * @version 1.0 23 | */ 24 | 25 | // platform imports 26 | import { Component } from '@angular/core'; 27 | import { Directive } from '@angular/core'; 28 | import { NgModule } from '@angular/core'; 29 | import { FormsModule } from '@angular/forms'; 30 | import { BrowserModule } from '@angular/platform-browser'; 31 | import { HttpModule } from '@angular/http'; 32 | 33 | // main application component and supporting components 34 | import { AppComponent } from './app.component'; 35 | import { LeafletMap } from './LeafletMap.component'; 36 | import { LoadingComponent } from './loading/loading.component'; 37 | import { MapNavComponent } from './navigator/navigator.component'; 38 | 39 | // the shared model and dispatcher modules 40 | import { SharedModelModule } from './sharedModel.module'; 41 | import { SharedDispatcherModule } from './sharedDispatcher.module'; 42 | 43 | @NgModule({ 44 | declarations: [ 45 | AppComponent, LeafletMap, LoadingComponent, MapNavComponent 46 | ], 47 | 48 | imports: [ 49 | BrowserModule, 50 | HttpModule, 51 | FormsModule, 52 | SharedModelModule.forRoot(), 53 | SharedDispatcherModule.forRoot() 54 | ], 55 | 56 | bootstrap: [AppComponent] 57 | }) 58 | 59 | export class AppModule { } 60 | -------------------------------------------------------------------------------- /src/app/loading/loading.component.css: -------------------------------------------------------------------------------- 1 | .loadingText 2 | { 3 | text-align: left; 4 | padding: 4px; 5 | } 6 | 7 | /* in-lined ellipsis and dots animation from taiwan text-spinners; avoids issue with programmatic packaging of styles in Angular 2 CLI */ 8 | .loading { 9 | display: inline-block; 10 | overflow: hidden; 11 | height: 1.3em; 12 | margin-top: -0.3em; 13 | line-height: 1.5em; 14 | vertical-align: text-bottom; 15 | } 16 | 17 | .loading::after { 18 | display: inline-table; 19 | white-space: pre; 20 | text-align: left; 21 | } 22 | 23 | .loading::after { 24 | content: "\A.\A..\A..."; 25 | animation: spin4 2s steps(4) infinite; 26 | } 27 | 28 | .loading.dots2::after { 29 | content: "⠋\A⠙\A⠚\A⠞\A⠖\A⠦\A⠴\A⠲\A⠳"; 30 | animation: spin9 1s steps(9) infinite; 31 | } 32 | 33 | @keyframes spin1 { to { transform: translateY( -1.5em); } } 34 | @keyframes spin2 { to { transform: translateY( -3.0em); } } 35 | @keyframes spin3 { to { transform: translateY( -4.5em); } } 36 | @keyframes spin4 { to { transform: translateY( -6.0em); } } 37 | @keyframes spin5 { to { transform: translateY( -7.5em); } } 38 | @keyframes spin6 { to { transform: translateY( -9.0em); } } 39 | @keyframes spin7 { to { transform: translateY(-10.5em); } } 40 | @keyframes spin8 { to { transform: translateY(-12.0em); } } 41 | @keyframes spin9 { to { transform: translateY(-13.5em); } } 42 | @keyframes spin10 { to { transform: translateY(-15.0em); } } 43 | @keyframes spin11 { to { transform: translateY(-16.5em); } } 44 | @keyframes spin12 { to { transform: translateY(-18.0em); } } -------------------------------------------------------------------------------- /src/app/loading/loading.component.html: -------------------------------------------------------------------------------- 1 |
{{loadingText}}
2 | -------------------------------------------------------------------------------- /src/app/loading/loading.component.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2016 Jim Armstrong (www.algorithmist.net) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | // platform imports 18 | import { Component 19 | , Input 20 | } from '@angular/core'; 21 | 22 | @Component({ 23 | selector: 'loading', 24 | 25 | templateUrl: 'loading.component.html', 26 | 27 | styleUrls: ['loading.component.css'] 28 | }) 29 | 30 | /** 31 | * A very simple 'loading' component with text that may be set as an html attribute 32 | * 33 | * @author Jim Armstrong (www.algorithmist.net) 34 | * 35 | * @version 1.0 36 | */ 37 | export class LoadingComponent 38 | { 39 | @Input() loadingText: string; // loading text 40 | 41 | /** 42 | * Construct the loading component 43 | * 44 | * @return Nothing 45 | */ 46 | constructor() 47 | { 48 | // empty 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/app/navigator/navigator.component.css: -------------------------------------------------------------------------------- 1 | 2 | input 3 | { 4 | margin: 10px 6px 10px 0px; 5 | width: 350px; 6 | height: 35px; 7 | border: 2px solid rgba(77, 156, 237, 0.7); 8 | font-size: 16px; 9 | } 10 | 11 | .gotoBtn 12 | { 13 | height: 35px; 14 | } 15 | 16 | .clickable 17 | { 18 | cursor: pointer; 19 | } 20 | 21 | .leftBuffer 22 | { 23 | margin-left: 10px; 24 | } 25 | 26 | .topBuffer 27 | { 28 | margin-top: 12px; 29 | } -------------------------------------------------------------------------------- /src/app/navigator/navigator.component.html: -------------------------------------------------------------------------------- 1 | 2 |

Map Navigation

3 | 4 |
5 |
6 | 8 | 9 | 12 |
13 |
14 | {{_navProgressText}} 15 |
16 | 17 |
18 | 19 |
20 |
21 | {{_locProgressText}} 22 |
23 |
24 | -------------------------------------------------------------------------------- /src/app/navigator/navigator.component.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2016 Jim Armstrong (www.algorithmist.net) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | // platform imports 18 | import { Component } from '@angular/core'; 19 | 20 | // base Flux component & dispatcher 21 | import { FluxComponent } from '../shared/flux.component'; 22 | import { FluxDispatcher } from '../shared/FluxDispatcher'; 23 | 24 | // actions 25 | import { BasicActions } from '../shared/actions/BasicActions'; 26 | 27 | @Component({ 28 | selector: 'map-navigator', 29 | 30 | templateUrl: 'navigator.component.html', 31 | 32 | styleUrls: ['navigator.component.css'] 33 | }) 34 | 35 | /** 36 | * MapNavCompoennt - allows a text address to be entered as a place to navigate the map or click a button to move the map to a location based 37 | * on current IP address 38 | * 39 | * @author Jim Armstrong (www.algorithmist.net) 40 | * 41 | * @version 1.0 42 | */ 43 | export class MapNavComponent extends FluxComponent 44 | { 45 | protected _address: string; // the current address on which to center the map 46 | 47 | protected _showNavProgress: boolean = false; // true if nav progress display is shown 48 | protected _showLocProgress: boolean = false; // true if show-location display is shown 49 | 50 | protected _navProgressText: string = ""; // text shown to indicate navigation progress 51 | protected _locProgressText: string = ""; // text shown to indicate location progress 52 | 53 | protected _clicked: boolean = false; // protect against multiple submissions 54 | 55 | /** 56 | * Construct the main app component 57 | * 58 | * @param d: FluxDispatcher Inject Flux-style dispatcher used by all FluxComponents 59 | * 60 | * @return Nothing 61 | */ 62 | constructor(private _d: FluxDispatcher) 63 | { 64 | super(_d); 65 | } 66 | 67 | // update the component based on a new state of the global store 68 | protected __onModelUpdate(data:Object): void 69 | { 70 | this._clicked = false; 71 | 72 | switch (data['action']) 73 | { 74 | case BasicActions.ADDRESS: 75 | this._navProgressText = "Map moved to requested address"; 76 | this._clicked = false; 77 | break; 78 | 79 | case BasicActions.CURRENT_LOCATION: 80 | this._locProgressText = "Map moved to current IP location"; 81 | this._clicked = false; 82 | break; 83 | 84 | case BasicActions.ADDRESS_ERROR: 85 | this._navProgressText = "Error geocoding input address. Please enter a valid address."; 86 | this._clicked = false; 87 | break; 88 | 89 | case BasicActions.LOCATION_ERROR: 90 | this._locProgressText = "Unable to geocode current IP location"; 91 | this._clicked = false; 92 | break; 93 | } 94 | } 95 | 96 | protected __onNavigate(): void 97 | { 98 | if (!this._clicked) 99 | { 100 | this._clicked = true; 101 | this._showLocProgress = false; 102 | this._showNavProgress = true; 103 | 104 | if (this._address && this._address != "") 105 | { 106 | this._navProgressText = "Geocoding requested address, please wait ..."; 107 | 108 | this._dispatcher.dispatchAction(BasicActions.ADDRESS, {address:this._address} ); 109 | } 110 | else 111 | { 112 | this._navProgressText = "Please enter an address"; 113 | this._clicked = false; 114 | } 115 | } 116 | else 117 | this._locProgressText = "Address fetch in progress, waiting for service return ..."; 118 | } 119 | 120 | protected __onCurrentLocation(): void 121 | { 122 | if (!this._clicked) 123 | { 124 | this._clicked = true; 125 | this._locProgressText = "Fetching location ... please wait"; 126 | this._showLocProgress = true; 127 | this._showNavProgress = false; 128 | 129 | this._dispatcher.dispatchAction(BasicActions.CURRENT_LOCATION, null); 130 | } 131 | else 132 | this._locProgressText = "Location fetch in progress, waiting for service return ..."; 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /src/app/shared/FluxDispatcher.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2016 Jim Armstrong (www.algorithmist.net) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | /** 18 | * A minmimal implementation of Flux-style dispatcher that serves as a reusable mediator between a generic component and a model instance that implements the 19 | * IReduxModel interface. Assign a model reference first and then add components as subscribers. 20 | * 21 | * @author Jim Armstrong (www.algorithmist.net) 22 | * 23 | * @version 1.0 24 | */ 25 | 26 | // platform imports 27 | import { Injectable } from '@angular/core'; 28 | 29 | // rxjs 30 | import { Subject } from 'rxjs/Subject'; 31 | import { IReduxModel } from './interfaces/IReduxModel'; 32 | 33 | @Injectable() 34 | export class FluxDispatcher 35 | { 36 | // singleton instance 37 | private static _instance: FluxDispatcher; 38 | 39 | // direct reference to a redux-style model 40 | protected _model: IReduxModel; 41 | 42 | // cache subscribers until a model reference is set 43 | protected _subscribers: Array>; 44 | 45 | /** 46 | * Construct a new FluxDispatcher 47 | * 48 | * @return nothing 49 | */ 50 | constructor() 51 | { 52 | // this is not strictly necessary if the dispatcher is used only via DI; it allows reusability outside the Angular inversion of control framework 53 | if (FluxDispatcher._instance instanceof FluxDispatcher) 54 | return FluxDispatcher._instance; 55 | 56 | this._subscribers = new Array>(); 57 | 58 | FluxDispatcher._instance = this; 59 | } 60 | 61 | /** 62 | * Assign a model to link up with this dispatcher 63 | * 64 | * @param m: IReduxModel Reference to a model that implements the IReduxModel interface 65 | * 66 | * @return nothing Any actions dispatched by a FluxComponent will be sent to this model 67 | */ 68 | public set model( m: IReduxModel ) 69 | { 70 | if (m) 71 | { 72 | this._model = m; 73 | 74 | // model assignment is allowed to be lazy; i.e. subscribers may be set before the model reference 75 | if (this._subscribers.length > 0) 76 | { 77 | this._subscribers.map( (subject: Subject): void => {this._model.subscribe(subject)} ); 78 | this._subscribers.length = 0; 79 | } 80 | } 81 | } 82 | 83 | /** 84 | * Subscribe to updates 85 | * 86 | * @param subject: Subject This Subject will be subscribed to future updates from the asigned model 87 | * 88 | * @return nothing 89 | */ 90 | public subscribe( subject: Subject ): void 91 | { 92 | if (subject) 93 | { 94 | if (this._model) 95 | this._model.subscribe(subject); 96 | else 97 | this._subscribers.push(subject); // defer subscription until model reference is assigned 98 | } 99 | } 100 | 101 | /** 102 | * Unsubscribe to updates 103 | * 104 | * @param subject: Subject This Subject will be unsubscribed to future updates from the asigned model 105 | * 106 | * @return nothing 107 | */ 108 | public unsubscribe( subject: Subject ): void 109 | { 110 | if (this._model) 111 | { 112 | this._model.unsubscribe(subject); 113 | } 114 | } 115 | 116 | public dispatchAction( action: number, payload: Object ): void 117 | { 118 | if (this._model && !isNaN(action)) 119 | { 120 | this._model.dispatchAction(action, payload); 121 | } 122 | } 123 | } -------------------------------------------------------------------------------- /src/app/shared/Location.ts: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Copyright 2016 Jim Armstrong (www.algorithmist.net) 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | /** 19 | * Typescript Math Toolkit: A generic location class that may be used in a variety of mapping applications. 20 | * 21 | * Note: This is an alpha release - the 'address' property will be deprecated in the future in favor of a more general Address class 22 | * 23 | * @author Jim Armstrong (www.algorithmist.net) 24 | * 25 | * @version 1.0 26 | */ 27 | 28 | export class TSMT$Location 29 | { 30 | // constants 31 | protected static TO_MILES: number = 0.621371; // km to miles 32 | protected static DEG_TO_RAD: number = 0.01745329251; // PI/180.0; 33 | protected static RADIUS_KM: number = 6378.5; // radius of earth in km. 34 | 35 | // public properties 36 | public id: string; // an optional string identifier that may be associated with this location 37 | public isError: boolean; // flag an error associated with attempting to set this location 38 | public info: string; // supplemental information associated with an error condition or other setting of this location 39 | 40 | // internal 41 | protected _lat: number; // latitude of this location in degrees (in range -90 to 90, south of equator is negative) 42 | protected _long: number; // longitude of this location in degrees (in range -180 t0 180, west of prime meridian is negative) 43 | protected _address: string; // an optional, physical address associated with this location 44 | protected _data: Object; // optional data for this location as name-value pairs 45 | 46 | /** 47 | * Construct a new Location 48 | * 49 | * @return nothing A default location at (0,0) with a blank address is created 50 | */ 51 | constructor() 52 | { 53 | this.clear(); 54 | } 55 | 56 | /** 57 | * Access the latitude of this location 58 | * 59 | * @return number Latitude of current location in degrees in the range [-90,90] 60 | */ 61 | public get latitude(): number 62 | { 63 | return this._lat; 64 | } 65 | 66 | /** 67 | * Access the longitude of this location 68 | * 69 | * @return number Longitude of current location in degress in the range [-180, 180] 70 | */ 71 | public get longitude(): number 72 | { 73 | return this._long; 74 | } 75 | 76 | /** 77 | * Access the address of this location 78 | * 79 | * @return string Current address (note that this will be deprecated in the future) 80 | */ 81 | public get address(): string 82 | { 83 | return this._address; 84 | } 85 | 86 | /** 87 | * Access a named data item associated with this location 88 | * 89 | * @param name: string Name of data item 90 | * 91 | * @return any Data value associated with the named item or null if no such named data item exists 92 | */ 93 | public getData(name: string): any 94 | { 95 | if (this._data.hasOwnProperty(name)) 96 | return this._data[name]; 97 | } 98 | 99 | /** 100 | * Assign the latitude of this location 101 | * 102 | * @param value: number Latitude value in degrees that should be in the range [-90,90] 103 | * 104 | * @return nothing Assigns the input latitude to this location as long as the value is valid 105 | */ 106 | public set latitude(value: number) 107 | { 108 | if (!isNaN(value) && isFinite(value) && value >= -90 && value <= 90) 109 | this._lat = value; 110 | } 111 | 112 | /** 113 | * Assign the longitude of this location 114 | * 115 | * @param value: number Longitude value in degrees that should be in the range [-180,180] 116 | * 117 | * @return nothing Assigns the input longitude to this location as long as the value is valid 118 | */ 119 | public set longitude(value: number) 120 | { 121 | if (!isNaN(value) && isFinite(value) && value >= -180 && value <= 180) 122 | this._long = value; 123 | } 124 | 125 | /** 126 | * Assign the address of this location 127 | * 128 | * @param value: string Address string such as 1234 Somewhere Lane, Anywhere, YourState, 12345 USA 129 | * 130 | * @return nothing Assigns the input address even if the string is blank; this will be replaced in the future by a more general address structure 131 | */ 132 | public set address(value: string) 133 | { 134 | this._address = value; 135 | } 136 | 137 | /** 138 | * Assign a named data attribute to this location 139 | * 140 | * @param name: string Name of the data attribute 141 | * 142 | * @param value: any Value of the data attribute 143 | * 144 | * @return nothing Assigns the name-value pair to the internal data associated with this location as long as the name is not a null or single-blank string 145 | */ 146 | public setData(name: string, value: any) 147 | { 148 | if (name != "" && name != " ") 149 | this._data[name] = value; 150 | } 151 | 152 | /** 153 | * Clear this location 154 | * 155 | * @return nothing Clears all data associated with this location - this is equivalent to constructing a new Location 156 | */ 157 | public clear(): void 158 | { 159 | this.id = ""; 160 | this.info = ""; 161 | this.isError = false; 162 | this._lat = 0; 163 | this._long = 0; 164 | 165 | this._address = ""; 166 | this._data = new Object(); 167 | } 168 | 169 | /** 170 | * Clone of this location 171 | * 172 | * @return TSMT$Location A clone of the current Location 173 | */ 174 | public clone(): TSMT$Location 175 | { 176 | let location: TSMT$Location = new TSMT$Location(); 177 | 178 | location.id = this.id; 179 | location.isError = this.isError; 180 | location.info = this.info; 181 | location.address = this._address; 182 | location.latitude = this.latitude; 183 | location.longitude = this.longitude; 184 | 185 | let keys:Array = Object.keys(this._data); 186 | keys.map( (name: string): void => {location.setData(name, this._data[name])} ); 187 | 188 | return location; 189 | } 190 | 191 | /** 192 | * Return the great-circle distance between this and another location specified by (lat, long) 193 | * 194 | * @param lat: number Latitude value in degrees that should be in the range [-90,90] 195 | * 196 | * @param long: number Longitude value in degrees that should be in the range [-180, 180] 197 | * 198 | * @param toMiles: boolean True if the distance is returned in miles 199 | * @default false 200 | * 201 | * @return number Great-circle distance between the current Location and the input (lat, long) in KM unless the toMiles parameter is set to true 202 | */ 203 | public gcd(lat: number, long: number, toMiles: boolean=false): number 204 | { 205 | let lat1: number = this.latitude*TSMT$Location.DEG_TO_RAD; 206 | let lat2: number = lat*TSMT$Location.DEG_TO_RAD; 207 | let long1: number = this.longitude*TSMT$Location.DEG_TO_RAD; 208 | let long2: number = long*TSMT$Location.DEG_TO_RAD; 209 | let dlat: number = Math.abs(lat2 - lat1); 210 | let dlon: number = Math.abs(long2 - long1); 211 | let sLat: number = Math.sin(dlat*0.5); 212 | let sLong: number = Math.sin(dlon*0.5); 213 | let a: number = sLat*sLat + Math.cos(lat1)*Math.cos(lat2)*sLong*sLong; 214 | let c: number = 2*Math.asin(Math.min(1.0,Math.sqrt(a))); 215 | 216 | let result: number = TSMT$Location.RADIUS_KM*c; // result in km 217 | 218 | return toMiles ? result*TSMT$Location.TO_MILES : result; 219 | } 220 | } -------------------------------------------------------------------------------- /src/app/shared/actions/BasicActions.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2016 Jim Armstrong (www.algorithmist.net) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | export enum BasicActions 18 | { 19 | CURRENT_LOCATION, // request map location based on current ip address 20 | ADDRESS, // request map location based on physical address 21 | GET_MAP_PARAMS, // request initial map parameters 22 | LOCATION_ERROR, // error in requesting current location 23 | ADDRESS_ERROR, // error in requesting navigate to specified address 24 | NONE, // no action 25 | ALL // entire application updated (to be implemented as an exercise) 26 | } -------------------------------------------------------------------------------- /src/app/shared/flux.component.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2016 Jim Armstrong (www.algorithmist.net) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | // platform imports 18 | import { OnDestroy } from '@angular/core'; 19 | 20 | // Reference to a dispatcher 21 | import { FluxDispatcher } from './FluxDispatcher'; 22 | 23 | // rxjs imports 24 | import { Subject } from 'rxjs/Subject'; 25 | import { Subscription } from 'rxjs/Subscription'; 26 | 27 | /** 28 | * A generic Flux-style component. In terms of interaction with the oustide world, this component may only dispatch actions and subscribe to updates, using a Dispatcher AS 29 | * as an intermediary between the component and some Redux-style store. Any component may be inserted anywhere in the heirarchy and the application flow remains deterministic. 30 | * 31 | * @author Jim Armstrong (www.algorithmist.net) 32 | * 33 | * @version 1.0 34 | */ 35 | export class FluxComponent implements OnDestroy 36 | { 37 | protected _subject: Subject; // Referebce to a Subject 38 | 39 | /** 40 | * Construct a new Flux-style component 41 | * 42 | * @param _dispatcher: FluxDispatcher Reference to the generic dispatcher 43 | * 44 | * @return Nothing 45 | */ 46 | constructor(protected _dispatcher: FluxDispatcher) 47 | { 48 | // subscribe this component to model updates 49 | this._subject = new Subject(); 50 | let subscription: Subscription = this._subject.subscribe( (data:Object): void => this.__onModelUpdate(data) ); 51 | 52 | this._dispatcher.subscribe(this._subject); 53 | } 54 | 55 | /** 56 | * Unsubscribe to updates when the component is destroyed 57 | * 58 | * @return Nothing This is very important for routable components that will be instantiated and then destroyed when navigating to and from a specific route 59 | */ 60 | public ngOnDestroy(): void 61 | { 62 | this._dispatcher.unsubscribe(this._subject); 63 | } 64 | 65 | // update the component based on new model data. 66 | protected __onModelUpdate(data:Object): void 67 | { 68 | // override in sub-class 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/app/shared/interfaces/IReduxModel.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2016 Jim Armstrong (www.algorithmist.net) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | /** 18 | * Specify the minimal interface for a Redux-style model 19 | * 20 | * @author Jim Armstrong (www.algorithmist.net) 21 | * 22 | * @version 1.0 23 | */ 24 | 25 | // rxjs 26 | import { Subject } from 'rxjs/Subject'; 27 | 28 | export interface IReduxModel 29 | { 30 | subscribe( subject: Subject ): void; 31 | 32 | unsubscribe( subject: Subject ): void; 33 | 34 | dispatchAction(action: number, payload: Object): void; 35 | } -------------------------------------------------------------------------------- /src/app/shared/model/LeafletModel.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2016 Jim Armstrong (www.algorithmist.net) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | /** 18 | * LeafletModel - This is the global store for the Leaflet example application. Data placed into the store is derived from the Angular 2 19 | * Leaflet starter, https://github.com/haoliangyu/angular2-leaflet-starter 20 | * 21 | * The model is Redux-style in the sense that it maintains immutability, accepts action dispatch with type and payload, 22 | * internally reduces the model as needed, and then sends copies of relevant slices of the model to subscribers. 23 | * 24 | * @author Jim Armstrong (www.algorithmist.net) 25 | * 26 | * @version 1.0 27 | */ 28 | 29 | // platform imports 30 | import { Injectable } from '@angular/core'; 31 | 32 | // interfaces 33 | import { IReduxModel } from '../interfaces/IReduxModel'; 34 | 35 | // actions 36 | import { BasicActions } from '../actions/BasicActions'; 37 | 38 | // services - adding an actual service layer to the application is left as an exercise 39 | import { LocationService } from '../services/location.service'; 40 | import { Geocode } from '../services/Geocode'; 41 | 42 | // typescript math toolkit 43 | import { TSMT$Location } from '../Location'; 44 | 45 | // leaflet 46 | import * as L from 'leaflet'; 47 | 48 | // rxjs 49 | import { Subject } from 'rxjs/Subject'; 50 | import { Observable } from 'rxjs/Observable'; 51 | 52 | @Injectable() 53 | export class LeafletModel implements IReduxModel 54 | { 55 | // singleton instance; this is not necessary, but allows the model to be used outside the Angular DI system 56 | private static _instance: LeafletModel; 57 | 58 | // reference to actual store - this remains private to support compile-time immutability 59 | private _store: Object = new Object(); 60 | 61 | // current action 62 | private _action: number; 63 | 64 | // has airport data been fetched? 65 | private _airportsFetched: boolean = false; 66 | 67 | // subscribers to model updates 68 | private _subscribers:Array>; 69 | 70 | /** 71 | * Construct a new Leaflet model 72 | * 73 | * @param geocoder: Geocode Injected geocoding service (convert string address to TSMT$Location) 74 | * 75 | * @param locationService: LocationServide Injected location service (get current location based on IP address) 76 | */ 77 | constructor(private _geocoder: Geocode, private _locationService: LocationService) 78 | { 79 | if (LeafletModel._instance instanceof LeafletModel) 80 | return LeafletModel._instance; 81 | 82 | // define the structure of the global application store 83 | this._store['location'] = new TSMT$Location(); 84 | 85 | this._store['tileData'] = { 86 | url: "http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", 87 | attribution: '© OpenStreetMap, Tiles courtesy of Humanitarian OpenStreetMap Team' 88 | }; 89 | 90 | this._store['mapParams'] = { 91 | zoomControl: true 92 | , center: L.latLng(32.9866, -96.9271) // I live in Carrollton, TX 93 | , zoom: 12 94 | , minZoom: 4 95 | , maxZoom: 19 96 | }; 97 | 98 | // list of subscribers for updates to the store 99 | this._subscribers = new Array>(); 100 | 101 | // current action 102 | this._action = BasicActions.NONE; 103 | 104 | // singleton instance 105 | LeafletModel._instance = this; 106 | } 107 | 108 | /** 109 | * Subscribe a new Subject to the model 110 | * 111 | * @param subject: Subject A Subject with at least an 'next' handler 112 | * 113 | * @return Nothing - The Subject is added to the subscriber list 114 | */ 115 | public subscribe( subject: Subject ): void 116 | { 117 | // for a full-on, production app, would want to make this test tighter 118 | if (subject) 119 | this._subscribers.push(subject); 120 | } 121 | 122 | /** 123 | * Unsubscribe an existing Subject from the model 124 | * 125 | * @param subject: Subject Existing subscribed Subject 126 | * 127 | * @return Nothing - If found, the Subject is removed from the subscriber list (typically executed when a component is destructed) 128 | */ 129 | public unsubscribe( subject: Subject ): void 130 | { 131 | // for a full-on, production app, would want to make this test tighter 132 | if (subject) 133 | { 134 | let len: number = this._subscribers.length; 135 | let i: number; 136 | 137 | for (i=0; i this._store['location']; 178 | 179 | this._store['action'] = this._action; 180 | 181 | this._locationService.getLocation() 182 | .subscribe( data => this.__onCurrentLocation(data), 183 | error => this.__onLocationError() ); 184 | 185 | validAction = false; // wait until service data is completely processed before responding 186 | break; 187 | 188 | case BasicActions.ADDRESS: 189 | if (payload.hasOwnProperty('address')) 190 | { 191 | this._geocoder.toLocation(payload['address']) 192 | .subscribe( data => this.__onCurrentLocation(data), // same method does double-duty 193 | error => this.__onAddressError() ); 194 | 195 | this._store['action'] = this._action; 196 | validAction = false; // wait until service data is completely processed before responding 197 | } 198 | break; 199 | 200 | case BasicActions.ALL: 201 | // to be implemented as an exercise 202 | break; 203 | } 204 | 205 | // immediately update all subscribers? 206 | if (validAction) 207 | this.__updateSubscribers(); 208 | } 209 | 210 | private __updateSubscribers(): void 211 | { 212 | // send copy of the current store to subscribers, which includes most recent action - you could recopy for each subscriber or have the 213 | // subscribers make a copy of the required slice of the store. Former is more robust, latter is more efficient. Try it both ways; 214 | // the global store is immutable in either case. 215 | 216 | let location: TSMT$Location = this._store['location']; 217 | let store: Object = JSON.parse( JSON.stringify(this._store) ); // this isn't as robust as you may have been led to believe 218 | store['location'] = location.clone(); // this is the hack 219 | 220 | this._subscribers.map( (s:Subject) => s.next(store) ); 221 | } 222 | 223 | // update the location in the global store and broacast to subscribers 224 | private __onCurrentLocation(data: any): void 225 | { 226 | if (data) 227 | { 228 | if (data instanceof TSMT$Location) 229 | { 230 | let location = ( data).clone(); 231 | 232 | if (location.isError) 233 | this.__onAddressError(); 234 | else 235 | { 236 | this._store['location'] = location; 237 | 238 | this.__updateSubscribers(); 239 | } 240 | } 241 | } 242 | } 243 | 244 | // error handlers broken into separate methods to allow future flexibility to add customized handling based on error type; otherwise, these could 245 | // be folded into one method with the action as an argument 246 | private __onLocationError(): void 247 | { 248 | // for purposes of error information, we can 'fake' a global store as only the action is required for subsequent action 249 | this._subscribers.map( (s:Subject) => s.next({'action': BasicActions.LOCATION_ERROR}) ); 250 | } 251 | 252 | private __onAddressError(): void 253 | { 254 | this._subscribers.map( (s:Subject) => s.next({'action': BasicActions.ADDRESS_ERROR}) ); 255 | } 256 | } 257 | -------------------------------------------------------------------------------- /src/app/shared/services/Geocode.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2016 Jim Armstrong (www.algorithmist.net) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | // platform imports 18 | import { Http, Headers, Response } from "@angular/http"; 19 | import { Injectable } from "@angular/core"; 20 | 21 | // Leaflet 22 | import * as L from 'leaflet'; 23 | 24 | // TSMT Location 25 | import { TSMT$Location } from "../Location"; 26 | 27 | // Leaflet 28 | import { LatLngBounds } from "leaflet"; 29 | 30 | // RXJS 31 | import { Observable } from 'rxjs/Observable'; 32 | 33 | import "rxjs/add/operator/map"; 34 | import "rxjs/add/operator/mergeMap"; 35 | import 'rxjs/add/operator/catch'; 36 | 37 | /** 38 | * A simple geocoding service based on a similar service provided in the Angular2 leaflet starter, https://github.com/haoliangyu/angular2-leaflet-starter 39 | * 40 | */ 41 | @Injectable() 42 | export class Geocode 43 | { 44 | protected _http: Http; // reference to http service 45 | 46 | /** 47 | * Construct a new Geocoding service 48 | * 49 | * @param http: Http Injected Http service 50 | * 51 | * @return nothing 52 | */ 53 | constructor(http: Http) 54 | { 55 | this._http = http; 56 | } 57 | 58 | /** 59 | * Convert a string address to a geocoded Location 60 | * 61 | * @param address: string Address that could be as simple as 'Austin, TX' 62 | * 63 | * @return Observable Observable that emits a TSMT$Location instance representing the geocoded location of the address or has its 'isError' 64 | * property set to true if an error occurred. 65 | */ 66 | public toLocation(address: string): Observable 67 | { 68 | let location: TSMT$Location = new TSMT$Location(); 69 | 70 | return this._http 71 | .get("http://maps.googleapis.com/maps/api/geocode/json?address=" + encodeURIComponent(address)) 72 | .map(res => res.json()) 73 | .map(result => { 74 | if (result.status !== "OK") 75 | { 76 | console.log( "Error attempting to encode: ", address); 77 | location.address = address; 78 | location.isError = true; 79 | 80 | return location; 81 | } 82 | else 83 | { 84 | 85 | location.address = result.results[0].formatted_address; 86 | location.latitude = result.results[0].geometry.location.lat; 87 | location.longitude = result.results[0].geometry.location.lng; 88 | 89 | let viewPort: any = result.results[0].geometry.viewport; 90 | let bounds: Object = L.latLngBounds( 91 | { lat: viewPort.southwest.lat, lng: viewPort.southwest.lng}, 92 | { lat: viewPort.northeast.lat, lng: viewPort.northeast.lng} 93 | ); 94 | 95 | location.setData('viewBounds', bounds); 96 | 97 | return location; 98 | } 99 | }); 100 | } 101 | } -------------------------------------------------------------------------------- /src/app/shared/services/location.service.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2016 Jim Armstrong (www.algorithmist.net) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | // platform imports 18 | import { Injectable } from '@angular/core'; 19 | import { Http, Response } from '@angular/http'; 20 | 21 | // rxjs 22 | import { Observable } from 'rxjs/Observable'; 23 | import 'rxjs/add/operator/map'; 24 | import 'rxjs/add/operator/catch'; 25 | 26 | // TSMT Location 27 | import { TSMT$Location } from '../Location'; 28 | 29 | /** 30 | * A basic service to return current location based on IP address 31 | */ 32 | 33 | @Injectable() 34 | export class LocationService 35 | { 36 | /** 37 | * Construct a new location service 38 | * 39 | * @param _http: Http Injected Http instance from the platform 40 | */ 41 | constructor(protected _http: Http) 42 | { 43 | // empty 44 | } 45 | 46 | /** 47 | * Retrieve the current location of the user based on ip address 48 | * 49 | * @param _url: string URL of external service 50 | * 51 | * @return Observable 52 | */ 53 | public getLocation(): Observable 54 | { 55 | return this._http 56 | .get("http://ipv4.myexternalip.com/json") 57 | .map(res => res.json().ip) 58 | .mergeMap(ip => this._http.get("http://freegeoip.net/json/" + ip)) 59 | .map((res: Response) => res.json()) 60 | .map(result => { 61 | let location = new TSMT$Location(); 62 | 63 | location.address = result.city + ", " + result.region_code + " " + result.zip_code + ", " + result.country_code; 64 | location.latitude = result.latitude; 65 | location.longitude = result.longitude; 66 | 67 | return location; 68 | }); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/app/sharedDispatcher.module.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2016 Jim Armstrong (www.algorithmist.net) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | /** 18 | * Shared Dispatcher module (necessary to enforce a singleton FluxDispatcher across eagerly and lazy-loaded routes 19 | * 20 | * @author Jim Armstrong (www.algorithmist.net) 21 | * 22 | * @version 1.0 23 | */ 24 | 25 | // platform imports 26 | import { NgModule, ModuleWithProviders } from '@angular/core'; 27 | 28 | // providers 29 | import { FluxDispatcher } from './shared/FluxDispatcher'; 30 | 31 | @NgModule({}) 32 | export class SharedDispatcherModule 33 | { 34 | static forRoot():ModuleWithProviders 35 | { 36 | return { 37 | ngModule: SharedDispatcherModule, 38 | providers: [FluxDispatcher] 39 | }; 40 | } 41 | } -------------------------------------------------------------------------------- /src/app/sharedModel.module.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2016 Jim Armstrong (www.algorithmist.net) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | /** 18 | * Shared Model module (necessary to enforce a singleton Model across eagerly and lazy-loaded routes 19 | * 20 | * @author Jim Armstrong (www.algorithmist.net) 21 | * 22 | * @version 1.0 23 | */ 24 | 25 | // platform imports 26 | import { NgModule, ModuleWithProviders } from '@angular/core'; 27 | 28 | // providers 29 | import { LeafletModel } from './shared/model/LeafletModel'; 30 | import { LocationService } from './shared/services/location.service'; 31 | import { Geocode } from './shared/services/Geocode'; 32 | 33 | @NgModule({}) 34 | export class SharedModelModule 35 | { 36 | static forRoot():ModuleWithProviders 37 | { 38 | return { 39 | ngModule: SharedModelModule, 40 | providers: [LeafletModel, LocationService, Geocode] 41 | }; 42 | } 43 | } -------------------------------------------------------------------------------- /src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theAlgorithmist/Angular2Leaflet/174bfcb8506a7b56f93a4099d4a699c655941aa0/src/assets/.gitkeep -------------------------------------------------------------------------------- /src/assets/eyes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theAlgorithmist/Angular2Leaflet/174bfcb8506a7b56f93a4099d4a699c655941aa0/src/assets/eyes.png -------------------------------------------------------------------------------- /src/assets/preloader.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theAlgorithmist/Angular2Leaflet/174bfcb8506a7b56f93a4099d4a699c655941aa0/src/assets/preloader.gif -------------------------------------------------------------------------------- /src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true 3 | }; 4 | -------------------------------------------------------------------------------- /src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | // The file contents for the current environment will overwrite these during build. 2 | // The build system defaults to the dev environment which uses `environment.ts`, but if you do 3 | // `ng build --env=prod` then `environment.prod.ts` will be used instead. 4 | // The list of which env maps to which file can be found in `angular-cli.json`. 5 | 6 | export const environment = { 7 | production: false 8 | }; 9 | -------------------------------------------------------------------------------- /src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theAlgorithmist/Angular2Leaflet/174bfcb8506a7b56f93a4099d4a699c655941aa0/src/favicon.ico -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Angular 2 CLI and Leaflet 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 |

Angular 2 Leaflet demo with Flux/Redux-style architecture. (JIT Compiled)

15 |

Author: Jim Armstrong, The Algorithmist

16 |
17 | 18 |

Loading Three Stooges Application

19 | 20 |
21 | © 2016 The Algorithmist. 22 |
23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import './polyfills.ts'; 2 | 3 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 4 | import { enableProdMode } from '@angular/core'; 5 | import { environment } from './environments/environment'; 6 | import { AppModule } from './app/app.module'; 7 | 8 | if (environment.production) { 9 | enableProdMode(); 10 | } 11 | 12 | platformBrowserDynamic().bootstrapModule(AppModule); 13 | -------------------------------------------------------------------------------- /src/polyfills.ts: -------------------------------------------------------------------------------- 1 | // This file includes polyfills needed by Angular and is loaded before 2 | // the app. You can add your own extra polyfills to this file. 3 | import 'core-js/es6/symbol'; 4 | import 'core-js/es6/object'; 5 | import 'core-js/es6/function'; 6 | import 'core-js/es6/parse-int'; 7 | import 'core-js/es6/parse-float'; 8 | import 'core-js/es6/number'; 9 | import 'core-js/es6/math'; 10 | import 'core-js/es6/string'; 11 | import 'core-js/es6/date'; 12 | import 'core-js/es6/array'; 13 | import 'core-js/es6/regexp'; 14 | import 'core-js/es6/map'; 15 | import 'core-js/es6/set'; 16 | import 'core-js/es6/reflect'; 17 | 18 | import 'core-js/es7/reflect'; 19 | import 'zone.js/dist/zone'; 20 | -------------------------------------------------------------------------------- /src/styles.css: -------------------------------------------------------------------------------- 1 | .mainContent 2 | { 3 | font-size: 14px; 4 | margin-left: 10px; 5 | margin-top: 10px; 6 | } 7 | 8 | a.main 9 | { 10 | text-decoration: none; 11 | } 12 | 13 | a.main:hover 14 | { 15 | text-decoration: underline; 16 | } 17 | 18 | p.padded 19 | { 20 | padding: 50px; 21 | } 22 | -------------------------------------------------------------------------------- /src/test.ts: -------------------------------------------------------------------------------- 1 | import './polyfills.ts'; 2 | 3 | import 'zone.js/dist/long-stack-trace-zone'; 4 | import 'zone.js/dist/proxy.js'; 5 | import 'zone.js/dist/sync-test'; 6 | import 'zone.js/dist/jasmine-patch'; 7 | import 'zone.js/dist/async-test'; 8 | import 'zone.js/dist/fake-async-test'; 9 | import { getTestBed } from '@angular/core/testing'; 10 | import { 11 | BrowserDynamicTestingModule, 12 | platformBrowserDynamicTesting 13 | } from '@angular/platform-browser-dynamic/testing'; 14 | 15 | // Unfortunately there's no typing for the `__karma__` variable. Just declare it as any. 16 | declare var __karma__: any; 17 | declare var require: any; 18 | 19 | // Prevent Karma from running prematurely. 20 | __karma__.loaded = function () {}; 21 | 22 | // First, initialize the Angular testing environment. 23 | getTestBed().initTestEnvironment( 24 | BrowserDynamicTestingModule, 25 | platformBrowserDynamicTesting() 26 | ); 27 | // Then we find all the tests. 28 | const context = require.context('./', true, /\.spec\.ts$/); 29 | // And load the modules. 30 | context.keys().map(context); 31 | // Finally, start Karma to run the tests. 32 | __karma__.start(); 33 | -------------------------------------------------------------------------------- /src/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": "", 4 | "declaration": false, 5 | "emitDecoratorMetadata": true, 6 | "experimentalDecorators": true, 7 | "lib": ["es6", "dom"], 8 | "mapRoot": "./", 9 | "module": "es6", 10 | "moduleResolution": "node", 11 | "outDir": "../dist/out-tsc", 12 | "sourceMap": true, 13 | "target": "es5", 14 | "typeRoots": [ 15 | "../node_modules/@types" 16 | ] 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rulesDirectory": [ 3 | "node_modules/codelyzer" 4 | ], 5 | "rules": { 6 | "callable-types": true, 7 | "class-name": true, 8 | "comment-format": [ 9 | true, 10 | "check-space" 11 | ], 12 | "curly": true, 13 | "eofline": true, 14 | "forin": true, 15 | "import-blacklist": [true, "rxjs"], 16 | "import-spacing": true, 17 | "indent": [ 18 | true, 19 | "spaces" 20 | ], 21 | "interface-over-type-literal": true, 22 | "label-position": true, 23 | "max-line-length": [ 24 | true, 25 | 140 26 | ], 27 | "member-access": false, 28 | "member-ordering": [ 29 | true, 30 | "static-before-instance", 31 | "variables-before-functions" 32 | ], 33 | "no-arg": true, 34 | "no-bitwise": true, 35 | "no-console": [ 36 | true, 37 | "debug", 38 | "info", 39 | "time", 40 | "timeEnd", 41 | "trace" 42 | ], 43 | "no-construct": true, 44 | "no-debugger": true, 45 | "no-duplicate-variable": true, 46 | "no-empty": false, 47 | "no-empty-interface": true, 48 | "no-eval": true, 49 | "no-inferrable-types": true, 50 | "no-shadowed-variable": true, 51 | "no-string-literal": false, 52 | "no-string-throw": true, 53 | "no-switch-case-fall-through": true, 54 | "no-trailing-whitespace": true, 55 | "no-unused-expression": true, 56 | "no-use-before-declare": true, 57 | "no-var-keyword": true, 58 | "object-literal-sort-keys": false, 59 | "one-line": [ 60 | true, 61 | "check-open-brace", 62 | "check-catch", 63 | "check-else", 64 | "check-whitespace" 65 | ], 66 | "prefer-const": true, 67 | "quotemark": [ 68 | true, 69 | "single" 70 | ], 71 | "radix": true, 72 | "semicolon": [ 73 | "always" 74 | ], 75 | "triple-equals": [ 76 | true, 77 | "allow-null-check" 78 | ], 79 | "typedef-whitespace": [ 80 | true, 81 | { 82 | "call-signature": "nospace", 83 | "index-signature": "nospace", 84 | "parameter": "nospace", 85 | "property-declaration": "nospace", 86 | "variable-declaration": "nospace" 87 | } 88 | ], 89 | "typeof-compare": true, 90 | "unified-signatures": true, 91 | "variable-name": false, 92 | "whitespace": [ 93 | true, 94 | "check-branch", 95 | "check-decl", 96 | "check-operator", 97 | "check-separator", 98 | "check-type" 99 | ], 100 | 101 | "directive-selector": [true, "attribute", "app", "camelCase"], 102 | "component-selector": [true, "element", "app", "kebab-case"], 103 | "use-input-property-decorator": true, 104 | "use-output-property-decorator": true, 105 | "use-host-property-decorator": true, 106 | "no-input-rename": true, 107 | "no-output-rename": true, 108 | "use-life-cycle-interface": true, 109 | "use-pipe-transform-interface": true, 110 | "component-class-suffix": true, 111 | "directive-class-suffix": true, 112 | "no-access-missing-member": true, 113 | "templates-use-public": true, 114 | "invoke-injectable": true 115 | } 116 | } 117 | --------------------------------------------------------------------------------