import { Injectable } from '@angular/core'; import { GeoItem } from '../geo-item.interface'; import { forkJoin, from, Observable } from 'rxjs'; import { distinctUntilChanged, filter, map, mergeMap, scan, switchMap, take, tap } from 'rxjs/operators'; import { GEO_TARGETING_SELECTED_KEY, GeoSelectedState } from './geo-selected.reducer'; import { GeoRadiusService } from '../geo-radius/geo-radius.service'; import { typeMap } from './geo-selected.constants'; import { Store } from '@ngrx/store'; import { GeoApiService } from '../geo-api/geo-api.service'; import { TranslateService } from '@ngx-translate/core'; import { GeoSelectedActions } from './geo-selected.actions'; import { GeoInfoService } from '../geo-info/geo-info.service'; import { GeoModeService } from '../geo-mode/geo-mode.service'; import { GEO_TARGETING_STATE_KEY, GeoState } from '../geo.reducer'; import { GeoLocationTypeService } from '../geo-location-type/geo-location-type.service'; import { GeoIdService } from '../geo.id'; import { AppState } from '../../../../../../app/reducers/index'; import { TargetingSpec } from '../../../interfaces/targeting-spec.interface'; import { SdkError } from '../../../../../shared/errors/sdkError'; import { City, CustomLocation, Key } from '../../../interfaces/targeting-spec-geo.interface'; @Injectable() export class GeoSelectedService { model$; getModel = (_store): Observable => { return _store.select(GEO_TARGETING_STATE_KEY) .pipe( map((geo) => { const id = this.geoIdService.id$.getValue(); return geo[id]; }), filter((geoState: GeoState) => Boolean(geoState)), map((geoState: GeoState) => { return geoState[GEO_TARGETING_SELECTED_KEY]; }), distinctUntilChanged() ); }; /** * Show info message that some locations were replaced */ informAboutReplaced(items: GeoItem[]) { this.model$ .pipe( take(1), map(({itemsReplaced}) => itemsReplaced), filter((itemsReplaced: GeoItem[]) => Boolean(itemsReplaced.length)), map((itemsReplaced: GeoItem[]) => itemsReplaced.map((replacedItem) => replacedItem.name) .join(', ')) ) .subscribe((fromNames) => { const message = this.translateService.instant(`app-geo-info.MESSAGE`, { fromNames: fromNames, toName: items.map((item) => item.name) .join(', ') }); this.geoInfoService.showInfo({ message, canRevert: true, revertKeys: [GEO_TARGETING_SELECTED_KEY] }); }); } /** * Set suggested radius for passed location * @param item */ setSuggestedRadius = (item: GeoItem): Observable => { return Observable.create((observer) => { switch (item.type) { case 'city': item = GeoRadiusService.setDefaultRadius(item, this.translateService.currentLang); observer.next(item); break; case 'custom_location': case 'place': this.geoApiService.suggestRadius(item) .subscribe((suggestedRadius: null | Array<{ suggested_radius: number, distance_unit: 'mile' | 'kilometer' }>) => { item.radius = suggestedRadius[0].suggested_radius; item.distance_unit = suggestedRadius[0].distance_unit; observer.next(item); }); break; default: observer.next(item); break; } }) .pipe(take(1)); }; /** * Set latitude, longitude or optional polygons for passed item * @returns {GeoItem} * @param items */ setCoordinates = (items: GeoItem[]): Observable => { const simplifiedGeoLocations = {}; items.forEach((item) => { const mappedType = typeMap[item.type]; let key: string | number = item.key; simplifiedGeoLocations[mappedType] = simplifiedGeoLocations[mappedType] || []; if (item.type === 'regions' || item.type === 'cities') { key = Number(item.key); } simplifiedGeoLocations[mappedType].push(key); }); return this.geoApiService.metaData(simplifiedGeoLocations) .pipe( map((metaData) => { let extendedItems: GeoItem[] = []; for (const mappedType in metaData) { if (metaData.hasOwnProperty(mappedType)) { extendedItems = extendedItems.concat(extendedItems, Object.values(metaData[mappedType])); } } return extendedItems; }) ); }; extendItems(items: GeoItem[]) { return this.setCoordinates(items) .pipe( mergeMap((extendedItems: GeoItem[]) => { return forkJoin( extendedItems.map((extendedItem) => this.setSuggestedRadius(extendedItem)) ); }), switchMap((extendedItems: GeoItem[]) => { return this._store.pipe( this.geoModeService.getModel, take(1), map(({selectedMode}) => selectedMode.id === 'exclude'), map((excluded) => extendedItems.map((item) => Object.assign(item, {excluded}))) ); }) ); } addItems(items: GeoItem[]) { this.extendItems(items) .pipe( tap((extendedItems: GeoItem[]) => { this._store.dispatch(this.geoSelectedActions.addItems(extendedItems)); }), switchMap(() => this.model$.pipe(take(1))) ) .subscribe({ next: () => { this.informAboutReplaced(items); }, error: (error) => { if (error instanceof SdkError) { this.geoInfoService.showInfo({level: 'error', message: error.message}); } } }); } setItems(items: GeoItem[]) { this._store.dispatch(this.geoSelectedActions.setItems(items)); } updateItems(items: GeoItem[]) { this._store.dispatch(this.geoSelectedActions.updateItems(items)); } removeItems(items: GeoItem[]) { this._store.dispatch(this.geoSelectedActions.removeItems(items)); } /** * Return final targeting spec with included and excluded locations * @returns {TargetingSpec} */ getSpec() { const initialSpec$: Observable = this._store.pipe( this.geoLocationTypeService.getModel, take(1), map(({selectedType}) => selectedType), filter((selectedType) => Boolean(selectedType)), map((selectedType) => { return { geo_locations: { location_types: selectedType.value }, excluded_geo_locations: {} }; }) ); let selectedItems = []; this.model$ .pipe( take(1), map(({items}) => items) ) .subscribe((items) => { selectedItems = items; }); if (!selectedItems.length) { return initialSpec$; } const items$ = this.model$ .pipe( take(1), switchMap(({items}) => from(items)) ); return initialSpec$ .pipe( switchMap((initialSpec: TargetingSpec) => { return forkJoin( items$.pipe( scan((spec: TargetingSpec, item: GeoItem) => { let locations; // Switch location types depending on item mode if (item.excluded) { locations = spec.excluded_geo_locations; } else { locations = spec.geo_locations; } locations[typeMap[item.type]] = locations[typeMap[item.type]] || []; if (['country', 'country_group'].includes(item.type)) { locations[typeMap[item.type]].push(item.key); } else { const selectedValue: Key = {key: item.key, name: item.name, country_code: item.country_code}; if (item.type === 'city') { (selectedValue).radius = item.radius; (selectedValue).distance_unit = item.distance_unit; } if (['custom_location', 'place'].includes(item.type)) { (selectedValue).radius = item.radius; (selectedValue).distance_unit = item.distance_unit; (selectedValue).latitude = item.latitude; (selectedValue).longitude = item.longitude; (selectedValue).name = item.name; if (item.address_string !== item.name) { (selectedValue).address_string = item.address_string; } } locations[typeMap[item.type]].push(selectedValue); } return spec; }, initialSpec) ) ) .pipe( map(([spec]) => spec) ); }) ); } constructor(private _store: Store, private geoApiService: GeoApiService, private geoLocationTypeService: GeoLocationTypeService, private geoSelectedActions: GeoSelectedActions, private geoModeService: GeoModeService, private geoInfoService: GeoInfoService, private geoIdService: GeoIdService, private translateService: TranslateService) { this.model$ = this._store.pipe(this.getModel); } }