Commit 2251f654 authored by Léonard Treille's avatar Léonard Treille
Browse files

End contribute features

parent 7ddc3edc
......@@ -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",
......
......@@ -8,6 +8,7 @@ import { ContributionFirstStepComponent } from '@pages/contribution/contribution
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 = [
{
......@@ -23,11 +24,11 @@ const routes: Routes = [
}
},
{
path: 'contribution', component: ContributionWrapperComponent,
path: 'contribution', component: ContributionWrapperComponent, data: { totalStep: 3 },
children: [
{ path: '', component: ContributionFirstStepComponent },
{ path: ':stop', component: ContributionSecondStepComponent },
{ path: ':stop/:line/:dir', component: ContributionFinalStepComponent },
{ 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 } },
]
},
];
......
......@@ -17,6 +17,7 @@ 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';
......@@ -25,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';
......@@ -49,6 +47,7 @@ import { BackButtonComponent } from './components/back-button/back-button.compon
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: [
......@@ -56,9 +55,6 @@ import { SearchItemDirective } from './features/search/search-item.directive';
HorairesLignesComponent,
LogoLigneComponent,
GroupByPipe,
SearchDialogDirective,
DialogRecherchePointComponent,
RecherchePointComponent,
MListWrapperComponent,
IconTypeComponent,
CommunePipe,
......@@ -80,7 +76,8 @@ import { SearchItemDirective } from './features/search/search-item.directive';
BackButtonComponent,
SearchInputComponent,
SearchListComponent,
SearchItemDirective
SearchItemDirective,
IdPipe
],
imports: [
BrowserModule,
......@@ -102,9 +99,7 @@ import { SearchItemDirective } from './features/search/search-item.directive';
MatInputModule,
MatProgressBarModule,
MatToolbarModule,
],
entryComponents: [
DialogRecherchePointComponent,
MatSnackBarModule,
],
providers: [
WINDOW_PROVIDERS,
......
......@@ -87,7 +87,7 @@
<app-chart type="vertical-bar" [dataset]="obj | occupancyDataset | async" [tooltipDisabled]="true"></app-chart>
</button>
<div class="layout content-center">
<a routerLink="/contribution/lorem/ipsum/dolor" 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>
<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;
}
[matSuffix] {
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 { trigger, transition, style, animate } from '@angular/animations';
import { SearchService } from '@features/search/search.service';
@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 searchService: SearchService,
private dialogRef?: MatDialog) {
}
ngOnInit() {
this.favoris = [];
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 });
}
}
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) { }
getStoptimes(type: string, id: string, linesId: string[]): Observable<StopTime[]> {
getStoptimes(type: string, id: string, linesId: string[] = []): Observable<StopTime[]> {
const params: any = {};
if (linesId.length > 0) {
params.route = linesId.join(',');
......@@ -31,8 +31,8 @@ export class RealtimeDataService {
}
parseResultPP(response: StopTime[], nbEltToDisplay: number) {
if (!response) return;
if (this.linesService.lines.length < 1) {
if (!response) return [];
if (!this.linesService.lines || this.linesService.lines.length < 1) {
console.error('The realtime-data service need linesService.lines to be populated => require the lineResolver or the LinesResolver on this route.');
return;
}
......@@ -40,6 +40,9 @@ export class RealtimeDataService {
response.forEach(stopTime => {
const lineId = stopTime.pattern.id.split(':')[0] + ':' + stopTime.pattern.id.split(':')[1];
const line = this.linesService.find(lineId);
if (!line) {
return;
}
const desc = stopTime.pattern.desc;
if (!nextStops[desc]) {
const city = stopTime.pattern.lastStopName && stopTime.pattern.lastStopName.includes(',') ? stopTime.pattern.lastStopName.split(',')[0] : undefined;
......
......@@ -7,7 +7,7 @@
<div matSuffix @fade *ngIf="displayLoader" class="fixed">
<mat-spinner mode="indeterminate" diameter="24"></mat-spinner>
</div>
<button matSuffix @fade class="fixed clear-button" mat-icon-button *ngIf="input.value!=''" mat-icon-button aria-label="Vider" (click)="input.value=''">
<button matSuffix @fade class="fixed clear-button" mat-icon-button *ngIf="input.value!=''" mat-icon-button aria-label="Vider" (click)="clear()">
<mat-icon>close</mat-icon>
</button>
</mat-form-field>
......@@ -4,6 +4,7 @@ import { debounceTime, filter, tap, switchMap, finalize, map } from 'rxjs/operat
import { SearchService } from '../search.service';
import { SearchResponse } from '../search.model';
import { trigger, transition, style, animate } from '@angular/animations';
import { of } from 'rxjs';
@Component({
selector: 'app-search-input',
......@@ -48,9 +49,15 @@ export class SearchInputComponent implements OnInit {
this.searchControl.valueChanges
.pipe(
debounceTime(300),
filter(query => query.length >= 3),
// filter(query => query.length >= 3),
tap(() => this.displayLoader = true),
switchMap((value: string) => this.searchService.search(value, this.types)),
switchMap((value: string) => {
if (value.length >= 3) {
return this.searchService.search(value, this.types);
} else {
return of({ features: [] });
}
}),
map((data: SearchResponse) => {
if (data && Array.isArray(data.features)) {
data.features.sort((a: any, b: any) => {
......@@ -72,4 +79,8 @@ export class SearchInputComponent implements OnInit {
});
}
clear() {
this.searchControl.patchValue('');
}
}
import { Directive, ElementRef } from '@angular/core';
import { Directive, ElementRef, HostListener, Input } from '@angular/core';
@Directive({
selector: '[appSearchItem], [searchItem], [search-item]'
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment