diff --git a/src/app/actions.ts b/src/app/actions.ts index fdeacd0d..d2176aab 100644 --- a/src/app/actions.ts +++ b/src/app/actions.ts @@ -21,7 +21,6 @@ export const CLOSE_SIDEBARS = 'CLOSE_SIDEBARS'; export const NAVIGATE_TO_FOUNTAIN = 'NAVIGATE_TO_FOUNTAIN'; export const UPDATE_FILTER_CATEGORIES = 'UPDATE_FILTER_CATEGORIES'; export const CLOSE_NAVIGATION = 'CLOSE_NAVIGATION'; -export const CHANGE_LANG = 'CHANGE_LANG'; export const CHANGE_CITY = 'CHANGE_CITY'; export const CHANGE_MODE = 'CHANGE_MODE'; export const CHANGE_TRAVEL_MODE = 'CHANGE_TRAVEL_MODE'; diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 0d19e5ee..f3af8a03 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -19,7 +19,7 @@ import { DialogConfig, hideIntroVar } from './constants'; import { IssueListComponent } from './issue-list/issue-list.component'; import { finalize } from 'rxjs/operators'; import { IntroWindowComponent } from './intro-window/intro-window.component'; -import { TranslateService } from '@ngx-translate/core'; +import { LanguageService } from './core/language.service'; @Component({ selector: 'app-root', @@ -29,7 +29,6 @@ import { TranslateService } from '@ngx-translate/core'; export class AppComponent implements OnInit { title = 'app'; @select() mode; - @select() lang; @select() showList; @select() showMenu; @select() previewState; @@ -51,7 +50,7 @@ export class AppComponent implements OnInit { public media: MediaMatcher, private dialog: MatDialog, private ngRedux: NgRedux, - private translate: TranslateService, + private languageService: LanguageService, private iconRegistry: MatIconRegistry, private sanitizer: DomSanitizer ) { @@ -89,11 +88,7 @@ export class AppComponent implements OnInit { ngOnInit(): void { this.broadcastMediaChange(); - // MultiLanguages functionality default is en (English) - this.translate.use(this.ngRedux.getState().lang); - this.lang.subscribe(s => { - this.translate.use(s); - }); + this.languageService.init(); this.showList.subscribe(show => { if (this.listDrawer) { diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 3f37e73f..5de31fe6 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -7,43 +7,18 @@ import { BrowserModule } from '@angular/platform-browser'; import { NgModule } from '@angular/core'; -import { NgRedux, NgReduxModule, DevToolsExtension } from '@angular-redux/store'; +import { NgRedux, DevToolsExtension, NgReduxModule } from '@angular-redux/store'; import { AppComponent } from './app.component'; -import { NavbarComponent } from './navbar/navbar.component'; import { MapComponent } from './map/map.component'; import { ListComponent } from './list/list.component'; import { IAppState, INITIAL_STATE, rootReducer } from './store'; -import { HttpClientModule, HttpClient } from '@angular/common/http'; +import { HttpClient, HttpClientModule } from '@angular/common/http'; import { DataService } from './data.service'; import { MapConfig } from './map/map.config'; import { FormsModule } from '@angular/forms'; import { DetailComponent } from './detail/detail.component'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; -import { NgProgressModule } from '@ngx-progressbar/core'; -import { NgProgressHttpModule } from '@ngx-progressbar/http'; -import { MatBadgeModule } from '@angular/material/badge'; -import { MatBottomSheetModule } from '@angular/material/bottom-sheet'; -import { MatButtonModule } from '@angular/material/button'; -import { MatCardModule } from '@angular/material/card'; -import { MatCheckboxModule } from '@angular/material/checkbox'; -import { MatSnackBarModule } from '@angular/material/snack-bar'; -import { MatDialogModule } from '@angular/material/dialog'; -import { MatDividerModule } from '@angular/material/divider'; -import { MatExpansionModule } from '@angular/material/expansion'; -import { MatFormFieldModule } from '@angular/material/form-field'; -import { MatIconModule } from '@angular/material/icon'; -import { MatInputModule } from '@angular/material/input'; -import { MatListModule } from '@angular/material/list'; -import { MatMenuModule } from '@angular/material/menu'; -import { MatRadioModule } from '@angular/material/radio'; -import { MatSelectModule } from '@angular/material/select'; -import { MatSidenavModule } from '@angular/material/sidenav'; -import { MatSliderModule } from '@angular/material/slider'; -import { MatTableModule } from '@angular/material/table'; -import { MatTabsModule } from '@angular/material/tabs'; -import { MatToolbarModule } from '@angular/material/toolbar'; -import { MatTooltipModule } from '@angular/material/tooltip'; import { MediaMatcher } from '@angular/cdk/layout'; import { MobileMenuComponent } from './mobile-menu/mobile-menu.component'; import { FilterComponent } from './filter/filter.component'; @@ -60,9 +35,6 @@ import { FountainPropertyBadgeComponent } from './fountain-property-badge/founta import { FountainPropertyDialogComponent } from './fountain-property-dialog/fountain-property-dialog.component'; import { TruncatePipe } from './pipes/truncate'; import { MinuteSecondsPipe } from './pipes/minute.seconds'; -// Imports for Multilingual Integration -import { TranslateModule, TranslateLoader } from '@ngx-translate/core'; -import { TranslateHttpLoader } from '@ngx-translate/http-loader'; import { StateSelectorComponent } from './state-selector/state-selector.component'; import { RouterModule } from '@angular/router'; import { RouterComponent } from './router/router.component'; @@ -71,15 +43,17 @@ import { NgxGalleryModule } from '@kolkov/ngx-gallery'; import { IssueIndicatorComponent } from './issue-indicator/issue-indicator.component'; import { IssueListComponent } from './issue-list/issue-list.component'; -// Locales -import { registerLocaleData } from '@angular/common'; -import localeFr from '@angular/common/locales/fr'; -import localeDe from '@angular/common/locales/de'; -import localeIt from '@angular/common/locales/it'; -import localeTr from '@angular/common/locales/tr'; import { IntroWindowComponent } from './intro-window/intro-window.component'; import { LegendComponent } from './legend/legend.component'; import { EscapeHtmlPipe } from './pipes/keep-html.pipe'; +import { GoogleMaterialModule } from './core/google-material.module'; +import { NgProgressHttpModule } from '@ngx-progressbar/http'; +import { NgProgressModule } from '@ngx-progressbar/core'; +import { TranslateLoader, TranslateModule, TranslateService } from '@ngx-translate/core'; +import { NavbarComponent } from './navbar/navbar.component'; +import { LanguageSelectorComponent } from './core/language-selector.component'; +import { LanguageService } from './core/language.service'; +import { TranslateHttpLoader } from '@ngx-translate/http-loader'; @NgModule({ declarations: [ @@ -109,6 +83,7 @@ import { EscapeHtmlPipe } from './pipes/keep-html.pipe'; CallToActionComponent, IntroWindowComponent, LegendComponent, + LanguageSelectorComponent, ], entryComponents: [ GuideSelectorComponent, @@ -121,50 +96,26 @@ import { EscapeHtmlPipe } from './pipes/keep-html.pipe'; IntroWindowComponent, ], imports: [ + GoogleMaterialModule, BrowserAnimationsModule, BrowserModule, FormsModule, NgxGalleryModule, - HttpClientModule, - MatBadgeModule, - MatBottomSheetModule, - MatButtonModule, - MatCardModule, - MatCheckboxModule, - MatDialogModule, - MatSnackBarModule, - MatDividerModule, - MatExpansionModule, - MatFormFieldModule, - MatIconModule, - MatInputModule, - MatListModule, - MatMenuModule, - MatRadioModule, - MatSelectModule, - MatSidenavModule, - MatSliderModule, - MatTableModule, - MatTabsModule, - MatToolbarModule, - MatTooltipModule, NgProgressModule.forRoot(), NgProgressHttpModule.forRoot(), NgReduxModule, - RouterModule.forRoot( - [ - { - path: ':city', - component: RouterComponent, - }, - { - path: '', - redirectTo: '/ch-zh', - pathMatch: 'full', - }, - ], - { useHash: false, relativeLinkResolution: 'legacy' } - ), + HttpClientModule, + RouterModule.forRoot([ + { + path: ':city', + component: RouterComponent, + }, + { + path: '', + redirectTo: '/ch-zh', + pathMatch: 'full', + }, + ]), TranslateModule.forRoot({ loader: { provide: TranslateLoader, @@ -173,8 +124,8 @@ import { EscapeHtmlPipe } from './pipes/keep-html.pipe'; }, }), ], - exports: [TranslateModule, RouterModule], - providers: [DataService, ListComponent, MapConfig, MediaMatcher], + exports: [RouterModule, TranslateModule, NavbarComponent, LanguageSelectorComponent], + providers: [TranslateService, LanguageService, DataService, ListComponent, MapConfig, MediaMatcher], bootstrap: [AppComponent], }) // eslint-disable-next-line @typescript-eslint/no-extraneous-class @@ -183,25 +134,11 @@ export class AppModule { // When DevTools is active, the page shows up blank on browsers other than chrome const enhancers = devTools.isEnabled() ? [devTools.enhancer()] : []; ngRedux.configureStore(rootReducer, INITIAL_STATE, [], enhancers); - - // hide address bar after load - // window.addEventListener("load",function() { - // setTimeout(function(){ - // // This hides the address bar: - // window.scrollTo(0, 1); - // }, 0); - // }); } } // Multilingual HttpLoader // AoT requires an exported function for factories -export function HttpLoaderFactory(http: HttpClient) { +function HttpLoaderFactory(http: HttpClient) { return new TranslateHttpLoader(http); } - -// Register locales -registerLocaleData(localeFr, 'fr'); -registerLocaleData(localeDe, 'de'); -registerLocaleData(localeIt, 'it'); -registerLocaleData(localeTr, 'tr'); diff --git a/src/app/core/google-material.module.ts b/src/app/core/google-material.module.ts new file mode 100644 index 00000000..6dd76909 --- /dev/null +++ b/src/app/core/google-material.module.ts @@ -0,0 +1,77 @@ +import { NgModule } from '@angular/core'; +import { MatBadgeModule } from '@angular/material/badge'; +import { MatBottomSheetModule } from '@angular/material/bottom-sheet'; +import { MatButtonModule } from '@angular/material/button'; +import { MatCardModule } from '@angular/material/card'; +import { MatCheckboxModule } from '@angular/material/checkbox'; +import { MatDialogModule } from '@angular/material/dialog'; +import { MatDividerModule } from '@angular/material/divider'; +import { MatExpansionModule } from '@angular/material/expansion'; +import { MatIconModule } from '@angular/material/icon'; +import { MatInputModule } from '@angular/material/input'; +import { MatListModule } from '@angular/material/list'; +import { MatMenuModule } from '@angular/material/menu'; +import { MatRadioModule } from '@angular/material/radio'; +import { MatSelectModule } from '@angular/material/select'; +import { MatSidenavModule } from '@angular/material/sidenav'; +import { MatSliderModule } from '@angular/material/slider'; +import { MatSnackBarModule } from '@angular/material/snack-bar'; +import { MatTableModule } from '@angular/material/table'; +import { MatTabsModule } from '@angular/material/tabs'; +import { MatToolbarModule } from '@angular/material/toolbar'; +import { MatTooltipModule } from '@angular/material/tooltip'; +import { MatFormFieldModule, MAT_FORM_FIELD_DEFAULT_OPTIONS } from '@angular/material/form-field'; + +@NgModule({ + imports: [ + MatBadgeModule, + MatBottomSheetModule, + MatButtonModule, + MatCardModule, + MatCheckboxModule, + MatDialogModule, + MatSnackBarModule, + MatDividerModule, + MatExpansionModule, + MatFormFieldModule, + MatIconModule, + MatInputModule, + MatListModule, + MatMenuModule, + MatRadioModule, + MatSelectModule, + MatSidenavModule, + MatSliderModule, + MatTableModule, + MatTabsModule, + MatToolbarModule, + MatTooltipModule, + ], + exports: [ + MatBadgeModule, + MatBottomSheetModule, + MatButtonModule, + MatCardModule, + MatCheckboxModule, + MatDialogModule, + MatSnackBarModule, + MatDividerModule, + MatExpansionModule, + MatFormFieldModule, + MatIconModule, + MatInputModule, + MatListModule, + MatMenuModule, + MatRadioModule, + MatSelectModule, + MatSidenavModule, + MatSliderModule, + MatTableModule, + MatTabsModule, + MatToolbarModule, + MatTooltipModule, + ], + providers: [{ provide: MAT_FORM_FIELD_DEFAULT_OPTIONS, useValue: { appearance: 'outline' } }], +}) +// eslint-disable-next-line @typescript-eslint/no-extraneous-class +export class GoogleMaterialModule {} diff --git a/src/app/core/language-selector.component.css b/src/app/core/language-selector.component.css new file mode 100644 index 00000000..e69de29b diff --git a/src/app/core/language-selector.component.html b/src/app/core/language-selector.component.html new file mode 100644 index 00000000..3dd24789 --- /dev/null +++ b/src/app/core/language-selector.component.html @@ -0,0 +1,5 @@ + + + {{ 'lang.' + l | translate }} + + diff --git a/src/app/core/language-selector.component.ts b/src/app/core/language-selector.component.ts new file mode 100644 index 00000000..fb4e1e06 --- /dev/null +++ b/src/app/core/language-selector.component.ts @@ -0,0 +1,23 @@ +import { NgRedux } from '@angular-redux/store'; +import { Component } from '@angular/core'; +import { TOGGLE_MENU } from '../actions'; +import { IAppState } from '../store'; +import { LanguageService } from './language.service'; + +@Component({ + selector: 'app-lang-select', + templateUrl: './language-selector.component.html', + styleUrls: ['./language-selector.component.css'], +}) +export class LanguageSelectorComponent { + constructor(private languageService: LanguageService, private ngRedux: NgRedux) {} + + public languages = this.languageService.languages; + public langObservable = this.languageService.langObservable; + + changeLocale(event: { value: string }) { + this.languageService.changeLang(event.value); + // TODO replace by a service call + this.ngRedux.dispatch({ type: TOGGLE_MENU, payload: false }); + } +} diff --git a/src/app/core/language.service.ts b/src/app/core/language.service.ts new file mode 100644 index 00000000..50cf920a --- /dev/null +++ b/src/app/core/language.service.ts @@ -0,0 +1,93 @@ +import { Injectable } from '@angular/core'; +import { TranslateService } from '@ngx-translate/core'; +import { registerLocaleData } from '@angular/common'; +import localeFr from '@angular/common/locales/fr'; +import localeDe from '@angular/common/locales/de'; +import localeIt from '@angular/common/locales/it'; +import localeTr from '@angular/common/locales/tr'; +import { BehaviorSubject, Observable } from 'rxjs'; + +const defaultLang = 'de'; +// the given order of the language codes here +// determines the order in the language dropdown +const languageConfig = [ + { + code: 'en', + aliases: ['english', 'anglais', 'englisch', 'en', 'e'], + }, + { + code: 'de', + aliases: ['german', 'allemand', 'deutsch', 'de', 'd'], + }, + { + code: 'fr', + aliases: ['french', 'französisch', 'franzoesisch', 'francais', 'fr', 'f'], + }, + { + code: 'it', + aliases: ['italian', 'italiano', 'italien', 'italienisch', 'it', 'i'], + }, + { + code: 'tr', + aliases: ['turc', 'türkisch', 'turco', 'turkish', 'tr', 't'], + }, + { + code: 'sr', + aliases: ['srpski', 'serbian', 'serbian', 'sr', 'srb'], + }, +]; + +@Injectable() +export class LanguageService { + languages: string[] = languageConfig.map(l => l.code); + + private langSubject = new BehaviorSubject(defaultLang); + + constructor(private translateService: TranslateService) {} + + init(): void { + // Register locales + //TODO #394 @ralf.hauser, misses sr, on purpose? + registerLocaleData(localeDe, 'de'); + registerLocaleData(localeFr, 'fr'); + registerLocaleData(localeIt, 'it'); + registerLocaleData(localeTr, 'tr'); + + this.translateService.addLangs(this.languages); + this.translateService.setDefaultLang(defaultLang); + const currentLang = this.determineCurrentLang(); + this.translateService.use(currentLang); + } + + changeLang(newLang: string): void { + const lang = this.parseLang(newLang); + if (lang === undefined) { + throw new Error('given language is not supported: ' + newLang); + } + if (this.langSubject.value !== lang) { + this.translateService.use(lang); + this.langSubject.next(lang); + //TODO #359 store choice in localStorage + } + } + + determineCurrentLang(): string { + //TODO #395 and #369 use preferred language according to last chosen language and infer from browser on first visit + return defaultLang; + } + + private parseLang(lang: string | undefined): string | undefined { + if (lang === undefined || lang === null) return undefined; + + const langLower = lang.toLowerCase(); + return languageConfig.find(x => x.aliases.includes(langLower))?.code; + } + + get currentLang(): string { + return this.langSubject.value; + } + + get langObservable(): Observable { + return this.langSubject.asObservable(); + } +} diff --git a/src/app/core/subscription.service.ts b/src/app/core/subscription.service.ts new file mode 100644 index 00000000..d64c10c9 --- /dev/null +++ b/src/app/core/subscription.service.ts @@ -0,0 +1,35 @@ +import { Injectable, OnDestroy } from '@angular/core'; +import { Subject, Subscription } from 'rxjs'; +import { environment } from '../../environments/environment'; + +/** + * @author Tegonal GmbH + * @license AGPL + */ +@Injectable() +export class SubscriptionService implements OnDestroy { + private subscriptions = Array(); + private subjects = Array>(); + + public registerSubscriptions(...subscription: Subscription[]): void { + this.subscriptions.push(...subscription); + } + + public registerSubjects(...subjects: Subject[]): void { + this.subjects.push(...subjects); + } + + ngOnDestroy(): void { + this.cleanupSubscriptions(); + } + + public cleanupSubscriptions(): void { + if (!environment.production) { + console.log('cleanup ' + this.subscriptions.length + ' subscriptions and ' + this.subjects.length + ' subjects'); + } + this.subscriptions.forEach(subscripton => subscripton.unsubscribe()); + this.subscriptions = []; + this.subjects.forEach(subject => subject.complete()); + this.subjects = []; + } +} diff --git a/src/app/data.service.ts b/src/app/data.service.ts index 0c1b751e..cb9a57a7 100755 --- a/src/app/data.service.ts +++ b/src/app/data.service.ts @@ -18,6 +18,7 @@ import { versions as buildInfo } from '../environments/versions'; import { ADD_APP_ERROR, GET_DIRECTIONS_SUCCESS, PROCESSING_ERRORS_LOADED, SELECT_FOUNTAIN_SUCCESS } from './actions'; import { aliases } from './aliases'; import { defaultFilter, extImgPlaceholderI333pm, propertyStatuses } from './constants'; +import { LanguageService } from './core/language.service'; import { essenceOf, getId, getImageUrl, replaceFountain, sanitizeTitle } from './database.service'; // Import data from fountain_properties.ts. import { fountain_properties } from './fountain_properties'; @@ -39,7 +40,6 @@ export class DataService { @select() fountainId; @select() userLocation; @select() mode: Observable; - @select('lang') lang$: Observable; @select('city') city$: Observable; @select('travelMode') travelMode$; @Output() fountainSelectedSuccess: EventEmitter> = new EventEmitter>(); @@ -66,7 +66,12 @@ export class DataService { return null; } } - constructor(private translate: TranslateService, private http: HttpClient, private ngRedux: NgRedux) { + constructor( + private translateService: TranslateService, + private languageService: LanguageService, + private http: HttpClient, + private ngRedux: NgRedux + ) { console.log('constuctor start ' + new Date().toISOString()); // Subscribe to changes in application state @@ -79,7 +84,7 @@ export class DataService { this.getDirections(); } }); - this.lang$.subscribe(() => { + this.languageService.langObservable.subscribe(() => { if (this.ngRedux.getState().mode === 'directions') { this.getDirections(); } @@ -1088,10 +1093,10 @@ export class DataService { const s = this.ngRedux.getState(); if (s.fountainSelected !== null) { if (s.userLocation === null) { - this.translate.get('action.navigate_tooltip').subscribe(alert); + this.translateService.get('action.navigate_tooltip').subscribe(alert); return; } - const url = `https://api.mapbox.com/directions/v5/mapbox/${s.travelMode}/${s.userLocation[0]},${s.userLocation[1]};${s.fountainSelected.geometry.coordinates[0]},${s.fountainSelected.geometry.coordinates[1]}?access_token=${environment.mapboxApiKey}&geometries=geojson&steps=true&language=${s.lang}`; + const url = `https://api.mapbox.com/directions/v5/mapbox/${s.travelMode}/${s.userLocation[0]},${s.userLocation[1]};${s.fountainSelected.geometry.coordinates[0]},${s.fountainSelected.geometry.coordinates[1]}?access_token=${environment.mapboxApiKey}&geometries=geojson&steps=true&language=${this.languageService.currentLang}`; this.http.get(url).subscribe((data: FeatureCollection) => { this.ngRedux.dispatch({ type: GET_DIRECTIONS_SUCCESS, payload: data }); diff --git a/src/app/detail/detail.component.html b/src/app/detail/detail.component.html index 0117c741..b4c67293 100644 --- a/src/app/detail/detail.component.html +++ b/src/app/detail/detail.component.html @@ -4,385 +4,395 @@ * Use of this code is governed by the GNU Affero General Public License (https://www.gnu.org/licenses/agpl-3.0) * and the profit contribution agreement available at https://www.my-d.org/ProfitContributionAgreement --> -
- - - - {{ fountain.properties['name_' + lang].value }} - - {{ 'other.unnamed_fountain' | translate }} - - - - - - {{ fountain.properties['description_short_' + lang].value }}
-
- - - - - - - -
-
- - - + + +
+ + + + {{ fountain.properties['name_' + lang].value }} + + {{ 'other.unnamed_fountain' | translate }} + + + + + + {{ fountain.properties['description_short_' + lang].value }}
+
+ + + + + + + +
+
- - - {{ 'gallery.watermark' | translate }} - - + -
-
- -
+ - {{ 'gallery.call_to_action' | translate }} -
+
+
-
-
- - - - - - - open_in_new - - - - - {{ - fountain.properties['wikipedia_' + lang + '_url'].derived.summary | truncate: constas?.maxWikiCiteLgth - }} - open_in_new - - - - - person_outline - {{ fountain.properties.artist_name.derived.name }} - - - + + - open_in_new - + + + {{ + fountain.properties['wikipedia_' + lang + '_url'].derived.summary | truncate: constas?.maxWikiCiteLgth + }} + open_in_new - edit - - - + - - location_on - {{ fountain.properties.directions.value }} - + + + person_outline + {{ fountain.properties.artist_name.derived.name }} - - - location_city - {{ fountain.properties.operator_name.derived.name }} - - - - - open_in_new - - - edit - - - + + + + + open_in_new + + + edit + + + - - - + + location_on + {{ fountain.properties.directions.value }} + + + + + location_city + {{ fountain.properties.operator_name.derived.name }} + + + + + open_in_new + + + edit + + + + + + + + {{ pano.source_name }}  + open_in_new + + + + + edit + + + + + + + link {{ pano.source_name }}  - open_in_new - - - + + + + + + + directions_transit action.findNearestStations + + - edit + {{ station.name }} ({{ station.distance }} m) + open_in_new - - + + - - - link - {{ url.split('//')[1] | truncate: 20 }} - - + + + + video_library detail.youtube_videos + + + + + + - - - - directions_transit action.findNearestStations - - - - {{ station.name }} ({{ station.distance }} m) - open_in_new - - - - - - - - video_library detail.youtube_videos - - - - - - - - - - list {{ - ('detail.show_all_properties' | translate) + ' (' + propertyCount + ')' - }} - - - {{ ('detail.only_with_values' | translate) + ' (' + filteredPropertyCount + ')' }} - - - - - - - - - -
- {{ propMeta[p.id].name[lang] }} -
-

- {{ 'property.merge_notes' | translate }}: {{ fountain.properties.conflation_info.merge_notes }}
- {{ 'property.merge_distance' | translate }}: - {{ fountain.properties.conflation_info.merge_distance | number: '1.2' }} m
- - {{ 'property.merge_date' | translate }}: - {{ fountain.properties.conflation_info.merge_date | date: 'long' }}
-

+ + + list {{ + ('detail.show_all_properties' | translate) + ' (' + propertyCount + ')' + }} + + + {{ ('detail.only_with_values' | translate) + ' (' + filteredPropertyCount + ')' }} + + + + + + + + + +
+ {{ propMeta[p.id].name[lang] }} + + +
+

+ {{ 'property.merge_notes' | translate }}: {{ fountain.properties.conflation_info.merge_notes }}
+ {{ 'property.merge_distance' | translate }}: + {{ fountain.properties.conflation_info.merge_distance | number: '1.2' }} m
+ + {{ 'property.merge_date' | translate }}: + {{ fountain.properties.conflation_info.merge_date | date: 'long' }}
+

-

detail.how_to_refresh_message

-
-
- - - - - - - +

detail.how_to_refresh_message

+ + + open_in_new{{ 'quicklink.' + quickLink.id | translate }} -
- -
-
-
+ + + + + + open_in_new{{ 'quicklink.' + quickLink.id | translate }} + + + + + + diff --git a/src/app/detail/detail.component.ts b/src/app/detail/detail.component.ts index 68db778e..77294b42 100644 --- a/src/app/detail/detail.component.ts +++ b/src/app/detail/detail.component.ts @@ -19,8 +19,10 @@ 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 { combineLatest, Observable } from 'rxjs'; import { City } from '../locations'; +import { LanguageService } from '../core/language.service'; +import { SubscriptionService } from '../core/subscription.service'; const wm_cat_url_root = 'https://commons.wikimedia.org/wiki/Category:'; const maxCaptionPartLgth = consts.maxWikiCiteLgth; // 150; @@ -29,17 +31,16 @@ const maxCaptionPartLgth = consts.maxWikiCiteLgth; // 150; selector: 'app-detail', templateUrl: './detail.component.html', styleUrls: ['./detail.component.css'], + providers: [SubscriptionService], }) export class DetailComponent implements OnInit { showImageCallToAction = true; fountain; public isMetadataLoaded = false; public propMeta: PropertyMetadataCollection = null; - @select('fountainSelected') fountain$; + @select('fountainSelected') fountain$: Observable; @select() mode: Observable; @select() city$: Observable; - @select() lang$: Observable; - lang = 'de'; @select('userLocation') userLocation$; @Output() closeDetails = new EventEmitter(); showindefinite = true; @@ -63,32 +64,13 @@ export class DetailComponent implements OnInit { id: '', }; - closeDetailsEvent() { - this.closeDetails.emit(); - } - - filterTable() { - this.tableProperties.filter = this.showindefinite ? 'yes' : 'no'; - } - - public navigateToFountain() { - this.dataService.getDirections(); - } - - public returnToMap() { - this.ngRedux.dispatch({ type: CLOSE_DETAIL }); - } - - public forceRefresh(id: string) { - console.log('refreshing ' + id + ' ' + new Date().toISOString()); - this.dataService.forceRefresh(); - } - constructor( private sanitizer: DomSanitizer, private ngRedux: NgRedux, private dataService: DataService, - private dialog: MatDialog + private dialog: MatDialog, + private languageService: LanguageService, + private subscriptionService: SubscriptionService ) {} ngOnInit(): void { @@ -111,122 +93,145 @@ export class DetailComponent implements OnInit { }; // update fountain - this.fountain$.subscribe( - f => { - try { - if (f !== null) { - this.fountain = f; - // determine which properties should be displayed in table - const fProps = f.properties; - let firstImg = null; - let cats = null; - let id = null; - let descShortTrLc = ''; - let nameTrLc = ''; - if (null != fProps) { - const gal = fProps.gallery; - if (null != gal) { - const galV = gal.value; - if (null != galV && 0 < galV.length) { - firstImg = galV[0]; + this.subscriptionService.registerSubscriptions( + combineLatest([this.fountain$, this.langObservable]).subscribe( + ([f, lang]) => { + try { + if (f !== null) { + this.fountain = f; + // determine which properties should be displayed in table + const fProps = f.properties; + let firstImg = null; + let cats = null; + let id = null; + let descShortTrLc = ''; + let nameTrLc = ''; + if (null != fProps) { + const gal = fProps.gallery; + if (null != gal) { + const galV = gal.value; + if (null != galV && 0 < galV.length) { + firstImg = galV[0]; + } + } + const catArr = fProps.wiki_commons_name; + if (null != catArr) { + cats = catArr.value; + } + if ( + fProps.id_wikidata !== null && + fProps.id_wikidata !== 'null' && + null != fProps.id_wikidata.value + ) { + id = fProps.id_wikidata.value; + } else if (fProps.id_osm !== null && fProps.id_osm !== 'null' && null != fProps.id_osm.value) { + id = fProps.id_osm.value; + } + const dscShort = fProps[`description_short_${lang}`]; + if (null != dscShort && null != dscShort.value && 0 < dscShort.value.trim().length) { + descShortTrLc = dscShort.value.trim().toLowerCase(); + } + const namShort = fProps[`name_${lang}`]; + if (null != namShort && null != namShort.value && 0 < namShort.value.trim().length) { + nameTrLc = namShort.value.trim().toLowerCase(); } } - const catArr = fProps.wiki_commons_name; - if (null != catArr) { - cats = catArr.value; - } - if (fProps.id_wikidata !== null && fProps.id_wikidata !== 'null' && null != fProps.id_wikidata.value) { - id = fProps.id_wikidata.value; - } else if (fProps.id_osm !== null && fProps.id_osm !== 'null' && null != fProps.id_osm.value) { - id = fProps.id_osm.value; - } - const dscShort = fProps[`description_short_${this.lang}`]; - if (null != dscShort && null != dscShort.value && 0 < dscShort.value.trim().length) { - descShortTrLc = dscShort.value.trim().toLowerCase(); - } - const namShort = fProps[`name_${this.lang}`]; - if (null != namShort && null != namShort.value && 0 < namShort.value.trim().length) { - nameTrLc = namShort.value.trim().toLowerCase(); + this.onImageChange(null, firstImg, cats, id, descShortTrLc, nameTrLc); + const list = _.filter(_.toArray(fProps), p => Object.prototype.hasOwnProperty.call(p, 'id')); + this.tableProperties.data = list; + this.propertyCount = list.length; + this.filteredPropertyCount = _.filter(list, p => p.value !== null).length; + this.filterTable(); + // clear nearest public transportation stops #142 + this.nearestStations = []; + // reset image call to action #136 + this.showImageCallToAction = true; + // create quick links array + this.createQuicklinks(f); + // sanitize YouTube Urls + this.videoUrls = []; + if (fProps.youtube_video_id.value) { + for (const id of fProps.youtube_video_id.value) { + this.videoUrls.push(this.getYoutubeEmbedUrl(id)); + } + } else { + console.log('no videoUrls ' + new Date().toISOString()); } - } - this.onImageChange(null, firstImg, cats, id, descShortTrLc, nameTrLc); - const list = _.filter(_.toArray(fProps), p => Object.prototype.hasOwnProperty.call(p, 'id')); - this.tableProperties.data = list; - this.propertyCount = list.length; - this.filteredPropertyCount = _.filter(list, p => p.value !== null).length; - this.filterTable(); - // clear nearest public transportation stops #142 - this.nearestStations = []; - // reset image call to action #136 - this.showImageCallToAction = true; - // create quick links array - this.createQuicklinks(f); - // sanitize YouTube Urls - this.videoUrls = []; - if (fProps.youtube_video_id.value) { - for (const id of fProps.youtube_video_id.value) { - this.videoUrls.push(this.getYoutubeEmbedUrl(id)); + // update issue api + const cityMetadata = this.dataService.currentLocationsCollection; + if ( + 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; + this.issue_api_url = _.template(cityMetadata.issue_api.url_template)({ + lat: f.geometry.coordinates[1], + lon: f.geometry.coordinates[0], + }); + } else { + console.log('setting to null: issue_api_img_url, issue_api_url ' + new Date().toISOString()); + this.issue_api_img_url = null; + this.issue_api_url = null; } - } else { - console.log('no videoUrls ' + new Date().toISOString()); - } - // update issue api - const cityMetadata = this.dataService.currentLocationsCollection; - if ( - 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; - this.issue_api_url = _.template(cityMetadata.issue_api.url_template)({ - lat: f.geometry.coordinates[1], - lon: f.geometry.coordinates[0], - }); - } else { - console.log('setting to null: issue_api_img_url, issue_api_url ' + new Date().toISOString()); - this.issue_api_img_url = null; - this.issue_api_url = null; - } - // // check if there is only one image in gallery, then hide thumbnails - // // does not work until - // https://github.com/lukasz-galka/ngx-gallery/issues/208 is fixed - // if(f.properties.gallery.value.length < 2){ - // this.galleryOptions[0].thumbnailsRows = 0; - // this.galleryOptions[0].imagePercent = 100; - // this.galleryOptions[0].thumbnailsPercent = 0; - // }else{ - // this.galleryOptions[0].thumbnailsRows = 1; - // this.galleryOptions[0].imagePercent = 80; - // this.galleryOptions[0].thumbnailsPercent = 20; - // } + // // check if there is only one image in gallery, then hide thumbnails + // // does not work until + // https://github.com/lukasz-galka/ngx-gallery/issues/208 is fixed + // if(f.properties.gallery.value.length < 2){ + // this.galleryOptions[0].thumbnailsRows = 0; + // this.galleryOptions[0].imagePercent = 100; + // this.galleryOptions[0].thumbnailsPercent = 0; + // }else{ + // this.galleryOptions[0].thumbnailsRows = 1; + // this.galleryOptions[0].imagePercent = 80; + // this.galleryOptions[0].thumbnailsPercent = 20; + // } + } + // eslint-disable-next-line @typescript-eslint/no-implicit-any-catch + } catch (err) { + console.trace('fountain update: ' + err.stack); } - // eslint-disable-next-line @typescript-eslint/no-implicit-any-catch - } catch (err) { - console.trace('fountain update: ' + err.stack); + }, + err => { + console.log(`fountain update: ${err} - ${new Date().toISOString()}`); } - }, - err => { - console.log(`fountain update: ${err} - ${new Date().toISOString()}`); - } + ), + this.mode.subscribe(mode => { + if (mode == 'map') { + this.closeDetails.emit(); + } + }) ); - - this.mode.subscribe(mode => { - if (mode == 'map') { - this.closeDetails.emit(); - } - }); - - this.lang$.subscribe(l => { - if (l !== null) { - this.lang = l; - } - }); } catch (err: unknown) { console.trace(err); } } + langObservable = this.languageService.langObservable; + + //TODO @ralf.hauser, looks like this function is not used, correct? + closeDetailsEvent() { + this.closeDetails.emit(); + } + + filterTable() { + this.tableProperties.filter = this.showindefinite ? 'yes' : 'no'; + } + + navigateToFountain() { + this.dataService.getDirections(); + } + + returnToMap() { + this.ngRedux.dispatch({ type: CLOSE_DETAIL }); + } + + forceRefresh(id: string) { + console.log('refreshing ' + id + ' ' + new Date().toISOString()); + this.dataService.forceRefresh(); + } + getNearestStations() { // Function to request nearest public transport station data and display it. created for #142 // only perform request if it hasn't been done yet @@ -242,7 +247,7 @@ export class DetailComponent implements OnInit { } } - getYoutubeEmbedUrl(id) { + private getYoutubeEmbedUrl(id) { return this.sanitizer.bypassSecurityTrustResourceUrl(`https://www.youtube.com/embed/${id}`); } @@ -309,10 +314,10 @@ export class DetailComponent implements OnInit { } } - setCaption(img: any, wmd: any, dbg: string, descShortTrLc: string, nameTrLc: string) { + private setCaption(img: any, wmd: any, dbg: string, descShortTrLc: string, nameTrLc: string) { //https://github.com/water-fountains/proximap/issues/285 let imgLinkAdded = false; - const claimFldNam = 'claim_' + this.lang; + const claimFldNam = 'claim_' + this.languageService.currentLang; if (Object.prototype.hasOwnProperty.call(img, claimFldNam)) { const claim = img[claimFldNam]; if (null != claim) { diff --git a/src/app/filter/filter.component.html b/src/app/filter/filter.component.html index a9f7aac9..fade3771 100755 --- a/src/app/filter/filter.component.html +++ b/src/app/filter/filter.component.html @@ -4,7 +4,6 @@ * Use of this code is governed by the GNU Affero General Public License (https://www.gnu.org/licenses/agpl-3.0) * and the profit contribution agreement available at https://www.my-d.org/ProfitContributionAgreement --> - @@ -13,7 +12,7 @@
- +
- + + {{ propMeta[property].name[lang] }} { this.propMeta = metadata; this.isLoaded = true; }); - this.lang$.subscribe(l => { - if (l !== null) { - this.lang = l; - } - }); this.dataService.fountainsLoadedSuccess.subscribe(fountains => { this.dateMin = (_.min(_.map(fountains.features, f => f.properties.construction_date)) || new Date().getFullYear()) - 1; @@ -54,6 +43,11 @@ export class FilterComponent implements OnInit { }); } + updateFilters() { + // for #115 - #118 additional filtering functions + this.dataService.filterFountains(this.filter); + } + // Show/Hide more filters. openSubfilter() { this.isSubfilterOpen = !this.isSubfilterOpen; diff --git a/src/app/fountain-property-badge/fountain-property-badge.component.html b/src/app/fountain-property-badge/fountain-property-badge.component.html index 860a319d..ce6d8162 100644 --- a/src/app/fountain-property-badge/fountain-property-badge.component.html +++ b/src/app/fountain-property-badge/fountain-property-badge.component.html @@ -4,20 +4,25 @@ * Use of this code is governed by the GNU Affero General Public License (https://www.gnu.org/licenses/agpl-3.0) * and the profit contribution agreement available at https://www.my-d.org/ProfitContributionAgreement --> - - {{ iconMap[property.id].id }} - - - {{ property.value }} + + + + {{ iconMap[property.id].id }} + + + {{ property.value }} + - + diff --git a/src/app/fountain-property-badge/fountain-property-badge.component.ts b/src/app/fountain-property-badge/fountain-property-badge.component.ts index caf61631..e09b468f 100644 --- a/src/app/fountain-property-badge/fountain-property-badge.component.ts +++ b/src/app/fountain-property-badge/fountain-property-badge.component.ts @@ -5,12 +5,13 @@ * and the profit contribution agreement available at https://www.my-d.org/ProfitContributionAgreement */ import { Component, Input, OnInit } from '@angular/core'; -import { NgRedux, select } from '@angular-redux/store'; +import { NgRedux } from '@angular-redux/store'; import { SELECT_PROPERTY } from '../actions'; import { IAppState } from '../store'; import { propertyStatuses } from '../constants'; import { PropertyMetadata, PropertyMetadataCollection } from '../types'; import { DataService } from '../data.service'; +import { LanguageService } from '../core/language.service'; @Component({ selector: 'app-property-badge', @@ -23,7 +24,6 @@ export class FountainPropertyBadgeComponent implements OnInit { WARN = propertyStatuses.warning; INFO = propertyStatuses.info; OK = propertyStatuses.ok; - @select('lang') lang$; public iconMap = { access_wheelchair: { id: 'accessible', @@ -56,18 +56,20 @@ export class FountainPropertyBadgeComponent implements OnInit { }; public propMeta: PropertyMetadataCollection; public isLoaded = false; - public lang: string; - constructor(private ngRedux: NgRedux, private dataService: DataService) {} + constructor( + private ngRedux: NgRedux, + private dataService: DataService, + private languageService: LanguageService + ) {} + + public langObservable = this.languageService.langObservable; ngOnInit(): void { this.dataService.fetchPropertyMetadata().then(metadata => { this.propMeta = metadata; this.isLoaded = true; }); - this.lang$.subscribe(l => { - if (l !== null) this.lang = l; - }); } viewProperty(): void { diff --git a/src/app/fountain-property-dialog/fountain-property-dialog.component.html b/src/app/fountain-property-dialog/fountain-property-dialog.component.html index 1bff5da5..3b1a43c4 100644 --- a/src/app/fountain-property-dialog/fountain-property-dialog.component.html +++ b/src/app/fountain-property-dialog/fountain-property-dialog.component.html @@ -4,166 +4,174 @@ * Use of this code is governed by the GNU Affero General Public License (https://www.gnu.org/licenses/agpl-3.0) * and the profit contribution agreement available at https://www.my-d.org/ProfitContributionAgreement --> - - -
- {{ metadata[(p | async).id].name[lang] }}
- {{ metadata[(p | async).id].descriptions[lang] }} -
- - -
- - - - - {{ 'quicklink.id_' + source | translate }} - - - {{ 'dialog.preferred_source_short' | translate }} - - - {{ 'dialog.used_source_short' | translate }} - - - - -

- + + + +

+ + + +
+ + + + + {{ 'quicklink.id_' + source | translate }} + + - {{ metadata[(p | async).id].src_config[source].src_instructions[lang].join(' > ') }} - info - + {{ 'dialog.preferred_source_short' | translate }} + + + {{ 'dialog.used_source_short' | translate }} + + + + +

+ + {{ metadata[(p | async).id].src_config[source].src_instructions[lang].join(' > ') }} + info + +

+ +

-

+
+ + + +

+ {{ 'dialog.status.fountain_not_exist' | translate }}

- -

-

- - - - -

- {{ 'dialog.status.fountain_not_exist' | translate }} -

-

- {{ 'dialog.status.property_not_available' | translate }} -

- - - - -
- - - {{ 'dialog.fountain_no_exist' | translate }} - -

- {{ metadata[(p | async).id].src_config[source].src_info[lang] }} +

+ {{ 'dialog.status.property_not_available' | translate }}

- - + + + +
+ + + {{ 'dialog.fountain_no_exist' | translate }} - edit -
- - - dialog.data_extracted - - + {{ metadata[(p | async).id].src_config[source].src_info[lang] }} +

+ + - info -
-
-
- - -
+ dialog.data_raw + + + edit +
+ + + + dialog.data_extracted + + + info + + +
+ +
+
- -

{{ (p | async).comments }}

-
-
- - - - + +

{{ (p | async).comments }}

+
+ + + + + +
diff --git a/src/app/fountain-property-dialog/fountain-property-dialog.component.ts b/src/app/fountain-property-dialog/fountain-property-dialog.component.ts index feb98d60..1ad96702 100644 --- a/src/app/fountain-property-dialog/fountain-property-dialog.component.ts +++ b/src/app/fountain-property-dialog/fountain-property-dialog.component.ts @@ -10,6 +10,7 @@ import { Component, OnInit } from '@angular/core'; import { MatDialog } from '@angular/material/dialog'; import _ from 'lodash'; import { DialogConfig } from '../constants'; +import { LanguageService } from '../core/language.service'; import { DataService } from '../data.service'; import { ImagesGuideComponent, NewFountainGuideComponent, PropertyGuideComponent } from '../guide/guide.component'; import { illegalState } from '../shared/illegalState'; @@ -24,9 +25,7 @@ import { PropertyMetadataCollection } from '../types'; export class FountainPropertyDialogComponent implements OnInit { @select('propertySelected') p; @select('fountainSelected') f; - @select('lang') lang$; - lang: string; - _: _; + metadata: PropertyMetadataCollection; show_property_details = { osm: false, @@ -48,14 +47,20 @@ export class FountainPropertyDialogComponent implements OnInit { 'water_flow', ]; - constructor(public dataService: DataService, private ngRedux: NgRedux, private dialog: MatDialog) {} + constructor( + public dataService: DataService, + private ngRedux: NgRedux, + private dialog: MatDialog, + private languageService: LanguageService + ) {} + + langObservable = this.languageService.langObservable; ngOnInit(): void { this.dataService.fetchPropertyMetadata().then(metadata => { this.metadata = metadata; this.isLoaded = true; }); - this.lang$.subscribe(l => (this.lang = l)); // choose whether to show all details this.p.subscribe(p => { diff --git a/src/app/fountain-property/fountain-property.component.ts b/src/app/fountain-property/fountain-property.component.ts index 52e6e277..94e7e1bd 100644 --- a/src/app/fountain-property/fountain-property.component.ts +++ b/src/app/fountain-property/fountain-property.component.ts @@ -5,46 +5,50 @@ * and the profit contribution agreement available at https://www.my-d.org/ProfitContributionAgreement */ -import { Component, Input, OnInit } from '@angular/core'; +import { Component, Input } from '@angular/core'; import { NgRedux, select } from '@angular-redux/store'; import { SELECT_PROPERTY } from '../actions'; import { IAppState } from '../store'; import { propertyStatuses } from '../constants'; import { PropertyMetadata } from '../types'; import { TranslateService } from '@ngx-translate/core'; +import { LanguageService } from '../core/language.service'; @Component({ selector: 'app-f-property', templateUrl: './fountain-property.component.html', styleUrls: ['./fountain-property.component.css'], }) -export class FountainPropertyComponent implements OnInit { +export class FountainPropertyComponent { @Input() property: PropertyMetadata; @Input() propMeta: PropertyMetadata; @select('fountainSelected') f; - @select('lang') lang$; - lang: 'en' | 'fr' | 'de' | 'it' | 'tr' | 'sr' = 'en'; + WARN = propertyStatuses.warning; INFO = propertyStatuses.info; OK = propertyStatuses.ok; title = ''; - constructor(private ngRedux: NgRedux, private translateService: TranslateService) {} - - ngOnInit(): void { - this.lang$.subscribe(l => (this.lang = l)); - } + constructor( + private ngRedux: NgRedux, + private translateService: TranslateService, + private languageService: LanguageService + ) {} viewProperty(): void { // let p = this.ngRedux.getState().fountainSelected.properties[this.pName]; this.ngRedux.dispatch({ type: SELECT_PROPERTY, payload: this.property.id }); } + // TODO @ralf.hauser: it is in general discouraged to use functions in templates as this needs to be + // recalculated for every template change, so over and over again where in this case it would suffice to + // calcuclate it once during onInit or such makeTitle() { // creates title string const texts = []; for (const src of this.propMeta[this.property.id].src_pref) { - const property_txt = this.propMeta[this.property.id].src_config[src].src_instructions[this.lang].join(' > '); + const property_txt = + this.propMeta[this.property.id].src_config[src].src_instructions[this.languageService.currentLang].join(' > '); texts.push(`${property_txt} in ${this.translateService.instant('quicklink.id_' + src)}`); } return texts.join(' or '); diff --git a/src/app/guide/guide.component.ts b/src/app/guide/guide.component.ts index e4d9abce..031ca43e 100644 --- a/src/app/guide/guide.component.ts +++ b/src/app/guide/guide.component.ts @@ -16,6 +16,7 @@ import { PropertyMetadataCollection } from '../types'; import _ from 'lodash'; import { SELECT_PROPERTY } from '../actions'; +import { LanguageService } from '../core/language.service'; const property_dict = [ { @@ -88,19 +89,25 @@ const property_dict = [ export class GuideSelectorComponent implements OnInit { @select('fountainSelected') fountain; @select('propertySelected') property; - @select() lang$; metadata: PropertyMetadataCollection = {}; available_properties: string[]; current_property_id: string; guides: string[] = ['images', 'name', 'fountain']; - constructor(private dialog: MatDialog, private ngRedux: NgRedux, private dataService: DataService) { + constructor( + private dialog: MatDialog, + private ngRedux: NgRedux, + private dataService: DataService, + private languageService: LanguageService + ) { this.dataService.fetchPropertyMetadata().then(metadata => { this.metadata = metadata; this.available_properties = _.map(this.metadata, 'id'); }); } + langObservable = this.languageService.langObservable; + ngOnInit(): void { this.property.subscribe(p => { if (p) { @@ -161,7 +168,7 @@ export class ImagesGuideComponent extends GuideSelectorComponent {} export class NewFountainGuideComponent extends GuideSelectorComponent {} @Component({ - selector: 'app-fountain-guide', + selector: 'app-property-guide', styleUrls: ['./guide.component.css'], templateUrl: './property.guide.component.html', }) diff --git a/src/app/guide/images.guide.component.html b/src/app/guide/images.guide.component.html index 233b36a0..395fa525 100644 --- a/src/app/guide/images.guide.component.html +++ b/src/app/guide/images.guide.component.html @@ -10,11 +10,10 @@ action.close -

- {{ (fountain | async).properties['name_' + (lang$ | async)].value }} - {{ - 'other.unnamed_fountain' | translate - }} + +

+ {{ (fountain | async).properties['name_' + lang].value }} + {{ 'other.unnamed_fountain' | translate }}

Add one or more images

diff --git a/src/app/guide/new-fountain.guide.component.html b/src/app/guide/new-fountain.guide.component.html index 0f0a267b..3ad86eda 100644 --- a/src/app/guide/new-fountain.guide.component.html +++ b/src/app/guide/new-fountain.guide.component.html @@ -10,11 +10,10 @@ action.close -

- {{ (fountain | async).properties['name_' + (lang$ | async)].value }} - {{ - 'other.unnamed_fountain' | translate - }} + +

+ {{ (fountain | async).properties['name_' + lang].value }} + {{ 'other.unnamed_fountain' | translate }}

Create a new fountain

diff --git a/src/app/intro-window/intro-window.component.css b/src/app/intro-window/intro-window.component.css index 941f0d16..d0135929 100644 --- a/src/app/intro-window/intro-window.component.css +++ b/src/app/intro-window/intro-window.component.css @@ -10,6 +10,7 @@ background-size: 200px; height: 70px; width: 100%; + margin-bottom: 20px; } .never-show { @@ -47,6 +48,3 @@ h3 { padding: 0.5em; white-space: normal !important; } - -:host { -} diff --git a/src/app/intro-window/intro-window.component.html b/src/app/intro-window/intro-window.component.html index 50f3c21d..94d702fb 100644 --- a/src/app/intro-window/intro-window.component.html +++ b/src/app/intro-window/intro-window.component.html @@ -7,7 +7,7 @@
- +

intro.intro-text

diff --git a/src/app/issue-indicator/issue-indicator.component.ts b/src/app/issue-indicator/issue-indicator.component.ts index 8e597498..a914841c 100644 --- a/src/app/issue-indicator/issue-indicator.component.ts +++ b/src/app/issue-indicator/issue-indicator.component.ts @@ -20,10 +20,9 @@ import { IAppState } from '../store'; export class IssueIndicatorComponent { @select('dataIssues') dataIssues$; @select('appErrors') appErrors$; - @select('lang') lang$; constructor(private ngRedux: NgRedux, private dialog: MatDialog) { - //ngRedux is used implicitly via select, this is only for the compiler + //ngRedux is used implicitly via select, this is only for the compiler - I guess we could already remove it but I keep it to avoid unnecessary side effects (of the removal) this.ngRedux.getState(); } diff --git a/src/app/issue-list/issue-list.component.ts b/src/app/issue-list/issue-list.component.ts index 8896ef10..67b3f8d8 100644 --- a/src/app/issue-list/issue-list.component.ts +++ b/src/app/issue-list/issue-list.component.ts @@ -19,7 +19,6 @@ import { AppError, DataIssue } from '../types'; export class IssueListComponent { @select('dataIssues') dataIssues$: Observable; @select('appErrors') appErrors$: Observable; - @select('lang') lang$: Observable; @select('city') city$: Observable; // issue_count:number; } diff --git a/src/app/list/list.component.html b/src/app/list/list.component.html index 1cb90d0f..429df903 100644 --- a/src/app/list/list.component.html +++ b/src/app/list/list.component.html @@ -4,80 +4,85 @@ * Use of this code is governed by the GNU Affero General Public License (https://www.gnu.org/licenses/agpl-3.0) * and the profit contribution agreement available at https://www.my-d.org/ProfitContributionAgreement --> -
- -
- list.showing
-
-
- -

- - {{ - fountain.properties['name_' + lang] | truncate - }} - {{ 'other.unnamed_fountain' | translate }} - {{ - getDistSignificantIss219(fountain) - }} -

-

- - - {{ fountain.properties.construction_date }} - - - {{ fountain.properties.water_type | translate }} + + +

+ +
+ list.showing
- accessible - pets - - -

- - - -
+
+ +

+ + {{ + fountain.properties['name_' + lang] | truncate + }} + {{ 'other.unnamed_fountain' | translate }} + {{ + getDistSignificantIss219(fountain) + }} +

+

+ + + {{ fountain.properties.construction_date }} + + + {{ fountain.properties.water_type | translate }} + + accessible + pets + + +

+
+ + +
+ diff --git a/src/app/list/list.component.ts b/src/app/list/list.component.ts index 92d64d5a..2cd8476d 100644 --- a/src/app/list/list.component.ts +++ b/src/app/list/list.component.ts @@ -12,6 +12,7 @@ import { DeviceMode, PropertyMetadataCollection } from '../types'; import { Feature } from 'geojson'; import { getId } from '../database.service'; import { BehaviorSubject } from 'rxjs'; +import { LanguageService } from '../core/language.service'; @Component({ selector: 'app-list', templateUrl: './list.component.html', @@ -22,14 +23,16 @@ export class ListComponent implements OnInit { isLoaded = false; propMeta: PropertyMetadataCollection = null; public fountains: Feature[] = []; - @select() lang$; + @select() device$; @select() fountainSelected$; device: BehaviorSubject = new BehaviorSubject('mobile'); - lang = 'de'; + total_fountain_count = 0; - constructor(public dataService: DataService) {} + constructor(public dataService: DataService, private languageService: LanguageService) {} + + langObservable = this.languageService.langObservable; ngOnInit(): void { // watch for device type changes @@ -51,11 +54,6 @@ export class ListComponent implements OnInit { this.filtered_fountain_count = 0; } }); - this.lang$.subscribe(l => { - if (l !== null) { - this.lang = l; - } - }); this.device$.subscribe(d => { if (d !== null) { this.device = d; diff --git a/src/app/map/map.component.ts b/src/app/map/map.component.ts index 191a63c6..a383fbea 100644 --- a/src/app/map/map.component.ts +++ b/src/app/map/map.component.ts @@ -14,6 +14,7 @@ import { BehaviorSubject, Observable } from 'rxjs'; import { EMPTY_LINESTRING } from '../../assets/defaultData'; import { environment } from '../../environments/environment'; import { SET_USER_LOCATION } from '../actions'; +import { LanguageService } from '../core/language.service'; import { DataService } from '../data.service'; import { City } from '../locations'; import { IAppState } from '../store'; @@ -38,7 +39,6 @@ export class MapComponent implements OnInit { private satelliteShown = false; @select() showList; @select() mode$: Observable; - @select() lang$: Observable; @select() city$: Observable; @select() device$; device: BehaviorSubject = new BehaviorSubject('mobile'); @@ -49,135 +49,12 @@ export class MapComponent implements OnInit { constructor( private dataService: DataService, - private mc: MapConfig, - private translate: TranslateService, + private mapConfig: MapConfig, + private translateService: TranslateService, + private languageService: LanguageService, private ngRedux: NgRedux ) {} - setUserLocation(coordinates) { - this.ngRedux.dispatch({ type: SET_USER_LOCATION, payload: coordinates }); - } - - zoomToFountain() { - if (this._selectedFountain !== null) { - this.map.flyTo({ - center: this._selectedFountain.geometry.coordinates, - zoom: this.mc.map.maxZoom, - pitch: 55, - bearing: 40, - maxDuration: 2500, - offset: [0, -180], - }); - } - } - - // Zoom to city bounds (only if current map bounds are outside of new city's bounds) - zoomToCity(city: City): void { - const options = { - maxDuration: 500, - pitch: 0, - bearing: 0, - }; - - this.dataService - .getLocationBounds(city) - .then(bounds => { - const waiting = () => { - if (!this.map.isStyleLoaded()) { - setTimeout(waiting, 200); - } else { - if (this._mode === 'map') - // only refit city bounds if not zoomed into a fountain - this.map.fitBounds(bounds, options); - } - }; - waiting(); - }) - .catch(err => console.log(err)); - } - - zoomOut() { - this.map.flyTo({ - zoom: this.mc.map.zoomAfterDetail, - pitch: this.mc.map.pitch, - bearing: 0, - maxDuration: 2500, - }); - } - - initializeMap() { - // Create map - M.accessToken = environment.mapboxApiKey; - this.map = new M.Map( - Object.assign(this.mc.map, { - container: 'map', - }) - ).on('load', () => { - // zoom to city - // this.zoomToCity(this.ngRedux.getState().city); - }); - - // Add navigation control to map - this.navControl = new M.NavigationControl({ - showCompass: true, - }); - this.map.addControl(this.navControl, 'top-left'); - - // Add geolocate control to the map. - this.geolocator = new M.GeolocateControl({ - positionOptions: { - enableHighAccuracy: true, - }, - fitBoundsOptions: { - maxZoom: this.mc.map.maxZoom, - }, - trackUserLocation: false, - }); - this.map.addControl(this.geolocator); - - this.geolocator.on('geolocate', position => { - this.setUserLocation([position.coords.longitude, position.coords.latitude]); - }); - - // highlight popup - this.highlightPopup = new M.Popup({ - closeButton: false, - closeOnClick: false, - offset: 10, - }); - - // popup for selected fountain - this.selectPopup = new M.Popup({ - closeButton: false, - closeOnClick: false, - offset: 10, - }); - - // // directions control - // this.directions = new MapboxDirections({ - // accessToken: environment.mapboxApiKey, - // unit: 'metric', - // profile: 'mapbox/walking', - // interactive: false, - // controls: { - // inputs: false - // } - // }); - - // user marker - const el = document.createElement('div'); - el.className = 'userMarker'; - el.style.backgroundImage = 'url(/assets/user_icon.png)'; - el.style.backgroundSize = 'cover'; - el.style.backgroundPosition = 'center'; - el.style.backgroundRepeat = 'no-repeat'; - el.style.boxShadow = 'box-shadow: 0 0 4px rgba(0, 0, 0, 0.4);'; - el.style.width = '30px'; - el.style.height = '37px'; - el.style.top = '-15px'; - this.userMarker = new M.Marker(el); - } - ngOnInit(): void { this.initializeMap(); @@ -207,8 +84,8 @@ export class MapComponent implements OnInit { }); // when the language is changed, update popups - this.lang$.subscribe(() => { - console.log('lang "' + this.lang$ + '", mode ' + this._mode + ' ' + new Date().toISOString()); + this.languageService.langObservable.subscribe(lang => { + console.log('lang "' + lang + '", mode ' + this._mode + ' ' + new Date().toISOString()); if (this._mode !== 'map') { this.showSelectedPopupOnMap(); } @@ -296,6 +173,130 @@ export class MapComponent implements OnInit { }); } + private setUserLocation(coordinates) { + this.ngRedux.dispatch({ type: SET_USER_LOCATION, payload: coordinates }); + } + + private zoomToFountain() { + if (this._selectedFountain !== null) { + this.map.flyTo({ + center: this._selectedFountain.geometry.coordinates, + zoom: this.mapConfig.map.maxZoom, + pitch: 55, + bearing: 40, + maxDuration: 2500, + offset: [0, -180], + }); + } + } + + // Zoom to city bounds (only if current map bounds are outside of new city's bounds) + private zoomToCity(city: City): void { + const options = { + maxDuration: 500, + pitch: 0, + bearing: 0, + }; + + this.dataService + .getLocationBounds(city) + .then(bounds => { + const waiting = () => { + if (!this.map.isStyleLoaded()) { + setTimeout(waiting, 200); + } else { + if (this._mode === 'map') + // only refit city bounds if not zoomed into a fountain + this.map.fitBounds(bounds, options); + } + }; + waiting(); + }) + .catch(err => console.log(err)); + } + + private zoomOut() { + this.map.flyTo({ + zoom: this.mapConfig.map.zoomAfterDetail, + pitch: this.mapConfig.map.pitch, + bearing: 0, + maxDuration: 2500, + }); + } + + initializeMap() { + // Create map + M.accessToken = environment.mapboxApiKey; + this.map = new M.Map( + Object.assign(this.mapConfig.map, { + container: 'map', + }) + ).on('load', () => { + // zoom to city + // this.zoomToCity(this.ngRedux.getState().city); + }); + + // Add navigation control to map + this.navControl = new M.NavigationControl({ + showCompass: true, + }); + this.map.addControl(this.navControl, 'top-left'); + + // Add geolocate control to the map. + this.geolocator = new M.GeolocateControl({ + positionOptions: { + enableHighAccuracy: true, + }, + fitBoundsOptions: { + maxZoom: this.mapConfig.map.maxZoom, + }, + trackUserLocation: false, + }); + this.map.addControl(this.geolocator); + + this.geolocator.on('geolocate', position => { + this.setUserLocation([position.coords.longitude, position.coords.latitude]); + }); + + // highlight popup + this.highlightPopup = new M.Popup({ + closeButton: false, + closeOnClick: false, + offset: 10, + }); + + // popup for selected fountain + this.selectPopup = new M.Popup({ + closeButton: false, + closeOnClick: false, + offset: 10, + }); + + // // directions control + // this.directions = new MapboxDirections({ + // accessToken: environment.mapboxApiKey, + // unit: 'metric', + // profile: 'mapbox/walking', + // interactive: false, + // controls: { + // inputs: false + // } + // }); + + // user marker + const el = document.createElement('div'); + el.className = 'userMarker'; + el.style.backgroundImage = 'url(/assets/user_icon.png)'; + el.style.backgroundSize = 'cover'; + el.style.backgroundPosition = 'center'; + el.style.backgroundRepeat = 'no-repeat'; + el.style.boxShadow = 'box-shadow: 0 0 4px rgba(0, 0, 0, 0.4);'; + el.style.width = '30px'; + el.style.height = '37px'; + el.style.top = '-15px'; + this.userMarker = new M.Marker(el); + } + getId(fountain: Feature) { const prop = fountain.properties; if (null != prop.id_wikidata) { @@ -317,8 +318,10 @@ export class MapComponent implements OnInit { // move to location this.highlightPopup.setLngLat(fountain.geometry.coordinates); //set popup content - let name = fountain.properties['name_' + this.ngRedux.getState().lang]; - name = !name || name == 'null' ? this.translate.instant('other.unnamed_fountain') : name; + let name = fountain.properties['name_' + this.languageService.currentLang]; + // TODO @ralf.hauser, using instant will have the effect that it is not translated if a user changes the language + // might be it does not matter in this specific case but could be a bug + name = !name || name == 'null' ? this.translateService.instant('other.unnamed_fountain') : name; const phot = fountain.properties.photo; let popUpHtml = `

${name}

`; if (phot == null) { @@ -344,14 +347,14 @@ export class MapComponent implements OnInit { } } - removeDirections() { + private removeDirections() { EMPTY_LINESTRING.features[0].geometry.coordinates = []; if (this.map.getSource('navigation-line')) { this.map.getSource('navigation-line').setData(EMPTY_LINESTRING); } } - showSelectedPopupOnMap() { + private showSelectedPopupOnMap() { if (this._selectedFountain !== null) { // hide popup this.selectPopup.remove(); @@ -360,8 +363,10 @@ export class MapComponent implements OnInit { this.selectPopup.setLngLat(this._selectedFountain.geometry.coordinates); //set popup content const fountainTitle = - this._selectedFountain.properties['name_' + this.ngRedux.getState().lang].value || - this.translate.instant('other.unnamed_fountain'); + this._selectedFountain.properties['name_' + this.languageService.currentLang].value || + // TODO @ralf.hauser, using instant will have the effect that it is not translated if a user changes the language + // might be it does not matter in this specific case but could be a bug + this.translateService.instant('other.unnamed_fountain'); this.selectPopup.setHTML(`

${fountainTitle}

`); this.selectPopup.addTo(this.map); } diff --git a/src/app/mobile-menu/mobile-menu.component.html b/src/app/mobile-menu/mobile-menu.component.html index c369bae7..131bbba8 100644 --- a/src/app/mobile-menu/mobile-menu.component.html +++ b/src/app/mobile-menu/mobile-menu.component.html @@ -5,7 +5,7 @@ * and the profit contribution agreement available at https://www.my-d.org/ProfitContributionAgreement --> - +
@@ -23,14 +23,12 @@

intro.legend-title

@@ -38,7 +36,7 @@

intro.legend-title

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

intro.disclaimer-title

@@ -79,15 +77,11 @@

intro.similar-title

{{ 'settings.lang' | translate }}

- +

{{ 'settings.city' | translate }}

- {{ 'settings.city_last_scan' | translate }}: {{ last_scan | date: 'long':'':(lang$ | async) }}. + {{ 'settings.city_last_scan' | translate }}: {{ last_scan | date: 'long':'':lang }}. {{ 'settings.city_next_scan_info' | translate }} {{ publicSharedConsts?.CACHE_FOR_HRS_i45db }}