Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
…tains#489 switch between cities

moreover:
- water-fountains#490 do not handle programmatic route changes
- fix some null vs. undefined issues
- update mapboxgl to latest version

not included yet in this commit:
- loading fountains outside of a city
  • Loading branch information
robstoll committed Dec 17, 2021
1 parent 94929bd commit 18f2762
Show file tree
Hide file tree
Showing 29 changed files with 611 additions and 398 deletions.
25 changes: 15 additions & 10 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 5 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,12 +40,13 @@
"axios": "^0.21.2",
"core-js": "^2.6.12",
"font-awesome": "^4.7.0",
"fp-ts": "^2.11.5",
"geojson": "^0.5.0",
"git-repo-info": "^2.1.1",
"haversine": "^1.1.0",
"js-md5": "^0.7.3",
"lodash": "^4.17.21",
"mapbox-gl": "^2.5.1",
"mapbox-gl": "^2.6.1",
"rxjs": "^6.6.7",
"tassign": "^1.0.0",
"ts-md5": "^1.2.8",
Expand All @@ -68,11 +69,11 @@
"@types/haversine": "^1.1.0",
"@types/jasmine": "^3.10.1",
"@types/jasminewd2": "^2.0.9",
"@types/leaflet": "^1.7.0",
"@types/js-md5": "^0.4.3",
"@types/node": "^14.0.0",
"@types/mapbox-gl": "^2.4.2",
"@types/leaflet": "^1.7.0",
"@types/lodash": "^4.14.175",
"@types/mapbox-gl": "^2.6.0",
"@types/node": "^14.0.0",
"@typescript-eslint/eslint-plugin": "4.23.0",
"@typescript-eslint/parser": "4.23.0",
"eslint": "^7.32.0",
Expand Down
5 changes: 4 additions & 1 deletion src/app/app.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,10 @@
>
<mat-icon>arrow_back</mat-icon> <span translate>action.return</span>
</button>
<div class="detail-from-bottom" *ngIf="mode === 'details' && (fountainSelectorObservable | async) !== null">
<div
class="detail-from-bottom"
*ngIf="mode === 'details' && (fountainSelectorObservable | async) !== undefined"
>
<div class="centering">
<button mat-mini-fab color="basic" (click)="returnToMap()" class="mat-elevation-z4">
<mat-icon>close</mat-icon>
Expand Down
29 changes: 13 additions & 16 deletions src/app/app.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ import { IssueService } from './issues/issue.service';
import { SubscriptionService } from './core/subscription.service';
import { LayoutService } from './core/layout.service';
import { FountainService } from './fountain/fountain.service';
import { of } from 'rxjs';
import { MatSidenav } from '@angular/material/sidenav';
import { filterUndefined } from './shared/ObservableExtensions';

@Component({
selector: 'app-root',
Expand Down Expand Up @@ -109,24 +109,21 @@ export class AppComponent implements OnInit {
}
}),
this.fountainService.selectedProperty
.switchMap(p => {
if (p !== null) {
if (!this.propertyDialogIsOpen) {
this.propertyDialog = this.dialog.open(FountainPropertyDialogComponent, {
maxWidth: 1000,
width: '800px',
});
this.propertyDialogIsOpen = true;
}
return this.propertyDialog.afterClosed().tap(() => {
this.fountainService.deselectProperty();
this.propertyDialogIsOpen = false;
.pipe(filterUndefined())
.switchMap(_ => {
if (!this.propertyDialogIsOpen) {
this.propertyDialog = this.dialog.open(FountainPropertyDialogComponent, {
maxWidth: 1000,
width: '800px',
});
} else {
return of(undefined);
this.propertyDialogIsOpen = true;
}
return this.propertyDialog.afterClosed().tap(() => {
this.fountainService.deselectProperty();
this.propertyDialogIsOpen = false;
});
})
.subscribe(_ => undefined /* nothing to do in addition as side effect happens in tap */)
.subscribe(_ => undefined /* nothing to do in addition as side effect happens in switchMap and tap */)
);

// intro dialog for
Expand Down
4 changes: 2 additions & 2 deletions src/app/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ import { LayoutService } from './core/layout.service';
import { DirectionsService } from './directions/directions.service';
import { FountainService } from './fountain/fountain.service';
import { ConfigBasedParserService } from './core/config-based-parser.service';
import { CityService } from './city/city.service';
import { MapService } from './city/map.service';
import { CitySelectorComponent } from './city/city-selector.component';
import { LanguageSelectorComponent } from './core/language-selector.component';

Expand Down Expand Up @@ -149,7 +149,7 @@ import { LanguageSelectorComponent } from './core/language-selector.component';
LayoutService,
DirectionsService,
FountainService,
CityService,
MapService,
ConfigBasedParserService,
],
bootstrap: [AppComponent],
Expand Down
1 change: 1 addition & 0 deletions src/app/city/city-selector.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@
[options]="cities"
[valueObservable]="cityObservable"
[changeHook]="changeCity.bind(this)"
pleaseSelectKey="selectCity"
></app-select>
10 changes: 3 additions & 7 deletions src/app/city/city-selector.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { Observable } from 'rxjs';
import { LayoutService } from '../core/layout.service';
import { DataService } from '../data.service';
import { City } from '../locations';
import { CityService } from './city.service';
import { MapService } from './map.service';

@Component({
selector: 'app-city-selector',
Expand All @@ -13,14 +13,10 @@ import { CityService } from './city.service';
export class CitySelectorComponent {
@Input() tooltipText!: string;

constructor(
private dataService: DataService,
private cityService: CityService,
private layoutService: LayoutService
) {}
constructor(private dataService: DataService, private mapService: MapService, private layoutService: LayoutService) {}

public cities: City[] = this.dataService.getLocationMetadata()[1];
public cityObservable: Observable<City | undefined> = this.cityService.city;
public cityObservable: Observable<City | undefined> = this.mapService.city;

changeCity(city: City): void {
this.layoutService.flyToCity(city);
Expand Down
101 changes: 93 additions & 8 deletions src/app/city/map.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,12 @@ import { Injectable } from '@angular/core';
import { BehaviorSubject, Observable } from 'rxjs';
import { fountainAliases } from '../fountain-aliases';
import { Config, ConfigBasedParserService } from '../core/config-based-parser.service';
import { City } from '../locations';
import { City, getCentre, getLocationBounds as getCityBounds } from '../locations';
import { Bounds, LngLat, SharedLocation } from '../types';
import _ from 'lodash';
import { distinctUntilChanged } from 'rxjs/operators';
import { filterUndefined } from '../shared/ObservableExtensions';
import { MapConfig } from '../map/map.config';

export const defaultCity: City = 'ch-zh';

Expand Down Expand Up @@ -85,19 +90,99 @@ type ExpectNever<_T extends never> = void;
// eslint-disable-next-line @typescript-eslint/no-unused-vars
type _CheckCityAndFountainDoNotOverlapp = ExpectNever<Overlaps<[AllCityRelatedIdentifiers, FountainAliases]>>;

export interface MapState {
bounds: Bounds;
city: City | undefined;
location: LngLat;
zoom: number | 'auto';
}
export type MapStateOmitCalculatedFields = Omit<MapState, 'location'>;

@Injectable()
export class CityService {
constructor(private configBasedParser: ConfigBasedParserService) {}
export class MapService {
constructor(private configBasedParser: ConfigBasedParserService, private mapConfig: MapConfig) {}

private stateSubject = new BehaviorSubject<MapState | undefined>(undefined);

get state(): Observable<MapState> {
// we don't want to emit the first state where it is undefined
return this.stateSubject.pipe(filterUndefined());
}

updatedStateBasedOnSharedLocation(sharedLocation: SharedLocation, cityIdOrAlias: string | undefined): void {
const lng = sharedLocation.location.lng;
const lat = sharedLocation.location.lat;
let bounds: Bounds;
const currentState = this.stateSubject.value;
if (currentState !== undefined && _.isEqual(this.mapStateToSharedLocation(currentState), sharedLocation)) {
bounds = currentState.bounds;
} else {
// we don't know yet what bounds the map has, so we are faking it.
bounds = Bounds(LngLat(lng - 2, lat - 2), LngLat(lng + 2, lat + 2));
}

this.updateState({
bounds: bounds,
zoom: sharedLocation.zoom,
city: this.parseCity(cityIdOrAlias),
});
}

updateState(mapState: MapStateOmitCalculatedFields): void {
const calculcatedMapState: MapState = this.calculateFields(mapState);

if (!_.isEqual(calculcatedMapState, this.stateSubject.value)) {
console.log('updating state to', calculcatedMapState);
this.stateSubject.next(calculcatedMapState);
}
}

private readonly citySubject = new BehaviorSubject<City | undefined>(undefined);
calculateFields(mapState: MapStateOmitCalculatedFields): MapState {
return { ...mapState, location: getCentre(mapState.bounds) };
}

get currentCity(): City | undefined {
return this.stateSubject.value?.city;
}
get city(): Observable<City | undefined> {
return this.citySubject.asObservable();
return (
this.state
.map(s => s.city)
// we don't want to propagate a city change if something else changed
.pipe(distinctUntilChanged())
);
}
setCity(city: City) {
this.citySubject.next(city);

get sharedLocation(): Observable<SharedLocation> {
return (
this.state
.map(s => this.mapStateToSharedLocation(s))
// we don't want to propagate a change if e.g. the bounds change (due to resizing of the browser for instance)
.tap(x => console.log('sharedLocation before', x))
.pipe(
distinctUntilChanged((x, y) => {
console.log('there we are', x, y);
return _.isEqual(x, y);
})
)
.tap(x => console.log('sharedLocation after', x))
);
}

parse(value: string | null | undefined): City | undefined {
private mapStateToSharedLocation(state: MapState): SharedLocation {
return { location: state.location, zoom: state.zoom };
}

parseCity(value: string | null | undefined): City | undefined {
return this.configBasedParser.parse(value, cityConfigs);
}

setCity(city: City) {
this.updateState({
city: city,
bounds: getCityBounds(city),
// if we have not yet zoomed, then we want to fit to bounds
zoom: 'auto',
});
}
}
16 changes: 8 additions & 8 deletions src/app/core/layout.service.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { BreakpointObserver } from '@angular/cdk/layout';
import { Injectable } from '@angular/core';
import { BehaviorSubject, Observable } from 'rxjs';
import { shareReplay } from 'rxjs/operators';
import { distinctUntilChanged, shareReplay } from 'rxjs/operators';
import { Directions, DirectionsService } from '../directions/directions.service';
import { FountainService } from '../fountain/fountain.service';
import { City } from '../locations';
import { CityService } from '../city/city.service';
import { MapService } from '../city/map.service';
import { Fountain, FountainSelector } from '../types';

export type PreviewState = 'open' | 'closed';
Expand All @@ -15,7 +15,7 @@ export class LayoutService {
constructor(
private breakpointObserver: BreakpointObserver,
private fountainService: FountainService,
private cityService: CityService,
private mapService: MapService,
private directionService: DirectionsService
) {}

Expand All @@ -28,15 +28,15 @@ export class LayoutService {

private readonly showListSubject = new BehaviorSubject<boolean>(false);
get showList(): Observable<boolean> {
return this.showListSubject.asObservable();
return this.showListSubject.pipe(distinctUntilChanged());
}
setShowList(shallShow: boolean): void {
this.showListSubject.next(shallShow);
}

private readonly showMenuSubject = new BehaviorSubject<boolean>(false);
get showMenu(): Observable<boolean> {
return this.showMenuSubject.asObservable();
return this.showMenuSubject.pipe(distinctUntilChanged());
}
setShowMenu(shallShow: boolean): void {
this.showMenuSubject.next(shallShow);
Expand All @@ -49,15 +49,15 @@ export class LayoutService {

private readonly previewStateSubject = new BehaviorSubject<PreviewState>('closed');
get previewState(): Observable<PreviewState> {
return this.previewStateSubject.asObservable();
return this.previewStateSubject.pipe(distinctUntilChanged());
}
setPreviewState(state: PreviewState) {
this.previewStateSubject.next(state);
}

private readonly modeSubject = new BehaviorSubject<Mode>('map');
get mode(): Observable<Mode> {
return this.modeSubject.asObservable();
return this.modeSubject.pipe(distinctUntilChanged());
}
setMode(mode: Mode): void {
if (mode === 'map') {
Expand Down Expand Up @@ -86,7 +86,7 @@ export class LayoutService {
}

flyToCity(city: City): void {
this.cityService.setCity(city);
this.mapService.setCity(city);
this.closeDetail();
}
}
1 change: 1 addition & 0 deletions src/app/core/selector.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
class="responsive"
(selectionChange)="changeValue($event)"
[value]="valueObservable | async"
[placeholder]="pleaseSelectKey | translate"
>
<mat-option *ngFor="let o of options" [value]="o">{{ translationPrefix + '.' + o | translate }}</mat-option>
</mat-select>
Expand Down
Loading

0 comments on commit 18f2762

Please sign in to comment.