Disaster Response Map
blocklogisticsIncident management map with wildfire hazard perimeter, shelter capacities, and emergency staging points.
Installation
$pnpm dlx uipkge-ng@latest add disaster-response-map$npx uipkge-ng@latest add disaster-response-map$yarn dlx uipkge-ng@latest add disaster-response-map$bunx uipkge-ng@latest add disaster-response-mapManifest:
https://uipkge.dev/r/angular/disaster-response-map.jsonInstalls to:src/app/components/blocks/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.
ResponseShelterinterface ResponseShelter {
id: string
name: string
type: 'Shelter' | 'Medical Staging' | 'Command Post'
coords: [number, number]
capacity: number
occupied: number
status: 'Open' | 'Near Capacity' | 'Closed'
}Files installed (2)
src/app/components/blocks/disaster-response-map/disaster-response-map.component.ts11.8 kB
// Incident management map on Mapbox: wildfire hazard perimeter, shelter capacities and // emergency staging points with an incident control dashboard. Port of the React block // 1:1; markers follow the last-mile-courier-dispatch Angular pattern. import { type AfterViewInit, ChangeDetectionStrategy, Component, type ElementRef, Input, type OnDestroy, type QueryList, ViewChild, ViewChildren, signal, } from '@angular/core' import type * as mapboxgl from 'mapbox-gl' 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 { UiMapComponent, type MapLayerInput, type MapSourceInput } from '@/ui/map/map.component' export interface ResponseShelter { id: string name: string type: 'Shelter' | 'Medical Staging' | 'Command Post' coords: [number, number] capacity: number occupied: number status: 'Open' | 'Near Capacity' | 'Closed' } export const HAZARD_CENTER: [number, number] = [-105.28, 40.0] export const MAP_CENTER: [number, number] = [-105.27, 40.01] export const FIRE_ORIGIN: [number, number] = [-105.29, 40.0] // Simulated wildfire hazard perimeter coordinates (near Boulder, CO) export const FIRE_PERIMETER_COORDS: [number, number][] = [ [-105.32, 40.01], [-105.29, 40.03], [-105.26, 40.02], [-105.25, 39.99], [-105.28, 39.97], [-105.31, 39.98], [-105.32, 40.01], ] export const RESPONSE_FACILITIES: ResponseShelter[] = [ { id: 's-1', name: 'County Event Center Shelter', type: 'Shelter', coords: [-105.24, 40.04], capacity: 500, occupied: 340, status: 'Open', }, { id: 's-2', name: 'West Valley Medical Staging', type: 'Medical Staging', coords: [-105.22, 39.98], capacity: 120, occupied: 98, status: 'Near Capacity', }, { id: 's-3', name: 'Unified Incident Command Post', type: 'Command Post', coords: [-105.25, 40.07], capacity: 80, occupied: 45, status: 'Open', }, ] @Component({ changeDetection: ChangeDetectionStrategy.Eager, selector: 'ui-disaster-response-map, [ui-disaster-response-map]', standalone: true, host: { '[attr.data-slot]': '"disaster-response-map"', '[class]': 'hostClass', }, imports: [ UiBadgeComponent, UiButtonComponent, UiCardComponent, UiCardContentComponent, UiCardDescriptionComponent, UiCardHeaderComponent, UiCardTitleComponent, UiMapComponent, ], template: ` <div class="grid gap-4 lg:grid-cols-[1fr_320px]"> <div class="border-border bg-card relative h-[480px] overflow-hidden rounded-xl border"> <div ui-map variant="light" [accessToken]="accessToken ?? ''" [center]="mapCenter" [zoom]="11.2" class="h-full w-full" [sources]="hazardSources" [layers]="hazardLayers" (created)="onMapCreated($event)" > <!-- Marker elements: handed to mapbox-gl Markers once the map exists, which moves each inner #fireMarker / #facilityMarker node onto the map; the outer node stays for @for. --> <div class="hidden"> <!-- Active Fire Origin Marker --> <div> <div #fireMarker> <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> </div> </div> <!-- Shelters and Medical Staging --> @for (f of facilities; track f.id) { <div> <div #facilityMarker> <div class="group flex cursor-pointer flex-col items-center" (click)="focusFacility(f)"> <div class="flex size-7 items-center justify-center rounded-full shadow-md ring-2" [class]="facilityPinClass(f.type)" > {{ facilityIcon(f.type) }} </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" > {{ firstWord(f.name) }} </span> </div> </div> </div> } </div> </div> </div> <!-- Incident Control Dashboard --> <div ui-card class="flex flex-col justify-between"> <div 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> <h3 ui-card-title class="mt-2 text-lg">{{ activeFacility().name }}</h3> <p ui-card-description class="text-xs"> Capacity: {{ activeFacility().occupied }} / {{ activeFacility().capacity }} beds occupied </p> </div> <div 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">{{ occupancyPercent(activeFacility()) }}%</span> </div> <div class="bg-muted h-2 w-full overflow-hidden rounded-full"> <div class="h-full rounded-full transition-[width,background-color]" [class]="occupancyBarClass(activeFacility())" [style.width.%]="occupancyPercent(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]="siteRowClass(f.id)" (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)="focusFacility(activeFacility())" > Focus Site </button> </div> </div> </div> </div> `, }) export class UiDisasterResponseMapComponent implements AfterViewInit, OnDestroy { @Input() accessToken?: string @Input('class') className?: string @ViewChildren('facilityMarker') private facilityMarkerEls?: QueryList<ElementRef<HTMLDivElement>> @ViewChild('fireMarker') private fireMarkerEl?: ElementRef<HTMLDivElement> readonly facilities: ResponseShelter[] = RESPONSE_FACILITIES.map((f) => ({ ...f, coords: [...f.coords] as [number, number], })) readonly mapCenter: [number, number] = [...MAP_CENTER] as [number, number] readonly activeId = signal(RESPONSE_FACILITIES[0]?.id ?? '') /** The wildfire hazard exclusion zone (the React block's MapSource). */ readonly hazardSources: MapSourceInput[] = [ { id: 'hazard-zone', type: 'geojson', data: { type: 'Feature', properties: {}, geometry: { type: 'Polygon', coordinates: [FIRE_PERIMETER_COORDS.map((c) => [...c])] }, }, }, ] /** The red fill + dashed outline (the React block's MapLayers). */ readonly hazardLayers: MapLayerInput[] = [ { id: 'hazard-zone-fill', type: 'fill', source: 'hazard-zone', paint: { 'fill-color': '#ef4444', 'fill-opacity': 0.28 }, }, { id: 'hazard-zone-line', type: 'line', source: 'hazard-zone', paint: { 'line-color': '#dc2626', 'line-width': 2.5, 'line-dasharray': [2, 1] }, }, ] private map: mapboxgl.Map | null = null private markers: mapboxgl.Marker[] = [] private destroyed = false get hostClass(): string { return cn('block w-full', this.className) } ngAfterViewInit(): void { // A new facilities list re-renders the marker elements; put the new set on the map. this.facilityMarkerEls?.changes.subscribe(() => void this.syncMarkers()) } ngOnDestroy(): void { this.destroyed = true this.clearMarkers() this.map = null } activeFacility(): ResponseShelter { return this.facilities.find((f) => f.id === this.activeId()) ?? this.facilities[0]! } occupancyPercent(f: ResponseShelter): number { return Math.round((f.occupied / f.capacity) * 100) } occupancyBarClass(f: ResponseShelter): string { return f.occupied / f.capacity > 0.8 ? 'bg-warning' : 'bg-success' } facilityIcon(type: ResponseShelter['type']): string { return type === 'Shelter' ? '🏠' : type === 'Medical Staging' ? '✚' : '★' } facilityPinClass(type: ResponseShelter['type']): string { return cn( type === 'Shelter' ? 'bg-success text-white ring-emerald-400/40' : type === 'Medical Staging' ? 'bg-info text-white ring-blue-400/40' : 'bg-primary text-primary-foreground ring-primary/40', ) } siteRowClass(id: string): string { return cn( 'flex w-full items-center justify-between rounded-md px-2 py-1 text-xs transition-colors', this.activeFacility().id === id ? 'bg-accent text-accent-foreground font-semibold' : 'hover:bg-muted text-muted-foreground', ) } firstWord(name: string): string { return name.split(' ')[0] ?? name } focusFacility(f: ResponseShelter): void { this.activeId.set(f.id) this.map?.flyTo?.({ center: f.coords, zoom: 12.5 }) } focusHazard(): void { this.map?.flyTo?.({ center: [...HAZARD_CENTER] as [number, number], zoom: 11.5 }) } onMapCreated(map: mapboxgl.Map): void { this.map = map void this.syncMarkers() } /** One mapbox-gl Marker for the fire origin plus one per facility, anchored at the bottom (the React MapMarkers). */ private async syncMarkers(): Promise<void> { const map = this.map if (!map) return const facilityEls = this.facilityMarkerEls?.toArray() ?? [] const fireEl = this.fireMarkerEl?.nativeElement const mod = await import('mapbox-gl') const mapbox = mod.default ?? mod if (this.destroyed || map !== this.map) return this.clearMarkers() const facilities = this.facilities this.markers = facilityEls.flatMap((el, i) => { const coords = facilities[i]?.coords if (!coords) return [] return [new mapbox.Marker({ element: el.nativeElement, anchor: 'bottom' }).setLngLat(coords).addTo(map)] }) if (fireEl) { this.markers.push(new mapbox.Marker({ element: fireEl, anchor: 'center' }).setLngLat([...FIRE_ORIGIN]).addTo(map)) } } private clearMarkers(): void { for (const m of this.markers) m.remove() this.markers = [] } }src/app/components/blocks/disaster-response-map/index.ts0.2 kB
export { UiDisasterResponseMapComponent, RESPONSE_FACILITIES, FIRE_PERIMETER_COORDS, FIRE_ORIGIN, HAZARD_CENTER, MAP_CENTER, type ResponseShelter, } from './disaster-response-map.component'
Raw manifest:https://uipkge.dev/r/angular/disaster-response-map.json