diff --git a/frontend/OSM_OAUTH_SETUP.md b/frontend/OSM_OAUTH_SETUP.md
new file mode 100644
index 0000000..4352cb9
--- /dev/null
+++ b/frontend/OSM_OAUTH_SETUP.md
@@ -0,0 +1,41 @@
+# Configuration OAuth2 OpenStreetMap
+
+## Variables d'environnement requises
+
+Pour utiliser l'authentification OSM, vous devez configurer les variables suivantes :
+
+### Frontend (environments/environment.ts)
+```typescript
+export const environment = {
+ production: false,
+ osmClientId: 'your_osm_client_id_here',
+ osmClientSecret: 'your_osm_client_secret_here', // Ne pas utiliser côté client
+ apiBaseUrl: 'http://localhost:5000'
+};
+```
+
+### Backend (.env)
+```bash
+OSM_CLIENT_ID=your_osm_client_id_here
+OSM_CLIENT_SECRET=your_osm_client_secret_here
+API_BASE_URL=http://localhost:5000
+```
+
+## Configuration OSM
+
+1. Allez sur https://www.openstreetmap.org/user/your_username/oauth_clients
+2. Créez une nouvelle application OAuth
+3. Configurez l'URL de redirection : `http://localhost:4200/oauth/callback`
+4. Copiez le Client ID et Client Secret
+
+## Fonctionnalités
+
+- Connexion/déconnexion OSM
+- Persistance des données utilisateur en localStorage
+- Ajout automatique du pseudo OSM dans `last_modified_by` pour les nouveaux événements
+- Interface utilisateur pour gérer l'authentification
+
+## Sécurité
+
+⚠️ **Important** : Le Client Secret ne doit jamais être exposé côté client.
+L'échange du code d'autorisation contre un token d'accès doit se faire côté serveur.
diff --git a/frontend/public/static/oedb.png b/frontend/public/static/oedb.png
new file mode 100644
index 0000000..6cda090
Binary files /dev/null and b/frontend/public/static/oedb.png differ
diff --git a/frontend/src/app/app.routes.ts b/frontend/src/app/app.routes.ts
index 8886554..792b582 100644
--- a/frontend/src/app/app.routes.ts
+++ b/frontend/src/app/app.routes.ts
@@ -2,6 +2,7 @@ import { Routes } from '@angular/router';
import {Home} from './pages/home/home';
import { Agenda } from './pages/agenda/agenda';
import { NouvellesCategories } from './pages/nouvelles-categories/nouvelles-categories';
+import { UnlocatedEventsPage } from './pages/unlocated-events/unlocated-events';
export const routes: Routes = [
{
@@ -15,5 +16,9 @@ export const routes: Routes = [
{
path : 'nouvelles-categories',
component: NouvellesCategories
+ },
+ {
+ path : 'unlocated-events',
+ component: UnlocatedEventsPage
}
];
diff --git a/frontend/src/app/forms/osm/osm.html b/frontend/src/app/forms/osm/osm.html
index 9fedcb8..65d6c25 100644
--- a/frontend/src/app/forms/osm/osm.html
+++ b/frontend/src/app/forms/osm/osm.html
@@ -1,17 +1,35 @@
-
- osm works!
-
-
- @if(isLogginIn){
-
- {{osmPseudo}}
-
-
-}
-@else{
-
- pas connecté
-
-
-}
-
\ No newline at end of file
+
+ @if (isAuthenticated) {
+
+
+ @if (currentUser?.img?.href) {
+
![]()
+ } @else {
+
👤
+ }
+
+
+
{{getUsername()}}
+
+ {{currentUser?.changesets?.count || 0}} changesets
+
+
+
+
+ } @else {
+
+
+
Connectez-vous à votre compte OpenStreetMap pour :
+
+ - Ajouter automatiquement votre pseudo aux événements créés
+ - Bénéficier de fonctionnalités avancées
+
+
+
+
+ }
+
\ No newline at end of file
diff --git a/frontend/src/app/forms/osm/osm.scss b/frontend/src/app/forms/osm/osm.scss
index e69de29..eef128f 100644
--- a/frontend/src/app/forms/osm/osm.scss
+++ b/frontend/src/app/forms/osm/osm.scss
@@ -0,0 +1,114 @@
+.osm-auth {
+ padding: 15px;
+ border: 1px solid #e9ecef;
+ border-radius: 8px;
+ background: #f8f9fa;
+ margin-bottom: 15px;
+
+ .user-info {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+
+ .user-avatar {
+ .avatar {
+ width: 40px;
+ height: 40px;
+ border-radius: 50%;
+ object-fit: cover;
+ border: 2px solid #007bff;
+ }
+
+ .avatar-placeholder {
+ width: 40px;
+ height: 40px;
+ border-radius: 50%;
+ background: #007bff;
+ color: white;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 18px;
+ }
+ }
+
+ .user-details {
+ flex: 1;
+
+ .username {
+ font-weight: 600;
+ color: #333;
+ margin-bottom: 4px;
+ }
+
+ .user-stats {
+ font-size: 12px;
+ color: #666;
+
+ .stat {
+ background: #e9ecef;
+ padding: 2px 6px;
+ border-radius: 12px;
+ margin-right: 8px;
+ }
+ }
+ }
+ }
+
+ .login-prompt {
+ text-align: center;
+
+ .login-text {
+ margin-bottom: 15px;
+
+ p {
+ margin: 0 0 10px 0;
+ color: #555;
+ font-size: 14px;
+ }
+
+ ul {
+ margin: 0;
+ padding-left: 20px;
+ text-align: left;
+ font-size: 13px;
+ color: #666;
+
+ li {
+ margin-bottom: 4px;
+ }
+ }
+ }
+
+ .btn {
+ padding: 10px 20px;
+ border: none;
+ border-radius: 6px;
+ cursor: pointer;
+ font-size: 14px;
+ font-weight: 500;
+ transition: all 0.2s ease;
+
+ &.btn-primary {
+ background: #007bff;
+ color: white;
+
+ &:hover {
+ background: #0056b3;
+ transform: translateY(-1px);
+ }
+ }
+
+ &.btn-outline {
+ background: transparent;
+ color: #6c757d;
+ border: 1px solid #6c757d;
+
+ &:hover {
+ background: #6c757d;
+ color: white;
+ }
+ }
+ }
+ }
+}
diff --git a/frontend/src/app/forms/osm/osm.ts b/frontend/src/app/forms/osm/osm.ts
index 1a03de3..7562b39 100644
--- a/frontend/src/app/forms/osm/osm.ts
+++ b/frontend/src/app/forms/osm/osm.ts
@@ -1,20 +1,44 @@
-import { Component } from '@angular/core';
+import { Component, inject, OnInit, OnDestroy } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { OsmAuth, OsmUser } from '../../services/osm-auth';
+import { Subscription } from 'rxjs';
@Component({
selector: 'app-osm',
- imports: [],
+ standalone: true,
+ imports: [CommonModule],
templateUrl: './osm.html',
styleUrl: './osm.scss'
})
-export class Osm {
- osmPseudo: string='';
- isLogginIn: any = false;
+export class Osm implements OnInit, OnDestroy {
+ private osmAuth = inject(OsmAuth);
+ private subscription?: Subscription;
+
+ currentUser: OsmUser | null = null;
+ isAuthenticated = false;
- logout() {
+ ngOnInit() {
+ this.subscription = this.osmAuth.currentUser$.subscribe(user => {
+ this.currentUser = user;
+ this.isAuthenticated = !!user;
+ });
+ }
+ ngOnDestroy() {
+ if (this.subscription) {
+ this.subscription.unsubscribe();
+ }
}
login() {
+ this.osmAuth.initiateOAuthLogin();
+ }
+ logout() {
+ this.osmAuth.logout();
+ }
+
+ getUsername(): string {
+ return this.osmAuth.getUsername() || '';
}
}
diff --git a/frontend/src/app/maps/all-events/all-events.ts b/frontend/src/app/maps/all-events/all-events.ts
index 338a4ef..a13dd7f 100644
--- a/frontend/src/app/maps/all-events/all-events.ts
+++ b/frontend/src/app/maps/all-events/all-events.ts
@@ -175,7 +175,7 @@ export class AllEvents implements OnInit, OnDestroy {
} else if (coords.length === 4) {
const maplibregl = (window as any).maplibregl;
const bounds = new maplibregl.LngLatBounds([coords[0], coords[1]], [coords[2], coords[3]]);
- this.map.fitBounds(bounds, { padding: 40 });
+ // this.map.fitBounds(bounds, { padding: 40 });
}
}
} catch {}
@@ -237,18 +237,26 @@ export class AllEvents implements OnInit, OnDestroy {
el.style.boxShadow = '0 0 0 4px rgba(25,118,210,0.25)';
el.style.borderRadius = '50%';
}
- el.addEventListener('click', () => {
+ const popupHtml = this.buildPopupHtml(p, (p && (p.id ?? p.uuid)) ?? f?.id);
+ const marker = new maplibregl.Marker({ element: el })
+ .setLngLat(coords)
+ .setPopup(new maplibregl.Popup({
+ offset: 12,
+ closeOnClick: false, // Empêcher la fermeture au clic sur la carte
+ closeButton: true
+ }).setHTML(popupHtml))
+ .addTo(this.map);
+
+ el.addEventListener('click', (e) => {
+ e.stopPropagation(); // Empêcher la propagation du clic vers la carte
+ // Ouvrir la popup du marqueur
+ marker.togglePopup();
this.select.emit({
id: fid,
properties: p,
geometry: { type: 'Point', coordinates: coords }
});
});
- const popupHtml = this.buildPopupHtml(p, (p && (p.id ?? p.uuid)) ?? f?.id);
- const marker = new maplibregl.Marker({ element: el })
- .setLngLat(coords)
- .setPopup(new maplibregl.Popup({ offset: 12 }).setHTML(popupHtml))
- .addTo(this.map);
const popup = marker.getPopup && marker.getPopup();
if (popup && popup.on) {
@@ -272,17 +280,15 @@ export class AllEvents implements OnInit, OnDestroy {
bounds.extend(coords);
});
- // Ne pas faire de fitBounds lors du chargement initial si on a des paramètres URL
+ // Ne faire fitBounds que lors du chargement initial et seulement si pas de paramètres URL
if (!bounds.isEmpty() && this.isInitialLoad) {
const hasUrlParams = this.route.snapshot.queryParams['lat'] || this.route.snapshot.queryParams['lon'] || this.route.snapshot.queryParams['zoom'];
if (!hasUrlParams) {
- this.map.fitBounds(bounds, { padding: 40, maxZoom: 12 });
+ // this.map.fitBounds(bounds, { padding: 40, maxZoom: 12 });
}
this.isInitialLoad = false;
- } else if (!bounds.isEmpty() && !this.isInitialLoad) {
- // Pour les mises à jour suivantes, on peut faire un fitBounds léger
- this.map.fitBounds(bounds, { padding: 40, maxZoom: 12 });
}
+ // Supprimer le fitBounds automatique lors des mises à jour pour éviter le dézoom
}
private buildMarkerElement(props: any): HTMLDivElement {
@@ -365,15 +371,54 @@ export class AllEvents implements OnInit, OnDestroy {
private buildPopupHtml(props: any, id?: any): string {
const title = this.escapeHtml(String(props?.name || props?.label || props?.what || 'évènement'));
const titleId = typeof id !== 'undefined' ? String(id) : '';
- const rows = Object.keys(props || {}).sort().map(k => {
+
+ // Informations principales à afficher en priorité
+ const mainInfo = [];
+ if (props?.what) mainInfo.push({ key: 'Type', value: props.what });
+ if (props?.where) mainInfo.push({ key: 'Lieu', value: props.where });
+ if (props?.start) mainInfo.push({ key: 'Début', value: this.formatDate(props.start) });
+ if (props?.stop) mainInfo.push({ key: 'Fin', value: this.formatDate(props.stop) });
+ if (props?.url) mainInfo.push({ key: 'Lien', value: `Voir l'événement` });
+
+ const mainRows = mainInfo.map(info =>
+ `${this.escapeHtml(info.key)} | ${info.value} |
`
+ ).join('');
+
+ // Autres propriétés
+ const otherProps = Object.keys(props || {})
+ .filter(k => !['name', 'label', 'what', 'where', 'start', 'stop', 'url', 'id', 'uuid'].includes(k))
+ .sort();
+
+ const otherRows = otherProps.map(k => {
const v = props[k];
- const value = typeof v === 'object' ? `${this.escapeHtml(JSON.stringify(v, null, 2))}
` : this.escapeHtml(String(v));
- return `${this.escapeHtml(k)} | ${value} |
`;
+ const value = typeof v === 'object' ? `${this.escapeHtml(JSON.stringify(v, null, 2))}
` : this.escapeHtml(String(v));
+ return `${this.escapeHtml(k)} | ${value} |
`;
}).join('');
- const clickable = `
-
${title}
+
+ const clickable = `
`;
- return `
`;
+
+ return `
+ ${clickable}
+
+ ${otherRows ? `
Plus de détails
` : ''}
+
`;
+ }
+
+ private formatDate(dateStr: string): string {
+ try {
+ const date = new Date(dateStr);
+ return date.toLocaleString('fr-FR', {
+ year: 'numeric',
+ month: 'short',
+ day: 'numeric',
+ hour: '2-digit',
+ minute: '2-digit'
+ });
+ } catch {
+ return dateStr;
+ }
}
private escapeHtml(s: string): string {
diff --git a/frontend/src/app/page/unlocated-events/unlocated-events.html b/frontend/src/app/page/unlocated-events/unlocated-events.html
new file mode 100644
index 0000000..3ec8458
--- /dev/null
+++ b/frontend/src/app/page/unlocated-events/unlocated-events.html
@@ -0,0 +1 @@
+
unlocated-events works!
diff --git a/frontend/src/app/page/unlocated-events/unlocated-events.scss b/frontend/src/app/page/unlocated-events/unlocated-events.scss
new file mode 100644
index 0000000..e69de29
diff --git a/frontend/src/app/page/unlocated-events/unlocated-events.spec.ts b/frontend/src/app/page/unlocated-events/unlocated-events.spec.ts
new file mode 100644
index 0000000..ea13b85
--- /dev/null
+++ b/frontend/src/app/page/unlocated-events/unlocated-events.spec.ts
@@ -0,0 +1,23 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { UnlocatedEvents } from './unlocated-events';
+
+describe('UnlocatedEvents', () => {
+ let component: UnlocatedEvents;
+ let fixture: ComponentFixture
;
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ imports: [UnlocatedEvents]
+ })
+ .compileComponents();
+
+ fixture = TestBed.createComponent(UnlocatedEvents);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/frontend/src/app/page/unlocated-events/unlocated-events.ts b/frontend/src/app/page/unlocated-events/unlocated-events.ts
new file mode 100644
index 0000000..c8e6537
--- /dev/null
+++ b/frontend/src/app/page/unlocated-events/unlocated-events.ts
@@ -0,0 +1,11 @@
+import { Component } from '@angular/core';
+
+@Component({
+ selector: 'app-unlocated-events',
+ imports: [],
+ templateUrl: './unlocated-events.html',
+ styleUrl: './unlocated-events.scss'
+})
+export class UnlocatedEvents {
+
+}
diff --git a/frontend/src/app/pages/agenda/agenda.html b/frontend/src/app/pages/agenda/agenda.html
index 4eae446..1d16b8d 100644
--- a/frontend/src/app/pages/agenda/agenda.html
+++ b/frontend/src/app/pages/agenda/agenda.html
@@ -22,9 +22,52 @@
(click)="setView(CalendarView.Day)">
Jour
+
+
+ @if (showFiltersPanel) {
+
+
Filtres d'événements
+
+
+
+
+
+
+
Types d'événements
+
+ @for (eventType of availableEventTypes; track eventType) {
+
+ }
+
+
+
+
+
+
+
+ }
+
;
events: OedbEvent[] = [];
+ filteredEvents: OedbEvent[] = [];
calendarEvents: CalendarEvent[] = [];
selectedEvent: OedbEvent | null = null;
showSidePanel = false;
+ showFiltersPanel = false;
view: CalendarView = CalendarView.Month;
viewDate: Date = new Date();
oedbPresets = oedb.presets.what;
+ // Propriétés pour les filtres
+ hideTrafficEvents = true; // Par défaut, masquer les événements de type traffic
+ selectedEventTypes: string[] = [];
+ availableEventTypes: string[] = [];
+
// Exposer CalendarView pour l'utiliser dans le template
CalendarView = CalendarView;
@@ -72,12 +80,44 @@ export class Agenda implements OnInit {
this.oedbApi.getEvents(params).subscribe((response: any) => {
this.events = Array.isArray(response?.features) ? response.features : [];
- this.organizeEventsByDay();
+ this.updateAvailableEventTypes();
+ this.applyFilters();
});
}
+ updateAvailableEventTypes() {
+ const eventTypes = new Set();
+ this.events.forEach(event => {
+ if (event?.properties?.what) {
+ eventTypes.add(event.properties.what);
+ }
+ });
+ this.availableEventTypes = Array.from(eventTypes).sort();
+ }
+
+ applyFilters() {
+ let filtered = [...this.events];
+
+ // Filtre par défaut : masquer les événements de type traffic
+ if (this.hideTrafficEvents) {
+ filtered = filtered.filter(event =>
+ !event?.properties?.what?.startsWith('traffic.')
+ );
+ }
+
+ // Filtre par types d'événements sélectionnés
+ if (this.selectedEventTypes.length > 0) {
+ filtered = filtered.filter(event =>
+ this.selectedEventTypes.includes(event?.properties?.what || '')
+ );
+ }
+
+ this.filteredEvents = filtered;
+ this.organizeEventsByDay();
+ }
+
organizeEventsByDay() {
- this.calendarEvents = this.events.map(event => {
+ this.calendarEvents = this.filteredEvents.map(event => {
const eventDate = this.getEventDate(event);
const preset = this.getEventPreset(event);
@@ -220,4 +260,33 @@ export class Agenda implements OnInit {
}: CalendarEventTimesChangedEvent): void {
console.log('Event times changed:', event, newStart, newEnd);
}
+
+ toggleFiltersPanel() {
+ this.showFiltersPanel = !this.showFiltersPanel;
+ }
+
+ onHideTrafficChange() {
+ this.applyFilters();
+ }
+
+ onEventTypeChange(eventType: string, checked: boolean) {
+ if (checked) {
+ if (!this.selectedEventTypes.includes(eventType)) {
+ this.selectedEventTypes.push(eventType);
+ }
+ } else {
+ this.selectedEventTypes = this.selectedEventTypes.filter(type => type !== eventType);
+ }
+ this.applyFilters();
+ }
+
+ isEventTypeSelected(eventType: string): boolean {
+ return this.selectedEventTypes.includes(eventType);
+ }
+
+ clearAllFilters() {
+ this.selectedEventTypes = [];
+ this.hideTrafficEvents = true;
+ this.applyFilters();
+ }
}
\ No newline at end of file
diff --git a/frontend/src/app/pages/home/home.html b/frontend/src/app/pages/home/home.html
index 0b84fe1..8ecd971 100644
--- a/frontend/src/app/pages/home/home.html
+++ b/frontend/src/app/pages/home/home.html
@@ -2,7 +2,7 @@
@if (!showTable) {
} @else {
@@ -67,7 +78,7 @@
- @for (f of features; track f.id) {
+ @for (f of filteredFeatures; track f.id) {
{{f?.properties?.what}} |
{{f?.properties?.label || f?.properties?.name}} |
diff --git a/frontend/src/app/pages/home/home.scss b/frontend/src/app/pages/home/home.scss
index 26a2afd..4bd892b 100644
--- a/frontend/src/app/pages/home/home.scss
+++ b/frontend/src/app/pages/home/home.scss
@@ -4,7 +4,7 @@
.layout {
display: grid;
- grid-template-columns: 340px 1fr;
+ grid-template-columns: 400px 1fr;
grid-template-rows: 100vh;
gap: 0;
}
@@ -14,6 +14,7 @@
border-right: 1px solid rgba(0,0,0,0.06);
box-shadow: 2px 0 12px rgba(0,0,0,0.03);
padding: 16px;
+ padding-bottom: 150px;
overflow: auto;
}
diff --git a/frontend/src/app/pages/home/home.ts b/frontend/src/app/pages/home/home.ts
index 0f51efa..0159b37 100644
--- a/frontend/src/app/pages/home/home.ts
+++ b/frontend/src/app/pages/home/home.ts
@@ -6,6 +6,8 @@ import { AllEvents } from '../../maps/all-events/all-events';
import { EditForm } from '../../forms/edit-form/edit-form';
import { OedbApi } from '../../services/oedb-api';
import { UnlocatedEvents } from '../../shared/unlocated-events/unlocated-events';
+import { OsmAuth } from '../../services/osm-auth';
+import { Osm } from '../../forms/osm/osm';
@Component({
selector: 'app-home',
standalone: true,
@@ -14,6 +16,7 @@ import { UnlocatedEvents } from '../../shared/unlocated-events/unlocated-events'
AllEvents,
UnlocatedEvents,
EditForm,
+ Osm,
FormsModule
],
templateUrl: './home.html',
@@ -23,8 +26,10 @@ export class Home implements OnInit, OnDestroy {
OedbApi = inject(OedbApi);
private router = inject(Router);
+ private osmAuth = inject(OsmAuth);
features: Array = [];
+ filteredFeatures: Array = [];
selected: any | null = null;
showTable = false;
@@ -33,6 +38,11 @@ export class Home implements OnInit, OnDestroy {
autoReloadInterval: any = null;
daysAhead = 7; // Nombre de jours dans le futur par défaut
isLoading = false;
+
+ // Propriétés pour les filtres
+ searchText = '';
+ selectedWhatFilter = '';
+ availableWhatTypes: string[] = [];
ngOnInit() {
this.loadEvents();
@@ -57,6 +67,8 @@ export class Home implements OnInit, OnDestroy {
this.OedbApi.getEvents(params).subscribe((events: any) => {
this.features = Array.isArray(events?.features) ? events.features : [];
+ this.updateAvailableWhatTypes();
+ this.applyFilters();
this.isLoading = false;
});
}
@@ -89,6 +101,50 @@ export class Home implements OnInit, OnDestroy {
this.loadEvents();
}
+ updateAvailableWhatTypes() {
+ const whatTypes = new Set();
+ this.features.forEach(feature => {
+ if (feature?.properties?.what) {
+ whatTypes.add(feature.properties.what);
+ }
+ });
+ this.availableWhatTypes = Array.from(whatTypes).sort();
+ }
+
+ onSearchChange() {
+ this.applyFilters();
+ }
+
+ onWhatFilterChange() {
+ this.applyFilters();
+ }
+
+ applyFilters() {
+ let filtered = [...this.features];
+
+ // Filtre par texte de recherche
+ if (this.searchText.trim()) {
+ const searchLower = this.searchText.toLowerCase();
+ filtered = filtered.filter(feature => {
+ const label = feature?.properties?.label || feature?.properties?.name || '';
+ const description = feature?.properties?.description || '';
+ const what = feature?.properties?.what || '';
+ return label.toLowerCase().includes(searchLower) ||
+ description.toLowerCase().includes(searchLower) ||
+ what.toLowerCase().includes(searchLower);
+ });
+ }
+
+ // Filtre par type d'événement
+ if (this.selectedWhatFilter) {
+ filtered = filtered.filter(feature =>
+ feature?.properties?.what === this.selectedWhatFilter
+ );
+ }
+
+ this.filteredFeatures = filtered;
+ }
+
goToNewCategories() {
this.router.navigate(['/nouvelles-categories']);
}
@@ -106,9 +162,16 @@ export class Home implements OnInit, OnDestroy {
geometry: { type: 'Point', coordinates: [lon, lat] }
};
} else {
+ const osmUsername = this.osmAuth.getUsername();
this.selected = {
id: null,
- properties: { label: '', description: '', what: '', where: '' },
+ properties: {
+ label: '',
+ description: '',
+ what: '',
+ where: '',
+ ...(osmUsername && { last_modified_by: osmUsername })
+ },
geometry: { type: 'Point', coordinates: [lon, lat] }
};
}
@@ -141,7 +204,7 @@ export class Home implements OnInit, OnDestroy {
}
downloadGeoJSON() {
- const blob = new Blob([JSON.stringify({ type: 'FeatureCollection', features: this.features }, null, 2)], { type: 'application/geo+json' });
+ const blob = new Blob([JSON.stringify({ type: 'FeatureCollection', features: this.filteredFeatures }, null, 2)], { type: 'application/geo+json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
@@ -154,7 +217,7 @@ export class Home implements OnInit, OnDestroy {
downloadCSV() {
const header = ['id', 'what', 'label', 'start', 'stop', 'lon', 'lat'];
- const rows = this.features.map((f: any) => [
+ const rows = this.filteredFeatures.map((f: any) => [
JSON.stringify(f?.properties?.id ?? f?.id ?? ''),
JSON.stringify(f?.properties?.what ?? ''),
JSON.stringify(f?.properties?.label ?? f?.properties?.name ?? ''),
diff --git a/frontend/src/app/pages/home/menu/menu.html b/frontend/src/app/pages/home/menu/menu.html
index 26b06d3..dddb3fe 100644
--- a/frontend/src/app/pages/home/menu/menu.html
+++ b/frontend/src/app/pages/home/menu/menu.html
@@ -1,6 +1,7 @@