Commit 56b10dba authored by Léonard Treille's avatar Léonard Treille
Browse files

Merge branch 'contribution' into 'master'

Contribution feature

See merge request m/m-affluence!1
parents 65469718 be6f59dc
......@@ -1627,6 +1627,15 @@
"integrity": "sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA==",
"dev": true
},
"@types/moment": {
"version": "2.13.0",
"resolved": "https://registry.npmjs.org/@types/moment/-/moment-2.13.0.tgz",
"integrity": "sha1-YE69GJvDvDShVIaJQE5hoqSqyJY=",
"dev": true,
"requires": {
"moment": "*"
}
},
"@types/node": {
"version": "12.12.37",
"resolved": "https://registry.npmjs.org/@types/node/-/node-12.12.37.tgz",
......@@ -7844,6 +7853,11 @@
"minimist": "^1.2.5"
}
},
"moment": {
"version": "2.25.3",
"resolved": "https://registry.npmjs.org/moment/-/moment-2.25.3.tgz",
"integrity": "sha512-PuYv0PHxZvzc15Sp8ybUCoQ+xpyPWvjOuK72a5ovzp2LI32rJXOiIfyoFoYvG3s6EwwrdkMyWuRiEHSZRLJNdg=="
},
"move-concurrently": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz",
......
......@@ -4,6 +4,11 @@ import { HorairesLignesComponent } from '@pages/horaires-lignes/horaires-lignes.
import { DetailLigneComponent } from '@pages/detail-ligne/detail-ligne.component';
import { LinesResolver } from '@features/lines/lines.resolver';
import { LineResolver } from '@features/line/line.resolver';
import { ContributionFirstStepComponent } from '@pages/contribution/contribution-first-step/contribution-first-step.component';
import { ContributionSecondStepComponent } from '@pages/contribution/contribution-second-step/contribution-second-step.component';
import { ContributionFinalStepComponent } from '@pages/contribution/contribution-final-step/contribution-final-step.component';
import { ContributionWrapperComponent } from '@pages/contribution/contribution-wrapper/contribution-wrapper.component';
import { PatternsResolver } from '@features/patterns/patterns.resolver';
const routes: Routes = [
{
......@@ -18,6 +23,14 @@ const routes: Routes = [
clusters: LineResolver
}
},
{
path: 'contribution', component: ContributionWrapperComponent, data: { totalStep: 3 },
children: [
{ path: '', component: ContributionFirstStepComponent, data: { step: 1 } },
{ path: ':cluster', component: ContributionSecondStepComponent, data: { step: 2 }, resolve: { patterns: PatternsResolver } },
{ path: ':stop/:line/:dir/:headsign', component: ContributionFinalStepComponent, data: { step: 3 } },
]
},
];
@NgModule({
......
......@@ -16,6 +16,8 @@ import { MatRippleModule } from '@angular/material/core';
import { MatExpansionModule } from '@angular/material/expansion';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input';
import { MatToolbarModule } from '@angular/material/toolbar';
import { MatSnackBarModule } from '@angular/material/snack-bar';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
......@@ -24,12 +26,9 @@ import { HorairesLignesComponent } from '@pages/horaires-lignes/horaires-lignes.
import { DetailLigneComponent } from '@pages/detail-ligne/detail-ligne.component';
import { LogoLigneComponent } from '@components/logo-ligne/logo-ligne.component';
import { DetailProchainspassagesComponent } from '@components/detail-prochainspassages/detail-prochainspassages.component';
import { DialogRecherchePointComponent } from '@components/dialog-recherche-point/dialog-recherche-point.component';
import { RecherchePointComponent } from '@components/recherche-point/recherche-point.component';
import { MListWrapperComponent } from '@components/m-list-wrapper/m-list-wrapper.component';
import { IconTypeComponent } from '@components/icon-type/icon-type.component';
import { RealtimeIconComponent } from '@components/realtime-icon/realtime-icon.component';
import { SearchDialogDirective } from '@directives/search-dialog.directive';
import { GroupByPipe } from '@pipes/groupBy.pipe';
import { CommunePipe } from '@pipes/commune.pipe';
import { LibellePipe } from '@pipes/libelle.pipe';
......@@ -40,6 +39,15 @@ import { OccupancyDatasetPipe } from '@features/occupancy/occupancy-dataset.pipe
import { VerticalBarChartType } from '@components/chart/types/vertical-bar.chart-type';
import { OccupancyChartType } from '@components/chart/types/occupancy.chart-type';
import { OccupancyChartDialogComponent } from '@features/occupancy/occupancy-chart-dialog/occupancy-chart-dialog.component';
import { ContributionFirstStepComponent } from './pages/contribution/contribution-first-step/contribution-first-step.component';
import { ContributionSecondStepComponent } from './pages/contribution/contribution-second-step/contribution-second-step.component';
import { ContributionFinalStepComponent } from './pages/contribution/contribution-final-step/contribution-final-step.component';
import { ContributionWrapperComponent } from './pages/contribution/contribution-wrapper/contribution-wrapper.component';
import { BackButtonComponent } from './components/back-button/back-button.component';
import { SearchInputComponent } from './features/search/search-input/search-input.component';
import { SearchListComponent } from './features/search/search-list/search-list.component';
import { SearchItemDirective } from './features/search/search-item.directive';
import { IdPipe } from './pipes/id.pipe';
@NgModule({
declarations: [
......@@ -47,9 +55,6 @@ import { OccupancyChartDialogComponent } from '@features/occupancy/occupancy-cha
HorairesLignesComponent,
LogoLigneComponent,
GroupByPipe,
SearchDialogDirective,
DialogRecherchePointComponent,
RecherchePointComponent,
MListWrapperComponent,
IconTypeComponent,
CommunePipe,
......@@ -63,7 +68,16 @@ import { OccupancyChartDialogComponent } from '@features/occupancy/occupancy-cha
TimeSchedulePipe,
ChartComponent,
OccupancyDatasetPipe,
OccupancyChartDialogComponent
OccupancyChartDialogComponent,
ContributionFirstStepComponent,
ContributionSecondStepComponent,
ContributionFinalStepComponent,
ContributionWrapperComponent,
BackButtonComponent,
SearchInputComponent,
SearchListComponent,
SearchItemDirective,
IdPipe
],
imports: [
BrowserModule,
......@@ -84,9 +98,8 @@ import { OccupancyChartDialogComponent } from '@features/occupancy/occupancy-cha
MatFormFieldModule,
MatInputModule,
MatProgressBarModule,
],
entryComponents: [
DialogRecherchePointComponent
MatToolbarModule,
MatSnackBarModule,
],
providers: [
WINDOW_PROVIDERS,
......
<button *ngIf="!isMobile" mat-button (click)="back()">
<mat-icon class="icon-left">arrow_back</mat-icon>
<span>Retour</span>
</button>
<button *ngIf="isMobile" mat-icon-button (click)="back()">
<mat-icon>arrow_back</mat-icon>
<span class="cdk-visually-hidden">Retour</span>
</button>
@import "../../../styles/variables";
:host {
display: block;
margin-right: $spacing * 2;
}
import { Component, OnInit, OnDestroy } from '@angular/core';
import { Subject } from 'rxjs';
import { BreakpointService } from '@services/breakpoint.service';
import { takeUntil } from 'rxjs/operators';
@Component({
selector: 'app-back-button',
templateUrl: './back-button.component.html',
styleUrls: ['./back-button.component.scss'],
})
export class BackButtonComponent implements OnInit, OnDestroy {
isMobile: boolean;
private unsubscriber = new Subject<void>();
constructor(private breakpointService: BreakpointService) { }
ngOnInit(): void {
this.breakpointService.breakpoint.pipe(takeUntil(this.unsubscriber)).subscribe((result) => {
this.isMobile = !result.matches;
});
}
ngOnDestroy(): void {
this.unsubscriber.next();
this.unsubscriber.complete();
}
back() {
history.back();
}
}
......@@ -87,7 +87,7 @@
<app-chart type="vertical-bar" [dataset]="obj | occupancyDataset | async" [tooltipDisabled]="true"></app-chart>
</button>
<div class="layout content-center">
<a href="#" mat-button color="primary" [disabled]="!enableContrib">Contribuer</a>
<a [routerLink]="['/contribution', obj.stopId, obj.ligne.id, obj.pattern.dir, obj.pattern.desc]" mat-button color="primary" [disabled]="!enableContrib">Contribuer</a>
</div>
</div>
</ng-template>
......
<app-recherche-point [idDialog]="myId" [state]="data"></app-recherche-point>
import { Component, OnInit, Inject } from '@angular/core';
import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
@Component({
selector: 'app-dialog-recherche-point',
templateUrl: './dialog-recherche-point.component.html'
})
export class DialogRecherchePointComponent implements OnInit {
myId: any;
constructor(
public dialogRef: MatDialogRef<DialogRecherchePointComponent>,
@Inject(MAT_DIALOG_DATA) public data: any) {
this.myId = this.dialogRef.id;
}
ngOnInit() {
}
}
<div class="m-toolbar search-point">
<button mat-icon-button aria-label="retour" mat-dialog-close>
<mat-icon>arrow_back</mat-icon>
</button>
<input class="search-bar-input" type="text" #myInput matInput [formControl]="myControl" placeholder="Chercher un lieu, un arrêt..." autocomplete="off" cdkFocusInitial />
<mat-spinner @fade mode="indeterminate" diameter="32" *ngIf="displayLoader"></mat-spinner>
<button @fade class="clear-button" mat-icon-button *ngIf="myInput.value!=''" mat-icon-button aria-label="Vider" (click)="myInput.value=''">
<mat-icon>close</mat-icon>
</button>
</div>
<div class="content">
<app-m-list-wrapper *ngIf="myInput.value?.length" [seeMoreEnable]="rechercheOptions.length > 3" #searchList="mListWrapper">
<mat-action-list class="m-theme flat">
<button mat-list-item *ngFor="let option of rechercheOptions | slice:0:searchList.seeMore ? rechercheOptions.length : 3" (click)="selectPoint(option,true)"
class="dark-overlay-8 light-overlay">
<app-icon-type matListIcon [type]="option.properties.type"></app-icon-type>
<span matLine>{{ option | libelle }}</span>
<span matLine class="texteCommune">{{ option | commune }}</span>
</button>
</mat-action-list>
</app-m-list-wrapper>
<mat-action-list *ngIf="!myInput.value?.length && estPresentRue && geolocationEnable" class="m-theme flat important">
<button mat-list-item (click)="selectPosition()" class="dark-overlay-8 light-overlay">
<mat-icon matListIcon>my_location</mat-icon>
<span matLine>Ma Position</span>
</button>
</mat-action-list>
<app-m-list-wrapper *ngIf="!myInput.value?.length && favoris.length > 0" [seeMoreEnable]="favoris.length > 3" #favoriteList="mListWrapper">
<mat-action-list class="m-theme flat important">
<span matSubheader>Favoris</span>
<ng-container *ngFor="let obj of favoris | slice:0:favoriteList.seeMore ? favoris.length : 3">
<button mat-list-item (click)="selectPoint(obj,false)" class="dark-overlay-8 light-overlay">
<app-icon-type matListIcon aria-hidden="true" [type]="obj.properties.type"></app-icon-type>
<span matLine>{{ obj | libelle }}</span>
<span matLine class="texteCommune">{{ obj | commune }}</span>
</button>
</ng-container>
</mat-action-list>
</app-m-list-wrapper>
<mat-action-list *ngIf="!myInput.value?.length" class="m-theme flat">
<span matSubheader>
Recherches récentes
</span>
<ng-container *ngFor="let obj of recherchesRecentes">
<button mat-list-item (click)="selectPoint(obj,false)" class="dark-overlay-8 light-overlay">
<app-icon-type matListIcon aria-hidden="true" [type]="obj.properties.type"></app-icon-type>
<span matLine>{{ obj | libelle }}</span>
<span matLine class="texteCommune">{{ obj | commune }}</span>
</button>
</ng-container>
</mat-action-list>
</div>
@import "../../../styles/variables";
@import "../../../styles/helpers";
[mat-dialog-close] {
position: absolute;
left: $spacing * 2;
}
.clear-button {
position: absolute;
right: $spacing * 2;
}
mat-spinner {
position: absolute;
top: $spacing;
right: $spacing * 2 + ($spacing / 2);
}
import { Component, OnInit, ViewChild, Input } from '@angular/core';
import { FormControl } from '@angular/forms';
import { MatDialog } from '@angular/material/dialog';
import { debounceTime, switchMap, map, filter, tap, finalize } from 'rxjs/operators';
// import { GeolocationService } from '@services/geolocation.service';
// import { ApiMService } from '@services/api-m.service';
// import { DataService } from '@services/data.service';
// import { FavorisService } from '@services/favoris.service';
import { trigger, transition, style, animate } from '@angular/animations';
import { SearchService } from '@features/search/search.service';
import { HttpClient } from '@angular/common/http';
import { domain } from '@helpers/domain.helpers';
@Component({
selector: 'app-recherche-point',
templateUrl: './recherche-point.component.html',
styleUrls: ['./recherche-point.component.scss'],
animations: [
trigger('fade', [
transition(':enter', [
style({ opacity: 0 }),
animate('250ms', style({ opacity: 1 }))
]),
transition(':leave', [
style({ opacity: 1 }),
animate('250ms', style({ opacity: 0 }))
]),
])
]
})
export class RecherchePointComponent implements OnInit {
myControl = new FormControl();
@ViewChild('myInput', { static: false }) myInput;
rechercheOptions: any[] = [];
favoris: any[] = [];
recherchesRecentes: any[] = [];
@Input() state: any;
@Input() idDialog: any;
estPresentRue = false;
geolocationEnable: boolean;
displayLoader = false;
constructor(
// private apiMService: ApiMService,
// private geolocationService: GeolocationService,
// public dataService: DataService,
// public favorisService: FavorisService,
private searchService: SearchService,
private dialogRef?: MatDialog) {
}
ngOnInit() {
// this.geolocationEnable = this.geolocationService.isAvailable() && this.geolocationService.isEnable();
// this.geolocationService.enable$.subscribe((enable) => this.geolocationEnable = enable);
this.favoris = [];
// const fclust = this.favorisService.getFavoris('clusters');
// const favorisAdresses = this.favorisService.getFavoris('adresses');
// fclust.forEach(f => this.favoris.push(f.object));
// favorisAdresses.forEach(f => this.favoris.push(f.object));
this.favoris = this.favoris.filter((f) => !this.state.types || this.state.types.includes(f.properties.type));
this.recherchesRecentes = this.searchService.recherchesRecentes.filter((f) => !this.state.types || this.state.types.includes(f.properties.type));
let types = '';
if (this.state && this.state.types && this.state.types.length > 0) {
types = this.state.types.join(',');
if (types.indexOf('rue') !== -1) this.estPresentRue = true;
}
this.myControl.valueChanges
.pipe(
debounceTime(300),
filter(query => query.length >= 3),
tap(query => this.rechercheOptions = []),
tap(() => this.displayLoader = true),
switchMap((value: string) => this.searchService.search(value, types)
.pipe(
map((data: any) => {
if (data && Array.isArray(data.features)) {
data.features.sort((a: any, b: any) => {
if (parseInt(a.properties.dist, 10) < parseInt(b.properties.dist, 10))
return 1; // tri decroissant on inverse
if (parseInt(a.properties.dist, 10) > parseInt(b.properties.dist, 10))
return -1;
// a doit être égal à b
return 0;
});
data.features = data.features.slice(0, 30);
return data;
}
return { type: '', features: [] };
}),
finalize(() => this.displayLoader = false)
)
),
).subscribe((data: any) => this.rechercheOptions = data.features);
}
displayFn(point?: any): string | undefined {
return point ? point.properties.LIBELLE : undefined;
}
selectPoint(point: any, addRecherche: boolean) {
if (addRecherche) this.searchService.addRecherchesRecentes(point);
if (point.properties.code === undefined && point.properties.id) {
point.properties.code = point.properties.id;
}
this.dialogRef.getDialogById(this.idDialog).close({ feature: point });
}
selectPosition() {
// this.geolocationService.getCurrentPosition().then((coordinates) => {
// const positionFeature = {
// geometry: {
// coordinates: coordinates,
// type: 'point'
// },
// properties: this.geolocationService.position,
// type: 'position'
// };
// (positionFeature.properties as any).LIBELLE = 'Position';
// (positionFeature.properties as any).type = 'position';
// this.dialogRef.getDialogById(this.idDialog).close({ feature: positionFeature });
// });
}
}
import { Directive, HostListener, Input, Output, EventEmitter } from '@angular/core';
import { MatDialog } from '@angular/material/dialog';
import { DialogRecherchePointComponent } from '@components/dialog-recherche-point/dialog-recherche-point.component';
@Directive({
selector: '[appSearchDialog]'
})
export class SearchDialogDirective {
/**
* A comma separated list of POI type.
*/
@Input() types = 'clusters,lieux,rue,PAR,PKG';
@Output() closed = new EventEmitter<any>();
constructor(private dialog: MatDialog) { }
@HostListener('click') onClick() {
this.dialog.open(DialogRecherchePointComponent, {
panelClass: ['m-theme', 'search-point'],
data: { types: this.types.split(',') }
}).afterClosed().subscribe(result => this.closed.emit(result));
}
}
......@@ -14,13 +14,7 @@ export class LinesService {
constructor(private http: HttpClient) { }
getLines(): Observable<Line[]> {
return this.http.get<Line[]>(`${domain}/api/routers/default/index/routes`, {
params: new HttpParams({
fromObject: {
reseaux: 'TRAM,CHRONO,PROXIMO'
}
})
});
return this.http.get<Line[]>(`${domain}/api/routers/default/index/routes`);
}
find(id: string): Line {
......
import { Line } from '@features/line/line.model';
export interface Pattern {
// BE properties:
id: string;
desc: string;
dir: number;
shortDesc: string;
lastStop: string;
lastStopName: string;
// FE properties:
line?: Line;
stopId: string;
_id: string; // use to remove duplicates.
}
import { Injectable } from '@angular/core';
import { Resolve, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router';
import { LinesResolver } from '@features/lines/lines.resolver';
import { PatternsService } from './patterns.service';
import { LinesService } from '@features/lines/lines.service';
import { groupByField } from '@helpers/array.helper';
@Injectable({
providedIn: 'root'
})
export class PatternsResolver implements Resolve<any> {
constructor(
private patternsService: PatternsService,
private linesResolver: LinesResolver,
private linesService: LinesService
) { }
async resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
await this.linesResolver.resolve(route, state);
return new Promise((resolve) => {
this.patternsService.getPatterns(route.params.cluster).subscribe((response) => {
const patterns = [];
Object.entries(response).forEach(([stopId, _patterns]) => {
_patterns
.map(p => {
const [prefix, line, _, __] = p.id.split(':');
p.line = this.linesService.find(`${prefix}:${line}`);
p._id = `${prefix}:${line}:${p.dir}`;
p.stopId = stopId;
return p;
})
.forEach((pattern) => {
// Remove duplicates.
if (!patterns.find(p => pattern._id === p._id)) {
patterns.push(pattern);
}
});
});
resolve(groupByField(patterns, 'line.id'));
});
});
}
}
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { domain } from '@helpers/domain.helpers';
import { Observable } from 'rxjs';
import { Pattern } from './patterns.model';
@Injectable({
providedIn: 'root'
})
export class PatternsService {
constructor(private http: HttpClient) { }
getPatterns(clusterId: string): Observable<{ [key: string]: Pattern[] }> {
return this.http.get<{ [key: string]: Pattern[] }>(`${domain}/api/clusters/${clusterId}/patterns`);
}
}
import { Line } from '@features/line/line.model'
import { Pattern } from '@features/patterns/patterns.model';
export interface StopTime {
pattern: Pattern;
......@@ -20,14 +21,6 @@ export interface Time {
tripId: number;
}
export interface Pattern {
id: string;
desc: string;
dir: number;
shortDesc: string;
lastStop: string;
lastStopName: string;
}
export interface NextStop {
pattern: Pattern;
......
......@@ -13,7 +13,7 @@ export class RealtimeDataService {
constructor(private http: HttpClient, private linesService: LinesService) { }