Skip to content
This repository has been archived by the owner on Aug 21, 2023. It is now read-only.

Create projects from locations #342

Merged
merged 7 commits into from
Mar 5, 2019
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 1 addition & 4 deletions Jenkinsfile
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,7 @@ node {

stage('test') {
wrap([$class: 'AnsiColorBuildWrapper']) {
writeFile file: 'src/app/constants.ts', text: """
export const defaultCity = {\'id\': 7, \'properties\': {\'name\': \'Philadelphia\', \'admin\': \'PA\'}};
export const apiHost = \'${env.API_HOST}\';
export const defaultScenario = \'RCP85\';\n"""
sh "aws s3 cp 's3://${env.CC_SETTINGS_BUCKET}/angular/constants.ts' 'src/app/constants.ts'"

sh './scripts/test --jenkins'

Expand Down
10 changes: 8 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,18 @@ Alternatively, you can bring up the vagrant VM that has the dependencies install
If your development host machine meets the requirements above, simply:

- Clone this repo and run `yarn install`
- `cp example/constants.ts.example src/app/constants.ts`
- Edit `constants.ts` to set the API server name
- Copy the "Climate lab dev config" from LastPass to `src/app/constants.ts` (See below if you do not have access to this resource)
- `yarn run serve`

The site will then be available at [http://localhost:4200](http://localhost:4200) on your host machine.

#### Setting up constants.ts

For users who don't have access to our internal configuration file, you can create your own with the following steps:
- Enable the "Maps JavaScript API" and "Places API" from the [Google API library](https://console.cloud.google.com/apis/library), then create a new [Google API key](https://console.cloud.google.com/apis/credentials/). You can restrict it to the "Maps JavaScript API" and "Places API" with referrers set to `http://localhost:4200/*`
- `cp example/constants.ts.example src/app/constants.ts`
- Edit `constants.ts` to set the API server name and Google API key

### Setup via Vagrant VM

_Recommended only if you don't have the requirements above installed on your host system and are unable to install. If you do, strongly consider instructions in #Setup instead._
Expand Down
1 change: 1 addition & 0 deletions example/constants.ts.example
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@
*/

export const apiHost = 'https://app.climate.azavea.com';
export const googleApiKey = 'Add API key here';
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
},
"private": true,
"dependencies": {
"@agm/core": "1.0.0-beta.2",
"@angular/animations": "^4.4.7",
"@angular/common": "^4.4.7",
"@angular/compiler": "^4.4.7",
Expand Down Expand Up @@ -55,6 +56,7 @@
"@angular/cli": "1.3.2",
"@angular/compiler-cli": "^4.4.7",
"@angular/language-service": "^4.4.7",
"@types/googlemaps": "^3.30.16",
"@types/jasmine": "2.8.9",
"@types/jquery": "^3.3.29",
"@types/node": "~11.9.3",
Expand Down
14 changes: 13 additions & 1 deletion src/app/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@ import { FormsModule } from '@angular/forms';
import { HttpModule, XHRBackend, RequestOptions } from '@angular/http';

// 3rd party modules
import { AgmCoreModule } from '@agm/core';
import {
BsDropdownModule,
CollapseModule,
PopoverModule,
TooltipModule } from 'ngx-bootstrap';
import { ClipboardModule } from 'ngx-clipboard';
import { NouisliderModule } from 'ng2-nouislider';
Expand Down Expand Up @@ -44,16 +46,23 @@ import { AuthGuard } from './auth/auth.guard';

import { DataExportService } from './services/data-export.service';
import { ImageExportService } from './services/image-export.service';
import { MapCellService } from './services/map-cell.service';
import { ProjectService } from './services/project.service';

import { apiHost } from './constants';
import { apiHost, googleApiKey } from './constants';

// Custom app providers
const locationStrategyProvider = {
provide: LocationStrategy,
useClass: HashLocationStrategy
};

// Google maps config
const AGM_CONFIG = {
apiKey: googleApiKey,
libraries: ['places']
};

@NgModule({
bootstrap: [ AppComponent ],
declarations: [
Expand All @@ -75,9 +84,11 @@ const locationStrategyProvider = {
FormsModule,
HttpModule,
routing,
AgmCoreModule.forRoot(AGM_CONFIG),
BsDropdownModule.forRoot(),
CollapseModule.forRoot(),
TooltipModule.forRoot(),
PopoverModule.forRoot(),
Ng2AutoCompleteModule,
ClipboardModule,
NouisliderModule,
Expand All @@ -95,6 +106,7 @@ const locationStrategyProvider = {
AuthGuard,
DataExportService,
ImageExportService,
MapCellService,
ProjectService
]
})
Expand Down
2 changes: 1 addition & 1 deletion src/app/charts/chart.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ export class ChartComponent implements OnChanges, OnDestroy, AfterViewInit {

ngOnChanges($event) {
// happens if different chart selected
if (!this.scenario || !this.city || !this.models || !this.dataset) { return; }
if (!this.scenario || !this.city || !this.city.properties || !this.models || !this.dataset) { return; }
this.updateChart($event);
}

Expand Down
171 changes: 141 additions & 30 deletions src/app/lab/components/city-dropdown.component.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
import { Component, Input } from '@angular/core';
import { Component, ElementRef, EventEmitter, Input, NgZone, OnInit, ViewChild, forwardRef } from '@angular/core';
import { ControlValueAccessor, FormControl, NG_VALUE_ACCESSOR, NG_VALIDATORS, Validator, ValidationErrors } from '@angular/forms';

import { ProjectData } from '../../models/project-data.model';
import { MapsAPILoader } from '@agm/core';
import { City } from 'climate-change-components';
import { Point } from 'geojson';
import { Observable } from 'rxjs/Observable';

import { MapCell } from '../../models/map-cell.model';
import { MapCellService } from '../../services/map-cell.service';
import { apiHost } from '../../constants';
import { environment } from '../../../environments/environment';

/* City Dropdown Component
Expand All @@ -12,47 +19,151 @@ import { apiHost } from '../../constants';
<ccl-city-dropdown
[projectData]="your_project.project_data">
*/

@Component({
selector: 'ccl-city-dropdown',
template: `<div class="dropdown dropdown-location">
<div class="input">
<input auto-complete
[(ngModel)]="projectData.city"
[source]="apiCities"
[list-formatter]="cityListFormatter"
[value-formatter]="cityValueFormatter"
display-property-name="name"
path-to-data="features"
<ng-template #cityError>
<i class="icon-attention"></i>
No climate data for this location
</ng-template>
<input #input
id="project.city"
class="autocomplete"
[ngClass]="errors === null ? '' : 'error'"
type="text"
placeholder="Enter your city"
min-chars="2" />
[popover]="cityError"
triggers=""
container="body"
[isOpen]="errors !== null"
containerClass="error-popover"
placeholder="Enter your city" />
</div>
</div>`
</div>`,
providers: [{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => CityDropdownComponent),
multi: true,
}, {
provide: NG_VALIDATORS,
useExisting: forwardRef(() => CityDropdownComponent),
multi: true,
}]
})
export class CityDropdownComponent {
export class CityDropdownComponent implements OnInit, ControlValueAccessor, Validator {

@ViewChild('input') input: ElementRef;

public apiCities: string = apiHost + '/api/city/?search=:keyword';
public errors: ValidationErrors = null;

@Input() projectData: ProjectData;
@Input() showIcon = true;
private autocomplete: google.maps.places.Autocomplete;
private onChange = (_: any) => { };

constructor() {}
constructor(private el: ElementRef, private mapsApiLoader: MapsAPILoader,
private zone: NgZone, private mapCellService: MapCellService) {}

// custom formatter to display list of options as City, State
public cityListFormatter(data: any): string {
let html = '';
html += data.properties.name ?
`<span>${data.properties.name}, ${data.properties.admin}</span>` : data;
return html;
ngOnInit() {
try {
this.setupAutocomplete();
} catch (error) {
this.mapsApiLoader.load().then(() => this.setupAutocomplete());
}
}

// custom formatter to display string for selected city as City, State
public cityValueFormatter(data: any): string {
let displayValue = '';
if (data && data.properties) {
displayValue += data.properties.name + ', ' + data.properties.admin;
writeValue(city: any) {
if (city && city.properties) {
this.input.nativeElement.value = `${city.properties.name}, ${city.properties.admin}`;
}
return displayValue;
}

registerOnChange(fn: any) {
this.onChange = fn;
}

// Required by interface, not used
registerOnTouched(fn: any) {}

validate(c: FormControl): ValidationErrors {
return this.errors;
}

private setupAutocomplete() {
const options = {
types: ['(cities)']
};
this.autocomplete = new google.maps.places.Autocomplete(this.input.nativeElement, options);
this.autocomplete.addListener('place_changed', () => this.onPlaceChanged());

const forceAutocompleteOff = () => {
// Chrome ignores 'autocomplete="off"' but will turn off autocomplete for
// invalid options. Google Places sets 'autocomplete="off"' regardless of
// what was set on the <input> before, so we need to override that in JS
this.input.nativeElement.autocomplete = 'forced-false';
this.input.nativeElement.removeEventListener('focus', forceAutocompleteOff);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like this work-around is no longer working... I'm going to open an issue to address this - this will also be an issue on Temperate, which this was copied from.

};
this.input.nativeElement.addEventListener('focus', forceAutocompleteOff);
}

private onPlaceChanged() {
const place = this.autocomplete.getPlace();
const point = {
type: 'Point',
coordinates: [
place.geometry.location.lng(),
place.geometry.location.lat(),
]
} as Point;

// This event is handled outside of the Angular change detection cycle so we
// need to emit any changes inside a zone.run handler to trigger a detection cycle
this.zone.run(() => {
// Unset city while API call is in progress to let parent component
// disable creation button
this.onChange({} as City);
});

this.mapCellService.nearest(point, environment.distance)
.catch((err: Response) => {
return Observable.of([]);
})
.subscribe((cells: MapCell[]) => {
let city;
if (cells.length > 0) {
this.errors = null;
const datasets = new Set<string>();
for (const cell of cells) {
for (const dataset of cell.properties.datasets) {
datasets.add(dataset);
}
}
city = {
type: 'Feature',
geometry: point,
properties: {
name: place.name,
admin: this.getAdminFromAddress(place),
datasets: [...datasets],
region: undefined
}
} as City;
} else {
this.errors = { missing: true };
city = {} as City;
}

this.zone.run(() => {
this.onChange(city);
});
});
}

private getAdminFromAddress(address: google.maps.places.PlaceResult): string {
let admin = '';
address.address_components.forEach(component => {
if (component.types.includes('administrative_area_level_1')) {
admin = component.short_name;
}
});
return admin;
}
}
2 changes: 1 addition & 1 deletion src/app/lab/lab.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
<!-- city selector -->
<div class="control-group">
<label>Location</label>
<ccl-city-dropdown [projectData]="project.project_data"></ccl-city-dropdown>
<ccl-city-dropdown [(ngModel)]="project.project_data.city"></ccl-city-dropdown>
</div>
<!-- scenario selector -->
<div class="control-group">
Expand Down
19 changes: 19 additions & 0 deletions src/app/models/map-cell.model.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { Point } from 'geojson';

export interface ProximityProperties {
ocean: boolean;
}

/* tslint:disable:variable-name */
export interface MapCellProperties {
datasets: string[];
distance_meters: number;
proximity: ProximityProperties;
}
/* tslint:enable:variable-name */

export interface MapCell {
type: string;
geometry: Point;
properties: MapCellProperties;
}
5 changes: 3 additions & 2 deletions src/app/project/add-edit-project.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ <h3 *ngIf="edit">Edit project</h3>
class="form-control"
id="project.name"
name="project.name"
required
[(ngModel)]="model.project.project_data.name">
<label for="project.description">Description</label>
<textarea type="text"
Expand All @@ -23,7 +24,7 @@ <h3 *ngIf="edit">Edit project</h3>
[(ngModel)]="model.project.project_data.description"></textarea>
<div *ngIf="!edit" class="project-options">
<label for="project.city">Choose a city*</label>
<ccl-city-dropdown [projectData]="model.project.project_data" [showIcon]="false"></ccl-city-dropdown>
<ccl-city-dropdown [(ngModel)]="model.project.project_data.city" name="project.city" #city="ngModel"></ccl-city-dropdown>
<div class="row">
<div class="column-5 flex-column">
<label for="project.scenario">Scenario*</label>
Expand Down Expand Up @@ -53,7 +54,7 @@ <h3 *ngIf="edit">Edit project</h3>
<div class="row align-center">
<button type="submit"
class="button button-primary"
[disabled]="!projectForm.form.valid || !model.project.project_data.city.id || !model.project.project_data.scenario.name ||
[disabled]="!projectForm.form.valid || !model.project.project_data.city.geometry || !model.project.project_data.scenario.name ||
!model.project.project_data.dataset.name"
*ngIf="!edit">Create Project</button>
<button type="submit"
Expand Down
22 changes: 22 additions & 0 deletions src/app/services/map-cell.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { Injectable } from '@angular/core';
import { Response } from '@angular/http';
import { Point } from 'geojson';
import { Observable } from 'rxjs/Observable';

import { MapCell } from '../models/map-cell.model';
import { LabApiHttp } from '../auth/api-http.service';
import { apiHost } from '../constants';

@Injectable()
export class MapCellService {

constructor(private apiHttp: LabApiHttp) {}

nearest(point: Point, distance: number): Observable<MapCell[]> {
const url = `${apiHost}/api/map-cell/${point.coordinates[1]}/${point.coordinates[0]}?distance=${distance}`;
maurizi marked this conversation as resolved.
Show resolved Hide resolved
return this.apiHttp.get(url)
.map((resp) => {
maurizi marked this conversation as resolved.
Show resolved Hide resolved
return resp.json() || [] as MapCell[];
});
}
}
Loading