854 lines
27 KiB
TypeScript
854 lines
27 KiB
TypeScript
import {Component, inject, signal, OnDestroy, OnInit} from '@angular/core';
|
|
import {Router} from '@angular/router';
|
|
import {FormsModule} from '@angular/forms';
|
|
import {Menu} from './menu/menu';
|
|
import {AllEvents} from '../../maps/all-events/all-events';
|
|
import {EditForm} from '../../forms/edit-form/edit-form';
|
|
import {OedbApi} from '../../services/oedb-api';
|
|
import {ActivatedRoute} from '@angular/router';
|
|
import oedb from '../../../oedb-types';
|
|
|
|
import {UnlocatedEvents} from '../../shared/unlocated-events/unlocated-events';
|
|
import {OsmAuth} from '../../services/osm-auth';
|
|
import {Osm} from '../../forms/osm/osm';
|
|
import {WhatFilterComponent} from '../../shared/what-filter/what-filter';
|
|
import {NgClass} from '@angular/common';
|
|
|
|
@Component({
|
|
selector: 'app-home',
|
|
standalone: true,
|
|
imports: [
|
|
Menu,
|
|
AllEvents,
|
|
UnlocatedEvents,
|
|
EditForm,
|
|
Osm,
|
|
FormsModule,
|
|
WhatFilterComponent,
|
|
NgClass
|
|
],
|
|
templateUrl: './home.html',
|
|
styleUrl: './home.scss'
|
|
})
|
|
export class Home implements OnInit, OnDestroy {
|
|
|
|
OedbApi = inject(OedbApi);
|
|
route = inject(ActivatedRoute);
|
|
router = inject(Router);
|
|
private osmAuth = inject(OsmAuth);
|
|
|
|
features: Array<any> = [];
|
|
filteredFeatures: Array<any> = [];
|
|
selected: any | null = null;
|
|
showTable = false;
|
|
showFilters = false;
|
|
showEditForm = false;
|
|
showOptions = false;
|
|
pleinAirMode = false;
|
|
addMode: string | null = null;
|
|
civilianMode = false;
|
|
toasts: Array<{ id: number, type: 'success' | 'error' | 'info', message: string }> = [];
|
|
|
|
selectionMode: 'none' | 'rectangle' | 'polygon' = 'none';
|
|
selectedIds: Array<string | number> = [];
|
|
batchAction: 'none' | 'changeWhat' | 'setField' | 'delete' = 'none';
|
|
batchWhat = '';
|
|
batchFieldKey = '';
|
|
batchFieldValue: any = '';
|
|
batchSummary: { success: number; failed: number; networkErrors: number } | null = null;
|
|
|
|
// 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;
|
|
// Formulaire de recherche
|
|
startDateStr: string | null = null;
|
|
endDateStr: string | null = null;
|
|
|
|
// Propriétés pour les filtres
|
|
searchText = '';
|
|
selectedWhatFilter = '';
|
|
availableWhatTypes: string[] = [];
|
|
theme = signal<string | null>(null);
|
|
subthemes: Array<{ key: string, label: string, emoji: string }> = [];
|
|
activeSubtheme = signal<string | null>(null);
|
|
// Option bbox
|
|
useBboxFilter = true;
|
|
currentBbox: { minLng: number, minLat: number, maxLng: number, maxLat: number } | null = null;
|
|
|
|
// Bbox par défaut pour l'Île-de-France
|
|
private readonly IDF_BBOX = {
|
|
minLng: 1.4,
|
|
minLat: 48.1,
|
|
maxLng: 3.6,
|
|
maxLat: 49.2
|
|
};
|
|
// Debounce pour la recherche
|
|
protected searchDebounceTimer: any = null;
|
|
// Non localisés / en ligne
|
|
unlocatedOrOnline: Array<any> = [];
|
|
showUnlocatedList = false;
|
|
protected showQuickActions: boolean = true;
|
|
firstToastDone = false;
|
|
|
|
ngOnInit() {
|
|
// Écouteur global pour toasts
|
|
try {
|
|
(window as any).addEventListener('toast', (e: any) => {
|
|
const d = e?.detail || {};
|
|
this.pushToast(d.type || 'info', d.message || '');
|
|
});
|
|
} catch {
|
|
}
|
|
|
|
// Initialiser la bbox par défaut pour l'Île-de-France
|
|
this.currentBbox = {...this.IDF_BBOX};
|
|
|
|
this.route.queryParamMap.subscribe(map => {
|
|
const id = (map.get('id') || '').trim();
|
|
const what = (map.get('what') || 'culture').trim();
|
|
const add = (map.get('add') || '').trim();
|
|
const pleinAir = (map.get('pleinair') || '').trim().toLowerCase();
|
|
const preset = (map.get('preset') || '').trim().toLowerCase();
|
|
const limitParam = map.get('limit');
|
|
const limit = limitParam ? Number(limitParam) : null;
|
|
|
|
// Gérer le paramètre add pour activer le formulaire de création
|
|
if (add) {
|
|
this.addMode = add;
|
|
this.selectedWhatFilter = add;
|
|
this.showEditForm = true;
|
|
this.showOptions = true; // Afficher aussi le panel d'options
|
|
// Créer un événement temporaire avec le type what défini
|
|
this.selected = {
|
|
id: null,
|
|
properties: {
|
|
what: add,
|
|
label: '',
|
|
description: '',
|
|
start: new Date().toISOString(),
|
|
stop: new Date(Date.now() + 24 * 3600 * 1000).toISOString()
|
|
},
|
|
geometry: { type: 'Point', coordinates: [0, 0] }
|
|
};
|
|
} else {
|
|
this.addMode = null;
|
|
// Si pas de paramètre add, s'assurer que showEditForm est géré correctement
|
|
if (!this.selected) {
|
|
this.showEditForm = false;
|
|
}
|
|
}
|
|
|
|
// 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 (sauf si add est défini)
|
|
if (what && !add) {
|
|
this.selectedWhatFilter = what;
|
|
}
|
|
// Activer mode plein air via query param
|
|
// if (pleinAir === '1' || pleinAir === 'true' || pleinAir === 'yes') {
|
|
// this.enablePleinAirMode();
|
|
// }
|
|
// Support: preset=plein_air
|
|
if (preset === 'plein_air') {
|
|
if (!this.firstToastDone) {
|
|
this.selectedWhatFilter = "traffic"
|
|
this.pushToast('info', "mode plein air activé")
|
|
this.useBboxFilter = true;
|
|
this.loadEvents({ what: 'traffic' });
|
|
}
|
|
this.firstToastDone = true
|
|
this.enablePleinAirMode();
|
|
}
|
|
});
|
|
|
|
// Gérer aussi les paramètres du fragment (pour les URLs avec #)
|
|
this.route.fragment.subscribe(fragment => {
|
|
console.log('🔗 Fragment reçu:', fragment);
|
|
if (fragment) {
|
|
// Nettoyer le fragment en supprimant le & initial s'il existe
|
|
const cleanFragment = fragment.startsWith('&') ? fragment.substring(1) : fragment;
|
|
console.log('🧹 Fragment nettoyé:', cleanFragment);
|
|
const params = new URLSearchParams(cleanFragment);
|
|
const add = params.get('add');
|
|
const what = params.get('what');
|
|
console.log('🎯 Paramètre add extrait:', add);
|
|
|
|
// Gérer le paramètre add du fragment
|
|
if (add) {
|
|
this.addMode = add;
|
|
this.selectedWhatFilter = add;
|
|
this.showEditForm = true;
|
|
this.showOptions = true; // Afficher aussi le panel d'options
|
|
// Créer un événement temporaire avec le type what défini
|
|
this.selected = {
|
|
id: null,
|
|
properties: {
|
|
what: add,
|
|
label: '',
|
|
description: '',
|
|
start: new Date().toISOString(),
|
|
stop: new Date(Date.now() + 24 * 3600 * 1000).toISOString()
|
|
},
|
|
geometry: { type: 'Point', coordinates: [0, 0] }
|
|
};
|
|
console.log('✅ Formulaire de création activé pour:', add);
|
|
} else if (what) {
|
|
this.selectedWhatFilter = what;
|
|
console.log('✅ Filtre what défini:', this.selectedWhatFilter);
|
|
this.loadEvents({ what: what });
|
|
}
|
|
}
|
|
});
|
|
|
|
this.startAutoReload();
|
|
|
|
this.loadEvents({what: "culture", limit: 100});
|
|
}
|
|
|
|
ngOnDestroy() {
|
|
this.stopAutoReload();
|
|
// Nettoyer le timer de debounce
|
|
if (this.searchDebounceTimer) {
|
|
clearTimeout(this.searchDebounceTimer);
|
|
}
|
|
}
|
|
|
|
createEvent() {
|
|
this.selected = null;
|
|
//this.showTable = false;
|
|
//this.showFilters = true;
|
|
this.showEditForm = true;
|
|
|
|
}
|
|
|
|
loadEvents(overrides: { what?: string; limit?: number; start?: string; end?: string; daysAhead?: number } = {}) {
|
|
this.isLoading = true;
|
|
const today = new Date();
|
|
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];
|
|
}
|
|
|
|
const params: any = {
|
|
start: startIso,
|
|
end: endIso,
|
|
limit: overrides.limit ?? 10000
|
|
};
|
|
if (overrides.what) {
|
|
params.what = overrides.what;
|
|
} else if (this.selectedWhatFilter && this.selectedWhatFilter !== '') {
|
|
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}`;
|
|
}
|
|
|
|
this.OedbApi.getEvents(params).subscribe((events: any) => {
|
|
this.features = Array.isArray(events?.features) ? events.features : [];
|
|
this.computeUnlocatedOrOnline();
|
|
this.updateAvailableWhatTypes();
|
|
this.applyFilters();
|
|
this.isLoading = false;
|
|
});
|
|
}
|
|
|
|
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;
|
|
}
|
|
});
|
|
}
|
|
|
|
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() {
|
|
this.loadEvents({daysAhead: this.daysAhead, what: this.selectedWhatFilter || undefined});
|
|
}
|
|
|
|
updateAvailableWhatTypes() {
|
|
const whatTypes = new Set<string>();
|
|
this.features.forEach(feature => {
|
|
if (feature?.properties?.what) {
|
|
whatTypes.add(feature.properties.what);
|
|
}
|
|
});
|
|
|
|
this.route.queryParams.subscribe(p => {
|
|
const t = (p?.['theme'] || '').trim();
|
|
this.theme.set(t || null);
|
|
this.buildSubthemes();
|
|
});
|
|
|
|
// Ajouter les catégories principales
|
|
whatTypes.add('culture');
|
|
whatTypes.add('traffic');
|
|
|
|
this.availableWhatTypes = Array.from(whatTypes).sort();
|
|
}
|
|
|
|
onSearchChange() {
|
|
// 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);
|
|
}
|
|
|
|
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 => {
|
|
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;
|
|
});
|
|
}
|
|
|
|
// 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 || ''));
|
|
}
|
|
|
|
this.filteredFeatures = filtered;
|
|
}
|
|
|
|
togglePleinAir() {
|
|
this.pleinAirMode = !this.pleinAirMode;
|
|
this.applyFilters();
|
|
}
|
|
|
|
enablePleinAirMode() {
|
|
|
|
if (!this.pleinAirMode) {
|
|
this.pleinAirMode = true;
|
|
this.applyFilters();
|
|
}
|
|
}
|
|
|
|
// Actions rapides plein air
|
|
quickCreate(what: string) {
|
|
const osmUsername = this.osmAuth.getUsername();
|
|
this.selectedPreset = what;
|
|
this.showGuidePresetPlace = true;
|
|
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;
|
|
}
|
|
cancelSubmitPreset(){
|
|
this.selected = null;
|
|
this.presetMoreDetails = ''
|
|
this.showGuidePresetMoreInfo = false
|
|
this.showGuidePresetPlace = false;
|
|
this.showQuickActions = true;
|
|
}
|
|
submitPreset() {
|
|
const now = new Date();
|
|
const w = this.selected.properties.what;
|
|
// fin du signalement par défaut dans 20 jours
|
|
const stop = new Date(now.getTime() + 20 * 24 * 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,
|
|
reporter: this.guidePresetMoreInfoPseudo,
|
|
'reporter:description': this.presetMoreDetails,
|
|
where: this.selected.properties.where || '',
|
|
start: now.toISOString(),
|
|
stop: stop.toISOString()
|
|
},
|
|
geometry: {type: 'Point', coordinates: [this.selected.lon, this.selected.lat]}
|
|
} as any;
|
|
this.OedbApi.createEvent(feature).subscribe({
|
|
next: () => {
|
|
this.pushToast('success', 'Évènement créé');
|
|
this.selected = null;
|
|
this.presetMoreDetails = ''
|
|
this.showGuidePresetMoreInfo = false
|
|
this.showGuidePresetPlace = false;
|
|
// Après création rapide en plein air: recharger uniquement ce type pour feedback instantané
|
|
this.selectedWhatFilter = w;
|
|
this.loadEvents({what: 'traffic'});
|
|
},
|
|
error: () => {
|
|
this.pushToast('error', 'Échec de création');
|
|
}
|
|
});
|
|
}
|
|
|
|
goToNewCategories() {
|
|
this.router.navigate(['/nouvelles-categories']);
|
|
}
|
|
|
|
onSelect(feature: any) {
|
|
this.selected = feature;
|
|
}
|
|
|
|
onSelectFromCalendarView(feature: any) {
|
|
this.selected = feature;
|
|
this.showEditForm = false;
|
|
}
|
|
|
|
onPickCoords(coords: [number, number]) {
|
|
const [lon, lat] = coords;
|
|
if (this.selected && this.selected.properties) {
|
|
this.selected = {
|
|
...this.selected,
|
|
geometry: {type: 'Point', coordinates: [lon, lat]}
|
|
};
|
|
// this.showOptions = true;
|
|
// 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)) {
|
|
|
|
this.showGuidePresetPlace = true;
|
|
this.showGuidePresetMoreInfo = true;
|
|
this.showQuickActions = false;
|
|
const self: this = this;
|
|
const ok = typeof window !== 'undefined' ? window.confirm('Envoyer cet évènement maintenant ?') : true;
|
|
if (ok) {
|
|
self.submitPreset()
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
const osmUsername = this.osmAuth.getUsername();
|
|
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 || '';
|
|
}
|
|
}
|
|
this.selected = {
|
|
id: null,
|
|
properties: {
|
|
label: '',
|
|
description: '',
|
|
what: whatKey || '',
|
|
where: '',
|
|
...(osmUsername && {last_modified_by: osmUsername})
|
|
},
|
|
geometry: {type: 'Point', coordinates: [lon, lat]}
|
|
};
|
|
}
|
|
}
|
|
|
|
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);
|
|
}, 6000);
|
|
}
|
|
|
|
onSaved(_res: any) {
|
|
// refresh list after update
|
|
this.loadEvents();
|
|
}
|
|
|
|
onCreated(_res: any) {
|
|
this.selected = null;
|
|
this.showEditForm = false;
|
|
this.addMode = null;
|
|
this.loadEvents();
|
|
// Retirer le paramètre add de l'URL (query params ou fragment)
|
|
if (this.route.snapshot.queryParams['add']) {
|
|
this.router.navigate([], {
|
|
relativeTo: this.route,
|
|
queryParams: { ...this.route.snapshot.queryParams, add: null },
|
|
queryParamsHandling: 'merge'
|
|
});
|
|
} else if (this.route.snapshot.fragment && this.route.snapshot.fragment.includes('add=')) {
|
|
// Nettoyer le fragment s'il contient add
|
|
const fragment = this.route.snapshot.fragment || '';
|
|
const params = new URLSearchParams(fragment.startsWith('&') ? fragment.substring(1) : fragment);
|
|
params.delete('add');
|
|
const newFragment = params.toString();
|
|
this.router.navigate([], {
|
|
relativeTo: this.route,
|
|
fragment: newFragment ? '?' + newFragment : undefined
|
|
});
|
|
}
|
|
}
|
|
|
|
onDeleted(_res: any) {
|
|
this.selected = null;
|
|
this.loadEvents();
|
|
}
|
|
|
|
// 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 = '';
|
|
this.batchFieldKey = '';
|
|
this.batchFieldValue = '';
|
|
this.batchSummary = null;
|
|
}
|
|
|
|
async applyBatch() {
|
|
if (!this.selectedIds.length || this.batchAction === 'none') return;
|
|
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();
|
|
}
|
|
});
|
|
});
|
|
};
|
|
|
|
if (this.batchAction === 'delete') {
|
|
for (const id of this.selectedIds) {
|
|
await new Promise<void>((resolve) => {
|
|
this.OedbApi.deleteEvent(id).subscribe({
|
|
next: () => {
|
|
success++;
|
|
resolve();
|
|
}, error: (err) => {
|
|
(err?.status === 0 ? networkErrors++ : failed++);
|
|
resolve();
|
|
}
|
|
});
|
|
});
|
|
}
|
|
} else if (this.batchAction === 'changeWhat') {
|
|
const what = this.batchWhat.trim();
|
|
if (!what) return;
|
|
for (const id of this.selectedIds) {
|
|
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}
|
|
}));
|
|
}
|
|
}
|
|
|
|
this.batchSummary = {success, failed, networkErrors};
|
|
this.loadEvents();
|
|
}
|
|
|
|
closeEditForm() {
|
|
this.selected = null;
|
|
this.showEditForm = false;
|
|
this.addMode = null;
|
|
// Retirer le paramètre add de l'URL si présent (query params ou fragment)
|
|
if (this.route.snapshot.queryParams['add']) {
|
|
this.router.navigate([], {
|
|
relativeTo: this.route,
|
|
queryParams: { ...this.route.snapshot.queryParams, add: null },
|
|
queryParamsHandling: 'merge'
|
|
});
|
|
} else if (this.route.snapshot.fragment && this.route.snapshot.fragment.includes('add=')) {
|
|
// Nettoyer le fragment s'il contient add
|
|
const fragment = this.route.snapshot.fragment || '';
|
|
const params = new URLSearchParams(fragment.startsWith('&') ? fragment.substring(1) : fragment);
|
|
params.delete('add');
|
|
const newFragment = params.toString();
|
|
this.router.navigate([], {
|
|
relativeTo: this.route,
|
|
fragment: newFragment ? '?' + newFragment : undefined
|
|
});
|
|
}
|
|
}
|
|
|
|
onCanceled() {
|
|
this.showEditForm = false;
|
|
this.addMode = null;
|
|
this.selected = null;
|
|
// Retirer le paramètre add de l'URL si présent (query params ou fragment)
|
|
if (this.route.snapshot.queryParams['add']) {
|
|
this.router.navigate([], {
|
|
relativeTo: this.route,
|
|
queryParams: { ...this.route.snapshot.queryParams, add: null },
|
|
queryParamsHandling: 'merge'
|
|
});
|
|
} else if (this.route.snapshot.fragment && this.route.snapshot.fragment.includes('add=')) {
|
|
// Nettoyer le fragment s'il contient add
|
|
const fragment = this.route.snapshot.fragment || '';
|
|
const params = new URLSearchParams(fragment.startsWith('&') ? fragment.substring(1) : fragment);
|
|
params.delete('add');
|
|
const newFragment = params.toString();
|
|
this.router.navigate([], {
|
|
relativeTo: this.route,
|
|
fragment: newFragment ? '?' + newFragment : undefined
|
|
});
|
|
}
|
|
}
|
|
|
|
ngAfterViewInit() {
|
|
// reserved
|
|
}
|
|
|
|
toggleView() {
|
|
this.showTable = !this.showTable;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
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));
|
|
}
|
|
|
|
downloadGeoJSON() {
|
|
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;
|
|
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'];
|
|
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 ?? ''),
|
|
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();
|
|
}
|
|
|
|
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();
|
|
}
|
|
}
|
|
|
|
// Méthode pour recharger les événements quand la carte bouge
|
|
showGuidePresetPlace: boolean = false;
|
|
showGuidePresetMoreInfo: boolean = false;
|
|
|
|
guidePresetMoreInfoPseudo: string = '';
|
|
presetMoreDetails: string = '';
|
|
selectedPreset: string = '';
|
|
|
|
onMapMove(bbox: { minLng: number, minLat: number, maxLng: number, maxLat: number }) {
|
|
this.setCurrentBbox(bbox);
|
|
}
|
|
}
|