Menu

Change language

GitHub repo

Property Boundary Map

blockreal-estate

Cadastral parcel boundaries with zoning classifications, lot acreage, and tax assessment valuation.

Installation

$npx uipkge-ng@latest add property-boundary-map
Manifest:https://uipkge.dev/r/angular/property-boundary-map.jsonInstalls to:src/app/components/blocks/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
  lotSqFt: string
  acres: number
  assessedValue: string
  taxYear: number
  coords: [number, number]
  polygon: [number, number][]
}

Files installed (2)

  • src/app/components/blocks/property-boundary-map/property-boundary-map.component.ts7.1 kB
    // Cadastral parcel boundaries with zoning classifications, lot acreage, and tax assessment
    // valuation on a Mapbox satellite map. Port of the React block 1:1 (same lib: map).
    import { ChangeDetectionStrategy, Component, Input, ViewChild, computed, 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 { UiMapComponent, UiMapMarkerComponent, type MapLayerInput, type MapSourceInput } from '@/ui/map/map.component'
    
    export interface Parcel {
      apn: string
      address: string
      zoning: string
      lotSqFt: string
      acres: number
      assessedValue: string
      taxYear: number
      coords: [number, number]
      polygon: [number, number][]
    }
    
    export const DEFAULT_PARCELS: Parcel[] = [
      {
        apn: '0482-104-012',
        address: '450 Mountain View Rd, Austin, TX',
        zoning: 'SF-3 (Single Family)',
        lotSqFt: '14,280 sq ft',
        acres: 0.33,
        assessedValue: '$1,280,000',
        taxYear: 2025,
        coords: [-97.7431, 30.2672],
        polygon: [
          [-97.7438, 30.2678],
          [-97.7424, 30.2678],
          [-97.7424, 30.2666],
          [-97.7438, 30.2666],
          [-97.7438, 30.2678],
        ],
      },
      {
        apn: '0482-104-013',
        address: '460 Mountain View Rd, Austin, TX',
        zoning: 'SF-3 (Single Family)',
        lotSqFt: '16,500 sq ft',
        acres: 0.38,
        assessedValue: '$1,410,000',
        taxYear: 2025,
        coords: [-97.741, 30.2672],
        polygon: [
          [-97.7424, 30.2678],
          [-97.741, 30.2678],
          [-97.741, 30.2666],
          [-97.7424, 30.2666],
          [-97.7424, 30.2678],
        ],
      },
    ]
    
    @Component({
      changeDetection: ChangeDetectionStrategy.Eager,
      selector: 'ui-property-boundary-map, [ui-property-boundary-map]',
      standalone: true,
      host: {
        '[attr.data-slot]': '"property-boundary-map"',
        '[class]': 'hostClass',
      },
      imports: [
        UiBadgeComponent,
        UiButtonComponent,
        UiCardComponent,
        UiCardContentComponent,
        UiCardDescriptionComponent,
        UiCardHeaderComponent,
        UiCardTitleComponent,
        UiMapComponent,
        UiMapMarkerComponent,
      ],
      template: `
        <div class="border-border bg-card relative h-[480px] overflow-hidden rounded-xl border">
          <div
            ui-map
            [accessToken]="accessToken ?? ''"
            variant="satellite-streets"
            [center]="activeParcel().coords"
            [zoom]="16.5"
            class="h-full w-full"
            [sources]="mapSources()"
            [layers]="mapLayers()"
          >
            @for (p of parcels; track p.apn) {
              <ui-map-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 ? 'border-warning text-warning' : 'text-foreground'"
                  >
                    {{ p.apn }}
                  </span>
                </div>
              </ui-map-marker>
            }
          </div>
        </div>
    
        <!-- Cadastral Parcel Valuation Card -->
        <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="font-mono text-xs">APN {{ activeParcel().apn }}</span>
              <span ui-badge class="border-warning/20 bg-warning/10 text-warning">{{ activeParcel().zoning }}</span>
            </div>
            <h3 ui-card-title class="mt-2 text-base font-medium">{{ activeParcel().address }}</h3>
            <p ui-card-description class="font-mono text-xs">
              {{ activeParcel().lotSqFt }} • {{ activeParcel().acres }} Acres
            </p>
          </div>
          <div 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">{{ activeParcel().assessedValue }}</span>
              </div>
              <div class="flex items-center justify-between">
                <span class="text-muted-foreground">Zoning Code</span>
                <span class="text-foreground">{{ activeParcel().zoning }}</span>
              </div>
              <div class="flex items-center justify-between">
                <span class="text-muted-foreground">Assessment Roll Year</span>
                <span class="text-foreground">{{ activeParcel().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>
          </div>
        </div>
      `,
    })
    export class UiPropertyBoundaryMapComponent {
      @Input() parcels: Parcel[] = DEFAULT_PARCELS
      @Input() accessToken?: string
      @Input('class') className?: string
    
      @ViewChild(UiMapComponent) map?: UiMapComponent
    
      readonly activeApn = signal(DEFAULT_PARCELS[0]?.apn ?? '')
    
      readonly mapSources = computed<MapSourceInput[]>(() => [
        {
          id: 'parcel-primary',
          options: {
            type: 'geojson',
            data: {
              type: 'Feature',
              properties: {},
              geometry: { type: 'Polygon', coordinates: [this.activeParcel().polygon] },
            },
          },
        },
      ])
    
      readonly mapLayers = computed<MapLayerInput[]>(() => [
        {
          id: 'parcel-primary-fill',
          options: {
            type: 'fill',
            source: 'parcel-primary',
            paint: { 'fill-color': '#eab308', 'fill-opacity': 0.28 },
          },
        },
        {
          id: 'parcel-primary-line',
          options: {
            type: 'line',
            source: 'parcel-primary',
            paint: { 'line-color': '#facc15', 'line-width': 2.5 },
          },
        },
      ])
    
      get hostClass(): string {
        return cn('grid gap-4 lg:grid-cols-[1fr_320px]', this.className)
      }
    
      activeParcel(): Parcel {
        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 {
        this.selectParcel(this.activeParcel())
      }
    
      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/property-boundary-map/index.ts0.1 kB

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