From 7d1bf4b18f70ce8473f3675f5e316e01bc3085f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Charles-=C3=89ric?= Date: Mon, 8 May 2017 10:47:43 -0400 Subject: [PATCH] feat(form): map field (#44) --- src/demo-app/app/app.component.html | 31 ++++ src/demo-app/app/app.component.styl | 7 +- src/demo-app/app/app.component.ts | 20 ++- src/demo-app/assets/locale/en.json | 4 +- src/demo-app/assets/locale/fr.json | 4 +- src/lib/form/fields/index.ts | 1 + src/lib/form/fields/map-field/index.ts | 1 + .../fields/map-field/map-field.component.html | 11 ++ .../map-field/map-field.component.spec.ts | 34 ++++ .../fields/map-field/map-field.component.styl | 15 ++ .../fields/map-field/map-field.component.ts | 153 ++++++++++++++++++ src/lib/form/index.ts | 1 + src/lib/form/module.ts | 30 ++++ .../map/map-browser/map-browser.component.ts | 10 +- src/lib/map/shared/map.ts | 4 + src/lib/module.ts | 3 + src/lib/query/shared/query.directive.ts | 1 + 17 files changed, 322 insertions(+), 8 deletions(-) create mode 100644 src/lib/form/fields/index.ts create mode 100644 src/lib/form/fields/map-field/index.ts create mode 100644 src/lib/form/fields/map-field/map-field.component.html create mode 100644 src/lib/form/fields/map-field/map-field.component.spec.ts create mode 100644 src/lib/form/fields/map-field/map-field.component.styl create mode 100644 src/lib/form/fields/map-field/map-field.component.ts create mode 100644 src/lib/form/index.ts create mode 100644 src/lib/form/module.ts diff --git a/src/demo-app/app/app.component.html b/src/demo-app/app/app.component.html index 20b43dfb3f..a20e1d5276 100644 --- a/src/demo-app/app/app.component.html +++ b/src/demo-app/app/app.component.html @@ -139,4 +139,35 @@ + + Form module + map-field.component + +
+ +
+ + +
+ +
+ +
+ +
+
+
+ diff --git a/src/demo-app/app/app.component.styl b/src/demo-app/app/app.component.styl index 9022faa470..b94ed353e2 100644 --- a/src/demo-app/app/app.component.styl +++ b/src/demo-app/app/app.component.styl @@ -18,4 +18,9 @@ md-sidenav { md-sidenav p { width: 200px; padding: 20px; -} \ No newline at end of file +} + +.igo-map-field-container { + width: 300px; + height: 200px; +} diff --git a/src/demo-app/app/app.component.ts b/src/demo-app/app/app.component.ts index acbf5ef5ba..991e086287 100644 --- a/src/demo-app/app/app.component.ts +++ b/src/demo-app/app/app.component.ts @@ -1,4 +1,5 @@ import { Component, OnInit } from '@angular/core'; +import { FormGroup, FormBuilder, FormControl, Validators } from '@angular/forms'; import { ContextService, Feature, FeatureService, IgoMap, LanguageService, MessageService, OverlayService, @@ -13,13 +14,19 @@ export class AppComponent implements OnInit { public map = new IgoMap(); public searchTerm: string; + public demoForm: FormGroup; + + get locationField () { + return (this.demoForm.controls['location']); + } constructor(public contextService: ContextService, public featureService: FeatureService, public messageService: MessageService, public overlayService: OverlayService, public toolService: ToolService, - public language: LanguageService) {} + public language: LanguageService, + private formBuilder: FormBuilder) {} ngOnInit() { // If you do not want to load a context from a file, @@ -28,6 +35,12 @@ export class AppComponent implements OnInit { // as the contexts in ../contexts/ this.contextService.loadContext('_default'); + + this.demoForm = this.formBuilder.group({ + location: ['', [ + Validators.required + ]] + }); } handleSearch(term: string) { @@ -50,4 +63,9 @@ export class AppComponent implements OnInit { this.featureService.unfocusFeature(); this.overlayService.clear(); } + + handleFormSubmit(data: any, isValid: boolean) { + console.log(data); + console.log(isValid); + } } diff --git a/src/demo-app/assets/locale/en.json b/src/demo-app/assets/locale/en.json index 6cdbe04d0f..7d9f930bca 100644 --- a/src/demo-app/assets/locale/en.json +++ b/src/demo-app/assets/locale/en.json @@ -1,3 +1,5 @@ { - "Search for an address or a place": "Search for an address or a place" + "Location": "Location", + "Search for an address or a place": "Search for an address or a place", + "Submit": "Submit" } \ No newline at end of file diff --git a/src/demo-app/assets/locale/fr.json b/src/demo-app/assets/locale/fr.json index 729d76bb10..dc3cfa18a8 100644 --- a/src/demo-app/assets/locale/fr.json +++ b/src/demo-app/assets/locale/fr.json @@ -1,3 +1,5 @@ { - "Search for an address or a place": "Rechercher une adresse, un lieu ou une couche" + "Location": "Localisation", + "Search for an address or a place": "Rechercher une adresse, un lieu ou une couche", + "Submit": "Soumettre" } \ No newline at end of file diff --git a/src/lib/form/fields/index.ts b/src/lib/form/fields/index.ts new file mode 100644 index 0000000000..944f40ecc5 --- /dev/null +++ b/src/lib/form/fields/index.ts @@ -0,0 +1 @@ +export * from './map-field'; diff --git a/src/lib/form/fields/map-field/index.ts b/src/lib/form/fields/map-field/index.ts new file mode 100644 index 0000000000..63572da323 --- /dev/null +++ b/src/lib/form/fields/map-field/index.ts @@ -0,0 +1 @@ +export * from './map-field.component'; diff --git a/src/lib/form/fields/map-field/map-field.component.html b/src/lib/form/fields/map-field/map-field.component.html new file mode 100644 index 0000000000..1258806899 --- /dev/null +++ b/src/lib/form/fields/map-field/map-field.component.html @@ -0,0 +1,11 @@ +
+ + + + +
diff --git a/src/lib/form/fields/map-field/map-field.component.spec.ts b/src/lib/form/fields/map-field/map-field.component.spec.ts new file mode 100644 index 0000000000..eca14b43c1 --- /dev/null +++ b/src/lib/form/fields/map-field/map-field.component.spec.ts @@ -0,0 +1,34 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { IgoSharedModule } from '../../../shared'; +import { MapBrowserComponent } from '../../../map'; + +import { MapFieldComponent } from './map-field.component'; + +describe('MapFieldComponent', () => { + let component: MapFieldComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [ + IgoSharedModule, + ], + declarations: [ + MapBrowserComponent, + MapFieldComponent + ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(MapFieldComponent); + component = fixture.componentInstance; + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + +}); diff --git a/src/lib/form/fields/map-field/map-field.component.styl b/src/lib/form/fields/map-field/map-field.component.styl new file mode 100644 index 0000000000..3251e3d81a --- /dev/null +++ b/src/lib/form/fields/map-field/map-field.component.styl @@ -0,0 +1,15 @@ +@require '../../../../style/var.styl'; + +:host { + position: relative; + display: block; +} + +:host, .igo-map-browser-target { + width: 100%; + height: 100%; +} + +input { + width: 100% +} diff --git a/src/lib/form/fields/map-field/map-field.component.ts b/src/lib/form/fields/map-field/map-field.component.ts new file mode 100644 index 0000000000..19b92203ae --- /dev/null +++ b/src/lib/form/fields/map-field/map-field.component.ts @@ -0,0 +1,153 @@ +import { Component, Input, forwardRef, + AfterViewInit, OnDestroy } from '@angular/core'; +import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; + +import { IgoMap, MapViewOptions } from '../../../map'; +import { Layer } from '../../../layer'; + + +@Component({ + selector: 'igo-map-field', + templateUrl: './map-field.component.html', + styleUrls: ['./map-field.component.styl'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => MapFieldComponent), + multi: true + } + ] +}) +export class MapFieldComponent + implements AfterViewInit, OnDestroy, ControlValueAccessor { + + @Input() + get placeholder() { return this._placeholder; } + set placeholder(value: string) { + this._placeholder = value; + } + private _placeholder: string; + + @Input() + get decimals(): number { return this._decimals; } + set decimals(value: number) { + this._decimals = value; + } + private _decimals: number = 6; + + @Input() + get readonly(): boolean { return this._readonly; } + set readonly(value: boolean) { + this._readonly = value; + } + protected _readonly: boolean = false; + + @Input() + get value(): [number, number] { return this._value; } + set value(value: [number, number]) { + this.map.clearOverlay(); + + if (value === undefined) { + this._value = value; + this.onChange(this._value); + return; + } + + if (!isNaN(+value[0]) && !isNaN(+value[1])) { + const values = [+value[0], +value[1]] as [number, number]; + + this.addOverlay(values); + + this._value = [ + +values[0].toFixed(this.decimals), + +values[1].toFixed(this.decimals) + ]; + + this.onChange(this._value); + this.onTouched(); + } + } + private _value: [number, number]; + + @Input() + get view(): MapViewOptions { return this._view; } + set view(value: MapViewOptions) { + this._view = value; + if (this.map !== undefined) { + this.map.setView(value); + } + } + protected _view: MapViewOptions; + + @Input() + get layers(): Layer[] { return this._layers; } + set layers(value: Layer[]) { + this._layers = value; + this.map.removeLayers(); + this.map.addLayers(value); + } + private _layers: Layer[]; + + public map = new IgoMap(); + public projection = 'EPSG:4326'; + + onChange: any = () => {}; + onTouched: any = () => {}; + + constructor() {} + + ngAfterViewInit() { + this.map.olMap.on('singleclick', this.handleMapClick, this); + } + + ngOnDestroy() { + this.map.olMap.un('singleclick', this.handleMapClick, this); + } + + registerOnChange(fn: Function) { + this.onChange = fn; + } + + registerOnTouched(fn: Function) { + this.onTouched = fn; + } + + writeValue(value: [number, number]) { + if (value) { + this.value = value; + } + } + + handleValueChange(value: string) { + this.value = this.parseValue(value); + } + + private handleMapClick(event: ol.MapBrowserEvent) { + this.value = ol.proj.transform( + event.coordinate, this.map.projection, this.projection); + } + + private parseValue(value: string): [number, number] | undefined { + if (value === undefined || value === '') { + return undefined; + } + + const values = value.split(',').filter(v => v !== ''); + if (values.length === 2) { + return [+values[0], +values[1]]; + } + + return undefined; + } + + private addOverlay(coordinates: [number, number]) { + const geometry = new ol.geom.Point( + ol.proj.transform(coordinates, this.projection, this.map.projection)); + const extent = geometry.getExtent(); + const feature = new ol.Feature({geometry: geometry}); + + this.map.moveToExtent(extent); + this.map.addOverlay(feature); + } + +} diff --git a/src/lib/form/index.ts b/src/lib/form/index.ts new file mode 100644 index 0000000000..20a96c9a59 --- /dev/null +++ b/src/lib/form/index.ts @@ -0,0 +1 @@ +export * from './module'; diff --git a/src/lib/form/module.ts b/src/lib/form/module.ts new file mode 100644 index 0000000000..95d123bfb8 --- /dev/null +++ b/src/lib/form/module.ts @@ -0,0 +1,30 @@ +import { NgModule, ModuleWithProviders } from '@angular/core'; + +import { IgoSharedModule } from '../shared'; +import { IgoMapModule } from '../map'; + +import { MapFieldComponent } from './fields/map-field'; + + +@NgModule({ + imports: [ + IgoSharedModule, + IgoMapModule + ], + exports: [ + MapFieldComponent + ], + declarations: [ + MapFieldComponent + ] +}) +export class IgoFormModule { + static forRoot(): ModuleWithProviders { + return { + ngModule: IgoFormModule, + providers: [] + }; + } +} + +export * from './fields'; diff --git a/src/lib/map/map-browser/map-browser.component.ts b/src/lib/map/map-browser/map-browser.component.ts index dbd9bc27ec..b388765444 100644 --- a/src/lib/map/map-browser/map-browser.component.ts +++ b/src/lib/map/map-browser/map-browser.component.ts @@ -14,17 +14,19 @@ export class MapBrowserComponent implements AfterViewInit { set map(value: IgoMap) { this._map = value; } - private _map: IgoMap; + protected _map: IgoMap; @Input() get view(): MapViewOptions { return this._view; } set view(value: MapViewOptions) { this._view = value; - this.map.setView(value); + if (this.map !== undefined) { + this.map.setView(value); + } } - private _view: MapViewOptions; + protected _view: MapViewOptions; - public id: string = 'igo-map-target'; + public id: string = `igo-map-target-${new Date().getTime()}`; constructor() {} diff --git a/src/lib/map/shared/map.ts b/src/lib/map/shared/map.ts index a9195e4a70..15d5e02f2b 100644 --- a/src/lib/map/shared/map.ts +++ b/src/lib/map/shared/map.ts @@ -111,6 +111,10 @@ export class IgoMap { } } + addLayers(layers: Layer[], push = true) { + layers.forEach(layer => this.addLayer(layer, push)); + } + getLayerById(id: string): Layer { return this.layers.find(layer => { return layer.id && layer.id === id; diff --git a/src/lib/module.ts b/src/lib/module.ts index d102be07d4..a42305e02d 100644 --- a/src/lib/module.ts +++ b/src/lib/module.ts @@ -18,6 +18,7 @@ import { IgoLanguageModule } from './language/index'; import { IgoContextModule } from './context/index'; import { IgoDataSourceModule } from './datasource/index'; import { IgoFeatureModule } from './feature/index'; +import { IgoFormModule } from './form/index'; import { IgoFilterModule } from './filter/index'; import { IgoLayerModule } from './layer/index'; import { IgoMapModule } from './map/index'; @@ -33,6 +34,7 @@ const IGO_MODULES = [ IgoContextModule, IgoDataSourceModule, IgoFeatureModule, + IgoFormModule, IgoFilterModule, IgoLayerModule, IgoMapModule, @@ -53,6 +55,7 @@ const IGO_MODULES = [ IgoDataSourceModule.forRoot(), IgoContextModule.forRoot(), IgoFeatureModule.forRoot(), + IgoFormModule.forRoot(), IgoFilterModule.forRoot(), IgoLayerModule.forRoot(), IgoMapModule.forRoot(), diff --git a/src/lib/query/shared/query.directive.ts b/src/lib/query/shared/query.directive.ts index 26d6892f90..6a224e808b 100644 --- a/src/lib/query/shared/query.directive.ts +++ b/src/lib/query/shared/query.directive.ts @@ -36,6 +36,7 @@ export class QueryDirective implements AfterViewInit, OnDestroy { ngOnDestroy() { this.queryDataSources$$.unsubscribe(); + this.map.olMap.un('singleclick', this.handleMapClick, this); } private handleLayersChange(layers: Layer[]) {