From 2dbabf4d0ea1cfc8ae282b85334e0ae1f96cd6af Mon Sep 17 00:00:00 2001 From: Wykks Date: Thu, 19 Oct 2017 00:23:40 +0200 Subject: [PATCH] feat(layer): add click/mouseEnter/mouseLeave Output add language switch example add center on symbol example --- .vscode/settings.json | 3 +- e2e/language-switch.e2e-spec.ts | 42 ++++++++++ e2e/runtime-check.e2e-spec.ts | 3 +- .../examples/center-on-symbol.component.ts | 78 +++++++++++++++++++ ...alse.ts => interactive-false.component.ts} | 0 .../examples/language-switch.component.ts | 40 ++++++++++ src/app/demo/module.ts | 12 ++- src/app/lib/layer/layer.component.ts | 46 +++++++---- src/app/lib/map/map.component.spec.ts | 4 +- src/app/lib/map/map.service.ts | 44 +++++++++-- 10 files changed, 241 insertions(+), 31 deletions(-) create mode 100644 e2e/language-switch.e2e-spec.ts create mode 100644 src/app/demo/examples/center-on-symbol.component.ts rename src/app/demo/examples/{interactive-false.ts => interactive-false.component.ts} (100%) create mode 100644 src/app/demo/examples/language-switch.component.ts diff --git a/.vscode/settings.json b/.vscode/settings.json index 4752f8c32..14070d9e8 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -6,5 +6,6 @@ "**/CVS": true, "**/.DS_Store": true, ".ng_build": true - } + }, + "typescript.tsdk": "node_modules/typescript/lib" } diff --git a/e2e/language-switch.e2e-spec.ts b/e2e/language-switch.e2e-spec.ts new file mode 100644 index 000000000..cd5640687 --- /dev/null +++ b/e2e/language-switch.e2e-spec.ts @@ -0,0 +1,42 @@ +import { browser, element, by, ExpectedConditions as EC } from 'protractor'; +const PixelDiff = require('pixel-diff'); +const browserLogs = require('protractor-browser-logs'); + +describe('Language switch', () => { + let logs: any; + + beforeEach(() => { + logs = browserLogs(browser); + }); + + afterEach(() => { + return logs.verify(); + }); + + it('should change language', async () => { + await browser.get('/language-switch'); + const elm = element(by.tagName('canvas')); + await browser.wait(EC.presenceOf(elm), 2000); + const buttons = await browser.findElements(by.tagName('button')); + await browser.sleep(4000); + await buttons[0].click(); + await browser.sleep(2000); + const screen1 = await browser.takeScreenshot(); + await buttons[1].click(); + await browser.sleep(2000); + const screen2 = await browser.takeScreenshot(); + const result = new PixelDiff({ + imageA: new Buffer(screen1, 'base64'), + imageB: new Buffer(screen2, 'base64') + }).runSync(); + expect(result.differences).toBeGreaterThan(0); + await buttons[0].click(); + await browser.sleep(2000); + const screen1bis = await browser.takeScreenshot(); + const result2 = new PixelDiff({ + imageA: new Buffer(screen1, 'base64'), + imageB: new Buffer(screen1bis, 'base64') + }).runSync(); + expect(result2.differences).toBe(0); + }); +}); diff --git a/e2e/runtime-check.e2e-spec.ts b/e2e/runtime-check.e2e-spec.ts index c46fc7630..8a1a60f50 100644 --- a/e2e/runtime-check.e2e-spec.ts +++ b/e2e/runtime-check.e2e-spec.ts @@ -30,7 +30,8 @@ describe('Generic runtime error check', () => { 'locate-user', 'ngx-attribution', 'ngx-scale-control', - 'interactive-false' + 'interactive-false', + 'center-on-symbol' ].forEach((route: string) => { it(`should display a map without errors for /${route}`, async () => { await browser.get(`/${route}`); diff --git a/src/app/demo/examples/center-on-symbol.component.ts b/src/app/demo/examples/center-on-symbol.component.ts new file mode 100644 index 000000000..bcd8bc8f8 --- /dev/null +++ b/src/app/demo/examples/center-on-symbol.component.ts @@ -0,0 +1,78 @@ +import { MapComponent } from '../../lib'; +import { Component, ViewChild } from '@angular/core'; +import { MapMouseEvent } from 'mapbox-gl'; + +@Component({ + template: ` + + + + + + + + `, + styleUrls: ['./examples.css'] +}) +export class CenterOnSymbolComponent { + @ViewChild('map') map: MapComponent; + + center = [-90.96, -0.47]; + + geometries = [ + { + 'type': 'Point', + 'coordinates': [ + -91.395263671875, + -0.9145729757782163 + + ] + }, + { + 'type': 'Point', + 'coordinates': [ + -90.32958984375, + -0.6344474832838974 + ] + }, + { + 'type': 'Point', + 'coordinates': [ + -91.34033203125, + 0.01647949196029245 + ] + } + ]; + + centerMapTo(evt: MapMouseEvent) { + this.center = (evt).features[0].geometry.coordinates; + } + + changeCursorToPointer() { + this.map.mapInstance.getCanvas().style.cursor = 'pointer'; + } + + changeCursorToDefault() { + this.map.mapInstance.getCanvas().style.cursor = ''; + } +} diff --git a/src/app/demo/examples/interactive-false.ts b/src/app/demo/examples/interactive-false.component.ts similarity index 100% rename from src/app/demo/examples/interactive-false.ts rename to src/app/demo/examples/interactive-false.component.ts diff --git a/src/app/demo/examples/language-switch.component.ts b/src/app/demo/examples/language-switch.component.ts new file mode 100644 index 000000000..c43e888ea --- /dev/null +++ b/src/app/demo/examples/language-switch.component.ts @@ -0,0 +1,40 @@ +import { Component, ViewChild } from '@angular/core'; +import { MapComponent } from '../../lib'; + +@Component({ + template: ` + + + + + + + + + `, + styleUrls: ['./examples.css', './toggle-layers.component.css'] +}) +export class LanguageSwitchComponent { + @ViewChild('map') map: MapComponent; + + changeLangTo(language: string) { + this.map.mapInstance.setLayoutProperty('country-label-lg', 'text-field', '{name_' + language + '}'); + } +} diff --git a/src/app/demo/module.ts b/src/app/demo/module.ts index c7ae8f0a1..2ad0e8e43 100644 --- a/src/app/demo/module.ts +++ b/src/app/demo/module.ts @@ -4,7 +4,7 @@ import { FormsModule } from '@angular/forms'; import { MatButtonToggleModule, MatRadioModule, MatButtonModule } from '@angular/material'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { RouterModule, Routes } from '@angular/router'; -import { NgxMapboxGLModule } from '../lib/lib.module'; +import { NgxMapboxGLModule } from '../lib'; import { AddImageGeneratedComponent } from './examples/add-image-generated.component'; import { AddImageComponent } from './examples/add-image.component'; import { AttachPopupComponent } from './examples/attach-popup.component'; @@ -29,7 +29,9 @@ import { LocateUserComponent } from './examples/locate-user.component'; import { NgxAttributionComponent } from './examples/ngx-attribution.component'; import { NgxScaleControlComponent } from './examples/ngx-scale-control.component'; import { NgxCustomControlComponent } from './examples/ngx-custom-control.component'; -import { InteractiveFalseComponent } from './examples/interactive-false'; +import { InteractiveFalseComponent } from './examples/interactive-false.component'; +import { LanguageSwitchComponent } from './examples/language-switch.component'; +import { CenterOnSymbolComponent } from './examples/center-on-symbol.component'; export const demoRoutes: Routes = [ { path: '', component: IndexComponent }, @@ -56,6 +58,8 @@ export const demoRoutes: Routes = [ { path: 'ngx-scale-control', component: NgxScaleControlComponent }, { path: 'ngx-custom-control', component: NgxCustomControlComponent }, { path: 'interactive-false', component: InteractiveFalseComponent }, + { path: 'language-switch', component: LanguageSwitchComponent }, + { path: 'ngx-center-on-symbol', component: CenterOnSymbolComponent }, ]; @NgModule({ @@ -96,7 +100,9 @@ export const demoRoutes: Routes = [ NgxAttributionComponent, NgxScaleControlComponent, NgxCustomControlComponent, - InteractiveFalseComponent + InteractiveFalseComponent, + LanguageSwitchComponent, + CenterOnSymbolComponent ] }) export class DemoModule { } diff --git a/src/app/lib/layer/layer.component.ts b/src/app/lib/layer/layer.component.ts index f2014faf8..74f9340a4 100644 --- a/src/app/lib/layer/layer.component.ts +++ b/src/app/lib/layer/layer.component.ts @@ -1,4 +1,4 @@ -import { Component, Input, OnDestroy, OnInit, SimpleChanges, OnChanges } from '@angular/core'; +import { Component, EventEmitter, Input, OnChanges, OnDestroy, OnInit, Output, SimpleChanges } from '@angular/core'; import { BackgroundLayout, CircleLayout, @@ -20,7 +20,8 @@ import { LinePaint, SymbolPaint, RasterPaint, - CirclePaint + CirclePaint, + MapMouseEvent } from 'mapbox-gl'; import { MapService } from '../map/map.service'; @@ -34,9 +35,7 @@ export class LayerComponent implements OnInit, OnDestroy, OnChanges, Layer { @Input() source?: string | VectorSource | RasterSource | GeoJSONSource | ImageSource | VideoSource | GeoJSONSourceRaw; @Input() type?: 'symbol' | 'fill' | 'line' | 'circle' | 'fill-extrusion' | 'raster' | 'background'; @Input() metadata?: any; - @Input() ref?: string; @Input() sourceLayer?: string; - @Input() interactive?: boolean; /* Dynamic inputs */ @Input() filter?: any[]; @@ -46,6 +45,12 @@ export class LayerComponent implements OnInit, OnDestroy, OnChanges, Layer { @Input() minzoom?: number; @Input() maxzoom?: number; + @Output() click = new EventEmitter(); + @Output() mouseEnter = new EventEmitter(); + @Output() mouseLeave = new EventEmitter(); + + private layerAdded = false; + constructor( private MapService: MapService ) { } @@ -53,23 +58,32 @@ export class LayerComponent implements OnInit, OnDestroy, OnChanges, Layer { ngOnInit() { this.MapService.mapLoaded$.subscribe(() => { this.MapService.addLayer({ - id: this.id, - type: this.type, - source: this.source, - metadata: this.metadata, - ref: this.ref, - 'source-layer': this.sourceLayer, - minzoom: this.minzoom, - maxzoom: this.maxzoom, - interactive: this.interactive, - filter: this.filter, - layout: this.layout, - paint: this.paint + layerOptions: { + id: this.id, + type: this.type, + source: this.source, + metadata: this.metadata, + 'source-layer': this.sourceLayer, + minzoom: this.minzoom, + maxzoom: this.maxzoom, + filter: this.filter, + layout: this.layout, + paint: this.paint + }, + layerEvents: { + click: this.click, + mouseEnter: this.mouseEnter, + mouseLeave: this.mouseLeave + } }, this.before); + this.layerAdded = true; }); } ngOnChanges(changes: SimpleChanges) { + if (!this.layerAdded) { + return; + } if (changes.paint && !changes.paint.isFirstChange()) { this.MapService.setAllLayerPaintProperty(this.id, changes.paint.currentValue!); } diff --git a/src/app/lib/map/map.component.spec.ts b/src/app/lib/map/map.component.spec.ts index 6d3b028c3..12f5e8987 100644 --- a/src/app/lib/map/map.component.spec.ts +++ b/src/app/lib/map/map.component.spec.ts @@ -1,4 +1,4 @@ -import { MapService, SetupOptions } from './map.service'; +import { MapService, SetupMap } from './map.service'; import { async, ComponentFixture, TestBed } from '@angular/core/testing'; import { MapComponent } from './map.component'; @@ -43,7 +43,7 @@ describe('MapComponent', () => { it('should init with custom inputs', (done: DoneFn) => { component.accessToken = 'tokenTest'; component.style = 'style'; - msSpy.setup.and.callFake((options: SetupOptions) => { + msSpy.setup.and.callFake((options: SetupMap) => { expect(options.accessToken).toEqual('tokenTest'); expect(options.mapOptions.style).toEqual('style'); done(); diff --git a/src/app/lib/map/map.service.ts b/src/app/lib/map/map.service.ts index 0af3cc839..c0503bc06 100644 --- a/src/app/lib/map/map.service.ts +++ b/src/app/lib/map/map.service.ts @@ -1,18 +1,27 @@ import { AsyncSubject } from 'rxjs/AsyncSubject'; -import { Inject, Injectable, InjectionToken, NgZone, Optional } from '@angular/core'; +import { EventEmitter, Inject, Injectable, InjectionToken, NgZone, Optional } from '@angular/core'; import * as MapboxGl from 'mapbox-gl'; import { MapEvent, MapImageData, MapImageOptions } from './map.types'; import { Observable } from 'rxjs/Observable'; export const MAPBOX_API_KEY = new InjectionToken('MapboxApiKey'); -export interface SetupOptions { +export interface SetupMap { accessToken?: string; customMapboxApiUrl?: string; mapOptions: MapboxGl.MapboxOptions; mapEvents: MapEvent; } +export interface SetupLayer { + layerOptions: MapboxGl.Layer; + layerEvents: { + click: EventEmitter; + mouseEnter: EventEmitter; + mouseLeave: EventEmitter; + }; +} + export type AllSource = MapboxGl.VectorSource | MapboxGl.RasterSource | MapboxGl.GeoJSONSource | @@ -38,7 +47,7 @@ export class MapService { this.mapLoaded$ = this.mapLoaded.asObservable(); } - setup(options: SetupOptions) { + setup(options: SetupMap) { return this.zone.runOutsideAngular(() => { // Workaround rollup issue this.assign(MapboxGl, 'accessToken', options.accessToken || this.MAPBOX_API_KEY); @@ -148,17 +157,36 @@ export class MapService { }); } - addLayer(layer: MapboxGl.Layer, before?: string) { - return this.zone.runOutsideAngular(() => { - Object.keys(layer) + addLayer(layer: SetupLayer, before?: string) { + this.zone.runOutsideAngular(() => { + Object.keys(layer.layerOptions) .forEach((key: keyof MapboxGl.Layer) => - layer[key] === undefined && delete layer[key]); - this.mapInstance.addLayer(layer, before); + layer.layerOptions[key] === undefined && delete layer.layerOptions[key]); + this.mapInstance.addLayer(layer.layerOptions, before); + }); + this.mapInstance.on('click', layer.layerOptions.id, (evt: MapboxGl.MapMouseEvent) => { + this.zone.run(() => { + layer.layerEvents.click.emit(evt); + }); + }); + this.mapInstance.on('mouseenter', layer.layerOptions.id, (evt: MapboxGl.MapMouseEvent) => { + this.zone.run(() => { + layer.layerEvents.mouseEnter.emit(evt); + }); + }); + this.mapInstance.on('mouseleave', layer.layerOptions.id, (evt: MapboxGl.MapMouseEvent) => { + this.zone.run(() => { + layer.layerEvents.mouseLeave.emit(evt); + }); }); } removeLayer(layerId: string) { return this.zone.runOutsideAngular(() => { + // TEST THIS + this.mapInstance.off('click', layerId); + this.mapInstance.off('mouseenter', layerId); + this.mapInstance.off('mouseleave', layerId); this.mapInstance.removeLayer(layerId); }); }