Menu

Change language

GitHub repo

Leaflet Fleet Vehicle Map

blocklogistics

Live fleet map on free OpenStreetMap/Esri tiles — no API key. Select a unit to fly the camera and read status.

Installation

$npx uipkge-ng@latest add leaflet-fleet-vehicle-map
Manifest:https://uipkge.dev/r/angular/leaflet-fleet-vehicle-map.jsonInstalls to:src/app/components/blocks/leaflet-fleet-vehicle-map/

Variants

Loading interactive previews…

Schema

Type aliases exported from this item's source. Use these to shape the data you pass in.

FleetVehicle
interface FleetVehicle {
  id: string
  identifier: string
  model: string
  driver: string
  status: VehicleStatus
  speed: number
  lat: number
  lng: number
}

Files installed (2)

  • src/app/components/blocks/leaflet-fleet-vehicle-map/leaflet-fleet-vehicle-map.component.ts5.9 kB
    // Live fleet map on free OpenStreetMap/Esri tiles — no API key. Select a unit to fly the
    // camera and read status. 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 {
      UiCardComponent,
      UiCardContentComponent,
      UiCardHeaderComponent,
      UiCardTitleComponent,
    } from '@/ui/card/card.component'
    import { UiLeafletMapComponent, UiLeafletMarkerComponent } from '@/ui/leaflet-map/leaflet-map.component'
    
    export type VehicleStatus = 'moving' | 'idling' | 'parked'
    
    export interface FleetVehicle {
      id: string
      identifier: string
      model: string
      driver: string
      status: VehicleStatus
      speed: number
      lat: number
      lng: number
    }
    
    export const DEFAULT_VEHICLES: FleetVehicle[] = [
      {
        id: 'v-104',
        identifier: 'Truck #104',
        model: 'Volvo VNL 860',
        driver: 'Marcus Vance',
        status: 'moving',
        speed: 64,
        lat: 41.5868,
        lng: -93.625,
      },
      {
        id: 'v-208',
        identifier: 'Van #208',
        model: 'Ford E-Transit 350',
        driver: 'Elena Rostova',
        status: 'moving',
        speed: 48,
        lat: 41.6005,
        lng: -93.712,
      },
      {
        id: 'v-109',
        identifier: 'Truck #109',
        model: 'Freightliner Cascadia',
        driver: 'Derrick Hayes',
        status: 'idling',
        speed: 0,
        lat: 41.984,
        lng: -93.582,
      },
      {
        id: 'v-212',
        identifier: 'Van #212',
        model: 'Mercedes Sprinter',
        driver: 'Aaliyah Patel',
        status: 'moving',
        speed: 52,
        lat: 41.591,
        lng: -93.604,
      },
    ]
    
    const PIN_SELECTED =
      'border-primary bg-primary text-primary-foreground rounded-full border px-2 py-0.5 font-mono text-xs font-semibold shadow-xs'
    const PIN_IDLE =
      'border-border bg-card text-foreground rounded-full border px-2 py-0.5 font-mono text-xs font-semibold shadow-xs'
    
    @Component({
      changeDetection: ChangeDetectionStrategy.Eager,
      selector: 'ui-leaflet-fleet-vehicle-map, [ui-leaflet-fleet-vehicle-map]',
      standalone: true,
      imports: [
        UiBadgeComponent,
        UiCardComponent,
        UiCardHeaderComponent,
        UiCardTitleComponent,
        UiCardContentComponent,
        UiLeafletMapComponent,
        UiLeafletMarkerComponent,
      ],
      host: {
        'data-slot': 'leaflet-fleet-vehicle-map',
        '[class]': 'hostClass',
      },
      template: `
        <div class="flex flex-wrap items-center justify-between gap-3">
          <h2 class="text-foreground text-lg font-semibold tracking-tight">Fleet</h2>
          <p class="text-muted-foreground text-xs">{{ vehicles.length }} units</p>
        </div>
    
        <div class="grid min-h-0 flex-1 grid-cols-1 gap-4 lg:grid-cols-12">
          <ui-card class="border-border flex min-h-0 flex-col overflow-hidden shadow-xs lg:col-span-8">
            <div class="relative min-h-[360px] flex-1 overflow-hidden">
              <ui-leaflet-map variant="dark" [center]="mapCenter()" [zoom]="11" class="absolute inset-0 size-full">
                @for (vehicle of vehicles; track vehicle.id) {
                  <ui-leaflet-marker [lngLat]="[vehicle.lng, vehicle.lat]" anchor="center">
                    <button type="button" [class]="pinClass(vehicle)" (click)="selectVehicle(vehicle)">
                      {{ shortId(vehicle) }}
                    </button>
                  </ui-leaflet-marker>
                }
              </ui-leaflet-map>
            </div>
          </ui-card>
    
          <ui-card class="border-border flex min-h-0 flex-col overflow-hidden shadow-xs lg:col-span-4">
            <ui-card-header class="border-border border-b p-4">
              <ui-card-title class="text-sm font-semibold">Vehicles</ui-card-title>
            </ui-card-header>
            <ui-card-content class="divide-border min-h-0 flex-1 divide-y overflow-auto p-0">
              @for (vehicle of vehicles; track vehicle.id) {
                <button type="button" [class]="rowClass(vehicle)" (click)="selectVehicle(vehicle)">
                  <div class="min-w-0">
                    <p class="text-foreground truncate text-sm font-medium">{{ vehicle.identifier }}</p>
                    <p class="text-muted-foreground truncate text-xs">{{ vehicle.driver }} · {{ vehicle.model }}</p>
                  </div>
                  <span ui-badge [variant]="statusVariant(vehicle.status)" class="shrink-0 text-xs capitalize">
                    {{ statusLabel(vehicle) }}
                  </span>
                </button>
              }
            </ui-card-content>
          </ui-card>
        </div>
      `,
    })
    export class UiLeafletFleetVehicleMapComponent {
      @Input() vehicles: FleetVehicle[] = DEFAULT_VEHICLES
      @Input('class') className?: string
    
      @ViewChild(UiLeafletMapComponent) map?: UiLeafletMapComponent
    
      readonly selectedId = signal(DEFAULT_VEHICLES[0]?.id ?? '')
    
      get hostClass(): string {
        return cn('flex h-full min-h-0 flex-col gap-4', this.className)
      }
    
      selected(): FleetVehicle {
        return this.vehicles.find((v) => v.id === this.selectedId()) ?? this.vehicles[0]!
      }
    
      mapCenter(): [number, number] {
        const s = this.selected()
        return s ? [s.lng, s.lat] : [-93.625, 41.59]
      }
    
      selectVehicle(vehicle: FleetVehicle): void {
        this.selectedId.set(vehicle.id)
        this.map?.flyTo?.({
          center: [vehicle.lng, vehicle.lat],
          zoom: 12,
          duration: 800,
        })
      }
    
      statusVariant(status: VehicleStatus): 'info' | 'warning' | 'secondary' {
        if (status === 'moving') return 'info'
        if (status === 'idling') return 'warning'
        return 'secondary'
      }
    
      statusLabel(vehicle: FleetVehicle): string {
        return vehicle.status === 'moving' ? `${vehicle.speed} mph` : vehicle.status
      }
    
      shortId(vehicle: FleetVehicle): string {
        return vehicle.identifier.replace('Truck ', '').replace('Van ', '')
      }
    
      pinClass(vehicle: FleetVehicle): string {
        return this.selectedId() === vehicle.id ? PIN_SELECTED : PIN_IDLE
      }
    
      rowClass(vehicle: FleetVehicle): string {
        return cn(
          'hover:bg-accent/50 flex w-full items-center justify-between gap-3 px-4 py-3 text-left transition-colors',
          this.selectedId() === vehicle.id && 'bg-accent/60',
        )
      }
    }
    
  • src/app/components/blocks/leaflet-fleet-vehicle-map/index.ts0.2 kB

Raw manifest:https://uipkge.dev/r/angular/leaflet-fleet-vehicle-map.json