author Christopher Cook
*  copyright import { Utils } from '../../../core/Utils' ; import { MappingAPI , IMapProvider , MapOptions , IMapManager , MapType } from '../interfaces/mapping' ; import { Events } from '../../../services/Events' ; import { Observable } from 'rxjs' ; import { Dictionary } from 'typescript-collections' ; import { GeoPosition , GeoLatLng , GeoBounds } from './../../../model/GeoPosition' ; import { Logging , LogLevel } from './../../Logging' ; import * as mapboxgl from 'mapbox-gl' ; import { environment } from '../../../../environments/environment' ; import { PlaceSearchResult } from '../../../model/AppModels' ; import { Injectable } from '@angular/core' ; import { HttpClient } from '@angular/common/http' ; import { ExtendedPOIDetails } from '../../../model/CoreDataModel' ; module let _this = null ; @ Injectable ( ) export class MapBoxMapProvider implements IMapProvider { mapAPIType : MappingAPI ; mapReady : boolean ; providerError : string ; mapCanvasID : string ; private map : mapboxgl . Map ; private markerList : Dictionary < number , any > ; private polylinePath : any ; private _isMapAnimating : boolean = false ; private searchMarker : mapboxgl . Marker ; public mapTileSet : string ; constructor constructor ( private events , private logging , private http ) { this . events = events ; this . mapAPIType = MappingAPI . MAPBOX ; this . mapReady = false ; this . markerList = new Dictionary < number , any > ( ) ; } initAPI ( ) { if ( mapboxgl ) { ( < any > mapboxgl ) . accessToken = environment . mapBoxToken ; } } private getCurrentMapTileSet ( mapType ) { if ( mapType == 'SATELLITE' ) { return 'mapbox://styles/mapbox/satellite-streets-v11?optimize=true' ; } else { return 'mapbox://styles/mapbox/streets-v11?optimize=true' ; } } flyAroundPoint ( timestamp ) { if ( this . _isMapAnimating ) { this . map . rotateTo ( ( timestamp / 100 ) % 360 , { duration : 0 } ) ; const _context = this ; requestAnimationFrame ( function ( t ) { _context . flyAroundPoint ( t ) ; } ) ; } } param mapcanvasID param mapConfig initMap ( mapCanvasID , mapConfig , parentMapManager ) { this . mapCanvasID = mapCanvasID ; let apiLoaded = true ; if ( typeof mapboxgl === 'undefined' ) { apiLoaded = false ; } else { apiLoaded = true ; } if ( apiLoaded ) { if ( this . map == null ) { let mapCanvas = document . getElementById ( mapCanvasID ) ; this . initAPI ( ) ; this . map = new mapboxgl . Map ( { container : mapCanvasID , style : this . getCurrentMapTileSet ( mapConfig . mapType ) , zoom : 15 , attributionControl : false } ) ; this . map . addControl ( new mapboxgl . AttributionControl ( { compact : true , customAttribution : [ "Open Charge Map Contributors" ] } ) ) ; this . map . addControl ( new mapboxgl . NavigationControl ( ) ) ; this . mapReady = true ; this . map . getCanvas ( ) . focus ( ) ; let deltaDistance = 100 ; let deltaDegrees = 25 ; function easing ( t ) { return t * ( 2 - t ) ; } this . map . getCanvas ( ) . addEventListener ( 'keydown' , ( e ) => { e . preventDefault ( ) ; if ( e . which === 38 ) { this . map . panBy ( [ 0 , - deltaDistance ] , { easing : easing } ) ; } else if ( e . which === 40 ) { this . map . panBy ( [ 0 , deltaDistance ] , { easing : easing } ) ; } else if ( e . which === 37 ) { this . map . easeTo ( { bearing : this . map . getBearing ( ) - deltaDegrees , easing : easing } ) ; } else if ( e . which === 39 ) { this . map . easeTo ( { bearing : this . map . getBearing ( ) + deltaDegrees , easing : easing } ) ; } else if ( e . which === 32 ) { if ( ! this . _isMapAnimating ) { this . _isMapAnimating = true ; this . flyAroundPoint ( 0 ) ; } else { this . _isMapAnimating = false ; } } } , true ) ; this . map . on ( 'load' , ( ) => { this . events . publish ( 'ocm:mapping:ready' ) ; } ) ; let updateSearchMarker = Utils . debounce ( ( ) => { this . getMapCenter ( ) . subscribe ( pos => { this . searchMarker . setLngLat ( new mapboxgl . LngLat ( pos . coords . longitude , pos . coords . latitude ) ) ; } ) ; } , 500 , false ) ; this . map . on ( 'move' , ( ) => { if ( this . searchMarker ) { updateSearchMarker ( ) ; } } ) ; this . map . on ( 'moveend' , ( ) => { this . events . publish ( 'ocm:mapping:dragend' ) ; if ( mapConfig . onMapMoveCompleted ) { mapConfig . onMapMoveCompleted ( ) ; } } ) ; this . map . on ( 'zoomend' , ( ) => { this . events . publish ( 'ocm:mapping:zoom' ) ; } ) ; } } else { this . logging . log ( "Call to initMap before API is ready:" + MappingAPI [ this . mapAPIType ] , LogLevel . ERROR ) ; this . mapReady = false ; return false ; } } clearMarkers ( ) { if ( this . markerList != null ) { this . markerList . forEach ( ( key , marker ) => { try { marker . remove ( ) ; } catch { } } ) ; } this . markerList = new Dictionary < number , any > ( ) ; } param poiList param parentContext showPOIListOnMap ( poiList , parentContext , isNativePOI = true ) { let clearMarkersOnRefresh = false ; let map = this . map ; let bounds = new mapboxgl . LngLatBounds ( ) ; let markersAdded = 0 ; let mapProviderContext = this ; if ( clearMarkersOnRefresh ) { this . clearMarkers ( ) ; } let mapzoom = map . getZoom ( ) ; if ( poiList != null ) { let poiCount = poiList . length ; for ( let i = 0 ; i < poiList . length ; i ++ ) { if ( poiList [ i ] . AddressInfo != null && poiList [ i ] . AddressInfo . Latitude != null && poiList [ i ] . AddressInfo . Longitude != null ) { let poi = poiList [ i ] ; let addMarker = true ; if ( ! clearMarkersOnRefresh && this . markerList != null ) { if ( this . markerList . containsKey ( poi . ID ) ) { addMarker = false ; let existingMarker = this . markerList . getValue ( poi . ID ) ; if ( existingMarker && existingMarker . poi ) { if ( ( < ExtendedPOIDetails > existingMarker . poi ) . DateLastStatusUpdate != poi . DateLastStatusUpdate ) { existingMarker . remove ( ) ; addMarker = true ; } } } } if ( addMarker ) { let icon = null ; let color = null ; if ( ! isNativePOI ) { icon = null ; color = Utils . getColorForPOI ( poi ) ; } else { icon = document . createElement ( "img" ) ; icon . src = Utils . getIconForPOI ( poi ) ; icon . width = 34 ; icon . height = 50 ; } let markerOptions = { element : icon , color : color } ; let markerTooltip = ( isNativePOI ? "OCM-" : "EXT-" ) + poi . ID + ": " + poi . AddressInfo . Title + ":" ; if ( poi . UsageType != null ) { markerTooltip += " " + poi . UsageType . Title ; } if ( poi . StatusType != null ) { markerTooltip += " " + poi . StatusType . Title ; } let newMarker = new mapboxgl . Marker ( markerOptions ) . setLngLat ( [ poi . AddressInfo . Longitude , poi . AddressInfo . Latitude ] ) . addTo ( map ) ; ( < any > newMarker ) . poi = poi ; let markerElement = newMarker . getElement ( ) ; ( < any > markerElement ) . poi = poi ; markerElement . addEventListener ( 'click' , ( el ) => { const clickedPOI = ( < any > el . currentTarget ) . poi ; this . events . publish ( 'ocm:poi:selected' , { poiId : clickedPOI . ID , poi : clickedPOI } ) ; } ) ; bounds . extend ( newMarker . getLngLat ( ) ) ; this . markerList . setValue ( poi . ID , newMarker ) ; markersAdded ++ ; } } } this . logging . log ( markersAdded + " new map markers added out of a total " + this . markerList . size ( ) ) ; } let uiContext = this ; if ( poiList != null && poiList . length > 0 ) { if ( parentContext != null && ! parentContext . appConfig . enableLiveMapQuerying ) { this . logging . log ( "Fitting to marker bounds:" + bounds ) ; map . setCenter ( bounds . getCenter ( ) ) ; this . logging . log ( "zoom before fit bounds:" + map . getZoom ( ) ) ; map . fitBounds ( bounds ) ; let zoom = map . getZoom ( ) ; map . setZoom ( zoom < 6 ? 6 : zoom ) ; } else { if ( map . getCenter ( ) == undefined ) { map . setCenter ( bounds . getCenter ( ) ) ; } } } } refreshMapLayout ( ) { if ( this . map != null ) { setTimeout ( ( ) => { this . logging . log ( "MapBoxGL: refreshMapLayout" , LogLevel . VERBOSE ) ; this . map . resize ( ) ; } , 200 ) ; } } setMapCenter ( pos ) { if ( this . mapReady ) { this . map . setCenter ( new mapboxgl . LngLat ( pos . coords . longitude , pos . coords . latitude ) ) ; if ( ! this . searchMarker ) { this . searchMarker = new mapboxgl . Marker ( { color : '#99ccff' , anchor : 'top' } ) . setLngLat ( new mapboxgl . LngLat ( pos . coords . longitude , pos . coords . latitude ) ) . addTo ( this . map ) ; this . searchMarker . getElement ( ) . addEventListener ( 'click' , ( ) => { let searchPos = this . searchMarker . getLngLat ( ) ; this . events . publish ( 'ocm:mapping:addpoi' , new GeoLatLng ( searchPos . lat , searchPos . lng ) ) ; } ) ; } } } getMapCenter ( ) { let obs = new Observable < GeoPosition > ( observer => { if ( this . map != null ) { let pos = this . map . getCenter ( ) ; if ( pos != null ) { observer . next ( new GeoPosition ( pos . lat , pos . lng ) ) ; observer . complete ( ) ; } } } ) ; return obs ; } setMapZoom ( zoomLevel ) { this . map . setZoom ( zoomLevel ) ; } getMapZoom ( ) { let obs = new Observable < number > ( observer => { let zoom = this . map . getZoom ( ) ; observer . next ( zoom ) ; observer . complete ( ) ; } ) ; return obs ; } setMapType ( mapType ) { this . map . setStyle ( this . getCurrentMapTileSet ( mapType ) ) ; } getMapBounds ( ) { let obs = new Observable < Array < GeoLatLng > > ( observer => { let bounds = new Array < GeoLatLng > ( ) ; let mapBounds = this . map . getBounds ( ) ; bounds . push ( new GeoLatLng ( mapBounds . getSouthWest ( ) . lat , mapBounds . getSouthWest ( ) . lng ) ) ; bounds . push ( new GeoLatLng ( mapBounds . getNorthEast ( ) . lat , mapBounds . getNorthEast ( ) . lng ) ) ; observer . next ( bounds ) ; observer . complete ( ) ; } ) ; return obs ; } moveToMapBounds ( bounds ) { this . map . fitBounds ( new mapboxgl . LngLatBounds ( new mapboxgl . LngLat ( bounds . southWest . longitude , bounds . southWest . latitude ) , new mapboxgl . LngLat ( bounds . northEast . longitude , bounds . northEast . latitude ) ) ) ; } renderMap ( poiList , mapHeight , parentContext ) { document . getElementById ( this . mapCanvasID ) . style . height = mapHeight + "px" ; if ( this . mapReady ) { this . showPOIListOnMap ( poiList , parentContext ) ; } return true ; } renderPolyline ( polyline ) { this . clearPolyline ( ) ; } clearPolyline ( ) { if ( this . polylinePath != null ) { this . polylinePath . setMap ( null ) ; } } focusMap ( ) { } unfocusMap ( ) { } async placeSearch ( keyword , latitude ? , longitude ? ) { let api = `https://api.mapbox.com/geocoding/v5/mapbox.places/${ keyword ? keyword : longitude + ',' + latitude }.json?access_token=${ environment . mapBoxToken }` ; return new Promise < Array < PlaceSearchResult > > ( async ( resolve , reject ) => { this . http . get ( api ) . toPromise ( ) . then ( ( results ) => { let placeList = [ ] ; placeList = [ ] ; if ( results . features ) { results . features . map ( ( feature ) => { let placeResult = new PlaceSearchResult ( ) ; placeResult . Title = feature . place_name ; placeResult . Address = feature . place_name ; placeResult . Type = "place" ; placeResult . Location = new GeoLatLng ( feature . center [ 1 ] , feature . center [ 0 ] ) ; placeResult . Attribution = results . attribution ; placeList . push ( placeResult ) ; } ) ; } resolve ( placeList ) ; } ) ; } ) ; } addPOILayer ( data ) { this . logging . log ( "Add POI Layer not implemented in this provider." ) ; this . showPOIListOnMap ( data , null , false ) ; } }