load 3000 events
This commit is contained in:
parent
65d990af12
commit
fd2d51b662
4 changed files with 248 additions and 3 deletions
|
@ -13,8 +13,10 @@ export class AllEvents implements OnInit, OnDestroy {
|
|||
@Input() features: Array<any> = [];
|
||||
@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<any>();
|
||||
@Output() pickCoords = new EventEmitter<[number, number]>();
|
||||
@Output() selection = new EventEmitter<Array<string | number>>();
|
||||
|
||||
@ViewChild('mapContainer', { static: true }) mapContainer!: ElementRef<HTMLDivElement>;
|
||||
|
||||
|
@ -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<void> {
|
||||
|
@ -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<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 {
|
||||
|
|
|
@ -86,6 +86,13 @@
|
|||
<button class="button" (click)="downloadGeoJSON()" title="Télécharger GeoJSON">📥 GeoJSON</button>
|
||||
<button class="button" (click)="downloadCSV()" title="Télécharger CSV">📥 CSV</button>
|
||||
</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>
|
||||
|
||||
|
||||
|
@ -150,7 +157,7 @@ lastupdate:
|
|||
}
|
||||
@if (!showTable) {
|
||||
<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>
|
||||
} @else {
|
||||
<div class="table-wrapper" style="overflow:auto;height:100%;">
|
||||
|
@ -178,3 +185,28 @@ lastupdate:
|
|||
}
|
||||
</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>
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -40,7 +40,11 @@ export class Home implements OnInit, OnDestroy {
|
|||
showTable = false;
|
||||
showFilters = false;
|
||||
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
|
||||
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<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() {
|
||||
this.showEditForm = false;
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue