2025-10-07 14:42:39 +02:00
|
|
|
import { Component, inject, signal , OnDestroy, OnInit } from '@angular/core';
|
2025-10-04 16:14:42 +02:00
|
|
|
import { Router } from '@angular/router';
|
|
|
|
|
import { FormsModule } from '@angular/forms';
|
2025-10-02 23:19:15 +02:00
|
|
|
import {Menu} from './menu/menu';
|
2025-10-03 13:40:08 +02:00
|
|
|
import { AllEvents } from '../../maps/all-events/all-events';
|
|
|
|
|
import { EditForm } from '../../forms/edit-form/edit-form';
|
2025-10-03 11:56:55 +02:00
|
|
|
import { OedbApi } from '../../services/oedb-api';
|
2025-10-07 12:14:51 +02:00
|
|
|
import { ActivatedRoute } from '@angular/router';
|
|
|
|
|
import oedb from '../../../oedb-types';
|
|
|
|
|
|
2025-10-04 12:46:25 +02:00
|
|
|
import { UnlocatedEvents } from '../../shared/unlocated-events/unlocated-events';
|
2025-10-04 19:18:10 +02:00
|
|
|
import { OsmAuth } from '../../services/osm-auth';
|
|
|
|
|
import { Osm } from '../../forms/osm/osm';
|
2025-10-05 00:24:43 +02:00
|
|
|
import { WhatFilterComponent } from '../../shared/what-filter/what-filter';
|
2025-10-02 22:53:50 +02:00
|
|
|
@Component({
|
|
|
|
|
selector: 'app-home',
|
2025-10-04 12:58:44 +02:00
|
|
|
standalone: true,
|
2025-10-02 23:19:15 +02:00
|
|
|
imports: [
|
2025-10-03 13:40:08 +02:00
|
|
|
Menu,
|
|
|
|
|
AllEvents,
|
2025-10-04 12:46:25 +02:00
|
|
|
UnlocatedEvents,
|
2025-10-04 16:14:42 +02:00
|
|
|
EditForm,
|
2025-10-04 19:18:10 +02:00
|
|
|
Osm,
|
2025-10-05 00:24:43 +02:00
|
|
|
FormsModule,
|
|
|
|
|
WhatFilterComponent
|
2025-10-02 23:19:15 +02:00
|
|
|
],
|
2025-10-02 22:53:50 +02:00
|
|
|
templateUrl: './home.html',
|
|
|
|
|
styleUrl: './home.scss'
|
|
|
|
|
})
|
2025-10-04 16:14:42 +02:00
|
|
|
export class Home implements OnInit, OnDestroy {
|
2025-10-03 11:56:55 +02:00
|
|
|
|
|
|
|
|
OedbApi = inject(OedbApi);
|
2025-10-07 12:14:51 +02:00
|
|
|
route = inject(ActivatedRoute);
|
2025-10-04 16:14:42 +02:00
|
|
|
private router = inject(Router);
|
2025-10-04 19:18:10 +02:00
|
|
|
private osmAuth = inject(OsmAuth);
|
2025-10-07 12:14:51 +02:00
|
|
|
|
2025-10-03 13:40:08 +02:00
|
|
|
features: Array<any> = [];
|
2025-10-04 19:18:10 +02:00
|
|
|
filteredFeatures: Array<any> = [];
|
2025-10-03 13:40:08 +02:00
|
|
|
selected: any | null = null;
|
2025-10-03 14:00:35 +02:00
|
|
|
showTable = false;
|
2025-10-04 23:36:37 +02:00
|
|
|
showFilters = false;
|
2025-10-10 16:59:13 +02:00
|
|
|
showEditForm = false;
|
|
|
|
|
showOptions = false;
|
2025-10-14 17:29:37 +02:00
|
|
|
pleinAirMode = false;
|
|
|
|
|
toasts: Array<{ id: number, type: 'success' | 'error' | 'info', message: string }> = [];
|
2025-10-10 16:59:13 +02:00
|
|
|
|
2025-10-10 10:14:30 +02:00
|
|
|
selectionMode: 'none' | 'rectangle' | 'polygon' = 'none';
|
|
|
|
|
selectedIds: Array<string | number> = [];
|
2025-10-10 16:59:13 +02:00
|
|
|
batchAction: 'none' | 'changeWhat' | 'setField' | 'delete' = 'none';
|
2025-10-10 10:14:30 +02:00
|
|
|
batchWhat = '';
|
2025-10-10 16:59:13 +02:00
|
|
|
batchFieldKey = '';
|
|
|
|
|
batchFieldValue: any = '';
|
|
|
|
|
batchSummary: { success: number; failed: number; networkErrors: number } | null = null;
|
2025-10-10 10:14:30 +02:00
|
|
|
|
2025-10-04 16:14:42 +02:00
|
|
|
// Nouvelles propriétés pour le rechargement automatique et la sélection de jours
|
|
|
|
|
autoReloadEnabled = true;
|
|
|
|
|
autoReloadInterval: any = null;
|
|
|
|
|
daysAhead = 7; // Nombre de jours dans le futur par défaut
|
|
|
|
|
isLoading = false;
|
2025-10-10 16:59:13 +02:00
|
|
|
// Formulaire de recherche
|
|
|
|
|
startDateStr: string | null = null;
|
|
|
|
|
endDateStr: string | null = null;
|
2025-10-07 12:14:51 +02:00
|
|
|
|
2025-10-04 19:18:10 +02:00
|
|
|
// Propriétés pour les filtres
|
|
|
|
|
searchText = '';
|
2025-10-10 17:56:50 +02:00
|
|
|
selectedWhatFilter = '';
|
2025-10-04 19:18:10 +02:00
|
|
|
availableWhatTypes: string[] = [];
|
2025-10-07 12:14:51 +02:00
|
|
|
theme = signal<string | null>(null);
|
|
|
|
|
subthemes: Array<{ key: string, label: string, emoji: string }> = [];
|
|
|
|
|
activeSubtheme = signal<string | null>(null);
|
2025-10-10 16:59:13 +02:00
|
|
|
// Option bbox
|
2025-10-10 17:56:50 +02:00
|
|
|
useBboxFilter = true;
|
2025-10-10 16:59:13 +02:00
|
|
|
currentBbox: { minLng: number, minLat: number, maxLng: number, maxLat: number } | null = null;
|
|
|
|
|
// Debounce pour la recherche
|
|
|
|
|
private searchDebounceTimer: any = null;
|
|
|
|
|
// Non localisés / en ligne
|
|
|
|
|
unlocatedOrOnline: Array<any> = [];
|
|
|
|
|
showUnlocatedList = false;
|
2025-10-03 11:56:55 +02:00
|
|
|
|
2025-10-04 16:14:42 +02:00
|
|
|
ngOnInit() {
|
2025-10-14 17:29:37 +02:00
|
|
|
// Écouteur global pour toasts
|
|
|
|
|
try {
|
|
|
|
|
(window as any).addEventListener('toast', (e: any) => {
|
|
|
|
|
const d = e?.detail || {}; this.pushToast(d.type || 'info', d.message || '');
|
|
|
|
|
});
|
|
|
|
|
} catch {}
|
2025-10-10 16:59:13 +02:00
|
|
|
this.route.queryParamMap.subscribe(map => {
|
|
|
|
|
const id = (map.get('id') || '').trim();
|
|
|
|
|
const what = (map.get('what') || '').trim();
|
2025-10-14 17:29:37 +02:00
|
|
|
const pleinAir = (map.get('pleinair') || '').trim().toLowerCase();
|
2025-10-14 17:37:12 +02:00
|
|
|
const preset = (map.get('preset') || '').trim().toLowerCase();
|
2025-10-10 16:59:13 +02:00
|
|
|
const limitParam = map.get('limit');
|
|
|
|
|
const limit = limitParam ? Number(limitParam) : null;
|
|
|
|
|
// Charger selon les query params
|
|
|
|
|
if (id) {
|
|
|
|
|
this.loadSingleEvent(id);
|
|
|
|
|
} else {
|
|
|
|
|
this.loadEvents({ what: what || undefined, limit: limit || undefined });
|
|
|
|
|
}
|
|
|
|
|
// Appliquer filtre par what côté client si fourni
|
|
|
|
|
if (what) {
|
|
|
|
|
this.selectedWhatFilter = what;
|
|
|
|
|
}
|
2025-10-14 17:29:37 +02:00
|
|
|
// Activer mode plein air via query param
|
|
|
|
|
if (pleinAir === '1' || pleinAir === 'true' || pleinAir === 'yes') {
|
|
|
|
|
this.enablePleinAirMode();
|
|
|
|
|
}
|
2025-10-14 17:37:12 +02:00
|
|
|
// Support: preset=plein_air
|
|
|
|
|
if (preset === 'plein_air') {
|
|
|
|
|
this.enablePleinAirMode();
|
|
|
|
|
}
|
2025-10-10 16:59:13 +02:00
|
|
|
});
|
2025-10-04 16:14:42 +02:00
|
|
|
this.startAutoReload();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ngOnDestroy() {
|
|
|
|
|
this.stopAutoReload();
|
2025-10-10 16:59:13 +02:00
|
|
|
// Nettoyer le timer de debounce
|
|
|
|
|
if (this.searchDebounceTimer) {
|
|
|
|
|
clearTimeout(this.searchDebounceTimer);
|
|
|
|
|
}
|
2025-10-04 16:14:42 +02:00
|
|
|
}
|
|
|
|
|
|
2025-10-05 00:21:11 +02:00
|
|
|
createEvent() {
|
|
|
|
|
this.selected = null;
|
|
|
|
|
//this.showTable = false;
|
|
|
|
|
//this.showFilters = true;
|
|
|
|
|
this.showEditForm = true;
|
2025-10-07 12:14:51 +02:00
|
|
|
|
2025-10-05 00:21:11 +02:00
|
|
|
}
|
|
|
|
|
|
2025-10-10 16:59:13 +02:00
|
|
|
loadEvents(overrides: { what?: string; limit?: number; start?: string; end?: string; daysAhead?: number } = {}) {
|
2025-10-04 16:14:42 +02:00
|
|
|
this.isLoading = true;
|
|
|
|
|
const today = new Date();
|
2025-10-10 16:59:13 +02:00
|
|
|
const startIso = overrides.start || this.startDateStr || today.toISOString().split('T')[0];
|
|
|
|
|
let endIso = overrides.end || this.endDateStr || '';
|
|
|
|
|
if (!endIso) {
|
|
|
|
|
const d = new Date(today);
|
|
|
|
|
const span = overrides.daysAhead ?? this.daysAhead;
|
|
|
|
|
d.setDate(today.getDate() + span);
|
|
|
|
|
endIso = d.toISOString().split('T')[0];
|
|
|
|
|
}
|
2025-10-07 12:14:51 +02:00
|
|
|
|
2025-10-10 16:59:13 +02:00
|
|
|
const params: any = {
|
|
|
|
|
start: startIso,
|
|
|
|
|
end: endIso,
|
|
|
|
|
limit: overrides.limit ?? 10000
|
2025-10-04 16:14:42 +02:00
|
|
|
};
|
2025-10-10 16:59:13 +02:00
|
|
|
if (overrides.what) {
|
|
|
|
|
params.what = overrides.what;
|
2025-10-10 17:56:50 +02:00
|
|
|
} else if (this.selectedWhatFilter && this.selectedWhatFilter !== '') {
|
2025-10-10 16:59:13 +02:00
|
|
|
params.what = this.selectedWhatFilter;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Ajouter bbox si activé et disponible
|
|
|
|
|
if (this.useBboxFilter && this.currentBbox) {
|
|
|
|
|
params.bbox = `${this.currentBbox.minLng},${this.currentBbox.minLat},${this.currentBbox.maxLng},${this.currentBbox.maxLat}`;
|
|
|
|
|
}
|
2025-10-04 16:14:42 +02:00
|
|
|
|
|
|
|
|
this.OedbApi.getEvents(params).subscribe((events: any) => {
|
2025-10-03 13:40:08 +02:00
|
|
|
this.features = Array.isArray(events?.features) ? events.features : [];
|
2025-10-10 16:59:13 +02:00
|
|
|
this.computeUnlocatedOrOnline();
|
2025-10-04 19:18:10 +02:00
|
|
|
this.updateAvailableWhatTypes();
|
|
|
|
|
this.applyFilters();
|
2025-10-04 16:14:42 +02:00
|
|
|
this.isLoading = false;
|
2025-10-03 13:40:08 +02:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-10 16:59:13 +02:00
|
|
|
loadSingleEvent(id: string | number) {
|
|
|
|
|
this.isLoading = true;
|
|
|
|
|
this.OedbApi.getEventById(id).subscribe({
|
|
|
|
|
next: (feature: any) => {
|
|
|
|
|
const f = (feature && (feature as any).type === 'Feature') ? feature : (feature?.feature || null);
|
|
|
|
|
this.features = f ? [f] : [];
|
|
|
|
|
this.filteredFeatures = this.features;
|
|
|
|
|
this.updateAvailableWhatTypes();
|
|
|
|
|
this.isLoading = false;
|
|
|
|
|
},
|
|
|
|
|
error: () => {
|
|
|
|
|
this.features = [];
|
|
|
|
|
this.filteredFeatures = [];
|
|
|
|
|
this.isLoading = false;
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-04 16:14:42 +02:00
|
|
|
startAutoReload() {
|
|
|
|
|
if (this.autoReloadEnabled && !this.autoReloadInterval) {
|
|
|
|
|
this.autoReloadInterval = setInterval(() => {
|
|
|
|
|
this.loadEvents();
|
|
|
|
|
}, 60000); // 1 minute
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
stopAutoReload() {
|
|
|
|
|
if (this.autoReloadInterval) {
|
|
|
|
|
clearInterval(this.autoReloadInterval);
|
|
|
|
|
this.autoReloadInterval = null;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
toggleAutoReload() {
|
|
|
|
|
this.autoReloadEnabled = !this.autoReloadEnabled;
|
|
|
|
|
if (this.autoReloadEnabled) {
|
|
|
|
|
this.startAutoReload();
|
|
|
|
|
} else {
|
|
|
|
|
this.stopAutoReload();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
onDaysAheadChange() {
|
2025-10-10 16:59:13 +02:00
|
|
|
this.loadEvents({ daysAhead: this.daysAhead, what: this.selectedWhatFilter || undefined });
|
2025-10-04 16:14:42 +02:00
|
|
|
}
|
|
|
|
|
|
2025-10-04 19:18:10 +02:00
|
|
|
updateAvailableWhatTypes() {
|
|
|
|
|
const whatTypes = new Set<string>();
|
|
|
|
|
this.features.forEach(feature => {
|
|
|
|
|
if (feature?.properties?.what) {
|
|
|
|
|
whatTypes.add(feature.properties.what);
|
|
|
|
|
}
|
|
|
|
|
});
|
2025-10-07 12:14:51 +02:00
|
|
|
|
|
|
|
|
this.route.queryParams.subscribe(p => {
|
|
|
|
|
const t = (p?.['theme'] || '').trim();
|
|
|
|
|
this.theme.set(t || null);
|
|
|
|
|
this.buildSubthemes();
|
|
|
|
|
});
|
2025-10-10 17:56:50 +02:00
|
|
|
|
|
|
|
|
// Ajouter les catégories principales
|
|
|
|
|
whatTypes.add('culture');
|
|
|
|
|
whatTypes.add('traffic');
|
|
|
|
|
|
2025-10-04 19:18:10 +02:00
|
|
|
this.availableWhatTypes = Array.from(whatTypes).sort();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
onSearchChange() {
|
2025-10-10 16:59:13 +02:00
|
|
|
// Annuler le timer précédent s'il existe
|
|
|
|
|
if (this.searchDebounceTimer) {
|
|
|
|
|
clearTimeout(this.searchDebounceTimer);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Créer un nouveau timer de 500ms
|
|
|
|
|
this.searchDebounceTimer = setTimeout(() => {
|
|
|
|
|
this.applyFilters();
|
|
|
|
|
}, 500);
|
2025-10-04 19:18:10 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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) {
|
2025-10-10 17:56:50 +02:00
|
|
|
filtered = filtered.filter(feature => {
|
|
|
|
|
const what = feature?.properties?.what || '';
|
|
|
|
|
// Si c'est une catégorie (culture, traffic), filtrer par préfixe
|
|
|
|
|
if (this.selectedWhatFilter === 'culture' || this.selectedWhatFilter === 'traffic') {
|
|
|
|
|
return what.startsWith(this.selectedWhatFilter + '.');
|
|
|
|
|
}
|
|
|
|
|
// Sinon, correspondance exacte
|
|
|
|
|
return what === this.selectedWhatFilter;
|
|
|
|
|
});
|
2025-10-04 19:18:10 +02:00
|
|
|
}
|
|
|
|
|
|
2025-10-14 17:29:37 +02:00
|
|
|
// Mode plein air: ne garder que certains types
|
|
|
|
|
if (this.pleinAirMode) {
|
|
|
|
|
const allowed = new Set<string>([
|
|
|
|
|
'traffic.contestation',
|
|
|
|
|
'traffic.interruption',
|
|
|
|
|
'traffic.wrong_way'
|
|
|
|
|
]);
|
|
|
|
|
filtered = filtered.filter(f => allowed.has(f?.properties?.what || ''));
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-04 19:18:10 +02:00
|
|
|
this.filteredFeatures = filtered;
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-14 17:29:37 +02:00
|
|
|
togglePleinAir() {
|
|
|
|
|
this.pleinAirMode = !this.pleinAirMode;
|
|
|
|
|
this.applyFilters();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
enablePleinAirMode() {
|
|
|
|
|
if (!this.pleinAirMode) {
|
|
|
|
|
this.pleinAirMode = true;
|
|
|
|
|
this.applyFilters();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Actions rapides plein air
|
|
|
|
|
quickCreate(what: 'traffic.contestation' | 'traffic.interruption' | 'traffic.wrong_way') {
|
|
|
|
|
const osmUsername = this.osmAuth.getUsername();
|
|
|
|
|
this.selected = {
|
|
|
|
|
id: null,
|
|
|
|
|
properties: {
|
|
|
|
|
label: '',
|
|
|
|
|
description: '',
|
|
|
|
|
what,
|
|
|
|
|
where: '',
|
|
|
|
|
...(osmUsername && { last_modified_by: osmUsername })
|
|
|
|
|
},
|
|
|
|
|
geometry: { type: 'Point', coordinates: [0, 0] }
|
|
|
|
|
};
|
|
|
|
|
// Ensuite, l'utilisateur clique sur la carte: voir onPickCoords()
|
|
|
|
|
this.showEditForm = true;
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-04 16:14:42 +02:00
|
|
|
goToNewCategories() {
|
|
|
|
|
this.router.navigate(['/nouvelles-categories']);
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-03 13:40:08 +02:00
|
|
|
onSelect(feature: any) {
|
|
|
|
|
this.selected = feature;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
onPickCoords(coords: [number, number]) {
|
|
|
|
|
const [lon, lat] = coords;
|
|
|
|
|
if (this.selected && this.selected.properties) {
|
|
|
|
|
this.selected = {
|
|
|
|
|
...this.selected,
|
|
|
|
|
geometry: { type: 'Point', coordinates: [lon, lat] }
|
|
|
|
|
};
|
2025-10-10 16:59:13 +02:00
|
|
|
this.showOptions = true;
|
2025-10-14 17:29:37 +02:00
|
|
|
// En mode plein air, si c'est une création rapide, proposer l'envoi direct
|
|
|
|
|
if (this.pleinAirMode && (this.selected.id == null)) {
|
|
|
|
|
const w = this.selected.properties.what;
|
|
|
|
|
const allowed = new Set(['traffic.contestation','traffic.interruption','traffic.wrong_way']);
|
|
|
|
|
if (allowed.has(w)) {
|
|
|
|
|
const ok = typeof window !== 'undefined' ? window.confirm('Envoyer cet évènement maintenant ?') : true;
|
|
|
|
|
if (ok) {
|
|
|
|
|
const now = new Date();
|
|
|
|
|
const stop = new Date(now.getTime() + 6 * 3600 * 1000);
|
|
|
|
|
const feature = {
|
|
|
|
|
type: 'Feature',
|
|
|
|
|
properties: {
|
|
|
|
|
type: 'unscheduled',
|
|
|
|
|
label: this.selected.properties.label || (oedb.presets.what as any)[w]?.label || 'Évènement',
|
|
|
|
|
description: this.selected.properties.description || (oedb.presets.what as any)[w]?.description || '',
|
|
|
|
|
what: w,
|
|
|
|
|
where: this.selected.properties.where || '',
|
|
|
|
|
start: now.toISOString(),
|
|
|
|
|
stop: stop.toISOString()
|
|
|
|
|
},
|
|
|
|
|
geometry: { type: 'Point', coordinates: [lon, lat] }
|
|
|
|
|
} as any;
|
|
|
|
|
this.OedbApi.createEvent(feature).subscribe({
|
2025-10-14 17:37:12 +02:00
|
|
|
next: () => {
|
|
|
|
|
this.pushToast('success', 'Évènement créé');
|
|
|
|
|
this.selected = null;
|
|
|
|
|
// Après création rapide en plein air: recharger uniquement ce type pour feedback instantané
|
|
|
|
|
this.selectedWhatFilter = w;
|
|
|
|
|
this.loadEvents({ what: w });
|
|
|
|
|
},
|
2025-10-14 17:29:37 +02:00
|
|
|
error: () => { this.pushToast('error', 'Échec de création'); }
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-10-03 13:40:08 +02:00
|
|
|
} else {
|
2025-10-04 19:18:10 +02:00
|
|
|
const osmUsername = this.osmAuth.getUsername();
|
2025-10-07 12:14:51 +02:00
|
|
|
const whatKey = this.activeSubtheme();
|
|
|
|
|
let label = '';
|
|
|
|
|
let description = '';
|
|
|
|
|
if (whatKey) {
|
|
|
|
|
const preset = (oedb.presets.what as any)[whatKey];
|
|
|
|
|
if (preset) {
|
|
|
|
|
label = preset.label || '';
|
|
|
|
|
description = preset.description || '';
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-10-03 13:40:08 +02:00
|
|
|
this.selected = {
|
|
|
|
|
id: null,
|
2025-10-07 12:14:51 +02:00
|
|
|
properties: {
|
|
|
|
|
label: '',
|
|
|
|
|
description: '',
|
|
|
|
|
what: whatKey || '',
|
2025-10-04 19:18:10 +02:00
|
|
|
where: '',
|
|
|
|
|
...(osmUsername && { last_modified_by: osmUsername })
|
|
|
|
|
},
|
2025-10-03 13:40:08 +02:00
|
|
|
geometry: { type: 'Point', coordinates: [lon, lat] }
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-14 17:29:37 +02:00
|
|
|
private pushToast(type: 'success' | 'error' | 'info', message: string) {
|
|
|
|
|
if (!message) return;
|
|
|
|
|
const id = Date.now() + Math.random();
|
|
|
|
|
this.toasts.push({ id, type, message });
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
this.toasts = this.toasts.filter(t => t.id !== id);
|
|
|
|
|
}, 3000);
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-03 13:40:08 +02:00
|
|
|
onSaved(_res: any) {
|
|
|
|
|
// refresh list after update
|
2025-10-04 16:14:42 +02:00
|
|
|
this.loadEvents();
|
2025-10-03 13:40:08 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
onCreated(_res: any) {
|
|
|
|
|
this.selected = null;
|
2025-10-04 16:14:42 +02:00
|
|
|
this.loadEvents();
|
2025-10-03 13:40:08 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
onDeleted(_res: any) {
|
|
|
|
|
this.selected = null;
|
2025-10-04 16:14:42 +02:00
|
|
|
this.loadEvents();
|
2025-10-03 11:56:55 +02:00
|
|
|
}
|
2025-10-03 14:00:35 +02:00
|
|
|
|
2025-10-10 10:14:30 +02:00
|
|
|
// Selection from map
|
|
|
|
|
onSelection(ids: Array<string | number>) {
|
|
|
|
|
this.selectedIds = ids;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
startRectSelection() {
|
|
|
|
|
this.selectionMode = this.selectionMode === 'rectangle' ? 'none' : 'rectangle';
|
|
|
|
|
}
|
|
|
|
|
startPolySelection() {
|
|
|
|
|
this.selectionMode = this.selectionMode === 'polygon' ? 'none' : 'polygon';
|
|
|
|
|
}
|
|
|
|
|
clearSelection() {
|
|
|
|
|
this.selectionMode = 'none';
|
|
|
|
|
this.selectedIds = [];
|
|
|
|
|
this.batchAction = 'none';
|
|
|
|
|
this.batchWhat = '';
|
2025-10-10 16:59:13 +02:00
|
|
|
this.batchFieldKey = '';
|
|
|
|
|
this.batchFieldValue = '';
|
|
|
|
|
this.batchSummary = null;
|
2025-10-10 10:14:30 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async applyBatch() {
|
|
|
|
|
if (!this.selectedIds.length || this.batchAction === 'none') return;
|
2025-10-10 16:59:13 +02:00
|
|
|
let success = 0;
|
|
|
|
|
let failed = 0;
|
|
|
|
|
let networkErrors = 0;
|
|
|
|
|
|
|
|
|
|
const doUpdate = async (id: string | number, updater: (f: any) => any) => {
|
|
|
|
|
const feature = this.features.find(f => (f?.properties?.id ?? f?.id) === id);
|
|
|
|
|
if (!feature) { failed++; return; }
|
|
|
|
|
const updated = updater(feature);
|
|
|
|
|
await new Promise<void>((resolve) => {
|
|
|
|
|
this.OedbApi.updateEvent(id, updated).subscribe({
|
|
|
|
|
next: () => { success++; resolve(); },
|
|
|
|
|
error: (err) => { (err?.status === 0 ? networkErrors++ : failed++); resolve(); }
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
2025-10-10 10:14:30 +02:00
|
|
|
if (this.batchAction === 'delete') {
|
|
|
|
|
for (const id of this.selectedIds) {
|
|
|
|
|
await new Promise<void>((resolve) => {
|
2025-10-10 16:59:13 +02:00
|
|
|
this.OedbApi.deleteEvent(id).subscribe({ next: () => { success++; resolve(); }, error: (err) => { (err?.status === 0 ? networkErrors++ : failed++); resolve(); } });
|
2025-10-10 10:14:30 +02:00
|
|
|
});
|
|
|
|
|
}
|
2025-10-10 16:59:13 +02:00
|
|
|
} else if (this.batchAction === 'changeWhat') {
|
2025-10-10 10:14:30 +02:00
|
|
|
const what = this.batchWhat.trim();
|
|
|
|
|
if (!what) return;
|
|
|
|
|
for (const id of this.selectedIds) {
|
2025-10-10 16:59:13 +02:00
|
|
|
await doUpdate(id, (feature: any) => ({ ...feature, properties: { ...feature.properties, what } }));
|
|
|
|
|
}
|
|
|
|
|
} else if (this.batchAction === 'setField') {
|
|
|
|
|
const key = this.batchFieldKey.trim();
|
|
|
|
|
if (!key) return;
|
|
|
|
|
for (const id of this.selectedIds) {
|
|
|
|
|
await doUpdate(id, (feature: any) => ({ ...feature, properties: { ...feature.properties, [key]: this.batchFieldValue } }));
|
2025-10-10 10:14:30 +02:00
|
|
|
}
|
|
|
|
|
}
|
2025-10-10 16:59:13 +02:00
|
|
|
|
|
|
|
|
this.batchSummary = { success, failed, networkErrors };
|
|
|
|
|
this.loadEvents();
|
2025-10-10 10:14:30 +02:00
|
|
|
}
|
|
|
|
|
|
2025-10-05 00:21:11 +02:00
|
|
|
onCanceled() {
|
|
|
|
|
this.showEditForm = false;
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-03 14:00:35 +02:00
|
|
|
ngAfterViewInit() {
|
2025-10-07 12:14:51 +02:00
|
|
|
// reserved
|
2025-10-03 14:00:35 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
toggleView() {
|
|
|
|
|
this.showTable = !this.showTable;
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-10 16:59:13 +02:00
|
|
|
private isNonLocated(feature: any): boolean {
|
|
|
|
|
const geom = feature?.geometry;
|
|
|
|
|
if (!geom || geom.type !== 'Point') return true;
|
|
|
|
|
const coords = geom.coordinates;
|
|
|
|
|
if (!Array.isArray(coords) || coords.length !== 2) return true;
|
|
|
|
|
const [lon, lat] = coords;
|
|
|
|
|
if (lon == null || lat == null) return true;
|
|
|
|
|
if (lon === 0 && lat === 0) return true;
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private isOnline(feature: any): boolean {
|
|
|
|
|
const v = feature?.properties?.online;
|
|
|
|
|
return v === 'yes' || v === true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private computeUnlocatedOrOnline() {
|
|
|
|
|
this.unlocatedOrOnline = (this.features || []).filter(f => this.isNonLocated(f) || this.isOnline(f));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
toggleUnlocatedPanel() {
|
|
|
|
|
this.showUnlocatedList = !this.showUnlocatedList;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
createMammoth() {
|
|
|
|
|
const osmUsername = this.osmAuth.getUsername();
|
|
|
|
|
this.selected = {
|
|
|
|
|
id: null,
|
|
|
|
|
properties: {
|
|
|
|
|
label: '',
|
|
|
|
|
description: '',
|
|
|
|
|
what: 'traffic.mammoth',
|
|
|
|
|
where: '',
|
|
|
|
|
...(osmUsername && { last_modified_by: osmUsername })
|
|
|
|
|
},
|
|
|
|
|
geometry: { type: 'Point', coordinates: [0, 0] }
|
|
|
|
|
};
|
|
|
|
|
this.showEditForm = true;
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-07 12:14:51 +02:00
|
|
|
private buildSubthemes() {
|
|
|
|
|
const t = this.theme();
|
|
|
|
|
if (!t) { this.subthemes = []; this.activeSubtheme.set(null); return; }
|
|
|
|
|
const what = oedb.presets.what as Record<string, any>;
|
|
|
|
|
const list: Array<{ key: string, label: string, emoji: string }> = [];
|
|
|
|
|
Object.keys(what).forEach(k => {
|
|
|
|
|
if (k === t || k.startsWith(`${t}.`)) {
|
|
|
|
|
list.push({ key: k, label: what[k].label || k, emoji: what[k].emoji || '' });
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
this.subthemes = list.sort((a, b) => a.key.localeCompare(b.key));
|
|
|
|
|
const exact = this.subthemes.find(s => s.key === t);
|
|
|
|
|
this.activeSubtheme.set(exact ? exact.key : (this.subthemes[0]?.key || null));
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-03 14:00:35 +02:00
|
|
|
downloadGeoJSON() {
|
2025-10-04 19:18:10 +02:00
|
|
|
const blob = new Blob([JSON.stringify({ type: 'FeatureCollection', features: this.filteredFeatures }, null, 2)], { type: 'application/geo+json' });
|
2025-10-03 14:00:35 +02:00
|
|
|
const url = URL.createObjectURL(blob);
|
|
|
|
|
const a = document.createElement('a');
|
|
|
|
|
a.href = url;
|
|
|
|
|
a.download = 'events.geojson';
|
|
|
|
|
document.body.appendChild(a);
|
|
|
|
|
a.click();
|
|
|
|
|
URL.revokeObjectURL(url);
|
|
|
|
|
a.remove();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
downloadCSV() {
|
|
|
|
|
const header = ['id', 'what', 'label', 'start', 'stop', 'lon', 'lat'];
|
2025-10-04 19:18:10 +02:00
|
|
|
const rows = this.filteredFeatures.map((f: any) => [
|
2025-10-03 14:00:35 +02:00
|
|
|
JSON.stringify(f?.properties?.id ?? f?.id ?? ''),
|
|
|
|
|
JSON.stringify(f?.properties?.what ?? ''),
|
|
|
|
|
JSON.stringify(f?.properties?.label ?? f?.properties?.name ?? ''),
|
|
|
|
|
JSON.stringify(f?.properties?.start ?? f?.properties?.when ?? ''),
|
|
|
|
|
JSON.stringify(f?.properties?.stop ?? ''),
|
|
|
|
|
JSON.stringify(f?.geometry?.coordinates?.[0] ?? ''),
|
|
|
|
|
JSON.stringify(f?.geometry?.coordinates?.[1] ?? '')
|
|
|
|
|
].join(','));
|
|
|
|
|
const csv = [header.join(','), ...rows].join('\n');
|
|
|
|
|
const blob = new Blob([csv], { type: 'text/csv' });
|
|
|
|
|
const url = URL.createObjectURL(blob);
|
|
|
|
|
const a = document.createElement('a');
|
|
|
|
|
a.href = url;
|
|
|
|
|
a.download = 'events.csv';
|
|
|
|
|
document.body.appendChild(a);
|
|
|
|
|
a.click();
|
|
|
|
|
URL.revokeObjectURL(url);
|
|
|
|
|
a.remove();
|
|
|
|
|
}
|
2025-10-10 16:59:13 +02:00
|
|
|
|
|
|
|
|
onQuickSearchSubmit() {
|
|
|
|
|
const start = (this.startDateStr || '').trim() || undefined;
|
|
|
|
|
const end = (this.endDateStr || '').trim() || undefined;
|
|
|
|
|
const days = this.daysAhead;
|
|
|
|
|
const what = (this.selectedWhatFilter || '').trim() || undefined;
|
|
|
|
|
this.loadEvents({ start, end, daysAhead: days, what });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
onBboxFilterToggle() {
|
|
|
|
|
this.useBboxFilter = !this.useBboxFilter;
|
|
|
|
|
if (this.useBboxFilter) {
|
|
|
|
|
// Demander la bbox actuelle à la carte
|
|
|
|
|
this.requestCurrentBbox();
|
|
|
|
|
}
|
|
|
|
|
this.loadEvents();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
requestCurrentBbox() {
|
|
|
|
|
// Cette méthode sera appelée par le composant de carte
|
|
|
|
|
// pour obtenir la bbox actuelle
|
|
|
|
|
console.log('Demande de bbox actuelle...');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
setCurrentBbox(bbox: { minLng: number, minLat: number, maxLng: number, maxLat: number }) {
|
|
|
|
|
this.currentBbox = bbox;
|
|
|
|
|
if (this.useBboxFilter) {
|
|
|
|
|
this.loadEvents();
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-10-02 22:53:50 +02:00
|
|
|
}
|