oedb-backend/frontend/src/app/pages/home/home.ts

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);
}
}