Menu

Change language

GitHub repo

Leaflet Property Boundary Map

blockreal-estate

Cadastral parcel boundary polygons with zoning classes, lot acreage, and assessed valuation on free Esri satellite tiles — no API key.

Installation

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

Variants

Loading interactive previews…

Schema

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

Parcel
interface Parcel {
  apn: string
  address: string
  zoning: string
  /** Plain-hex zone fill — Leaflet renders paths to SVG/canvas, so CSS vars do not resolve there. */
  color: string
  lotSqFt: string
  acres: number
  assessedValue: string
  taxYear: number
  coords: [number, number]
  polygon: [number, number][]
}

Files installed (2)

  • src/app/components/blocks/leaflet-property-boundary-map/leaflet-property-boundary-map.component.ts8.7 kB
    // Cadastral parcel boundary polygons with zoning classes, lot acreage, and assessed valuation
    // on free Esri satellite tiles — no API key. 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 {
      UiLeafletMapComponent,
      UiLeafletMarkerComponent,
      UiLeafletPolygonComponent,
    } from '@/ui/leaflet-map/leaflet-map.component'
    
    export interface Parcel {
      apn: string
      address: string
      zoning: string
      /** Plain-hex zone fill — Leaflet renders paths to SVG/canvas, so CSS vars do not resolve there. */
      color: string
      lotSqFt: string
      acres: number
      assessedValue: string
      taxYear: number
      coords: [number, number]
      polygon: [number, number][]
    }
    
    export const DEFAULT_PARCELS: Parcel[] = [
      {
        apn: '093-142-018',
        address: '418 Kingsley Ave, Palo Alto, CA',
        zoning: 'R-1 (Single Family)',
        color: '#facc15',
        lotSqFt: '8,712 sq ft',
        acres: 0.2,
        assessedValue: '$3,940,000',
        taxYear: 2025,
        coords: [-122.1709, 37.4281],
        polygon: [
          [-122.1715, 37.4285],
          [-122.1703, 37.4285],
          [-122.1703, 37.4277],
          [-122.1715, 37.4277],
          [-122.1715, 37.4285],
        ],
      },
      {
        apn: '093-142-019',
        address: '424 Kingsley Ave, Palo Alto, CA',
        zoning: 'R-1 (Single Family)',
        color: '#facc15',
        lotSqFt: '9,580 sq ft',
        acres: 0.22,
        assessedValue: '$4,120,000',
        taxYear: 2025,
        coords: [-122.1693, 37.4281],
        polygon: [
          [-122.1699, 37.4285],
          [-122.1687, 37.4285],
          [-122.1687, 37.4277],
          [-122.1699, 37.4277],
          [-122.1699, 37.4285],
        ],
      },
      {
        apn: '093-142-021',
        address: '510 Lytton Ave, Palo Alto, CA',
        zoning: 'CS (Service Commercial)',
        color: '#c084fc',
        lotSqFt: '21,780 sq ft',
        acres: 0.5,
        assessedValue: '$6,850,000',
        taxYear: 2025,
        coords: [-122.1699, 37.4268],
        polygon: [
          [-122.1708, 37.4273],
          [-122.169, 37.4273],
          [-122.169, 37.4263],
          [-122.1708, 37.4263],
          [-122.1708, 37.4273],
        ],
      },
      {
        apn: '093-142-024',
        address: '777 Bryant Ct, Palo Alto, CA',
        zoning: 'RM-20 (Multi-Family)',
        color: '#38bdf8',
        lotSqFt: '13,068 sq ft',
        acres: 0.3,
        assessedValue: '$5,230,000',
        taxYear: 2025,
        coords: [-122.1723, 37.4268],
        polygon: [
          [-122.173, 37.4273],
          [-122.1716, 37.4273],
          [-122.1716, 37.4263],
          [-122.173, 37.4263],
          [-122.173, 37.4273],
        ],
      },
      {
        apn: '093-142-030',
        address: '250 Hamilton Ave, Palo Alto, CA',
        zoning: 'PF (Public Facility)',
        color: '#34d399',
        lotSqFt: '30,492 sq ft',
        acres: 0.7,
        assessedValue: '$8,900,000',
        taxYear: 2025,
        coords: [-122.1679, 37.4261],
        polygon: [
          [-122.1687, 37.4265],
          [-122.1671, 37.4265],
          [-122.1671, 37.4256],
          [-122.1687, 37.4256],
          [-122.1687, 37.4265],
        ],
      },
    ]
    
    // Downtown Palo Alto — stable reference so the map's center watcher never re-fires.
    export const MAP_CENTER: [number, number] = [-122.1697, 37.4275]
    
    @Component({
      changeDetection: ChangeDetectionStrategy.Eager,
      selector: 'ui-leaflet-property-boundary-map, [ui-leaflet-property-boundary-map]',
      standalone: true,
      imports: [
        UiBadgeComponent,
        UiButtonComponent,
        UiCardComponent,
        UiCardContentComponent,
        UiCardDescriptionComponent,
        UiCardHeaderComponent,
        UiCardTitleComponent,
        UiLeafletMapComponent,
        UiLeafletMarkerComponent,
        UiLeafletPolygonComponent,
      ],
      host: {
        'data-slot': 'leaflet-property-boundary-map',
        '[class]': 'hostClass',
      },
      template: `
        <div class="border-border bg-card relative h-[480px] overflow-hidden rounded-xl border">
          <ui-leaflet-map variant="satellite-streets" [center]="mapCenter" [zoom]="16" class="h-full w-full">
            <!-- Cadastral lot polygons, filled by zoning class -->
            @for (p of parcels; track 'lot-' + p.apn) {
              <ui-leaflet-polygon
                [lngLatPath]="p.polygon"
                [color]="p.color"
                [weight]="activeParcel()?.apn === p.apn ? 3 : 1.5"
                [opacity]="0.95"
                [fill]="true"
                [fillColor]="p.color"
                [fillOpacity]="activeParcel()?.apn === p.apn ? 0.35 : 0.15"
              />
            }
    
            @for (p of parcels; track p.apn) {
              <ui-leaflet-marker [lngLat]="p.coords" anchor="center">
                <div class="group flex cursor-pointer flex-col items-center" (click)="selectParcel(p)">
                  <span
                    class="border-border bg-background/90 rounded border px-1.5 py-0.5 font-mono text-xs font-bold shadow-md"
                    [class]="activeParcel()?.apn === p.apn ? 'text-foreground' : 'text-muted-foreground'"
                    [style]="activeParcel()?.apn === p.apn ? { color: p.color, borderColor: p.color } : null"
                  >
                    {{ p.apn }}
                  </span>
                </div>
              </ui-leaflet-marker>
            }
          </ui-leaflet-map>
        </div>
    
        <!-- Cadastral Parcel Valuation Card -->
        @if (activeParcel(); as parcel) {
          <ui-card class="flex flex-col justify-between">
            <ui-card-header>
              <div class="flex items-center justify-between">
                <span ui-badge variant="outline" class="font-mono text-xs">APN {{ parcel.apn }}</span>
                <span ui-badge variant="secondary" class="gap-1.5 text-xs">
                  <span class="size-2 rounded-full" [style.backgroundColor]="parcel.color"></span>
                  {{ parcel.zoning }}
                </span>
              </div>
              <ui-card-title class="mt-2 text-base font-medium">{{ parcel.address }}</ui-card-title>
              <ui-card-description class="font-mono text-xs"
                >{{ parcel.lotSqFt }} • {{ parcel.acres }} Acres</ui-card-description
              >
            </ui-card-header>
            <ui-card-content class="space-y-4">
              <div class="border-border space-y-2 border-t pt-3 font-mono text-xs">
                <div class="flex items-center justify-between">
                  <span class="text-muted-foreground">Assessed Valuation</span>
                  <span class="text-foreground text-base font-bold">{{ parcel.assessedValue }}</span>
                </div>
                <div class="flex items-center justify-between">
                  <span class="text-muted-foreground">Zoning Code</span>
                  <span class="text-foreground">{{ parcel.zoning }}</span>
                </div>
                <div class="flex items-center justify-between">
                  <span class="text-muted-foreground">Assessment Roll Year</span>
                  <span class="text-foreground">{{ parcel.taxYear }}</span>
                </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">
                  Adjacent Cadastral Lots
                </div>
                @for (p of parcels; track p.apn) {
                  <button type="button" [class]="rowClass(p)" (click)="selectParcel(p)">
                    <span class="font-mono">{{ p.apn }}</span>
                    <span class="font-mono text-xs opacity-75">{{ p.lotSqFt }}</span>
                  </button>
                }
              </div>
    
              <button ui-button variant="outline" size="sm" class="w-full font-mono text-xs" (click)="recenter()">
                Center Cadastral Lot
              </button>
            </ui-card-content>
          </ui-card>
        }
      `,
    })
    export class UiLeafletPropertyBoundaryMapComponent {
      @Input() parcels: Parcel[] = DEFAULT_PARCELS
      @Input('class') className?: string
    
      @ViewChild(UiLeafletMapComponent) map?: UiLeafletMapComponent
    
      protected readonly mapCenter = MAP_CENTER
    
      readonly activeApn = signal(DEFAULT_PARCELS[0]?.apn ?? '')
    
      get hostClass(): string {
        return cn('grid gap-4 lg:grid-cols-[1fr_320px]', this.className)
      }
    
      activeParcel(): Parcel | undefined {
        return this.parcels.find((p) => p.apn === this.activeApn()) ?? this.parcels[0]
      }
    
      selectParcel(p: Parcel): void {
        this.activeApn.set(p.apn)
        this.map?.flyTo({ center: p.coords, zoom: 17 })
      }
    
      recenter(): void {
        const parcel = this.activeParcel()
        if (parcel) this.selectParcel(parcel)
      }
    
      rowClass(p: Parcel): string {
        return cn(
          'flex w-full items-center justify-between rounded-md px-2 py-1 text-xs transition-colors',
          this.activeParcel()?.apn === p.apn
            ? 'bg-accent text-accent-foreground font-semibold'
            : 'hover:bg-muted text-muted-foreground',
        )
      }
    }
    
  • src/app/components/blocks/leaflet-property-boundary-map/index.ts0.1 kB

Raw manifest:https://uipkge.dev/r/angular/leaflet-property-boundary-map.json