Menu

Change language

GitHub repo

Leaflet Hub Locator Map

blockdashboard

Distribution hub locator on free OpenStreetMap tiles — no API key. Coverage rings, capacity badges, and fly-to on select.

Installation

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

Variants

Loading interactive previews…

Schema

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

DistributionHub
interface DistributionHub {
  id: string
  code: string
  name: string
  city: string
  state: string
  lat: number
  lng: number
  capacityPct: number
  bays: number
  /** Delivery coverage radius in miles. */
  coverageMiles: number
}

Files installed (2)

  • src/app/components/blocks/leaflet-hub-locator-map/leaflet-hub-locator-map.component.ts6.7 kB
    import { Component, Input, OnInit, ViewChild, signal, ChangeDetectionStrategy } 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 {
      UiLeafletCircleComponent,
      UiLeafletMapComponent,
      UiLeafletMarkerComponent,
    } from '@/ui/leaflet-map/leaflet-map.component'
    
    export interface DistributionHub {
      id: string
      code: string
      name: string
      city: string
      state: string
      lat: number
      lng: number
      capacityPct: number
      bays: number
      /** Delivery coverage radius in miles. */
      coverageMiles: number
    }
    
    export const DEFAULT_HUBS: DistributionHub[] = [
      {
        id: 'atl-01',
        code: 'ATL-01',
        name: 'South Atlanta DC',
        city: 'Atlanta',
        state: 'GA',
        lat: 33.629,
        lng: -84.402,
        capacityPct: 82,
        bays: 36,
        coverageMiles: 18,
      },
      {
        id: 'atl-02',
        code: 'ATL-02',
        name: 'Marietta Cross-Dock',
        city: 'Marietta',
        state: 'GA',
        lat: 33.953,
        lng: -84.55,
        capacityPct: 64,
        bays: 22,
        coverageMiles: 14,
      },
      {
        id: 'atl-03',
        code: 'ATL-03',
        name: 'McDonough Fulfillment',
        city: 'McDonough',
        state: 'GA',
        lat: 33.447,
        lng: -84.147,
        capacityPct: 91,
        bays: 48,
        coverageMiles: 22,
      },
      {
        id: 'atl-04',
        code: 'ATL-04',
        name: 'Peachtree City Hub',
        city: 'Peachtree City',
        state: 'GA',
        lat: 33.397,
        lng: -84.596,
        capacityPct: 57,
        bays: 18,
        coverageMiles: 15,
      },
      {
        id: 'atl-05',
        code: 'ATL-05',
        name: 'Douglasville Depot',
        city: 'Douglasville',
        state: 'GA',
        lat: 33.751,
        lng: -84.747,
        capacityPct: 73,
        bays: 26,
        coverageMiles: 16,
      },
      {
        id: 'atl-06',
        code: 'ATL-06',
        name: 'Conyers Annex',
        city: 'Conyers',
        state: 'GA',
        lat: 33.667,
        lng: -84.018,
        capacityPct: 88,
        bays: 30,
        coverageMiles: 20,
      },
    ]
    
    const METERS_PER_MILE = 1609.34
    
    // Downtown Atlanta as [lng, lat] — a stable reference so the map's center input never re-fires.
    const ATLANTA_CENTER: [number, number] = [-84.388, 33.749]
    
    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'
    
    /**
     * Distribution hub locator on free OpenStreetMap tiles — no API key. Coverage
     * rings, capacity badges, and fly-to on select (map pin or location row).
     */
    @Component({
      changeDetection: ChangeDetectionStrategy.Eager,
      selector: 'ui-leaflet-hub-locator-map, [ui-leaflet-hub-locator-map]',
      standalone: true,
      imports: [
        UiBadgeComponent,
        UiCardComponent,
        UiCardHeaderComponent,
        UiCardTitleComponent,
        UiCardContentComponent,
        UiLeafletMapComponent,
        UiLeafletMarkerComponent,
        UiLeafletCircleComponent,
      ],
      host: {
        'data-slot': 'leaflet-hub-locator-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">Distribution hubs</h2>
          <p class="text-muted-foreground text-xs">{{ hubs.length }} centers</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="streets" [center]="atlantaCenter" [zoom]="10.5" class="absolute inset-0 size-full">
                @for (hub of hubs; track hub.id + '-coverage') {
                  <ui-leaflet-circle
                    [center]="[hub.lng, hub.lat]"
                    [radius]="coverageMeters(hub)"
                    color="#3b82f6"
                    [weight]="selectedId() === hub.id ? 2 : 1.5"
                    [fill]="true"
                    fillColor="#3b82f6"
                    [fillOpacity]="selectedId() === hub.id ? 0.16 : 0.07"
                  />
                }
                @for (hub of hubs; track hub.id) {
                  <ui-leaflet-marker [lngLat]="[hub.lng, hub.lat]" anchor="center">
                    <button type="button" [class]="pinClass(hub)" (click)="selectHub(hub)">{{ hub.code }}</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">Locations</ui-card-title>
            </ui-card-header>
            <ui-card-content class="divide-border min-h-0 flex-1 divide-y overflow-auto p-0">
              @for (hub of hubs; track hub.id) {
                <button type="button" [class]="rowClass(hub)" (click)="selectHub(hub)">
                  <div class="min-w-0">
                    <p class="text-foreground truncate text-sm font-medium">{{ hub.name }}</p>
                    <p class="text-muted-foreground truncate text-xs">
                      {{ hub.city }}, {{ hub.state }} · {{ hub.bays }} bays · {{ hub.coverageMiles }} mi
                    </p>
                  </div>
                  <ui-badge [variant]="capacityVariant(hub.capacityPct)" class="shrink-0 font-mono text-xs tabular-nums">
                    {{ hub.capacityPct }}%
                  </ui-badge>
                </button>
              }
            </ui-card-content>
          </ui-card>
        </div>
      `,
    })
    export class UiLeafletHubLocatorMapComponent implements OnInit {
      @Input() hubs: DistributionHub[] = DEFAULT_HUBS
      @Input('class') className?: string
    
      @ViewChild(UiLeafletMapComponent) map?: UiLeafletMapComponent
    
      protected readonly atlantaCenter = ATLANTA_CENTER
      readonly selectedId = signal('')
    
      get hostClass(): string {
        return cn('flex h-full min-h-0 flex-col gap-4', this.className)
      }
    
      ngOnInit(): void {
        this.selectedId.set(this.hubs[0]?.id ?? '')
      }
    
      selectHub(hub: DistributionHub): void {
        this.selectedId.set(hub.id)
        this.map?.flyTo({
          center: [hub.lng, hub.lat],
          zoom: 11.5,
          duration: 900,
        })
      }
    
      capacityVariant(capacityPct: number): 'warning' | 'info' {
        return capacityPct >= 85 ? 'warning' : 'info'
      }
    
      coverageMeters(hub: DistributionHub): number {
        return hub.coverageMiles * METERS_PER_MILE
      }
    
      pinClass(hub: DistributionHub): string {
        return this.selectedId() === hub.id ? PIN_SELECTED : PIN_IDLE
      }
    
      rowClass(hub: DistributionHub): 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() === hub.id && 'bg-accent/60',
        )
      }
    }
    
  • src/app/components/blocks/leaflet-hub-locator-map/index.ts0.1 kB

Raw manifest:https://uipkge.dev/r/angular/leaflet-hub-locator-map.json