// =============================================================================
// map-dashboard.component.ts — Composant parent : pilote N iframes de cartes
// =============================================================================
// Démontre :
// - Affichage de plusieurs iframes côte-à-côte
// - Sélection de l'iframe active
// - Dispatch d'actions (immédiates ou différées)
// - Lecture réactive du store (signals)
// =============================================================================
import { Component, inject } from '@angular/core';
import { CommonModule } from '@angular/common';
import { MapSignalStore, MapObject } from './map-store';
import { IframeManagerService } from './iframe-manager.service';
import { IframeMapHostDirective } from './iframe-map-host.directive';
@Component({
selector: 'app-map-dashboard',
standalone: true,
imports: [CommonModule, IframeMapHostDirective],
template: `
<!-- ============================================================ -->
<!-- Toolbar -->
<!-- ============================================================ -->
<header class="toolbar">
<h1>Multi-Map Dashboard</h1>
<div class="toolbar-actions">
<button (click)="addSampleObjects()">
+ Add sample objects
</button>
<button (click)="store.clearSelection()">
Clear selection ({{ store.selectedCount() }})
</button>
<button (click)="broadcastFitBounds()">
Fit bounds (all maps)
</button>
</div>
</header>
<!-- ============================================================ -->
<!-- Iframe tabs -->
<!-- ============================================================ -->
<nav class="iframe-tabs">
@for (inst of manager.instanceList(); track inst.id) {
<button
class="tab"
[class.active]="inst.id === manager.activeId()"
[class.ready]="inst.ready"
(click)="manager.setActive(inst.id)"
>
<span class="status-dot" [class.ready]="inst.ready"></span>
{{ inst.label }}
@if (inst.pendingActions.length > 0) {
<span class="badge">{{ inst.pendingActions.length }}</span>
}
</button>
}
</nav>
<!-- ============================================================ -->
<!-- Iframes grid -->
<!-- ============================================================ -->
<section class="iframe-grid">
<!-- Iframe 1 : carte OpenStreetMap -->
<div class="iframe-wrapper"
[class.active]="manager.activeId() === 'map-osm'">
<iframe
[mapIframeHost]="'map-osm'"
iframeLabel="OpenStreetMap"
[src]="'/assets/map-osm.html'"
sandbox="allow-scripts allow-same-origin"
></iframe>
</div>
<!-- Iframe 2 : carte Satellite -->
<div class="iframe-wrapper"
[class.active]="manager.activeId() === 'map-satellite'">
<iframe
[mapIframeHost]="'map-satellite'"
iframeLabel="Satellite"
[src]="'/assets/map-satellite.html'"
sandbox="allow-scripts allow-same-origin"
></iframe>
</div>
<!-- Iframe 3 : carte 3D -->
<div class="iframe-wrapper"
[class.active]="manager.activeId() === 'map-3d'">
<iframe
[mapIframeHost]="'map-3d'"
iframeLabel="3D View"
[src]="'/assets/map-3d.html'"
sandbox="allow-scripts allow-same-origin"
></iframe>
</div>
</section>
<!-- ============================================================ -->
<!-- State inspector panel -->
<!-- ============================================================ -->
<aside class="state-panel">
<h2>Store State</h2>
<div class="stat">
<span class="label">Objects</span>
<span class="value">{{ store.objectList().length }}</span>
</div>
<div class="stat">
<span class="label">Visible</span>
<span class="value">{{ store.visibleObjects().length }}</span>
</div>
<div class="stat">
<span class="label">Selected</span>
<span class="value">{{ store.selectedCount() }}</span>
</div>
<div class="stat">
<span class="label">Pending actions</span>
<span class="value">{{ manager.pendingActionCount() }}</span>
</div>
<div class="stat">
<span class="label">Ready iframes</span>
<span class="value">
{{ manager.readyInstances().length }} / {{ manager.instanceList().length }}
</span>
</div>
<h3>Objects</h3>
<ul class="object-list">
@for (obj of store.objectList(); track obj.id) {
<li
[class.selected]="store.selection().selectedIds.has(obj.id)"
(click)="store.toggleSelection(obj.id)"
>
<span class="obj-type">{{ obj.type }}</span>
{{ obj.id }}
@if (!obj.visible) {
<span class="hidden-badge">hidden</span>
}
</li>
}
</ul>
</aside>
`,
styles: [`
:host {
display: grid;
grid-template-columns: 1fr 280px;
grid-template-rows: auto auto 1fr;
height: 100vh;
gap: 0;
font-family: 'IBM Plex Sans', system-ui, sans-serif;
background: #0f1117;
color: #e2e4e9;
}
/* Toolbar */
.toolbar {
grid-column: 1 / -1;
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 20px;
background: #161921;
border-bottom: 1px solid #2a2d37;
}
.toolbar h1 {
font-size: 16px;
font-weight: 600;
letter-spacing: -0.02em;
}
.toolbar-actions { display: flex; gap: 8px; }
.toolbar-actions button {
padding: 6px 14px;
border-radius: 6px;
border: 1px solid #3a3d47;
background: #1e2029;
color: #c8cad0;
font-size: 13px;
cursor: pointer;
transition: all 0.15s;
}
.toolbar-actions button:hover {
background: #2a2d37;
border-color: #5a5d67;
}
/* Tabs */
.iframe-tabs {
grid-column: 1 / 2;
display: flex;
gap: 2px;
padding: 8px 20px;
background: #131520;
}
.tab {
display: flex;
align-items: center;
gap: 6px;
padding: 6px 14px;
border: none;
background: transparent;
color: #7a7d87;
font-size: 13px;
cursor: pointer;
border-radius: 6px;
transition: all 0.15s;
}
.tab:hover { background: #1e2029; color: #c8cad0; }
.tab.active { background: #1e2029; color: #fff; }
.status-dot {
width: 8px; height: 8px;
border-radius: 50%;
background: #555;
transition: background 0.3s;
}
.status-dot.ready { background: #34d399; }
.badge {
background: #f59e0b;
color: #000;
font-size: 10px;
font-weight: 700;
padding: 1px 6px;
border-radius: 10px;
}
/* Iframe grid */
.iframe-grid {
grid-column: 1 / 2;
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 4px;
padding: 4px;
background: #0f1117;
overflow: hidden;
}
.iframe-wrapper {
position: relative;
border-radius: 8px;
overflow: hidden;
border: 2px solid transparent;
transition: border-color 0.2s;
}
.iframe-wrapper.active { border-color: #6366f1; }
.iframe-wrapper iframe {
width: 100%;
height: 100%;
min-height: 400px;
border: none;
background: #1a1c24;
}
/* State panel */
.state-panel {
grid-column: 2 / 3;
grid-row: 2 / 4;
background: #161921;
border-left: 1px solid #2a2d37;
padding: 16px;
overflow-y: auto;
}
.state-panel h2 {
font-size: 13px;
text-transform: uppercase;
letter-spacing: 0.08em;
color: #6366f1;
margin-bottom: 16px;
}
.state-panel h3 {
font-size: 12px;
text-transform: uppercase;
letter-spacing: 0.06em;
color: #7a7d87;
margin: 20px 0 8px;
}
.stat {
display: flex;
justify-content: space-between;
padding: 6px 0;
font-size: 13px;
border-bottom: 1px solid #1e2029;
}
.stat .label { color: #7a7d87; }
.stat .value { font-weight: 600; font-variant-numeric: tabular-nums; }
.object-list {
list-style: none;
padding: 0;
margin: 0;
}
.object-list li {
display: flex;
align-items: center;
gap: 8px;
padding: 6px 8px;
font-size: 12px;
border-radius: 4px;
cursor: pointer;
transition: background 0.1s;
}
.object-list li:hover { background: #1e2029; }
.object-list li.selected { background: #2d2566; }
.obj-type {
font-size: 10px;
text-transform: uppercase;
padding: 2px 6px;
border-radius: 3px;
background: #2a2d37;
color: #9ca3af;
}
.hidden-badge {
font-size: 10px;
color: #f59e0b;
margin-left: auto;
}
`],
})
export class MapDashboardComponent {
readonly store = inject(MapSignalStore);
readonly manager = inject(IframeManagerService);
private objectCounter = 0;
/** Ajoute des objets de démo dans le store */
addSampleObjects(): void {
const samples: MapObject[] = [
{
id: `marker-${++this.objectCounter}`,
type: 'marker',
coordinates: [2.3522 + Math.random() * 0.05, 48.8566 + Math.random() * 0.05],
properties: { name: `Point ${this.objectCounter}` },
visible: true,
},
{
id: `polygon-${++this.objectCounter}`,
type: 'polygon',
coordinates: [
[2.34, 48.85],
[2.36, 48.85],
[2.36, 48.87],
[2.34, 48.87],
],
properties: { name: `Zone ${this.objectCounter}` },
visible: true,
},
];
this.store.upsertMany(samples);
}
/** Broadcast FIT_BOUNDS à toutes les iframes */
broadcastFitBounds(): void {
this.manager.broadcast({
type: 'FIT_BOUNDS',
payload: {
bounds: this.store.visibleObjects().flatMap(o =>
Array.isArray(o.coordinates[0]) ? o.coordinates : [o.coordinates],
),
},
deduplicate: true,
});
}
}