diff --git a/frontend/src/app/maps/all-events/all-events.ts b/frontend/src/app/maps/all-events/all-events.ts index 441e0dd..acc04ed 100644 --- a/frontend/src/app/maps/all-events/all-events.ts +++ b/frontend/src/app/maps/all-events/all-events.ts @@ -13,8 +13,10 @@ export class AllEvents implements OnInit, OnDestroy { @Input() features: Array = []; @Input() selected: any | null = null; @Input() highlight: { id: string | number, type: 'saved' | 'deleted' } | null = null; + @Input() selectMode: 'none' | 'rectangle' | 'polygon' = 'none'; @Output() select = new EventEmitter(); @Output() pickCoords = new EventEmitter<[number, number]>(); + @Output() selection = new EventEmitter>(); @ViewChild('mapContainer', { static: true }) mapContainer!: ElementRef; @@ -26,6 +28,12 @@ export class AllEvents implements OnInit, OnDestroy { private isInitialLoad = true; private mapInitialized = false; + // selection state + private selectionActive = false; + private rectStartPoint: { x: number, y: number } | null = null; + private rectOverlay: HTMLDivElement | null = null; + private polygonPoints: Array<[number, number]> = []; + constructor( private route: ActivatedRoute, private router: Router @@ -66,6 +74,10 @@ export class AllEvents implements OnInit, OnDestroy { }, 1500); } } + // handle selectMode changes + if (this.mapInitialized) { + this.setupSelectionHandlers(); + } } private ensureMapLibre(): Promise { @@ -119,6 +131,156 @@ export class AllEvents implements OnInit, OnDestroy { }); this.mapInitialized = true; + this.setupSelectionHandlers(); + } + + private setupSelectionHandlers() { + const maplibregl = (window as any).maplibregl; + if (!this.map) return; + // Cleanup previous handlers + this.selectionActive = false; + this.removeRectOverlay(); + this.polygonPoints = []; + + // Disable default dragging while selecting + const enableDrag = () => this.map.dragPan && this.map.dragPan.enable && this.map.dragPan.enable(); + const disableDrag = () => this.map.dragPan && this.map.dragPan.disable && this.map.dragPan.disable(); + + // Remove prior listeners by re-adding map handlers only when needed + this.map.off('mousedown', this as any); + this.map.off('mousemove', this as any); + this.map.off('mouseup', this as any); + this.map.off('click', this as any); + + if (this.selectMode === 'rectangle') { + disableDrag(); + this.selectionActive = true; + this.map.on('mousedown', (e: any) => { + if (!this.selectionActive) return; + const p = this.map.project(e.lngLat); + this.rectStartPoint = { x: p.x, y: p.y }; + this.createRectOverlay(); + + const onMouseMove = (ev: any) => { + if (!this.rectStartPoint) return; + const pt = this.map.project(ev.lngLat); + this.updateRectOverlay(this.rectStartPoint, { x: pt.x, y: pt.y }); + }; + const onMouseUp = (ev: any) => { + this.map.off('mousemove', onMouseMove); + this.map.off('mouseup', onMouseUp); + enableDrag(); + if (!this.rectStartPoint) { this.removeRectOverlay(); return; } + const endPt = this.map.project(ev.lngLat); + const a = this.map.unproject(this.rectStartPoint); + const b = this.map.unproject(endPt); + const minLng = Math.min(a.lng, b.lng); + const maxLng = Math.max(a.lng, b.lng); + const minLat = Math.min(a.lat, b.lat); + const maxLat = Math.max(a.lat, b.lat); + const ids = this.collectIdsInBbox([minLng, minLat, maxLng, maxLat]); + this.selection.emit(ids); + this.removeRectOverlay(); + this.rectStartPoint = null; + this.selectionActive = false; + }; + this.map.on('mousemove', onMouseMove); + this.map.on('mouseup', onMouseUp); + }); + } else if (this.selectMode === 'polygon') { + disableDrag(); + this.selectionActive = true; + this.polygonPoints = []; + const clickHandler = (e: any) => { + if (!this.selectionActive) return; + const pt: [number, number] = [e.lngLat.lng, e.lngLat.lat]; + this.polygonPoints.push(pt); + // finish on double click (two close clicks) + }; + const dblHandler = () => { + if (!this.selectionActive || this.polygonPoints.length < 3) return; + const ids = this.collectIdsInPolygon(this.polygonPoints); + this.selection.emit(ids); + this.selectionActive = false; + this.polygonPoints = []; + enableDrag(); + }; + this.map.on('click', clickHandler); + this.map.on('dblclick', dblHandler); + } else { + // none + enableDrag(); + this.selectionActive = false; + this.removeRectOverlay(); + this.polygonPoints = []; + } + } + + private createRectOverlay() { + if (this.rectOverlay) this.removeRectOverlay(); + const el = document.createElement('div'); + el.style.position = 'absolute'; + el.style.border = '2px dashed #1976d2'; + el.style.background = 'rgba(25,118,210,0.1)'; + el.style.pointerEvents = 'none'; + this.rectOverlay = el; + this.mapContainer.nativeElement.appendChild(el); + } + + private updateRectOverlay(a: { x: number, y: number }, b: { x: number, y: number }) { + if (!this.rectOverlay) return; + const left = Math.min(a.x, b.x); + const top = Math.min(a.y, b.y); + const width = Math.abs(a.x - b.x); + const height = Math.abs(a.y - b.y); + this.rectOverlay.style.left = `${left}px`; + this.rectOverlay.style.top = `${top}px`; + this.rectOverlay.style.width = `${width}px`; + this.rectOverlay.style.height = `${height}px`; + } + + private removeRectOverlay() { + if (this.rectOverlay && this.rectOverlay.parentElement) { + this.rectOverlay.parentElement.removeChild(this.rectOverlay); + } + this.rectOverlay = null; + } + + private collectIdsInBbox(bbox: [number, number, number, number]): Array { + const [minLng, minLat, maxLng, maxLat] = bbox; + const ids: Array = []; + for (const f of this.features) { + const id = (f?.properties?.id ?? f?.id); + const c = f?.geometry?.coordinates; + if (!id || !Array.isArray(c)) continue; + const [lng, lat] = c as [number, number]; + if (lng >= minLng && lng <= maxLng && lat >= minLat && lat <= maxLat) ids.push(id); + } + return ids; + } + + private collectIdsInPolygon(poly: Array<[number, number]>): Array { + const ids: Array = []; + const inside = (pt: [number, number], polygon: Array<[number, number]>) => { + // ray casting + let x = pt[0], y = pt[1]; + let inside = false; + for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i++) { + const xi = polygon[i][0], yi = polygon[i][1]; + const xj = polygon[j][0], yj = polygon[j][1]; + const intersect = ((yi > y) !== (yj > y)) && (x < (xj - xi) * (y - yi) / ((yj - yi) || 1e-9) + xi); + if (intersect) inside = !inside; + } + return inside; + }; + for (const f of this.features) { + const id = (f?.properties?.id ?? f?.id); + const c = f?.geometry?.coordinates; + if (!id || !Array.isArray(c)) continue; + const pt: [number, number] = [c[0], c[1]]; + if (inside(pt, poly)) ids.push(id); + } + return ids; } private getEmojiForWhat(what: string): string { diff --git a/frontend/src/app/pages/home/home.html b/frontend/src/app/pages/home/home.html index 4852df3..23ddfb0 100644 --- a/frontend/src/app/pages/home/home.html +++ b/frontend/src/app/pages/home/home.html @@ -86,6 +86,13 @@ +
+ + + @if (selectedIds.length) { + {{selectedIds.length}} sélectionné(s) + } +
@@ -150,7 +157,7 @@ lastupdate: } @if (!showTable) {
- +
} @else {
@@ -178,3 +185,28 @@ lastupdate: }
+ +@if (selectedIds.length) { +
+
+
+ + +
+ @if (batchAction==='changeWhat') { +
+ + +
+ } +
+ + +
+
+
+} diff --git a/frontend/src/app/pages/home/home.scss b/frontend/src/app/pages/home/home.scss index bca2646..70ec1cb 100644 --- a/frontend/src/app/pages/home/home.scss +++ b/frontend/src/app/pages/home/home.scss @@ -131,6 +131,7 @@ app-edit-form{ top: 135px; margin-left: 397px; width: 40vw; + max-width: 350px; max-height: 77.7vh; display: block; overflow: auto; diff --git a/frontend/src/app/pages/home/home.ts b/frontend/src/app/pages/home/home.ts index 9e41792..9696609 100644 --- a/frontend/src/app/pages/home/home.ts +++ b/frontend/src/app/pages/home/home.ts @@ -40,7 +40,11 @@ export class Home implements OnInit, OnDestroy { showTable = false; showFilters = false; showEditForm = true; - + selectionMode: 'none' | 'rectangle' | 'polygon' = 'none'; + selectedIds: Array = []; + batchAction: 'none' | 'changeWhat' | 'delete' = 'none'; + batchWhat = ''; + // Nouvelles propriétés pour le rechargement automatique et la sélection de jours autoReloadEnabled = true; autoReloadInterval: any = null; @@ -81,7 +85,7 @@ export class Home implements OnInit, OnDestroy { const params = { start: today.toISOString().split('T')[0], end: endDate.toISOString().split('T')[0], - limit: 1000 + limit: 3000 }; this.OedbApi.getEvents(params).subscribe((events: any) => { @@ -226,6 +230,52 @@ export class Home implements OnInit, OnDestroy { this.loadEvents(); } + // Selection from map + onSelection(ids: Array) { + 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 = ''; + } + + async applyBatch() { + if (!this.selectedIds.length || this.batchAction === 'none') return; + if (this.batchAction === 'delete') { + for (const id of this.selectedIds) { + await new Promise((resolve) => { + this.OedbApi.deleteEvent(id).subscribe({ next: () => resolve(), error: () => resolve() }); + }); + } + this.loadEvents(); + this.clearSelection(); + return; + } + if (this.batchAction === 'changeWhat') { + const what = this.batchWhat.trim(); + if (!what) return; + for (const id of this.selectedIds) { + const feature = this.features.find(f => (f?.properties?.id ?? f?.id) === id); + if (!feature) continue; + const updated = { ...feature, properties: { ...feature.properties, what } }; + await new Promise((resolve) => { + this.OedbApi.updateEvent(id, updated).subscribe({ next: () => resolve(), error: () => resolve() }); + }); + } + this.loadEvents(); + this.clearSelection(); + } + } + onCanceled() { this.showEditForm = false; }