diff --git a/src/app/data.service.ts b/src/app/data.service.ts index e6aaa902..0c1b751e 100755 --- a/src/app/data.service.ts +++ b/src/app/data.service.ts @@ -7,11 +7,12 @@ import { NgRedux, select } from '@angular-redux/store'; import { HttpClient } from '@angular/common/http'; -import { Directive, EventEmitter, Injectable, Output } from '@angular/core'; +import { EventEmitter, Injectable, Output } from '@angular/core'; import { TranslateService } from '@ngx-translate/core'; import { Feature, FeatureCollection } from 'geojson'; import distance from 'haversine'; import _ from 'lodash'; +import { Observable } from 'rxjs'; import { environment } from '../environments/environment'; import { versions as buildInfo } from '../environments/versions'; import { ADD_APP_ERROR, GET_DIRECTIONS_SUCCESS, PROCESSING_ERRORS_LOADED, SELECT_FOUNTAIN_SUCCESS } from './actions'; @@ -21,11 +22,10 @@ import { essenceOf, getId, getImageUrl, replaceFountain, sanitizeTitle } from '. // Import data from fountain_properties.ts. import { fountain_properties } from './fountain_properties'; // Import data from locations.ts. -import { locations } from './locations'; +import { locationsCollection, LocationsCollection, City, cities, Location } from './locations'; import { FountainSelector, IAppState } from './store'; import { AppError, DataIssue, FilterData, PropertyMetadataCollection } from './types'; -@Directive() @Injectable() export class DataService { apiUrl = buildInfo.branch === 'stable' ? environment.apiUrlStable : environment.apiUrlBeta; @@ -33,16 +33,14 @@ export class DataService { private _fountainsAll: FeatureCollection = null; private _fountainsFiltered: any[] = null; private _filter: FilterData = defaultFilter; - private _city: string = null; - private _propertyMetadataCollection: PropertyMetadataCollection = null; - private _propertyMetadataCollectionPromise: Promise; - private _locationInfo: any = null; - private _locationInfoPromise: Promise; + private _city: City | null = null; + private _propertyMetadataCollection: PropertyMetadataCollection = fountain_properties; + private _locationsCollection = locationsCollection; @select() fountainId; @select() userLocation; - @select() mode; - @select('lang') lang$; - @select('city') city$; + @select() mode: Observable; + @select('lang') lang$: Observable; + @select('city') city$: Observable; @select('travelMode') travelMode$; @Output() fountainSelectedSuccess: EventEmitter> = new EventEmitter>(); @Output() apiError: EventEmitter = new EventEmitter(); @@ -51,96 +49,26 @@ export class DataService { @Output() directionsLoadedSuccess: EventEmitter = new EventEmitter(); @Output() fountainHighlightedEvent: EventEmitter> = new EventEmitter>(); - // Use location from locations.ts. - private _locations = locations; - - // Use fountain_properties from fountain_properties.ts. - private _fountain_properties: any = fountain_properties; - // public observables used by external components get fountainsAll() { return this._fountainsAll; } get propMeta() { - // todo: this souldn't return null if the api request is still pending - return this._propertyMetadataCollection || this._propertyMetadataCollectionPromise; + return this._propertyMetadataCollection; } - get currentLocationInfo() { - // todo: this souldn't return null if the api request is still pending - return this._locationInfo[this._city]; + get currentLocationsCollection(): Location | null { + const city = this._city; + if (city != null) { + return this._locationsCollection[city]; + } else { + return null; + } } constructor(private translate: TranslateService, private http: HttpClient, private ngRedux: NgRedux) { console.log('constuctor start ' + new Date().toISOString()); - // Load metadata - this._locationInfoPromise = new Promise(resolve => { - this._locationInfo = this._locations; - console.log('constuctor location info done ' + new Date().toISOString()); - resolve(this._locations); - /* - // Use location from server (DEPRECATED). - let metadataUrl = `${this.apiUrl}api/v1/metadata/locations`; - this.http.get(metadataUrl) - .subscribe( - (data: any) => { - this._locationInfo = data; - console.log("constuctor location info done "+new Date().toISOString()); - if (null == data) { - console.log("data.service.js: constuctor location null "+new Date().toISOString()); - } else { - if (null == data.gak) { - console.log("data.service.js: constuctor location.gak null "+new Date().toISOString()); - } else { - environment.gak = data.gak; - } - } - resolve(data); - },(httpResponse)=>{ - let err = 'error loading location metadata'; - console.log("constuctor: "+err +" "+new Date().toISOString()); - this.registerApiError(err, '', httpResponse, metadataUrl); - } - ); - */ - }); - - this._propertyMetadataCollectionPromise = new Promise(resolve => { - try { - this._propertyMetadataCollection = this._fountain_properties; - console.log('constuctor fountain properties done ' + new Date().toISOString()); - resolve(this._fountain_properties); - } catch (err: unknown) { - // eslint-disable-next-line no-console - console.trace(err + ' ' + new Date().toISOString()); - } - - /* - // Use fountain_properties from server (DEPRECATED). - let metadataUrl = `${this.apiUrl}api/v1/metadata/fountain_properties`; - console.log(metadataUrl+' '+new Date().toISOString()); - this.http.get(metadataUrl) - .subscribe( - (data: PropertyMetadataCollection) => { - try { - this._propertyMetadataCollection = data; - console.log("constuctor fountain properties done "+new Date().toISOString()); - resolve(data); - } catch (err:unknown) { - console.trace(err+ ' '+new Date().toISOString()); - } - }, httpResponse=>{ - // if in development mode, show a message. - let err = 'error loading fountain properties'; - console.log("constuctor: "+err +" "+new Date().toISOString()); - this.registerApiError(err, '', httpResponse, metadataUrl); - reject(httpResponse); - } - ); - */ - }); - // Subscribe to changes in application state this.userLocation.subscribe(() => { this.sortByProximity(); @@ -170,14 +98,14 @@ export class DataService { return this._fountainsAll.features.length; } - getLocationBounds(city) { + getLocationBounds(city: City) { return new Promise((resolve, reject) => { if (city !== null) { const waiting = () => { - if (this._locationInfo === null) { + if (this._locationsCollection === null) { setTimeout(waiting, 200); } else { - const bbox = this._locationInfo[city].bounding_box; + const bbox = this._locationsCollection[city].bounding_box; resolve([ [bbox.lngMin, bbox.latMin], [bbox.lngMax, bbox.latMax], @@ -225,26 +153,16 @@ export class DataService { // fetch fountain property metadata or return fetchPropertyMetadata() { - if (this._propertyMetadataCollection === null) { - return this._propertyMetadataCollectionPromise; - // if data already loaded, just resolve - } else { - return Promise.resolve(this._propertyMetadataCollection); - } + return Promise.resolve(this._propertyMetadataCollection); } // fetch location metadata - fetchLocationMetadata() { - if (this._locationInfo === null) { - return this._locationInfoPromise; - // if data already loaded, just resolve - } else { - return Promise.resolve(this._locationInfo); - } + fetchLocationMetadata(): Promise<[LocationsCollection, City[]]> { + return Promise.resolve([this._locationsCollection, cities]); } // Get the initial data - loadCityData(city, force_refresh = false) { + loadCityData(city: City | null, force_refresh = false) { if (city !== null) { console.log(city + ' loadCityData ' + new Date().toISOString()); const fountainsUrl = `${this.apiUrl}api/v1/fountains?city=${city}&refresh=${force_refresh}`; diff --git a/src/app/detail/detail.component.ts b/src/app/detail/detail.component.ts index 52d67454..68db778e 100644 --- a/src/app/detail/detail.component.ts +++ b/src/app/detail/detail.component.ts @@ -19,6 +19,8 @@ import { IAppState } from '../store'; import { PropertyMetadata, PropertyMetadataCollection, QuickLink } from '../types'; import { galleryOptions } from './detail.gallery.options'; import * as consts from '../constants'; +import { Observable } from 'rxjs'; +import { City } from '../locations'; const wm_cat_url_root = 'https://commons.wikimedia.org/wiki/Category:'; const maxCaptionPartLgth = consts.maxWikiCiteLgth; // 150; @@ -34,9 +36,9 @@ export class DetailComponent implements OnInit { public isMetadataLoaded = false; public propMeta: PropertyMetadataCollection = null; @select('fountainSelected') fountain$; - @select() mode; - @select() city$; - @select() lang$; + @select() mode: Observable; + @select() city$: Observable; + @select() lang$: Observable; lang = 'de'; @select('userLocation') userLocation$; @Output() closeDetails = new EventEmitter(); @@ -50,8 +52,8 @@ export class DetailComponent implements OnInit { @ViewChild('gallery') galleryElement: NgxGalleryComponent; nearestStations = []; videoUrls: any = []; - issue_api_img_url: ''; - issue_api_url: ''; + issue_api_img_url = ''; + issue_api_url = ''; constas = consts; imageCaptionData: any = { caption: '', @@ -169,10 +171,10 @@ export class DetailComponent implements OnInit { console.log('no videoUrls ' + new Date().toISOString()); } // update issue api - const cityMetadata = this.dataService.currentLocationInfo; + const cityMetadata = this.dataService.currentLocationsCollection; if ( - cityMetadata.issue_api.operator !== null && - cityMetadata.issue_api.operator === fProps.operator_name.value + cityMetadata?.issue_api.operator !== null && + cityMetadata?.issue_api.operator === fProps.operator_name.value ) { console.log('cityMetadata.issue_api.operator !== null ' + new Date().toISOString()); this.issue_api_img_url = cityMetadata.issue_api.thumbnail_url; diff --git a/src/app/issue-list/issue-list.component.ts b/src/app/issue-list/issue-list.component.ts index 319a8b50..8896ef10 100644 --- a/src/app/issue-list/issue-list.component.ts +++ b/src/app/issue-list/issue-list.component.ts @@ -8,6 +8,7 @@ import { select } from '@angular-redux/store'; import { Component } from '@angular/core'; import { Observable } from 'rxjs'; +import { City } from '../locations'; import { AppError, DataIssue } from '../types'; @Component({ @@ -18,7 +19,7 @@ import { AppError, DataIssue } from '../types'; export class IssueListComponent { @select('dataIssues') dataIssues$: Observable; @select('appErrors') appErrors$: Observable; - @select('lang') lang$; - @select('city') city$; + @select('lang') lang$: Observable; + @select('city') city$: Observable; // issue_count:number; } diff --git a/src/app/locations.ts b/src/app/locations.ts index 4a66ce16..c8f64aa2 100644 --- a/src/app/locations.ts +++ b/src/app/locations.ts @@ -10,6 +10,55 @@ */ // import location.json from assets folder. +import { environment } from '../environments/environment'; + +//TODO it would make more sense to just share the typescript constant instead of using json IMO import * as locationsJSON from './../assets/locations.json'; -export const locations = locationsJSON; +// TODO it would make more sense to move common types to an own library which is consumed by both, datablue and proximap +// if you change something here, then you need to change it in datablue as well +export interface Location { + name: string; + description: Translated; + description_more: Translated; + bounding_box: BoundingBox; + operator_fountain_catalog_qid: string; + issue_api: IssueApi; +} + +// TODO it would make more sense to move common types to an own library which is consumed by both, datablue and proximap +// if you change something here, then you need to change it in datablue as well +export interface BoundingBox { + latMin: number; + lngMin: number; + latMax: number; + lngMax: number; +} + +// TODO it would make more sense to move common types to an own library which is consumed by both, datablue and proximap +// if you change something here, then you need to change it in datablue as well +export interface IssueApi { + operator: string | null; + //TODO @ralfhauser, is always null at definition site, do we still use this information somehwere? + qid: null; + thumbnail_url: string; + url_template: string | null; +} + +// TODO it would make more sense to move common types to an own library which is consumed by both, datablue and proximap +// if you change something here, then you need to change it in datablue as well +export interface Translated { + en: T; + de: T; + fr: T; + it: T; + tr: T; +} + +export const locationsCollection = locationsJSON; +export type City = keyof typeof locationsCollection; +export type LocationsCollection = Record; + +export const cities: City[] = Object.keys(locationsCollection).filter( + city => city !== 'default' && (city !== 'test' || !environment.production) +) as City[]; diff --git a/src/app/map/map.component.ts b/src/app/map/map.component.ts index 0220d399..191a63c6 100644 --- a/src/app/map/map.component.ts +++ b/src/app/map/map.component.ts @@ -10,11 +10,12 @@ import { Component, OnInit } from '@angular/core'; import { TranslateService } from '@ngx-translate/core'; import { Feature } from 'geojson'; import * as M from 'mapbox-gl/dist/mapbox-gl.js'; -import { BehaviorSubject } from 'rxjs'; +import { BehaviorSubject, Observable } from 'rxjs'; import { EMPTY_LINESTRING } from '../../assets/defaultData'; import { environment } from '../../environments/environment'; import { SET_USER_LOCATION } from '../actions'; import { DataService } from '../data.service'; +import { City } from '../locations'; import { IAppState } from '../store'; import { DeviceMode } from '../types'; import { MapConfig } from './map.config'; @@ -36,9 +37,9 @@ export class MapComponent implements OnInit { private directionsGeoJson = EMPTY_LINESTRING; private satelliteShown = false; @select() showList; - @select() mode$; - @select() lang$; - @select() city$; + @select() mode$: Observable; + @select() lang$: Observable; + @select() city$: Observable; @select() device$; device: BehaviorSubject = new BehaviorSubject('mobile'); @select() fountainId; @@ -71,7 +72,7 @@ export class MapComponent implements OnInit { } // Zoom to city bounds (only if current map bounds are outside of new city's bounds) - zoomToCity(city: string): void { + zoomToCity(city: City): void { const options = { maxDuration: 500, pitch: 0, diff --git a/src/app/mobile-menu/mobile-menu.component.html b/src/app/mobile-menu/mobile-menu.component.html index 712dc2d3..c369bae7 100644 --- a/src/app/mobile-menu/mobile-menu.component.html +++ b/src/app/mobile-menu/mobile-menu.component.html @@ -22,19 +22,23 @@

intro.legend-title

{{ 'action.more' | translate }} -
+

intro.disclaimer-title

@@ -81,7 +85,7 @@

{{ 'settings.lang' | translate }}

[options]="publicSharedConsts.LANGS" >

{{ 'settings.city' | translate }}

- +

{{ 'settings.city_last_scan' | translate }}: {{ last_scan | date: 'long':'':(lang$ | async) }}. {{ 'settings.city_next_scan_info' | translate }} {{ publicSharedConsts?.CACHE_FOR_HRS_i45db }} diff --git a/src/app/mobile-menu/mobile-menu.component.ts b/src/app/mobile-menu/mobile-menu.component.ts index aaf7bbe8..f67dcd2e 100644 --- a/src/app/mobile-menu/mobile-menu.component.ts +++ b/src/app/mobile-menu/mobile-menu.component.ts @@ -8,9 +8,11 @@ import { NgRedux, select } from '@angular-redux/store'; import { Component, EventEmitter, OnInit, Output } from '@angular/core'; import _ from 'lodash'; +import { Observable } from 'rxjs'; import { versions } from '../../environments/versions'; import { TOGGLE_MENU } from '../actions'; import { DataService } from '../data.service'; +import { City, LocationsCollection } from '../locations'; import { IAppState } from '../store'; import * as sharedConstants from './../../assets/shared-constants.json'; @@ -21,12 +23,12 @@ import * as sharedConstants from './../../assets/shared-constants.json'; }) export class MobileMenuComponent implements OnInit { @select() device$; - @select('lang') lang$; - @select('city') city$; + @select('lang') lang$: Observable; + @select('city') city$: Observable; @Output() menuToggle = new EventEmitter(); publicSharedConsts = sharedConstants; - locationOptions = []; - locationInfo = false; + cities = []; + locationsCollection: LocationsCollection | null = null; showMoreLocationDescription = false; versionInfo = { url: `https://github.com/water-fountains/proximap/commit/${versions.revision}`, @@ -40,16 +42,16 @@ export class MobileMenuComponent implements OnInit { constructor(private dataService: DataService, private ngRedux: NgRedux) {} - toggleMenu(show: any): void { + toggleMenu(show: boolean): void { this.ngRedux.dispatch({ type: TOGGLE_MENU, payload: show }); // this.menuToggle.emit(true); } ngOnInit(): void { - this.dataService.fetchLocationMetadata().then(locationInfo => { + this.dataService.fetchLocationMetadata().then(([locationsCollection, cities]) => { // get location information - this.locationInfo = locationInfo; - this.locationOptions = _.keys(locationInfo); + this.locationsCollection = locationsCollection; + this.cities = cities; }); // watch for fountains to be loaded to obtain last scan time diff --git a/src/app/navbar/navbar.component.html b/src/app/navbar/navbar.component.html index 254b4115..d8ef6f93 100644 --- a/src/app/navbar/navbar.component.html +++ b/src/app/navbar/navbar.component.html @@ -26,7 +26,7 @@ diff --git a/src/app/navbar/navbar.component.ts b/src/app/navbar/navbar.component.ts index 671a1fda..65bd1dbe 100644 --- a/src/app/navbar/navbar.component.ts +++ b/src/app/navbar/navbar.component.ts @@ -29,27 +29,16 @@ export class NavbarComponent implements OnInit { @Output() menuToggle = new EventEmitter(); @select() device$; publicSharedConsts = sharedConstants; - public locationOptions = []; + public cities = []; public last_scan: Date = new Date(); constructor(private dataService: DataService, private ngRedux: NgRedux) {} ngOnInit(): void { - this.dataService.fetchLocationMetadata().then(locationInfo => { - // get location information - const keys = Object.keys(locationInfo); - for (const key of keys) { - //console.log(key); - if ('test' === key) { - if (environment.production) { - console.log('ignoring test ' + new Date().toISOString()); - continue; - } - } - this.locationOptions.push(key); - } + this.dataService.fetchLocationMetadata().then(([_, cities]) => { + this.cities = cities; if (!environment.production) { - console.log(this.locationOptions.length + '/' + keys.length + ' locations added ' + new Date().toISOString()); + console.log(this.cities.length + ' locations added ' + new Date().toISOString()); } }); @@ -60,7 +49,7 @@ export class NavbarComponent implements OnInit { }); } - toggleMenu(show) { + toggleMenu(show: boolean) { console.log('toggleMenu ' + show + ' ' + new Date().toISOString()); this.ngRedux.dispatch({ type: TOGGLE_MENU, payload: show }); // this.menuToggle.emit(true); @@ -71,7 +60,7 @@ export class NavbarComponent implements OnInit { this.ngRedux.dispatch({ type: EDIT_FILTER_TEXT, text: search_text }); } - toggleList(show) { + toggleList(show: boolean) { console.log('toggleList ' + show + ' ' + new Date().toISOString()); this.ngRedux.dispatch({ type: TOGGLE_LIST, payload: show }); } diff --git a/src/app/services/route-validator.service.ts b/src/app/services/route-validator.service.ts index afcd02df..02c942f1 100644 --- a/src/app/services/route-validator.service.ts +++ b/src/app/services/route-validator.service.ts @@ -452,43 +452,46 @@ export class RouteValidatorService { // loop through locations and see if coords are in a city this.dataService .fetchLocationMetadata() - .then(locations => { - const locKeys = Object.keys(locations); - const ll = locKeys.length; - for (let i = 0; i < ll; ++i) { - const key = locKeys[i]; - const b = locations[key].bounding_box; - if (lat > b.latMin && lat < b.latMax && lon > b.lngMin && lon < b.lngMax) { + .then(([locationsCollection, cities]) => { + cities.forEach(city => { + const boundingBox = locationsCollection[city].bounding_box; + if ( + lat > boundingBox.latMin && + lat < boundingBox.latMax && + lon > boundingBox.lngMin && + lon < boundingBox.lngMax + ) { console.log( 'checkCoordinatesInCity: found "' + - key + + city + '" ' + - i + - '/' + - ll + + cities.length + ' - lat ' + - b.latMin + + boundingBox.latMin + ' < ' + lat + ' < ' + - b.latMax + + boundingBox.latMax + ' && lon ' + - b.lngMin + + boundingBox.lngMin + ' < ' + lon + ' < ' + - b.lngMax + + boundingBox.lngMax + ' ' + new Date().toISOString() + ' ' + debug ); //if cities have box overlaps, then only the first one is found - resolve(key); - break; //don't understand why after resolve, it would continue ? + resolve(city); + return; //don't understand why after resolve, it would continue ? } - } - reject(`None of the ${ll} supported locations have those coordinates lat: ${lat}, lon: ${lon} - ` + debug); + }); + reject( + `None of the ${cities.length} supported locations have those coordinates lat: ${lat}, lon: ${lon} - ` + + debug + ); }) .catch(err => { reject( diff --git a/src/app/store.ts b/src/app/store.ts index 717dbff8..a2a767cf 100644 --- a/src/app/store.ts +++ b/src/app/store.ts @@ -33,6 +33,7 @@ import { tassign } from 'tassign'; import { Feature } from 'geojson'; import { AppError, DataIssue, DeviceMode } from './types'; import { _ } from 'lodash'; +import { City } from './locations'; export interface FountainProperty { id?: string; @@ -57,7 +58,7 @@ export interface IAppState { filterText: string; showList: boolean; showMenu: boolean; - city: string; + city: City | null; mode: string; fountainId: string; directions: Object;