Skip to content

Map SDK API

Package: @agastyadreamspty/map-sdk

This document is the developer reference for integrating the reusable Map SDK into Angular applications.

It is written for engineers who need to:

  • understand the supported public API
  • embed the full host or individual widgets
  • wire custom adapters or renderers
  • control runtime behavior through configuration
  • verify which responsibilities belong to the SDK versus the consumer app

The SDK is designed to be composable, so consumers can adopt:

  • the full map explorer page
  • filters only
  • legend only
  • parcel search only
  • zoom and layer status only
  • a custom map renderer
  • a custom backend adapter

The package name is SDK-first, and the exported contract names in this package follow the latest SDK surface.

Public Surface

The long-term supported entry points are:

  • MapFeatureConfig
  • MapFeatureZoomPolicy
  • MapFeatureAdapter
  • MapFeatureAuthAdapter
  • MapFeaturePermissionAdapter
  • MapFeatureRendererAdapter
  • MapFeatureProviderOptions
  • MapFeatureAngularAdapter
  • MapFeatureRuntimeConfig
  • MapFeatureHttpConfig
  • MapFeatureThemeVars
  • FeatureEventBus
  • FeatureSdkEvent
  • FeatureSdkSelectionEvent
  • FeatureSdkLayerVisibilityEvent
  • MapFeatureLayerClickEvent
  • FeatureError
  • FeatureErrorCode
  • provideMapSdk(...)
  • provideMapFeatureRuntimeConfig(...)
  • provideMapFeatureHttpConfig(...)
  • provideMapFeatureAdapter(...)
  • provideMapFeatureDataSource(...)
  • provideMapFeatureRendererAdapter(...)
  • MapSdkHostComponent
  • MapSdkHostShellComponent
  • MapSdkFiltersComponent
  • MapSdkLegendPanelComponent
  • MapSdkParcelSearchComponent
  • MapSdkZoomLayerStatusComponent
  • MapSdkService
  • LocationService
  • MapSdkDirectApiFacade
  • MapSdkRequestStateTracker
  • MapSdkRequestViewStateHelper
  • MapSdkVectorTileLayerManager
  • LeafletParcelTileHelper
  • MapboxGlParcelTileHelper
  • OpenLayersParcelTileHelper
  • OpenLayersTileLayerFactories
  • MapSdkParcelClickContext
  • resolveParcelClickContext(...)
  • MapFeatureHttpDataSource
  • MapSdkDetailDialogData
  • MapFeatureFilterOption
  • MapFeatureParcelSearchSuggestion
  • MapFeatureGeometryResponse
  • MapFeatureLegendGroup
  • MapFeatureLegendItem
  • MapLegendResponse
  • MapLegendGuards
  • MapLegendGroupApi
  • MapLegendEntryApi
  • MapLegendStyling
  • MapLegendMatching
  • MAP_FEATURE_DATA_SOURCE
  • MAP_FEATURE_RUNTIME_CONFIG
  • MAP_FEATURE_HTTP_CONFIG
  • MAP_SDK_RENDERER

Everything else should be treated as implementation detail unless it is documented here.

API-only consumption

Consumers that only want the data client can import the SDK root and use the exported data-access services directly. No separate entry point is required.

Typical API-only entry points:

  • MapSdkDirectApiFacade
  • MapSdkRequestStateTracker
  • MapSdkRequestViewStateHelper
  • MapSdkVectorTileLayerManager
  • LeafletParcelTileHelper
  • MapboxGlParcelTileHelper
  • OpenLayersParcelTileHelper
  • OpenLayersTileLayerFactories
  • MapSdkParcelClickContext
  • resolveParcelClickContext(...)
  • MapSdkService
  • LocationService
  • MapFeatureHttpDataSource
  • MapFeatureDataSource
  • MapFeatureParcelSearchSuggestion
  • MapFeatureGeometryResponse

The SDK still ships the UI widgets, but they are optional for API-only consumers.

Example: request-staleness helper

When a consumer app owns the canvas, it can use a small token tracker to ignore late responses from an older province, municipality, or search scope:

import { Injectable } from '@angular/core';
import {
  MapSdkDirectApiFacade,
  MapSdkRequestStateTracker,
  MapFeatureParcelSearchSuggestion
} from '@agastyadreamspty/map-sdk';

@Injectable({ providedIn: 'root' })
export class ParcelRequestFacade {
  private readonly parcelsRequest = new MapSdkRequestStateTracker();

  constructor(private readonly api: MapSdkDirectApiFacade) {}

  beginParcelsRequest(): number {
    return this.parcelsRequest.begin();
  }

  isCurrentParcelsRequest(token: number): boolean {
    return this.parcelsRequest.isCurrent(token);
  }

  resetParcelsRequest(): void {
    this.parcelsRequest.reset();
  }

  searchParcels(provinceId: number, municipalityId: number | null, search: string) {
    return this.api.loadParcels(provinceId, municipalityId, search, 10);
  }

  searchParcelsForCurrentScope(scope: ReturnType<MapSdkDirectApiFacade['buildParcelScope']>, search: string) {
    return this.api.loadParcelsForScope(scope, search, 10);
  }

  getParcelGeometry(provinceId: number, suggestion: MapFeatureParcelSearchSuggestion) {
    return this.api.getParcelGeometry(provinceId, suggestion);
  }

  getDistrictBoundaryDetails(provinceId: number, sourceId?: string | null) {
    return this.api.getDistrictBoundaryDetails(provinceId, sourceId ?? null);
  }

  getSuburbBoundaryDetails(provinceId: number, sourceId?: string | null) {
    return this.api.getSuburbBoundaryDetails(provinceId, sourceId ?? null);
  }

  getTownshipBoundaryDetails(provinceId: number, sourceId?: string | null) {
    return this.api.getTownshipBoundaryDetails(provinceId, sourceId ?? null);
  }
}

The common pattern is:

  1. call begin() before the request starts
  2. keep the returned token with that request flow
  3. call isCurrent(token) before you write into component state
  4. call reset() when the user changes province, municipality, or a similar scope

The same facade also gives you district, township, and suburb detail shortcuts when you need to open an admin-boundary popup without calling the generic admin helper directly.

Example: loading / error view-state helper

When the consumer app owns the canvas, it can keep a tiny state helper beside the request tracker. That gives the page a consistent loading, empty, stale, and error model without pushing UI state into the SDK:

import { Injectable } from '@angular/core';
import {
  MapFeatureParcelSearchResponse,
  MapSdkDirectApiFacade,
  MapSdkRequestViewStateHelper
} from '@agastyadreamspty/map-sdk';

@Injectable({ providedIn: 'root' })
export class ParcelBrowserFacade {
  private readonly parcelsState = new MapSdkRequestViewStateHelper<MapFeatureParcelSearchResponse>();

  constructor(private readonly api: MapSdkDirectApiFacade) {}

  loadParcels(provinceId: number, municipalityId: number | null, search: string): void {
    const token = this.parcelsState.begin();

    this.api.loadParcels(provinceId, municipalityId, search, 10).subscribe({
      next: (response) => {
        const hasRows = Boolean(response?.suggestions?.length);

        if (!this.parcelsState.isCurrent(token)) {
          return;
        }

        if (!hasRows) {
          this.parcelsState.markEmpty(token);
          return;
        }

        this.parcelsState.markSuccess(token, response);
      },
      error: (error) => this.parcelsState.markError(token, error)
    });
  }

  get viewState() {
    return this.parcelsState.getState();
  }
}

The pattern is:

  1. call begin() before you issue the request
  2. keep the returned token with that request flow
  3. call markSuccess(...), markEmpty(...), or markError(...) only if the token is still current
  4. let the component render the current status value instead of hard-coding loading logic in multiple places

Example: vendor parcel tile helpers

These helpers wrap the repetitive layer-building code for the three supported map vendors.

Leaflet:

import { LeafletParcelTileHelper } from '@agastyadreamspty/map-sdk';

constructor(private readonly helper: LeafletParcelTileHelper) {}

this.helper.attach(leafletMap);
this.helper.onParcelClick((detail) => this.parcelDetail = detail);
this.helper.setScope({ provinceId: 101, municipalityId: 202, parcelTypes: 'ERF,FARM' });

Mapbox GL:

import { MapboxGlParcelTileHelper } from '@agastyadreamspty/map-sdk';

constructor(private readonly helper: MapboxGlParcelTileHelper) {}

this.helper.attach(mapboxMap);
this.helper.onParcelClick((detail) => this.parcelDetail = detail);
this.helper.setScope({ provinceId: 101, municipalityId: 202 });

OpenLayers:

import {
  OpenLayersParcelTileHelper,
  OpenLayersTileLayerFactories
} from '@agastyadreamspty/map-sdk';

const factories: OpenLayersTileLayerFactories = {
  createBoundaryLayer(layerId, tileUrl, sourceLayer, color) {
    return buildYourOpenLayersBoundaryLayer(layerId, tileUrl, sourceLayer, color);
  },
  createParcelLayer(layerId, tileUrl, sourceLayer) {
    return buildYourOpenLayersParcelLayer(layerId, tileUrl, sourceLayer);
  }
};

constructor(private readonly helper: OpenLayersParcelTileHelper) {}

this.helper.attach(openLayersMap, factories);
this.helper.onParcelClick((detail) => this.parcelDetail = detail);
this.helper.setScope({ provinceId: 101, municipalityId: 202 });

The helper classes keep the scope handling, tile URL building, and parcel click flow consistent while the consumer app stays in charge of the real map object and its own canvas lifecycle.

Example: direct-API facade

The convenience facade bundles the services that direct-API consumers usually need in one injectable class:

import { Injectable } from '@angular/core';
import {
  MapSdkDirectApiFacade,
  MapFeatureParcelSearchSuggestion
} from '@agastyadreamspty/map-sdk';

@Injectable({ providedIn: 'root' })
export class ParcelLookupFacade {
  constructor(private readonly api: MapSdkDirectApiFacade) {}

  getProvinces() {
    return this.api.getProvinces();
  }

  getMunicipalities(provinceId: number) {
    return this.api.getMunicipalities(provinceId);
  }

  searchParcels(provinceId: number, municipalityId: number | null, search: string) {
    return this.api.loadParcels(provinceId, municipalityId, search, 10);
  }

  getParcelGeometry(provinceId: number, suggestion: MapFeatureParcelSearchSuggestion) {
    return this.api.getParcelGeometry(provinceId, suggestion);
  }

  buildParcelTileUrl(provinceId: number, municipalityId: number | null) {
    return this.api.buildParcelTileUrl(provinceId, municipalityId);
  }
}

If you want to feed a vendor helper with a single object, use buildParcelScope(...):

const scope = this.api.buildParcelScope(101, 202, 'ERF,FARM');
this.helper.setScope(scope);

The same scope object can also drive parcel search:

const scope = this.api.buildParcelScope(101, 202, 'ERF,FARM');
this.api.loadParcelsForScope(scope, 'MAIN ROAD 12', 10);

Example: API-only Angular facade

This pattern lets a consumer app call the SDK API layer without rendering the full host or any widgets:

import { Injectable } from '@angular/core';
import {
  LocationService,
  MapFeatureParcelSearchSuggestion,
  MapSdkService
} from '@agastyadreamspty/map-sdk';

@Injectable({ providedIn: 'root' })
export class ParcelApiFacade {
  constructor(
    private readonly mapSdk: MapSdkService,
    private readonly locations: LocationService
  ) {}

  search(provinceId: number, municipalityId: number | null, search: string) {
    return this.mapSdk.searchParcels(provinceId, municipalityId, search, 10);
  }

  getGeometry(provinceId: number, suggestion: MapFeatureParcelSearchSuggestion) {
    return this.mapSdk.getParcelGeometryBySuggestion(provinceId, suggestion);
  }

  getProvinces() {
    return this.locations.getProvinces();
  }

  getMunicipalities(provinceId: number) {
    return this.locations.getMunicipalities(provinceId);
  }
}

To use the facade in an application, provide the SDK runtime config once at app startup:

import { ApplicationConfig } from '@angular/core';
import { provideMapSdk } from '@agastyadreamspty/map-sdk';

export const appConfig: ApplicationConfig = {
  providers: [
    provideMapSdk({
      runtimeConfig: {
        apiBaseUrl: 'http://localhost:8080/api',
        legendEndpoint: '/map/legend',
        defaultIncludeInactive: false
      }
    })
  ]
};

If a consumer wants to bypass the MapSdkService wrapper, they can inject MapFeatureHttpDataSource directly and call the HTTP-backed methods from the SDK root package. For filter dropdowns, LocationService is the shortest path.

Example: consumer app integration

This shows a minimal Angular app setup that uses the SDK root package, a small facade, and a component that binds provinces, municipalities, and parcel search results end to end:

// app.config.ts
import { ApplicationConfig } from '@angular/core';
import { provideMapSdk } from '@agastyadreamspty/map-sdk';

export const appConfig: ApplicationConfig = {
  providers: [
    provideMapSdk({
      runtimeConfig: {
        apiBaseUrl: 'http://localhost:8080/api',
        legendEndpoint: '/map/legend',
        defaultIncludeInactive: false
      }
    })
  ]
};
// parcel-api-facade.ts
import { Injectable } from '@angular/core';
import {
  LocationService,
  MapFeatureParcelSearchSuggestion,
  MapSdkService
} from '@agastyadreamspty/map-sdk';

@Injectable({ providedIn: 'root' })
export class ParcelApiFacade {
  constructor(
    private readonly map: MapSdkService,
    private readonly locations: LocationService
  ) {}

  getProvinces() {
    return this.locations.getProvinces();
  }

  getMunicipalities(provinceId: number) {
    return this.locations.getMunicipalities(provinceId);
  }

  searchParcels(provinceId: number, municipalityId: number | null, term: string) {
    return this.map.searchParcels(provinceId, municipalityId, term, 10);
  }

  getGeometry(provinceId: number, suggestion: MapFeatureParcelSearchSuggestion) {
    return this.map.getParcelGeometryBySuggestion(provinceId, suggestion);
  }
}
// parcel-search.component.ts
import { CommonModule } from '@angular/common';
import { Component, OnInit, inject } from '@angular/core';
import { FormControl, ReactiveFormsModule } from '@angular/forms';
import { ParcelApiFacade } from './parcel-api-facade';

@Component({
  standalone: true,
  imports: [CommonModule, ReactiveFormsModule],
  template: `
    <select [formControl]="provinceId">
      <option [ngValue]="null">Select province</option>
      <option *ngFor="let province of provinces" [ngValue]="province.id">
        {{ province.name }}
      </option>
    </select>

    <select [formControl]="municipalityId">
      <option [ngValue]="null">Select municipality</option>
      <option *ngFor="let municipality of municipalities" [ngValue]="municipality.id">
        {{ municipality.name }}
      </option>
    </select>

    <input [formControl]="searchTerm" placeholder="Search parcels" />

    <ul>
      <li *ngFor="let suggestion of suggestions" (click)="openSuggestion(suggestion)">
        {{ suggestion.displayLabel }}
      </li>
    </ul>
  `
})
export class ParcelSearchComponent implements OnInit {
  private readonly api = inject(ParcelApiFacade);

  provinceId = new FormControl<number | null>(null);
  municipalityId = new FormControl<number | null>(null);
  searchTerm = new FormControl('', { nonNullable: true });

  provinces: Array<{ id: number; name: string }> = [];
  municipalities: Array<{ id: number; name: string }> = [];
  suggestions: Array<{ displayLabel: string }> = [];

  ngOnInit(): void {
    this.api.getProvinces().subscribe((provinces) => {
      this.provinces = provinces;
    });
  }

  loadMunicipalities(): void {
    const provinceId = this.provinceId.value;
    if (provinceId === null) {
      this.municipalities = [];
      return;
    }

    this.api.getMunicipalities(provinceId).subscribe((municipalities) => {
      this.municipalities = municipalities;
    });
  }

  runSearch(): void {
    const provinceId = this.provinceId.value;
    if (provinceId === null) {
      this.suggestions = [];
      return;
    }

    this.api.searchParcels(provinceId, this.municipalityId.value, this.searchTerm.value).subscribe((response) => {
      this.suggestions = response.suggestions ?? [];
    });
  }

  openSuggestion(suggestion: { displayLabel: string }): void {
    void suggestion;
  }
}

That pattern keeps the SDK as the source of truth for map API calls while the consumer app owns the page state and rendering.

Package Philosophy

The SDK is responsible for:

  • backend data loading
  • legend grouping and normalization
  • zoom thresholds and layer visibility
  • parcel search behavior
  • popup data normalization
  • theming variables
  • common events and callbacks

The consumer app is responsible for:

  • the surrounding page layout
  • the host app chrome
  • any custom map vendor if they do not want the full-page host
  • local wrapper components
  • app-specific styling overrides

Capability Matrix

Use this table to choose the right SDK mode for your app.

Capability Full host Widgets only Custom renderer Headless
Province and municipality filters Yes Yes Yes No
Boundary rendering Yes Yes Yes No
Parcel rendering Yes Yes Yes No
Parcel search Yes Yes Yes No
Legend panel Yes Yes Yes No
Built-in Leaflet canvas Yes Yes No No
Custom map vendor Optional Optional Yes Yes
SDK event bus Yes Yes Yes Yes
SDK fetches backend data Yes Yes Yes Yes

Typical usage:

  • Full host: use when you want the SDK to behave like the warehouse map explorer.
  • Widgets only: use when your app owns layout but wants the SDK filters, legend, and search.
  • Custom renderer: use when your app already owns the map canvas but wants SDK data and behavior.
  • Headless: use when your app wants data, events, and policy logic without any default UI.

Architectural Overview

The SDK sits between the consumer app and the map backend. In the default mode, the SDK owns data loading and rendering while the consumer owns the page shell.

flowchart LR
  subgraph Consumer["Consumer App"]
    A["Route / Page Shell"]
    B["Theme Wrapper"]
    C["Event Handlers"]
    D["Optional Custom Renderer"]
  end

  subgraph SDK["Map SDK"]
    E["MapSdkHostComponent"]
    F["MapSdkHostShellComponent"]
    G["Widgets\nFilters - Legend - Search - Status"]
    H["Providers\nConfig - Adapter - Renderer"]
    I["FeatureEventBus"]
  end

  subgraph Backend["Map Backend API"]
    J["Provinces"]
    K["Municipalities"]
    L["Legends"]
    M["Parcel Search"]
    N["Geometry + Popup Data"]
  end

  A --> E
  A --> G
  B --> E
  C --> I
  D --> H
  E --> H
  G --> H
  H --> Backend
  Backend --> N
  H --> I

Fishbone View

This fishbone-style diagram shows the main responsibility branches that make up the SDK experience.

flowchart LR
  SDK["Map SDK"]

  Consumer["Consumer Shell\nRoute - Layout - Theme"]
  Data["Data Contract\nAPI base URL - adapter - runtime config"]
  UI["UI Widgets\nFilters - Legend - Search - Status"]
  Render["Rendering\nLeaflet host - custom renderer"]
  Events["Events\noutputs - event bus - layer clicks"]
  Policy["Policy\nzoom rules - visibility - gating"]
  Docs["Docs + Deployment\nAPI - integration - theming"]

  Consumer --- SDK
  Data --- SDK
  UI --- SDK
  Render --- SDK
  Events --- SDK
  Policy --- SDK
  Docs --- SDK

Configuration

MapFeatureZoomPolicy

Controls when each boundary or parcel family becomes visible.

export interface MapFeatureZoomPolicy {
  provinceBoundaryZoom: number;
  districtBoundaryZoom: number;
  municipalityBoundaryZoom: number;
  townshipBoundaryZoom: number;
  suburbBoundaryZoom: number;
  parcelZoom: number;
  labelZoomByKind: Record<MapLayerKind, number>;
}

Default values:

  • province boundary: 7
  • district boundary: 8
  • municipality boundary: 9
  • suburb boundary: 10
  • township boundary: 11
  • parcel zoom: 15

MapFeatureConfig

This is the top-level host configuration.

export interface MapFeatureConfig {
  apiBaseUrl: string;
  includeInactive: boolean;
  zoomPolicy: Partial<MapFeatureZoomPolicy>;
  features: MapFeatureFeatureFlags;
  ui: MapFeatureUiConfig;
  search: MapFeatureSearchConfig;
  legend: MapFeatureLegendConfig;
}

Key fields

  • apiBaseUrl
  • Base URL used by the SDK when it talks to the backend.
  • Default: /api
  • Common runtime examples:
    • local: http://localhost:8080/api
    • production: https://api-warehouse.idti.dev/api
  • includeInactive
  • Whether inactive legend entries should be returned or hidden.
  • zoomPolicy
  • Partial override of the default zoom thresholds.
  • features
  • Feature gate block that turns major SDK behaviors on or off.
  • ui
  • Host labels, theming class, container ID, and mount mode.
  • search
  • Search prompt, debounce, and limit behavior.
  • legend
  • Legend toggles and default collapsed state.

MapFeatureFeatureFlags

These gates let the consumer decide which behaviors are active.

  • legend
  • search
  • provinceFilters
  • municipalityFilters
  • selectedMunicipalityOverlay
  • refreshButton
  • boundaries
  • parcels

MapFeatureUiConfig

export interface MapFeatureUiConfig {
  title: string;
  subtitle: string;
  themeClass: string;
  mapContainerId: string;
  showStatusBanner: boolean;
  showLoadingOverlay: boolean;
  mountMode: MapFeatureMountMode;
}

Supported mountMode values:

  • full-page
  • widgets
  • headless

Default UI values:

  • title: Map Explorer
  • subtitle: Browse boundaries, parcels, and live legend layers.
  • theme class: map-sdk-theme
  • map container ID: leaflet-map
  • mount mode: full-page

MapFeatureSearchConfig

export interface MapFeatureSearchConfig {
  placeholder: string;
  minLength: number;
  debounceMs: number;
  resultLimit: number;
}

Default search values:

  • placeholder: Search parcels by LPI, SG number, or description
  • minimum length: 2
  • debounce: 300ms
  • result limit: 8

MapFeatureLegendConfig

export interface MapFeatureLegendConfig {
  allowVisibilityToggle: boolean;
  allowColorEdit: boolean;
  defaultCollapsed: boolean;
}

Default legend behavior:

  • visibility toggle enabled
  • color editing enabled
  • legend expanded by default

Data Contracts

MapFeatureAdapter

MapFeatureAdapter is the main backend data contract. It is an alias for MapFeatureDataSource.

Use it when you want the SDK to fetch data from your API, but you need to replace the HTTP layer or adapt to your own backend.

Required method:

  • loadLegend(provinceId, includeInactive?, municipalityId?)

Optional methods:

  • loadProvinces()
  • loadMunicipalities(provinceId)
  • getParcelDetail(provinceId, parcelType, sourceId)
  • getProvinceGeometry(provinceId)
  • getMunicipalityGeometry(provinceId, municipalityId)
  • searchParcels(provinceId, municipalityId, search, limit?)
  • getParcelGeometryBySuggestion(provinceId, suggestion)
  • getAdminBoundaryDetails(sourceKind, provinceId, sourceId?, displayName?, locationId?)

Common request intent:

Method Purpose
loadLegend(...) Load visible legend groups and colors for the current scope
loadProvinces() Populate province dropdowns
loadMunicipalities(provinceId) Populate municipality dropdowns after a province is selected
getProvinceGeometry(provinceId) Render or refresh the selected province overlay
getMunicipalityGeometry(provinceId, municipalityId) Render or refresh the selected municipality overlay
searchParcels(...) Return parcel suggestions for the search panel
getParcelGeometryBySuggestion(...) Render the selected searched parcel geometry
getParcelDetail(...) Load the popup record for a clicked parcel
getAdminBoundaryDetails(...) Load the popup record for a clicked boundary or administrative feature

For parcel details, pass the tile feature's parcelType unchanged. Supported values are ERF, FARM, FARM_PORTION, and PARENT_FARM. The backend combines that value with sourceId and the province code resolved from provinceId.

MapFeatureHttpDataSource

MapFeatureHttpDataSource is the default HTTP-backed implementation used by the SDK. Consumers that own their own map canvas can also inject it directly when they want the SDK's tile URL builders and REST endpoints without using the full UI.

Useful methods for direct canvas integration:

  • loadProvinces()
  • loadMunicipalities(provinceId)
  • getProvinceGeometry(provinceId)
  • getMunicipalityGeometry(provinceId, municipalityId)
  • searchParcels(provinceId, municipalityId, search, limit?)
  • getParcelGeometryBySuggestion(provinceId, suggestion)
  • getParcelDetail(provinceId, parcelType, sourceId)
  • buildProvinceBoundaryTileUrl(provinceId)
  • buildDistrictBoundaryTileUrl(provinceId)
  • buildMunicipalityBoundaryTileUrl(provinceId)
  • buildTownshipBoundaryTileUrl(provinceId)
  • buildSuburbBoundaryTileUrl(provinceId)
  • buildParcelTileUrl(provinceId, municipalityId, parcelTypes?)

The tile URL builders return .../tiles/{z}/{x}/{y} templates that consumer map vendors can use directly when the app owns the canvas. This is the recommended path for province, district, municipality, township, suburb, and parcel MVT layers.

Example adapter sketch:

export class AppMapAdapter implements MapFeatureAdapter {
  loadLegend(provinceId: string, includeInactive = false, municipalityId?: string) {
    return this.http.get<MapLegendResponse>(
      `${this.baseUrl}/map/legend/${provinceId}`,
      { params: { includeInactive, municipalityId: municipalityId ?? '' } }
    );
  }

  searchParcels(provinceId: string, municipalityId: string, search: string, limit = 8) {
    return this.http.get<MapFeatureParcelSearchResponse>(
      `${this.baseUrl}/map/parcels/search`,
      { params: { provinceId, municipalityId, q: search, limit } }
    );
  }

  getParcelGeometryBySuggestion(provinceId: string, suggestion: MapFeatureParcelSearchSuggestion) {
    return this.http.get<MapFeatureGeometryResponse>(
      `${this.baseUrl}/map/parcels/${provinceId}/geometry`,
      { params: { lpi: suggestion.lpi, parcelType: suggestion.parcelType } }
    );
  }
}

MapSdkService

MapSdkService is the main SDK-first data-access service behind the package. It is what the full host, widgets, and API-only consumers use when they want the SDK's standard data contract without wiring the lower-level adapter directly.

It provides the same behavior as the HTTP data source, but it first checks whether the app supplied a custom MapFeatureDataSource through MAP_FEATURE_DATA_SOURCE.

Common methods:

  • loadLegend(provinceId, includeInactive?, municipalityId?)
  • loadProvinces()
  • loadMunicipalities(provinceId)
  • getParcelDetail(provinceId, parcelType, sourceId)
  • getProvinceGeometry(provinceId)
  • getMunicipalityGeometry(provinceId, municipalityId)
  • searchParcels(provinceId, municipalityId, search, limit?)
  • getParcelGeometryBySuggestion(provinceId, suggestion)
  • getAdminBoundaryDetails(sourceKind, provinceId, sourceId?, displayName?, locationId?)
  • normalizeLegendResponse(response, includeInactive?)
  • buildLegendItems(response, includeInactive?)
  • resolveZoomVisibility(zoom, policy?)
  • getBoundaryLabelState(kind, zoom, policy?)
  • getParcelLabelState(kind, zoom, policy?)
  • getBoundaryZoom(kind, policy?)
  • getParcelZoom(kind, policy?)
  • createFallbackLegendColor(key, label, groupKey)

Use this service when you want one injectable surface for legends, filters, boundary details, parcel search, and zoom/label rules.

LocationService

LocationService is the smallest filter-oriented service in the SDK.

It exposes:

  • getProvinces()
  • getMunicipalities(provinceId)
  • formatMunicipalityLabel(municipality)

Use it when the app only needs province and municipality dropdown data, or when you want a clean helper for formatting municipality labels in a search panel or filter toolbar.

MapFeatureAuthAdapter

export interface MapFeatureAuthAdapter {
  getAccessToken?(): Promise<string | null> | string | null;
  getUserId?(): string | null;
}

Use this when the SDK needs identity context for future auth-aware features or auditing.

MapFeaturePermissionAdapter

export interface MapFeaturePermissionAdapter {
  canAccessFeature?(permission: string): boolean | Promise<boolean>;
}

Use this when the consumer app wants to let the SDK ask whether a feature is allowed.

MapFeatureRendererAdapter

Use this when the consumer app owns the map canvas and wants the SDK to supply data, visibility rules, and parcel behavior while the app handles the actual rendering.

Methods:

  • mount(container)
  • setViewport(viewport)
  • setZoomPolicy(policy)
  • setLayerVisibility(layerId, visible)
  • setLayerLabelsVisible(layerId, visible)
  • setLayerStyle(layerId, style)
  • renderLayer(layerId, geometry, descriptor?)
  • clearLayer(layerId)
  • clearAll()
  • destroy()
  • optional getZoomState()

Typical usage:

  • mount the consumer-owned map into the supplied container
  • call SDK data services for province, municipality, legend, and parcel data
  • render boundary and parcel geometry into the app's canvas with renderLayer(...)
  • keep layer visibility and labels in sync with setLayerVisibility(...) and setLayerLabelsVisible(...)
  • clear or redraw parcel layers when the selected province, municipality, or search context changes

For parcel workflows, the consumer usually calls MapSdkService.searchParcels(...) and MapSdkService.getParcelGeometryBySuggestion(...), then paints the returned geometry into a stable parcel layer id in its own map vendor.

MapFeatureApiClient

Minimal HTTP abstraction:

  • get<TResponse>(url)
  • post<TResponse, TBody>(url, body)

MapFeatureProviderOptions

Options accepted by provideMapSdk(...).

export interface MapFeatureProviderOptions {
  adapter?: MapFeatureAdapter | null;
  rendererAdapter?: MapFeatureRendererAdapter | null;
  runtimeConfig?: Partial<MapFeatureRuntimeConfig> | null;
  httpConfig?: Partial<MapFeatureHttpConfig> | null;
}

MapFeatureAngularAdapter

Angular-specific adapter surface for auth and permission hooks:

  • getAccessToken?()
  • getUserId?()
  • canAccessFeature?(permission)

FeatureEventBus

Lightweight typed event channel for SDK-wide signaling.

export class FeatureEventBus {
  readonly events$: Observable<FeatureSdkEvent>;
  emit(event: FeatureSdkEvent): void;
}

Events

FeatureSdkEvent

Generic event envelope:

export interface FeatureSdkEvent<TPayload = unknown> {
  type: string;
  payload: TPayload;
}

FeatureSdkSelectionEvent

Selection-specific envelope:

export interface FeatureSdkSelectionEvent<TPayload = unknown> extends FeatureSdkEvent<TPayload> {
  type: 'selection';
}

FeatureSdkLayerVisibilityEvent

Emitted when a layer is shown or hidden.

export interface FeatureSdkLayerVisibilityEvent {
  layerId: string;
  visible: boolean;
  reason?: 'legend' | 'zoom' | 'selection' | 'refresh' | 'api' | 'consumer';
}

MapFeatureLayerClickEvent

Emitted when a boundary or parcel feature is clicked.

export interface MapFeatureLayerClickEvent {
  layerId: string;
  kind: string;
  sourceId: string;
  displayName: string;
  featureId?: string | number | null;
  properties: Record<string, unknown>;
  latLng?: [number, number] | null;
}

Event Lifecycle

User action Main SDK events Typical UI update
Select province selection + visibility updates Refresh municipality list, reload legend, refresh overlays
Select municipality selection + visibility updates Focus map, redraw selected boundary, reload parcel scope
Toggle a legend item layerVisibility Show or hide the target boundary or parcel layer
Zoom the map visibility update reason zoom Show or hide zoom-gated layers and labels
Search and select a parcel selection + layer click Render parcel geometry and open popup
Click a boundary or parcel layerClick Open popup with the normalized record data
Refresh map layers visibility update reason refresh Reload current legend and visible geometry

Angular Providers

provideMapSdk(...)

The top-level Angular helper for environment providers.

provideMapSdk({
  runtimeConfig,
  httpConfig,
  adapter,
  rendererAdapter
});

Use it when you want the SDK to be bootstrapped once at the application level.

provideMapFeatureRuntimeConfig(...)

Injects base URL and default legend endpoint values.

provideMapFeatureHttpConfig(...)

Injects the HTTP config override path.

provideMapFeatureAdapter(...)

Registers a custom backend adapter.

provideMapFeatureDataSource(...)

Registers a MapFeatureDataSource directly.

provideMapFeatureRendererAdapter(...)

Registers a custom renderer adapter for headless/custom-map usage.

Injection Tokens

  • MAP_FEATURE_DATA_SOURCE
  • MAP_FEATURE_RUNTIME_CONFIG
  • MAP_FEATURE_HTTP_CONFIG
  • MAP_SDK_RENDERER

UI Components

MapSdkHostComponent

Selector: dw-map-sdk-host

This is the main full-page host that wraps the explorer experience.

Inputs:

  • config
  • adapter
  • includeInactive
  • zoomPolicy
  • themeClass
  • mapContainerId
  • showLegend
  • showSearch
  • defaultProvinceId

Outputs:

  • provinceChange
  • municipalityChange
  • layerVisibilityChange
  • layerClick

Use this component when the SDK should own:

  • data loading
  • map canvas
  • legend rendering
  • search rendering
  • layer visibility rules

MapSdkHostShellComponent

Selector: dw-map-sdk-host-shell

This is the layout shell used by the host. It is useful when you want to compose around the SDK UI pieces but still keep the default explorer shell structure.

Inputs:

  • config
  • adapter
  • headerTitle
  • headerSubtitle
  • hostThemeClass
  • hostMapContainerId
  • provinceOptions
  • municipalityOptions
  • selectedProvinceId
  • selectedMunicipalityId
  • provinceLoading
  • municipalityLoading
  • includeInactive
  • zoom
  • legendResponse
  • mapLoading
  • mapLoadingLabel
  • zoomPolicy
  • showLegend
  • showSearch

Outputs:

  • provinceChange
  • municipalityChange
  • zoomChange
  • layerVisibilityChange
  • layerClick

MapSdkFiltersComponent

Selector: dw-map-sdk-filters

Inputs:

  • provinceOptions
  • municipalityOptions
  • selectedProvinceId
  • selectedMunicipalityId
  • provinceLoading
  • municipalityLoading
  • title
  • subtitle

Outputs:

  • provinceChange
  • municipalityChange

MapSdkLegendPanelComponent

Selector: dw-map-sdk-legend-panel

Inputs:

  • title
  • subtitle
  • groups
  • adminMode

Outputs:

  • legendVisibilityToggle
  • legendActiveToggle
  • legendColorChange

MapSdkParcelSearchComponent

Selector: dw-map-sdk-parcel-search

Inputs:

  • query
  • suggestions
  • loading
  • status
  • placeholder
  • autocompleteOpen
  • activeSuggestionIndex
  • isFocused

Outputs:

  • queryChange
  • focus
  • blur
  • keydown
  • clear
  • suggestionSelect

The suggestionSelect event returns:

{
  suggestion: MapFeatureParcelSearchSuggestion;
  event?: Event | MouseEvent | PointerEvent;
}

MapSdkZoomLayerStatusComponent

Selector: dw-map-sdk-zoom-layer-status

Inputs:

  • zoom
  • policy
  • summary

MapSdkDetailDialogData

The detail dialog is currently represented as a simple data contract:

export interface MapSdkDetailDialogData {
  title: string;
  body: string;
}

Response and DTO Models

These models are part of the public data contract and are useful when the consumer wants to understand the shape of backend responses or typed widget data.

Filter and search models

  • MapFeatureFilterOption
  • used for province and municipality dropdowns
  • fields: id, name, optional code, optional parentId
  • MapFeatureParcelSearchSuggestion
  • represents a parcel suggestion row
  • includes lpi, sgNo, displayLabel, sourceSchema, sourceTable, and location fields
  • MapFeatureParcelSearchResponse
  • contains suggestions and an optional reason

Geometry models

  • MapFeatureGeometryResponse
  • wraps a geometry payload plus status metadata
  • includes layerAvailable, status, reason, minZoom, and featureCollection
  • MapFeatureGeometryFeatureCollection
  • lightweight GeoJSON-style collection wrapper used by the SDK

Legend models

  • MapLegendResponse
  • the backend legend response shape
  • includes provinceId: number, optional guards?: MapLegendGuards, and groups: MapLegendGroupApi[]
  • MapLegendGuards
  • pre-calculated server-side guard maps for immediate O(1) active checks across consumer applications:
    • groups: Record<string, boolean>: active state of legend groups, with support for alias keys
    • layers: Record<string, boolean>: hierarchical effective active state (groupActive && entryActive) indexed by full keys (layer:province-boundary), stripped keys (province-boundary), canonical aliases (province), and source kinds (PROVINCE_BOUNDARY)
  • MapLegendGroupApi
  • backend legend group DTO
  • includes key, label, legendType, sortOrder, optional globalActive, optional description, optional icon, and entries
  • MapLegendEntryApi
  • backend legend entry DTO
  • includes key, label, globalActive, visibleByDefault, editable, matching, and styling
  • MapLegendMatching
  • matching keys that determine when a legend entry applies
  • MapLegendStyling
  • backend styling rules for color, opacity, and icon
  • MapFeatureLegendGroup
  • normalized group used by the UI widgets, including optional globalActive
  • MapFeatureLegendItem
  • normalized legend item used by the UI widgets

Default Runtime Behavior

If a consumer does not override anything, the SDK will:

  • load provinces and municipalities from its configured backend
  • load legends from the configured backend
  • show the full-page explorer host
  • apply the default theme token set
  • enforce zoom thresholds for boundaries and parcels
  • keep parcel rendering gated behind the parcel zoom policy

Default Backend URLs

The SDK uses its own backend config and does not require the consumer app to fetch map payloads directly.

  • local: http://localhost:8080/api
  • production: https://api-warehouse.idti.dev/api

Projected Host Slots

The full-page host supports native Angular content projection slots so consumers can add custom chrome around the map without replacing the SDK's host shell.

Supported slot markers:

  • mapSdkHeader
  • mapSdkSidebar
  • mapSdkFooter

Example:

<dw-map-sdk-host>
  <app-map-toolbar mapSdkHeader></app-map-toolbar>
  <app-map-filters mapSdkSidebar></app-map-filters>
  <app-map-footer mapSdkFooter></app-map-footer>
</dw-map-sdk-host>

This is the supported path for apps that want a custom toolbar, filter rail, or footer but still want the SDK to own the underlying map, parcel, and location logic.

If you do not need projected slots, you can still use the SDK as a pure API package by importing the root services directly.

Theming

Consumers can override the SDK tokens through a wrapper class:

.smoc-map-theme {
  --map-feature-surface: #ffffff;
  --map-feature-surface-elevated: #f6f8fc;
  --map-feature-border: rgba(15, 23, 42, 0.12);
  --map-feature-text: #0f172a;
  --map-feature-muted-text: #475569;
  --map-feature-accent: #0f766e;
  --map-feature-accent-strong: #115e59;
  --map-feature-chip-surface: rgba(15, 118, 110, 0.12);
  --map-feature-chip-text: #0f172a;
}

Build and Pack

From H:\workspace\data-warehouse\frontend:

npm run build:lib
npm pack

The package name is:

@agastyadreamspty/map-sdk

Consumer Guidance

Use the full host if you want the SDK to own the whole map explorer experience.

Use the widgets and adapters if you want:

  • only filters
  • only legend rendering
  • only parcel search
  • only layer status
  • a custom map vendor
  • custom event handling
  • app-specific layout and chrome

The SDK should remain the source of truth for:

  • API fetch behavior
  • legend gating
  • search gating
  • zoom thresholds
  • popup data normalization
  • layer visibility decisions
  • theming tokens

Troubleshooting

Layers do not appear after zooming

  • Confirm the active zoom policy still allows the layer to render.
  • Confirm the selected province and municipality are valid for the current API response.
  • Confirm the consumer is not hiding the layer via legend visibility.

Legend or search appears behind the map

  • Put the SDK host and overlay containers in a stacking context above the canvas.
  • Make sure the consumer wrapper does not reset the SDK z-index tokens.
  • Keep search and legend panels outside any container that clips overflow.

Search suggestions appear but clicking them does nothing

  • Confirm the consumer is using the SDK click handler, not a mouse-down override.
  • Confirm the adapter implements getParcelGeometryBySuggestion(...).
  • Confirm the parcel geometry endpoint returns a non-empty feature collection.
  • Confirm the active province and municipality are passed into the adapter request.
  • Confirm the backend response matches the current filter scope.
  • Confirm the consumer is not reusing cached popup data from a previous selection.