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 all 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
172 changes: 142 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,152 @@ 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.errors = null;
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" (ngModelChange)="saveLastCity($event)"></ccl-city-dropdown>
</div>
<!-- scenario selector -->
<div class="control-group">
Expand Down
11 changes: 11 additions & 0 deletions src/app/lab/lab.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { Subscription } from 'rxjs/Subscription';

import {
Chart,
City,
ClimateModel,
Dataset,
Indicator,
Expand All @@ -35,6 +36,7 @@ export class LabComponent implements OnInit, OnDestroy {
public chart: Chart;
public indicator: Indicator;
private routeParamsSubscription: Subscription;
private lastCity: City;

constructor(private projectService: ProjectService,
private route: ActivatedRoute,
Expand All @@ -58,6 +60,7 @@ export class LabComponent implements OnInit, OnDestroy {
if (this.project.project_data.charts[0]) {
this.indicator = this.project.project_data.charts[0].indicator;
}
this.saveLastCity(this.project.project_data.city);
},
error => this.router.navigate(['/']) // Reroute if error
);
Expand Down Expand Up @@ -106,16 +109,24 @@ export class LabComponent implements OnInit, OnDestroy {
this.removeChart();
/* Trigger lifecycle to truly destroy the chart component & its children
Reset defaults in fresh child components
Restore to last valid city in case the current one is invalid
Cleanly evaluate which children to have (e.g. extra params) */
setTimeout(() => {
this.indicator = indicator;
this.saveExtraParams({});
const chart = new Chart({indicator: indicator,
unit: indicator.default_units});
this.project.project_data.city = this.lastCity;
this.project.project_data.charts = [chart];
})
}

public saveLastCity(city: City) {
if (city && city.properties) {
this.lastCity = city;
}
}

public modelsChanged(models: ClimateModel[]) {
this.project.project_data.models = models;
}
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;
}
Loading