Leaflet Disaster Response Map
blocklogisticsIncident management map on free OpenStreetMap/Esri tiles — no API key. Evacuation perimeter rings, shelter capacities, and emergency staging points.
Installation
$pnpm dlx uipkge-ng@latest add leaflet-disaster-response-map$npx uipkge-ng@latest add leaflet-disaster-response-map$yarn dlx uipkge-ng@latest add leaflet-disaster-response-map$bunx uipkge-ng@latest add leaflet-disaster-response-mapManifest:
https://uipkge.dev/r/angular/leaflet-disaster-response-map.jsonInstalls to:src/app/components/blocks/leaflet-disaster-response-map/Variants
Loading interactive previews…
Loading interactive previews…
Schema
Type aliases exported from this item's source. Use these to shape the data you pass in.
Facilityinterface Facility {
id: string
name: string
type: FacilityType
/** [lng, lat] — Mapbox order. */
coords: [number, number]
capacity: number
occupied: number
status: FacilityStatus
}Includes
Files installed (2)
src/app/components/blocks/leaflet-disaster-response-map/leaflet-disaster-response-map.component.ts9.8 kB
// Incident management map on free OpenStreetMap/Esri tiles — no API key. Evacuation // perimeter rings, shelter capacities, and emergency staging points. Port of the React block 1:1. import { ChangeDetectionStrategy, Component, Input, ViewChild, signal } from '@angular/core' import { cn } from '@/lib/utils' import { UiBadgeComponent } from '@/ui/badge/badge.component' import { UiButtonComponent } from '@/ui/button/button.component' import { UiCardComponent, UiCardContentComponent, UiCardDescriptionComponent, UiCardHeaderComponent, UiCardTitleComponent, } from '@/ui/card/card.component' import { UiLeafletCircleComponent, UiLeafletMapComponent, UiLeafletMarkerComponent, } from '@/ui/leaflet-map/leaflet-map.component' export type FacilityType = 'Shelter' | 'Medical Staging' | 'Command Post' export type FacilityStatus = 'Open' | 'Near Capacity' | 'Closed' export interface Facility { id: string name: string type: FacilityType /** [lng, lat] — Mapbox order. */ coords: [number, number] capacity: number occupied: number status: FacilityStatus } // Module-level constants: stable references keep LeafletMap's center effect // from re-firing setView on every render and cancelling flyTo animations. const MAP_CENTER: [number, number] = [-122.2711, 37.8044] // Wildfire incident origin in the Oakland Hills + a secondary spot fire. const INCIDENT: [number, number] = [-122.165, 37.845] const SPOT_FIRE: [number, number] = [-122.145, 37.856] // Evacuation perimeter rings (meter radii): hard exclusion + warning zone. const EVAC_RADIUS_M = 1800 const WARNING_RADIUS_M = 3600 export const DEFAULT_FACILITIES: Facility[] = [ { id: 's-1', name: 'Oakland Civic Shelter', type: 'Shelter', coords: [-122.265, 37.795], capacity: 500, occupied: 340, status: 'Open', }, { id: 's-2', name: 'Eastmont Medical Staging', type: 'Medical Staging', coords: [-122.19, 37.77], capacity: 120, occupied: 98, status: 'Near Capacity', }, { id: 's-3', name: 'Unified Incident Command Post', type: 'Command Post', coords: [-122.225, 37.852], capacity: 80, occupied: 45, status: 'Open', }, ] @Component({ changeDetection: ChangeDetectionStrategy.Eager, selector: 'ui-leaflet-disaster-response-map, [ui-leaflet-disaster-response-map]', standalone: true, imports: [ UiBadgeComponent, UiButtonComponent, UiCardComponent, UiCardContentComponent, UiCardDescriptionComponent, UiCardHeaderComponent, UiCardTitleComponent, UiLeafletCircleComponent, UiLeafletMapComponent, UiLeafletMarkerComponent, ], host: { 'data-slot': 'leaflet-disaster-response-map', '[class]': 'hostClass', }, template: ` <div class="border-border bg-card relative h-[480px] overflow-hidden rounded-xl border"> <ui-leaflet-map variant="light" [center]="mapCenter" [zoom]="11" class="h-full w-full"> <!-- Evacuation perimeter rings (radii in meters) --> <ui-leaflet-circle [center]="incident" [radius]="evacRadiusM" color="#dc2626" [weight]="2" [opacity]="0.9" dashArray="6 4" fillColor="#ef4444" [fillOpacity]="0.15" /> <ui-leaflet-circle [center]="incident" [radius]="warningRadiusM" color="#d97706" [weight]="1.5" [opacity]="0.7" dashArray="4 6" fillColor="#f59e0b" [fillOpacity]="0.05" /> <!-- Active fire origin --> <ui-leaflet-marker [lngLat]="incident" anchor="center"> <div class="relative flex cursor-pointer items-center justify-center" (click)="focusHazard()"> <span class="bg-destructive/30 absolute size-9 animate-ping rounded-full"></span> <div class="bg-destructive flex size-7 items-center justify-center rounded-full font-mono text-xs font-bold text-white shadow-lg ring-2 ring-white" > 🔥 </div> </div> </ui-leaflet-marker> <!-- Secondary spot fire --> <ui-leaflet-marker [lngLat]="spotFire" anchor="center"> <div class="relative flex cursor-pointer items-center justify-center" (click)="focusHazard()"> <span class="bg-warning/30 absolute size-6 animate-ping rounded-full"></span> <div class="bg-warning flex size-6 items-center justify-center rounded-full font-mono text-xs font-bold text-white shadow-md ring-2 ring-white" > 🔥 </div> </div> </ui-leaflet-marker> <!-- Shelters and Medical Staging --> @for (f of facilities; track f.id) { <ui-leaflet-marker [lngLat]="f.coords" anchor="bottom"> <div class="group flex cursor-pointer flex-col items-center" (click)="focusFacility(f)"> <div [class]="facilityPinClass(f)"> {{ facilityGlyph(f) }} </div> <span class="border-border bg-background/90 mt-1 rounded border px-1 py-0.5 font-mono text-xs font-bold whitespace-nowrap shadow-xs" > {{ shortName(f) }} </span> </div> </ui-leaflet-marker> } </ui-leaflet-map> </div> <!-- Incident Control Dashboard --> <ui-card class="flex flex-col justify-between"> <ui-card-header> <div class="flex items-center justify-between"> <span ui-badge variant="outline" class="border-destructive/30 bg-destructive/10 text-destructive font-mono text-xs" > LEVEL 3 EVACUATION </span> <span ui-badge class="border-success/20 bg-success/10 text-success">{{ activeFacility().status }}</span> </div> <ui-card-title class="mt-2 text-lg">{{ activeFacility().name }}</ui-card-title> <ui-card-description class="text-xs"> Capacity: {{ activeFacility().occupied }} / {{ activeFacility().capacity }} beds occupied </ui-card-description> </ui-card-header> <ui-card-content class="space-y-4"> <!-- Capacity Bar --> <div class="border-border space-y-1.5 border-t pt-3"> <div class="flex items-center justify-between font-mono text-xs"> <span class="text-muted-foreground">Occupancy Rate</span> <span class="text-foreground font-bold"> {{ occupancyPct(activeFacility()) }}% </span> </div> <div class="bg-muted h-2 w-full overflow-hidden rounded-full"> <div [class]="occupancyBarClass(activeFacility())" [style.width.%]="occupancyPct(activeFacility())"></div> </div> </div> <div class="border-border space-y-1 border-t pt-3"> <div class="text-muted-foreground mb-1 font-mono text-xs tracking-wider uppercase">Staged Relief Sites</div> @for (f of facilities; track f.id) { <button type="button" [class]="rowClass(f)" (click)="focusFacility(f)"> <span class="max-w-[170px] truncate">{{ f.name }}</span> <span class="font-mono text-xs opacity-75"> {{ f.occupied }}/{{ f.capacity }} </span> </button> } </div> <div class="flex gap-2 pt-2"> <button ui-button variant="outline" size="sm" class="flex-1 font-mono text-xs" (click)="focusHazard()"> Exclusion Zone </button> <button ui-button variant="outline" size="sm" class="flex-1 font-mono text-xs" (click)="focusActive()"> Focus Site </button> </div> </ui-card-content> </ui-card> `, }) export class UiLeafletDisasterResponseMapComponent { @Input() facilities: Facility[] = DEFAULT_FACILITIES @Input('class') className?: string @ViewChild(UiLeafletMapComponent) map?: UiLeafletMapComponent protected readonly mapCenter = MAP_CENTER protected readonly incident = INCIDENT protected readonly spotFire = SPOT_FIRE protected readonly evacRadiusM = EVAC_RADIUS_M protected readonly warningRadiusM = WARNING_RADIUS_M readonly activeId = signal(DEFAULT_FACILITIES[0]?.id ?? '') get hostClass(): string { return cn('grid gap-4 lg:grid-cols-[1fr_320px]', this.className) } activeFacility(): Facility { return this.facilities.find((f) => f.id === this.activeId()) ?? this.facilities[0]! } focusFacility(f: Facility): void { this.activeId.set(f.id) this.map?.flyTo?.({ center: f.coords, zoom: 12.5, duration: 800 }) } focusActive(): void { this.focusFacility(this.activeFacility()) } focusHazard(): void { this.map?.flyTo?.({ center: INCIDENT, zoom: 12, duration: 800 }) } occupancyPct(f: Facility): number { return Math.round((f.occupied / f.capacity) * 100) } occupancyRate(f: Facility): number { return f.occupied / f.capacity } occupancyBarClass(f: Facility): string { return cn( 'h-full rounded-full transition-[width,background-color]', this.occupancyRate(f) > 0.8 ? 'bg-warning' : 'bg-success', ) } facilityPinClass(f: Facility): string { return cn( 'flex size-7 items-center justify-center rounded-full shadow-md ring-2', f.type === 'Shelter' ? 'bg-success text-white ring-emerald-400/40' : f.type === 'Medical Staging' ? 'bg-info text-white ring-blue-400/40' : 'bg-primary text-primary-foreground ring-primary/40', ) } facilityGlyph(f: Facility): string { return f.type === 'Shelter' ? '🏠' : f.type === 'Medical Staging' ? '✚' : '★' } shortName(f: Facility): string { return f.name.split(' ')[0] ?? f.name } rowClass(f: Facility): string { return cn( 'flex w-full items-center justify-between rounded-md px-2 py-1 text-xs transition-colors', this.activeId() === f.id ? 'bg-accent text-accent-foreground font-semibold' : 'hover:bg-muted text-muted-foreground', ) } }src/app/components/blocks/leaflet-disaster-response-map/index.ts0.2 kB
export { UiLeafletDisasterResponseMapComponent, DEFAULT_FACILITIES, type Facility, type FacilityType, type FacilityStatus, } from './leaflet-disaster-response-map.component'
Raw manifest:https://uipkge.dev/r/angular/leaflet-disaster-response-map.json