Skip to content

Commit

Permalink
feat: add markers clusters (#418)
Browse files Browse the repository at this point in the history
* feat: add markers clusters

* feat: reduce cluster density

* perf: optimize lieux list rendering

* feat: add cluster highlight on hover

* feat: add default zoom level for details page
  • Loading branch information
marc-gavanier committed Jan 10, 2024
1 parent 637efe0 commit 0a7e21e
Show file tree
Hide file tree
Showing 32 changed files with 392 additions and 188 deletions.
2 changes: 2 additions & 0 deletions src/features/cartographie/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,3 +74,5 @@ export const components = [
WebinaireComponent,
SourceFooterComponent
];

export * from './markers/lieu-mediation-numerique-markers/lieu-mediation-numerique-markers.component';
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,18 @@ <h3 class="mb-3 d-none d-print-block">
[center]="[lieuMediationNumerique.localisation.longitude, lieuMediationNumerique.localisation.latitude]">
<app-lieu-mediation-numerique-markers
*ngIf="lieuMediationNumerique.localisation"
[lieuxMediationNumeriques]="[
[lieuxMediationNumeriqueClusters]="[
{
id: lieuMediationNumerique.id,
nom: lieuMediationNumerique.nom,
labels_nationaux: lieuMediationNumerique.labels_nationaux,
latitude: lieuMediationNumerique.localisation.latitude,
longitude: lieuMediationNumerique.localisation.longitude
type: 'Feature',
geometry: {
type: 'Point',
coordinates: [lieuMediationNumerique.localisation.longitude, lieuMediationNumerique.localisation.latitude]
},
properties: {
id: lieuMediationNumerique.id,
nom: lieuMediationNumerique.nom,
labels_nationaux: lieuMediationNumerique.labels_nationaux
}
}
]"></app-lieu-mediation-numerique-markers>
</mgl-map>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,18 @@
<app-lieu-mediation-numerique-markers
*ngIf="localisation"
[displayTooltip]="false"
[lieuxMediationNumeriques]="[
[lieuxMediationNumeriqueClusters]="[
{
id: lieuMediationNumerique.id,
nom: lieuMediationNumerique.nom,
labels_nationaux: lieuMediationNumerique.labels_nationaux,
latitude: localisation.latitude,
longitude: localisation.longitude
type: 'Feature',
geometry: {
type: 'Point',
coordinates: [lieuMediationNumerique.localisation.longitude, lieuMediationNumerique.localisation.latitude]
},
properties: {
id: lieuMediationNumerique.id,
nom: lieuMediationNumerique.nom,
labels_nationaux: lieuMediationNumerique.labels_nationaux
}
}
]"></app-lieu-mediation-numerique-markers>
</mgl-map>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -179,13 +179,18 @@
<app-lieu-mediation-numerique-markers
*ngIf="localisation"
[displayTooltip]="false"
[lieuxMediationNumeriques]="[
[lieuxMediationNumeriqueClusters]="[
{
id: id,
nom: nom,
labels_nationaux: labels_nationaux,
latitude: localisation.latitude,
longitude: localisation.longitude
type: 'Feature',
geometry: {
type: 'Point',
coordinates: [localisation.longitude, localisation.latitude]
},
properties: {
id: id,
nom: nom,
labels_nationaux: labels_nationaux
}
}
]"></app-lieu-mediation-numerique-markers>
</mgl-map>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { ChangeDetectionStrategy, Component, EventEmitter, Inject, Input, Output
import { AbstractControl, FormControl, FormGroup, Validators } from '@angular/forms';
import { LabelNational, Localisation } from '@gouvfr-anct/lieux-de-mediation-numerique';
import { ASSETS_TOKEN, AssetsConfiguration } from '../../../../../root';
import { ModalComponent } from '../../../../core/components';
import { ModalComponent } from '../../../../core';
import { FilterPresentation } from '../../../../core/presenters';
import { OrientationSheetForm, PrescripteurOrientationSheetForm, UsagerOrientationSheetForm } from '../../../models';

Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
<div class="d-print-none my-4 mx-sm-4 mx-2">
<div class="d-print-none py-4 px-sm-4 px-2">
<div class="row g-2">
<a
class="text-decoration-none link-dark link-lieu stretched-link col"
[routerLink]="[lieuMediationNumerique.id, 'details']"
(click)="matomoTracking(lieuMediationNumerique.id)"
(click)="showDetails(lieuMediationNumerique)"
[relativeTo]="route.parent"
queryParamsHandling="preserve"
(mouseover)="enableHover.emit(lieuMediationNumerique.id)"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,8 @@ export class LieuMediationNumeriqueListItemComponent {
return dateMaj > new Date('1970-01-01');
}

public matomoTracking(lieuID: string): void {
this._matomoTracker?.trackEvent('Fiches', 'Début', `Ouverture de fiches - ${lieuID}`);
public showDetails(lieu: LieuMediationNumeriqueListItemPresentation): void {
this._matomoTracker?.trackEvent('Fiches', 'Début', `Ouverture de fiches - ${lieu.id}`);
}

public toLabelNom(label: LabelNational): string | undefined {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ <h2 class="h3 mb-3 pb-1 border-bottom border-dark border-2">
<tbody>
<tr>
<td class="row">
<div class="col-6" *ngFor="let lieuMediationNumerique of lieuxMediationNumerique">
<div class="col-6" *ngFor="let lieuMediationNumerique of lieuxMediationNumerique | slice : 0 : 100">
<app-mediation-numerique-list-item
class="d-block border-bottom h-100 py-1"
[lieuMediationNumerique]="lieuMediationNumerique"></app-mediation-numerique-list-item>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,20 +1,25 @@
<div class="h-100 d-flex flex-column border-end">
<div #container class="list-group list-group-flush flex-grow-1 overflow-auto" tabindex="-1">
<h1 class="visually-hidden">Liste des lieux d'inclusion numérique</h1>
<article
#item
*ngFor="let lieuMediationNumerique of lieuxMediationNumerique; trackBy: trackByLieuId"
[attr.id]="lieuMediationNumerique.id"
[class.list-group-item-hover]="hoverId === lieuMediationNumerique.id"
[ngClass]="{ 'bg-muted-light': lieuMediationNumerique.prive }"
class="list-group-item p-0">
<app-mediation-numerique-list-item
[lieuMediationNumerique]="lieuMediationNumerique"
(showLabel)="showLabel.emit($event)"
(showLabelInvokingContext)="showLabelInvokingContext.emit($event)"
(enableHover)="enableHover.emit($event)"
(disableHover)="disableHover.emit()"></app-mediation-numerique-list-item>
</article>
<cdk-virtual-scroll-viewport itemSize="80" class="h-100">
<article
#item
*cdkVirtualFor="let lieuMediationNumerique of lieuxMediationNumerique; trackBy: trackByLieuId"
[attr.id]="lieuMediationNumerique.id"
[class.list-group-item-hover]="
hoverId === lieuMediationNumerique.id ||
clustersPresenter.lieuxIdsInClusterId($any(hoverId)).includes(lieuMediationNumerique.id)
"
[ngClass]="{ 'bg-muted-light': lieuMediationNumerique.prive }"
class="list-group-item p-0">
<app-mediation-numerique-list-item
[lieuMediationNumerique]="lieuMediationNumerique"
(showLabel)="showLabel.emit($event)"
(showLabelInvokingContext)="showLabelInvokingContext.emit($event)"
(enableHover)="enableHover.emit($event)"
(disableHover)="disableHover.emit()"></app-mediation-numerique-list-item>
</article>
</cdk-virtual-scroll-viewport>
</div>
<div class="bg-primary py-3 px-4 g-0 row align-items-center">
<div class="col-auto">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { ActivatedRoute } from '@angular/router';
import { LabelNational } from '@gouvfr-anct/lieux-de-mediation-numerique';
import { FeatureConfiguration } from '../../../../../root';
import { LieuMediationNumeriqueListItemPresentation } from '../../../presenters';
import { ClustersPresenter } from '../../../../core/presenters/clusters';
import { CartographieLayout } from '../../../layouts';

const itemById =
Expand Down Expand Up @@ -53,7 +54,11 @@ export class LieuxMediationNumeriqueListComponent {

@ViewChildren('item') public items!: QueryList<ElementRef>;

public constructor(public readonly route: ActivatedRoute, public readonly cartographieLayout: CartographieLayout) {}
public constructor(
public readonly route: ActivatedRoute,
public readonly cartographieLayout: CartographieLayout,
public readonly clustersPresenter: ClustersPresenter
) {}

public scrollTo(focusId: string): void {
setTimeout((): void => {
Expand All @@ -63,7 +68,7 @@ export class LieuxMediationNumeriqueListComponent {
top: item.nativeElement.getBoundingClientRect().y - this.container.nativeElement.getBoundingClientRect().y,
behavior: 'smooth'
});
(item?.nativeElement.querySelectorAll('.link-lieu') as NodeListOf<HTMLElement>)[0].focus();
item && (item?.nativeElement.querySelectorAll('.link-lieu') as NodeListOf<HTMLElement>)[0].focus();
});
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,78 +1,108 @@
<ng-container *ngIf="displayTooltip">
<mgl-popup
*ngIf="highlight | async as lieuMediationNumerique"
anchor="bottom"
[offset]="[0, -40]"
[lngLat]="[lieuMediationNumerique.longitude, lieuMediationNumerique.latitude]"
[closeButton]="false">
<div *ngIf="lieuMediationNumerique.prise_rdv" class="text-center text-primary fs-6 p-2" style="background-color: #c0c0ff">
Prise de RDV en ligne disponible
</div>
<div
class="text-center p-3 border-bottom border-3 text-wrap"
[ngClass]="lieuMediationNumerique.status?.label === 'Ouvert' ? 'border-success' : 'border-muted'">
<b class="fs-5">{{ lieuMediationNumerique.nom }}</b>
<div *ngIf="lieuMediationNumerique.status; else noOpeningHours" class="text-muted">
<span [class.text-primary]="lieuMediationNumerique.status.label === 'Ouvert'">
{{ lieuMediationNumerique.status.label }}
</span>
·
<small>{{ lieuMediationNumerique.status.limite }}</small>
<ng-container *ngIf="highlightedLieu | async as highlighted">
<mgl-popup
*ngIf="highlighted.id"
anchor="bottom"
[offset]="[0, -40]"
[lngLat]="[highlighted.longitude, highlighted.latitude]"
[closeButton]="false">
<div *ngIf="highlighted.prise_rdv" class="text-center text-primary fs-6 p-2" style="background-color: #c0c0ff">
Prise de RDV en ligne disponible
</div>
<span class="my-1 fs-4" *ngIf="lieuMediationNumerique.prive">Ce lieu n'accueille pas de public</span>
<ng-template #noOpeningHours>
<div class="text-muted">Horaires inconnus</div>
</ng-template>
</div>
</mgl-popup>
</ng-container>
<ng-container *ngFor="let lieuMediationNumerique of lieuxMediationNumeriques; index as i; trackBy: trackByLieuId">
<mgl-marker
[lngLat]="[lieuMediationNumerique.longitude, lieuMediationNumerique.latitude]"
[className]="hoverId === lieuMediationNumerique.id || selectedId === lieuMediationNumerique.id ? 'marker-hover' : ''">
<button
tabindex="-1"
[ngClass]="{ 'opacity-50': lieuMediationNumerique.prive }"
class="btn"
(click)="showDetails.emit(lieuMediationNumerique)"
(mouseenter)="highlight.emit(lieuMediationNumerique)"
(mouseleave)="highlight.emit()">
<svg
[ngClass]="lieuMediationNumerique.labels_nationaux?.includes($any('CNFS')) ? 'marker-cnfs' : 'marker-default'"
[class.marker-status-open]="lieuMediationNumerique.status?.label === 'Ouvert'"
[class.marker-status-closed]="lieuMediationNumerique.status?.label === 'Fermé'"
[class.marker-status-unknown]="
lieuMediationNumerique.status?.label !== 'Fermé' && lieuMediationNumerique.status?.label !== 'Ouvert'
"
width="50"
height="85"
viewBox="0 0 45 80"
fill="none"
xmlns="http://www.w3.org/2000/svg">
<ellipse class="marker-base" cx="22.75" cy="59.5" rx="11.3" ry="4" />
<g *ngIf="lieuMediationNumerique.labels_nationaux?.includes($any('CNFS')); else defaultMarker" class="marker-body">
<path class="marker-shape" d="m23.527 54.153 13.6-23.6v-16.7l-14.4-8.3-14.4 8.3v16.7l13.6 23.6z" />
<path class="marker-shape-out" d="m32.327 16.653-9.6-5.5-9.6 5.5v11.1l9.6 5.5 9.6-5.5v-3.2h-5.5v-4.7h5.5z" />
<path class="marker-shape-in" d="M18.627 19.853v4.7l4.1 2.4 4.1-2.4v-4.7l-4.1-2.4z" />
</g>
<ng-template #defaultMarker>
<g class="marker-body">
<path
class="marker-shape"
d="M22.727 6c-8.8 0-16 7.2-16 16s16 32 16 32 16-23.2 16-32-7.2-16-16-16zm0 21c-2.9 0-5.3-2.4-5.3-5.3s2.4-5.3 5.3-5.3 5.3 2.4 5.3 5.3-2.4 5.3-5.3 5.3z" />
</g>
<div
class="text-center p-3 border-bottom border-3 text-wrap"
[ngClass]="highlighted.status?.label === 'Ouvert' ? 'border-success' : 'border-muted'">
<b class="fs-5">{{ highlighted.nom }}</b>
<div *ngIf="highlighted.status; else noOpeningHours" class="text-muted">
<span [class.text-primary]="highlighted.status.label === 'Ouvert'">
{{ highlighted.status.label }}
</span>
·
<small>{{ highlighted.status.limite }}</small>
</div>
<span class="my-1 fs-4" *ngIf="highlighted.prive">Ce lieu n'accueille pas de public</span>
<ng-template #noOpeningHours>
<div class="text-muted">Horaires inconnus</div>
</ng-template>
<g *ngIf="lieuMediationNumerique.prise_rdv">
<path d="M34.257 69.177v8h12l-4-4 4-4z" fill="#c0c0ff" />
<path d="m34.257 77.177 4-4h-4z" fill="#000091" />
<path d="M11.198 69.177v8h-12l4-4-4-4z" fill="#c0c0ff" />
<path d="m11.198 77.177-4-4h4z" fill="#000091" />
<path fill="#c0c0ff" d="M7.198 61.176h31.059v12H7.197Z" />
</div>
</mgl-popup>
</ng-container>
</ng-container>
<ng-container *ngFor="let cluster of lieuxMediationNumeriqueClusters">
<ng-container *ngIf="cluster.properties['cluster'] === true; else lieuMarker">
<mgl-marker
[lngLat]="$any(cluster.geometry.coordinates)"
[className]="clustersPresenter.clusterIdFromLieuId(hoverId) === cluster.properties['cluster_id'] ? 'marker-hover' : ''">
<button
tabindex="-1"
class="btn text-white marker-cluster"
(click)="selectCluster.emit($any(cluster))"
(mouseenter)="highlight.emit(cluster.properties['cluster_id'])"
(mouseleave)="highlight.emit()">
<svg width="52" height="64" viewBox="0 0 52 64" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M12.845 70.706v-7.059h2.085c1.5 0 2.427.797 2.427 2.108 0 .857-.4 1.492-1.102 1.825l2.135 3.126h-1.628l-1.822-2.854h-.711v2.854zm2.163-5.869h-.78v1.825h.78c.585 0 .926-.353.926-.927 0-.535-.341-.898-.926-.898zm4.161 5.87v-7.06h2.68c2.163 0 3.625 1.624 3.625 3.53 0 1.906-1.462 3.53-3.625 3.53zm2.631-5.759h-1.315v4.457H21.8c1.267 0 2.183-.978 2.183-2.228 0-1.26-.916-2.229-2.183-2.229zm3.833-1.3h1.471l2.017 5.515 2.017-5.516h1.472l-2.583 7.06h-1.812z"
d="M49.642 26.497C49.642 47.855 26 61.137 26 61.137S2.358 47.854 2.358 26.496c0-13.09 10.584-23.7 23.642-23.7s23.642 10.61 23.642 23.7z"
fill="#000091" />
</g>
</svg>
</button>
</mgl-marker>
<path
d="M49.642 26.497C49.642 47.855 26 61.137 26 61.137S2.358 47.854 2.358 26.496c0-13.09 10.584-23.7 23.642-23.7s23.642 10.61 23.642 23.7z"
stroke="#fff" />
</svg>
<div class="marker-cluster-fg d-flex">
<span class="m-auto text-white fw-bold fs-6 mt-4">
{{ cluster.properties['point_count'] ?? 0 }}
</span>
</div>
</button>
</mgl-marker>
</ng-container>
<ng-template #lieuMarker>
<mgl-marker
[lngLat]="$any(cluster.geometry.coordinates)"
[className]="hoverId === cluster.properties['id'] || selectedId === cluster.properties['id'] ? 'marker-hover' : ''">
<button
tabindex="-1"
[ngClass]="{ 'opacity-50': cluster.properties['prive'] }"
class="btn"
(click)="showDetails.emit($any(cluster.properties))"
(mouseenter)="highlight.emit(cluster.properties['id'])"
(mouseleave)="highlight.emit()">
<svg
[ngClass]="cluster.properties['labels_nationaux']?.includes($any('CNFS')) ? 'marker-cnfs' : 'marker-default'"
[class.marker-status-open]="cluster.properties['status']?.label === 'Ouvert'"
[class.marker-status-closed]="cluster.properties['status']?.label === 'Fermé'"
[class.marker-status-unknown]="
cluster.properties['status']?.label !== 'Fermé' && cluster.properties['status']?.label !== 'Ouvert'
"
width="50"
height="85"
viewBox="0 0 45 80"
fill="none"
xmlns="http://www.w3.org/2000/svg">
<ellipse class="marker-base" cx="22.75" cy="59.5" rx="11.3" ry="4" />
<g *ngIf="cluster.properties['labels_nationaux']?.includes($any('CNFS')); else defaultMarker" class="marker-body">
<path class="marker-shape" d="m23.527 54.153 13.6-23.6v-16.7l-14.4-8.3-14.4 8.3v16.7l13.6 23.6z" />
<path class="marker-shape-out" d="m32.327 16.653-9.6-5.5-9.6 5.5v11.1l9.6 5.5 9.6-5.5v-3.2h-5.5v-4.7h5.5z" />
<path class="marker-shape-in" d="M18.627 19.853v4.7l4.1 2.4 4.1-2.4v-4.7l-4.1-2.4z" />
</g>
<ng-template #defaultMarker>
<g class="marker-body">
<path
class="marker-shape"
d="M22.727 6c-8.8 0-16 7.2-16 16s16 32 16 32 16-23.2 16-32-7.2-16-16-16zm0 21c-2.9 0-5.3-2.4-5.3-5.3s2.4-5.3 5.3-5.3 5.3 2.4 5.3 5.3-2.4 5.3-5.3 5.3z" />
</g>
</ng-template>
<g *ngIf="cluster.properties['prise_rdv']">
<path d="M34.257 69.177v8h12l-4-4 4-4z" fill="#c0c0ff" />
<path d="m34.257 77.177 4-4h-4z" fill="#000091" />
<path d="M11.198 69.177v8h-12l4-4-4-4z" fill="#c0c0ff" />
<path d="m11.198 77.177-4-4h4z" fill="#000091" />
<path fill="#c0c0ff" d="M7.198 61.176h31.059v12H7.197Z" />
<path
d="M12.845 70.706v-7.059h2.085c1.5 0 2.427.797 2.427 2.108 0 .857-.4 1.492-1.102 1.825l2.135 3.126h-1.628l-1.822-2.854h-.711v2.854zm2.163-5.869h-.78v1.825h.78c.585 0 .926-.353.926-.927 0-.535-.341-.898-.926-.898zm4.161 5.87v-7.06h2.68c2.163 0 3.625 1.624 3.625 3.53 0 1.906-1.462 3.53-3.625 3.53zm2.631-5.759h-1.315v4.457H21.8c1.267 0 2.183-.978 2.183-2.228 0-1.26-.916-2.229-2.183-2.229zm3.833-1.3h1.471l2.017 5.515 2.017-5.516h1.472l-2.583 7.06h-1.812z"
fill="#000091" />
</g>
</svg>
</button>
</mgl-marker>
</ng-template>
</ng-container>

This file was deleted.

Loading

0 comments on commit 0a7e21e

Please sign in to comment.