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() 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 {
|
||||||
|
|
|
@ -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>
|
||||||
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue