diff --git a/projects/nshmp-apps/src/app/dev/gmm/hanging-wall-effects/state/app.facade.ts b/projects/nshmp-apps/src/app/dev/gmm/hanging-wall-effects/state/app.facade.ts
index 34a40e6e0fceb67ea7c10614e2bb82f18c2c5666..13c202d0d3bd2d631228fc2585f6914e6f30a742 100644
--- a/projects/nshmp-apps/src/app/dev/gmm/hanging-wall-effects/state/app.facade.ts
+++ b/projects/nshmp-apps/src/app/dev/gmm/hanging-wall-effects/state/app.facade.ts
@@ -1,105 +1,251 @@
-import {Injectable} from '@angular/core';
-import {ServiceCallInfo} from '@ghsc/nshmp-lib-ng/nshmp';
-import {NshmpPlot} from '@ghsc/nshmp-lib-ng/plot';
+import {Injectable, signal} from '@angular/core';
+import {AbstractControl, FormBuilder, Validators} from '@angular/forms';
+import {gmmUtils} from '@ghsc/nshmp-lib-no-ngrx/gmm';
+import {
+  FormGroupControls,
+  NshmpService,
+  ServiceCallInfo,
+  SpinnerService,
+} from '@ghsc/nshmp-lib-no-ngrx/nshmp';
+import {NshmpPlot} from '@ghsc/nshmp-lib-no-ngrx/plot';
 import {
   GmmDistanceResponse,
   GmmDistanceUsage,
 } from '@ghsc/nshmp-utils-ts/libs/nshmp-ws/gmm-services';
-import {EnumParameterValues} from '@ghsc/nshmp-utils-ts/libs/nshmp-ws-utils/metadata';
-import {select, Store} from '@ngrx/store';
-import {FormGroupState} from 'ngrx-forms';
-import {Observable} from 'rxjs';
-
-import {appActions} from './app.actions';
-import {hangingWallEffectsAppFeature} from './app.reducer';
-import {ControlPanelForm} from './app.state';
-
-/**
- * Entrypoint to NGRX store for hanging wall effects application.
- */
-@Injectable({
-  providedIn: 'root',
-})
+import {environment} from 'projects/nshmp-apps/src/environments/environment';
+import {catchError} from 'rxjs';
+
+import {createPlots} from '../utils/response-handler.utils';
+import {
+  AppState,
+  ControlPanelForm,
+  defaultFormValues,
+  defaultPlots,
+  initialState,
+  usageFormValues,
+} from './app.state';
+
+@Injectable({providedIn: 'root'})
 export class AppFacade {
-  constructor(private store: Store) {}
+  /** Application state */
+  readonly state = signal<AppState>({
+    ...initialState(),
+  });
+
+  /** Control panel form group */
+  private formGroup = this.formBuilder.group({
+    ...defaultFormValues(),
+    gmmSource: [],
+    MwMulti: [],
+    vs30Multi: [],
+  }) as FormGroupControls<ControlPanelForm>;
+
+  /** nshmp-ws base URL */
+  private baseUrl = environment.webServices.data.url;
+  /** GMM service URL */
+  private serviceUrl = `${this.baseUrl}${environment.webServices.data.services.gmmHwFw}`;
+
+  constructor(
+    private spinnerService: SpinnerService,
+    private nshmpService: NshmpService,
+    private formBuilder: FormBuilder
+  ) {
+    this.validators(this.formGroup);
+    this.formGroup.controls.gmmSource.setValue([]);
+    this.formGroup.controls.showEpistemicUncertainty.disable();
+  }
 
   /**
-   * Returns the control panel form state.
+   * Call the GMM distance service.
    */
-  get controlPanelForm$(): Observable<FormGroupState<ControlPanelForm>> {
-    return this.store.pipe(
-      select(hangingWallEffectsAppFeature.selectControlPanelForm)
+  callServices(): void {
+    if (this.formGroup.invalid) {
+      return;
+    }
+
+    this.spinnerService.show(SpinnerService.MESSAGE_SERVICE);
+
+    const urls = gmmUtils.serviceEndpoints(
+      this.serviceUrl,
+      this.formGroup.getRawValue(),
+      this.formGroup.getRawValue().multiSelectableParam
     );
+
+    this.nshmpService
+      .callServices$<GmmDistanceResponse>(urls)
+      .pipe(catchError((error: Error) => this.nshmpService.throwError$(error)))
+      .subscribe(serviceResponses =>
+        this.handleServiceResponses(serviceResponses)
+      );
   }
 
   /**
-   * Returns the plots.
+   * Returns the control panel form state.
    */
-  get plots$(): Observable<Map<string, NshmpPlot>> {
-    return this.store.pipe(select(hangingWallEffectsAppFeature.selectPlots));
+  get controlPanelForm(): FormGroupControls<ControlPanelForm> {
+    return this.formGroup;
   }
 
   /**
-   * Returns the service call info.
+   * Create plots based on current state and form group.
    */
-  get serviceCallInfo$(): Observable<ServiceCallInfo> {
-    return this.store.pipe(
-      select(hangingWallEffectsAppFeature.selectServiceCallInfo)
-    );
+  createPlots(): void {
+    this.updateState({
+      plots: createPlots(this.state(), this.formGroup),
+    });
   }
 
   /**
-   * Returns the service responses.
+   * Initialize application.
    */
-  get serviceResponses$(): Observable<GmmDistanceResponse[]> {
-    return this.store.pipe(
-      select(hangingWallEffectsAppFeature.selectServiceResponses)
-    );
+  init(): void {
+    this.spinnerService.show(SpinnerService.MESSAGE_METADATA);
+
+    this.nshmpService
+      .callService$<GmmDistanceUsage>(this.serviceUrl)
+      .pipe(catchError((error: Error) => this.nshmpService.throwError$(error)))
+      .subscribe(usageResponse => this.handleUsageResponse(usageResponse));
   }
 
   /**
-   * Returns the supported IMTs.
+   * Reset the control panel.
    */
-  get supportedImts$(): Observable<EnumParameterValues[]> {
-    return this.store.pipe(
-      select(hangingWallEffectsAppFeature.selectSupportedImts)
+  resetControlPanel(): void {
+    this.formGroup.reset(
+      usageFormValues(this.state().usageResponse.response.parameters)
     );
   }
 
   /**
-   * Return the usage for the selected model.
+   * Reset the plot settings.
    */
-  get usage$(): Observable<GmmDistanceUsage> {
-    return this.store.pipe(
-      select(hangingWallEffectsAppFeature.selectUsageResponse)
-    );
+  resetPlotSettings(): void {
+    this.state().plots.forEach((plot, id) => {
+      const defaultSettings = defaultPlots().get(id).settingsForm;
+      plot.settingsForm.patchValue(defaultSettings.getRawValue());
+    });
   }
 
   /**
-   * Call the GMM distance service.
+   * Reset the state.
    */
-  callServices(): void {
-    this.store.dispatch(appActions.callServices());
+  resetState(): void {
+    const serviceCallInfo = gmmUtils.serviceCallInfo({
+      multiSelectableParam: this.formGroup.getRawValue().multiSelectableParam,
+      serviceName: this.state().serviceCallInfo.serviceName,
+      serviceResponses: this.state().serviceResponses,
+      serviceUrl: this.serviceUrl,
+      values: this.formGroup.getRawValue(),
+    });
+
+    this.updateState({
+      plots: createPlots(this.state(), this.formGroup),
+      serviceCallInfo,
+      serviceResponses: null,
+    });
   }
 
   /**
-   * Initialize the application.
+   * Update a single plot in the state.
+   *
+   * @param plot Updated plot
+   * @param id Id of plot to update
    */
-  init(): void {
-    this.store.dispatch(appActions.init());
+  updatePlot(plot: NshmpPlot, id: string): void {
+    const plots = this.state().plots;
+    plots.set(id, plot);
+    this.updateState({
+      plots,
+    });
   }
 
   /**
-   * Reset the control panel.
+   * Update state.
+   *
+   * @param state Partial new state to update
    */
-  resetControlPanel(): void {
-    this.store.dispatch(appActions.resetControlPanel());
+  updateState(state: Partial<AppState>): void {
+    this.state.set({
+      ...this.state(),
+      ...state,
+    });
   }
 
   /**
-   * Reset the control panel.
+   * Handle service response result.
+   *
+   * @param serviceResponses The service responses
    */
-  resetPlotSettings(): void {
-    this.store.dispatch(appActions.resetSettings());
+  private handleServiceResponses(
+    serviceResponses: GmmDistanceResponse[]
+  ): void {
+    const means = serviceResponses.map(s => s.response.means);
+    const hasLogicTree = gmmUtils.hasTree(means);
+
+    if (hasLogicTree) {
+      this.formGroup.controls.showEpistemicUncertainty.enable();
+    } else {
+      this.formGroup.controls.showEpistemicUncertainty.disable();
+    }
+
+    this.updateState({
+      serviceCallInfo: gmmUtils.serviceCallInfo({
+        multiSelectableParam: this.formGroup.getRawValue().multiSelectableParam,
+        serviceName: this.state().serviceCallInfo.serviceName,
+        serviceResponses,
+        serviceUrl: this.serviceUrl,
+        values: this.formGroup.getRawValue(),
+      }),
+      serviceResponses,
+    });
+
+    this.createPlots();
+
+    this.spinnerService.remove();
+  }
+
+  /**
+   * Handle usage response.
+   *
+   * @param usageResponse Usage response
+   */
+  private handleUsageResponse(usageResponse: GmmDistanceUsage): void {
+    this.spinnerService.remove();
+    const parameters = usageResponse.response.parameters;
+    const values = usageFormValues(parameters);
+
+    this.formGroup.patchValue({
+      ...values,
+    });
+
+    const serviceCallInfo: ServiceCallInfo = {
+      ...this.state().serviceCallInfo,
+      usage: [this.serviceUrl],
+    };
+
+    this.updateState({
+      serviceCallInfo,
+      usageResponse,
+    });
+
+    this.spinnerService.remove();
+  }
+
+  /**
+   * Add validators to form controls.
+   *
+   * @param form The form group
+   */
+  private validators(form: FormGroupControls<ControlPanelForm>): void {
+    const required = (control: AbstractControl) => Validators.required(control);
+
+    form.controls.Mw.addValidators(required);
+    form.controls.dip.addValidators(required);
+    form.controls.gmmSource.addValidators(required);
+    form.controls.imt.addValidators(required);
+    form.controls.multiSelectableParam.addValidators(required);
+    form.controls.vs30.addValidators(required);
+    form.controls.width.addValidators(required);
+    form.controls.zTor.addValidators(required);
   }
 }