HonuaController Application API

HonuaController is the renderer-neutral application API for common map app flows. It composes existing HonuaMapRuntime, generated-app runtime, and ExplorationContext primitives instead of owning a separate state engine.

import { createHonuaController } from "@honua/sdk-js/app-controller";
import { loadMapPackage } from "@honua/sdk-js/runtime";

const runtime = await loadMapPackage(mapPackage, map, {
  client,
  skipCompatibilityCheck: true,
});

const controller = createHonuaController({
  runtime,
  layerGroups: {
    operations: ["incident-points", "unit-lines"],
  },
  legendItemLayers: {
    "incidents-0": ["incident-points"],
  },
});

controller.onSelectionChange((event) => {
  renderInspector(event.selection);
});

controller.onIdle((event) => {
  persistSessionState(event.snapshot);
});

controller.fitBounds([-159, 19, -155, 22], { padding: 32 });
controller.selectFeature("incidents", 1001);
controller.setVisibility({
  hide: [{ kind: "layer-group", id: "operations" }],
  show: [{ kind: "legend-item", id: "incidents-0" }],
});

controller.addOverlay({
  id: "dispatch-point",
  kind: "point",
  coordinate: [-157.85, 21.31],
  properties: { status: "active" },
});

controller.addAnnotation({
  id: "dispatch-note",
  kind: "note",
  coordinate: [-157.85, 21.31],
  text: "Temporary session note",
});

Layer and Source CRUD

HonuaController exposes Mapbox-style source and layer operations at the application boundary. When the controller wraps HonuaMapRuntime, these methods delegate to the runtime's MapLibre adapter; custom adapters can provide the same methods without exposing a renderer directly.

const controller = createHonuaController({
  runtime,
  layerSourcePersistence: {
    async save(payload) {
      await saveDraftMap({
        packageId: payload.mapPackage?.mapPackageId,
        style: payload.style,
        mutations: payload.mutations,
        ifMatch: payload.concurrencyToken,
      });
      return { version: "map-version-42" };
    },
    async discard(payload) {
      await discardDraftMap(payload.mapPackage?.mapPackageId);
    },
  },
});

controller.addSource("dispatch", {
  type: "geojson",
  data: { type: "FeatureCollection", features: [] },
});

controller.addLayer(
  {
    id: "dispatch-points",
    type: "circle",
    source: "dispatch",
    paint: { "circle-color": "#00a884", "circle-radius": 6 },
    metadata: { editable: true },
  },
  { beforeId: "incident-points" },
);

controller.setLayerPaint("dispatch-points", { "circle-color": "#006f5f" });
controller.moveLayer("dispatch-points", { position: "top" });
controller.setLayerVisibility("dispatch-points", false);

await controller.saveLayerSourceChanges({ concurrencyToken: "etag-from-server" });

The source shape follows MapLibre source entries for native geojson, vector, raster, raster-dem, and protocol-backed sources. Honua custom sources such as honua-feature-service, honua-map-service, honua-ogc-features, honua-wms, and honua-wmts can also be passed through when the runtime has the corresponding adapter support. Query-tile and hosted MapPackage bindings should be materialized as runtime source specifications before calling addSource.

Layer operations mirror the Felt-style app workflow: create a source, add one or more styled layers, update paint/layout/filter/metadata, move the layer in the render stack, toggle visibility, refresh the backing source, and remove the layer or source. Invalid mutations fail before the pending mutation list is updated; runtime-backed controllers surface HonuaRuntimeDiagnosticError diagnostics from the MapLibre style checks.

Runtime-only mutations are ephemeral. saveLayerSourceChanges() and discardLayerSourceChanges() call optional hooks with a traceable payload: pending mutations, current composed style, current MapPackage, and optional version or concurrency tokens. When no hook is configured, the controller still tracks and clears local pending mutation state, but it does not write to a server or restore a previous renderer style.

Snapshots carry exploration state, viewport, visibility, overlays, and annotations:

const snapshot = controller.snapshot();
controller.restore(snapshot);
controller.dispose();

After dispose(), controller operations throw HonuaControllerError with code: "disposed" so application teardown can be handled predictably.