load 3000 events

This commit is contained in:
Tykayn 2025-10-10 10:14:30 +02:00 committed by tykayn
parent 65d990af12
commit fd2d51b662
4 changed files with 248 additions and 3 deletions

View file

@ -13,8 +13,10 @@ export class AllEvents implements OnInit, OnDestroy {
@Input() features: Array<any> = []; @Input() features: Array<any> = [];
@Input() selected: any | null = null; @Input() selected: any | null = null;
@Input() highlight: { id: string | number, type: 'saved' | 'deleted' } | null = null; @Input() highlight: { id: string | number, type: 'saved' | 'deleted' } | null = null;
@Input() selectMode: 'none' | 'rectangle' | 'polygon' = 'none';
@Output() select = new EventEmitter<any>(); @Output() select = new EventEmitter<any>();
@Output() pickCoords = new EventEmitter<[number, number]>(); @Output() pickCoords = new EventEmitter<[number, number]>();
@Output() selection = new EventEmitter<Array<string | number>>();
@ViewChild('mapContainer', { static: true }) mapContainer!: ElementRef<HTMLDivElement>; @ViewChild('mapContainer', { static: true }) mapContainer!: ElementRef<HTMLDivElement>;
@ -26,6 +28,12 @@ export class AllEvents implements OnInit, OnDestroy {
private isInitialLoad = true; private isInitialLoad = true;
private mapInitialized = false; 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( constructor(
private route: ActivatedRoute, private route: ActivatedRoute,
private router: Router private router: Router
@ -66,6 +74,10 @@ export class AllEvents implements OnInit, OnDestroy {
}, 1500); }, 1500);
} }
} }
// handle selectMode changes
if (this.mapInitialized) {
this.setupSelectionHandlers();
}
} }
private ensureMapLibre(): Promise<void> { private ensureMapLibre(): Promise<void> {
@ -119,6 +131,156 @@ export class AllEvents implements OnInit, OnDestroy {
}); });
this.mapInitialized = true; 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<string | number> {
const [minLng, minLat, maxLng, maxLat] = bbox;
const ids: Array<string | number> = [];
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<string | number> {
const ids: Array<string | number> = [];
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 { private getEmojiForWhat(what: string): string {

View file

@ -86,6 +86,13 @@
<button class="button" (click)="downloadGeoJSON()" title="Télécharger GeoJSON">📥 GeoJSON</button> <button class="button" (click)="downloadGeoJSON()" title="Télécharger GeoJSON">📥 GeoJSON</button>
<button class="button" (click)="downloadCSV()" title="Télécharger CSV">📥 CSV</button> <button class="button" (click)="downloadCSV()" title="Télécharger CSV">📥 CSV</button>
</div> </div>
<div class="selectors">
<button class="button" [class.active]="selectionMode==='rectangle'" (click)="startRectSelection()" title="Sélection rectangulaire"></button>
<button class="button" [class.active]="selectionMode==='polygon'" (click)="startPolySelection()" title="Sélection polygone"></button>
@if (selectedIds.length) {
<span class="muted">{{selectedIds.length}} sélectionné(s)</span>
}
</div>
</div> </div>
@ -150,7 +157,7 @@ lastupdate:
} }
@if (!showTable) { @if (!showTable) {
<div class="map"> <div class="map">
<app-all-events [features]="filteredFeatures" [selected]="selected" (select)="onSelect($event)" (pickCoords)="onPickCoords($event)"></app-all-events> <app-all-events [features]="filteredFeatures" [selected]="selected" [selectMode]="selectionMode" (selection)="onSelection($event)" (select)="onSelect($event)" (pickCoords)="onPickCoords($event)"></app-all-events>
</div> </div>
} @else { } @else {
<div class="table-wrapper" style="overflow:auto;height:100%;"> <div class="table-wrapper" style="overflow:auto;height:100%;">
@ -178,3 +185,28 @@ lastupdate:
} }
</div> </div>
</div> </div>
@if (selectedIds.length) {
<div class="batch-panel">
<div class="panel">
<div class="row">
<label>Action de masse</label>
<select class="input" [(ngModel)]="batchAction">
<option value="none">Choisir...</option>
<option value="changeWhat">Changer le type d'évènement (what)</option>
<option value="delete">Supprimer</option>
</select>
</div>
@if (batchAction==='changeWhat') {
<div class="row">
<label>Nouveau "what"</label>
<input class="input" type="text" [(ngModel)]="batchWhat" placeholder="ex: traffic.roadwork" />
</div>
}
<div class="actions">
<button class="btn" (click)="applyBatch()" [disabled]="batchAction==='none'">Appliquer</button>
<button class="btn btn-ghost" (click)="clearSelection()">Annuler</button>
</div>
</div>
</div>
}

View file

@ -131,6 +131,7 @@ app-edit-form{
top: 135px; top: 135px;
margin-left: 397px; margin-left: 397px;
width: 40vw; width: 40vw;
max-width: 350px;
max-height: 77.7vh; max-height: 77.7vh;
display: block; display: block;
overflow: auto; overflow: auto;

View file

@ -40,7 +40,11 @@ export class Home implements OnInit, OnDestroy {
showTable = false; showTable = false;
showFilters = false; showFilters = false;
showEditForm = true; showEditForm = true;
selectionMode: 'none' | 'rectangle' | 'polygon' = 'none';
selectedIds: Array<string | number> = [];
batchAction: 'none' | 'changeWhat' | 'delete' = 'none';
batchWhat = '';
// Nouvelles propriétés pour le rechargement automatique et la sélection de jours // Nouvelles propriétés pour le rechargement automatique et la sélection de jours
autoReloadEnabled = true; autoReloadEnabled = true;
autoReloadInterval: any = null; autoReloadInterval: any = null;
@ -81,7 +85,7 @@ export class Home implements OnInit, OnDestroy {
const params = { const params = {
start: today.toISOString().split('T')[0], start: today.toISOString().split('T')[0],
end: endDate.toISOString().split('T')[0], end: endDate.toISOString().split('T')[0],
limit: 1000 limit: 3000
}; };
this.OedbApi.getEvents(params).subscribe((events: any) => { this.OedbApi.getEvents(params).subscribe((events: any) => {
@ -226,6 +230,52 @@ export class Home implements OnInit, OnDestroy {
this.loadEvents(); 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 = '';
}
async applyBatch() {
if (!this.selectedIds.length || this.batchAction === 'none') return;
if (this.batchAction === 'delete') {
for (const id of this.selectedIds) {
await new Promise<void>((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<void>((resolve) => {
this.OedbApi.updateEvent(id, updated).subscribe({ next: () => resolve(), error: () => resolve() });
});
}
this.loadEvents();
this.clearSelection();
}
}
onCanceled() { onCanceled() {
this.showEditForm = false; this.showEditForm = false;
} }