Repository: swisstopo/swissgeol-assets-suite
Pull request #471: Feature #302: Position der Suchergebnisse beibehalten
Author: login: daniel-va
Created: 2025-03-26T13:28:02Z | Merged: 2025-03-31T09:31:24Z

Resolves #302.

Refactor effects of viewer into `ViewerControllerService` for easier management of the loading behavior.

Linked issue #302: Position der Suchergebnisse beibehalten
**Beschreibung**

Wenn man in der Tabelle der Suchergebnisse nach unten scrollt, so werden Ergebnisse nachgeladen. Wenn man ein nachgeladenes Asset auswählt wird korrekt die Detailansicht gezeigt und das Asset in der Tabelle als ausgewählt markiert.

Sofern man aber wegnavigiert, z.B. um das Asset zu editieren, und anschliessend zurücknavigiert, so ist die Tabelle wieder mit den ersten Ergebnissen gefüllt, obwohl das Asset noch als Detail angewählt ist.

Stelle sicher, dass die Position der Suchergebnisse in diesem Szenario nicht verloren geht.


**Acceptance Criteria**

- [ ] Die Tabelle ist auch nach Navigation wieder am korrekten Platz

Linked issue comments: user: login: mPfifi
created_at: 2025-03-25T16:10:55Z
body: @daniel-va @vej-ananas  Ich habe das gerade noch kurz auf DEV getestet: die Applikation navigiert nach dem Wechsel in die Bearbeitungsansicht zwar ganz kurz an die richtige Position in der Liste, kehrt danach aber zum Anfang der Liste zurück. Die Listenanzeige sollte aber beim Zurücknavigieren wieder an der jeweiligen Stelle des ausgewählten Assets stehen und nicht zum Listenanfang zurückkehren (erwartetes Verhalten).
Hier stimmt also etwas noch nicht ganz.

Dasselbe gilt auch, wenn man z.B. von "Filter" nach "Favoriten" wechselt und wieder zurück: die Liste steht danach nicht mehr auf dem in der Detailansicht angezeigten Asset.
author_association: NONE
user: login: mPfifi
created_at: 2025-03-31T12:52:00Z
body: @daniel-va @vej-ananas 
Auf INT scheint es ganz gut zu funktionieren, ausser man wechselt vom Tab "Filter" zum Tab "Favoriten" und zurück - da geht vorangehende die Position in der Liste verloren, die Detailansicht ist aber korrekt.
Auf DEV landet man dagegen an der falschen Stelle in der Liste, die Detailansicht ist dagegen korrekt.
Die Anforderungen sind demnach noch nicht ganz erfüllt.
author_association: NONE

Diff:
diff --git a/apps/client-asset-sg/src/app/components/menu-bar/menu-bar.component.html b/apps/client-asset-sg/src/app/components/menu-bar/menu-bar.component.html
index 27c708459..c6b6003c1 100644
--- a/apps/client-asset-sg/src/app/components/menu-bar/menu-bar.component.html
+++ b/apps/client-asset-sg/src/app/components/menu-bar/menu-bar.component.html
@@ -4,10 +4,9 @@
       *rxLet="activeItem === 'home'; let isActive"
       asset-sg-menu-bar-item
       icon="assets"
-      [link]="isActive ? null : [translateService.currentLang]"
-      [badge]="isActive ? (activeFilters$ | async) : null"
+      [badge]="isActive ? (activeFilterCount$ | async) : null"
       [isActive]="isActive"
-      (click)="isActive ? toggleAssetDrawer() : null"
+      (click)="isActive ? toggleAssetDrawer() : goToViewer({ favoritesOnly: false })"
     >
       menuBar.filters
     </li>
@@ -16,10 +15,9 @@
       *rxLet="activeItem === 'favorites'; let isActive"
       asset-sg-menu-bar-item
       icon="favorite"
-      [link]="isActive ? null : [translateService.currentLang, 'favorites']"
-      [badge]="isActive ? (activeFilters$ | async) : null"
+      [badge]="isActive ? (activeFilterCount$ | async) : null"
       [isActive]="isActive"
-      (click)="isActive ? toggleAssetDrawer() : null"
+      (click)="isActive ? toggleAssetDrawer() : goToViewer({ favoritesOnly: true })"
     >
       menuBar.favourites
     </li>
diff --git a/apps/client-asset-sg/src/app/components/menu-bar/menu-bar.component.ts b/apps/client-asset-sg/src/app/components/menu-bar/menu-bar.component.ts
index 032c94e6b..070aae12f 100644
--- a/apps/client-asset-sg/src/app/components/menu-bar/menu-bar.component.ts
+++ b/apps/client-asset-sg/src/app/components/menu-bar/menu-bar.component.ts
@@ -5,12 +5,21 @@ import { AssetEditPolicy } from '@asset-sg/shared/v2';
 import { UntilDestroy } from '@ngneat/until-destroy';
 import { Store } from '@ngrx/store';
 import { TranslateService } from '@ngx-translate/core';
-import { filter, map, Observable, startWith } from 'rxjs';
+import { filter, firstValueFrom, map, Observable, startWith } from 'rxjs';
 
 // eslint-disable-next-line @nx/enforce-module-boundaries
-import { setFiltersOpen } from '../../../../../../libs/asset-viewer/src/lib/state/asset-search/asset-search.actions';
+import {
+  setFiltersState,
+  updateSearchQuery,
+  PanelState,
+} from '../../../../../../libs/asset-viewer/src/lib/state/asset-search/asset-search.actions';
+
 // eslint-disable-next-line @nx/enforce-module-boundaries
-import { selectActiveFilters } from '../../../../../../libs/asset-viewer/src/lib/state/asset-search/asset-search.selector';
+import {
+  selectActiveFilters,
+  selectIsFiltersOpen,
+} from '../../../../../../libs/asset-viewer/src/lib/state/asset-search/asset-search.selector';
+
 import { AppState } from '../../state/app-state';
 
 @UntilDestroy()
@@ -30,9 +39,10 @@ export class MenuBarComponent {
 
   readonly translateService = inject(TranslateService);
 
-  readonly activeFilters$ = this.store
+  readonly activeFilterCount$ = this.store
     .select(selectActiveFilters)
     .pipe(map((filters) => (filters.length > 0 ? filters.length : null)));
+
   readonly userExists$ = this.store.select(fromAppShared.selectIsAnonymousMode).pipe(map((anonymous) => !anonymous));
 
   readonly activeItem$: Observable<MenuItem | null> = this.router.events.pipe(
@@ -61,8 +71,27 @@ export class MenuBarComponent {
     startWith('home' as const)
   );
 
-  toggleAssetDrawer(): void {
-    this.store.dispatch(setFiltersOpen({ isOpen: 'toggle' }));
+  async toggleAssetDrawer(): Promise<void> {
+    const isOpen = await firstValueFrom(this.store.select(selectIsFiltersOpen));
+    if (isOpen) {
+      this.store.dispatch(setFiltersState({ state: PanelState.ClosedManually }));
+    } else {
+      this.store.dispatch(setFiltersState({ state: PanelState.OpenedManually }));
+    }
+  }
+
+  goToViewer({ favoritesOnly }: { favoritesOnly: boolean }): void {
+    const basePath = `/${this.translateService.currentLang}`;
+    const favoritesPath = `${basePath}/favorites`;
+
+    const [sourcePath, targetPath] = favoritesOnly ? [basePath, favoritesPath] : [favoritesPath, basePath];
+
+    const currentPath = this.router.url.split('?', 2)[0];
+    if (currentPath === sourcePath) {
+      this.store.dispatch(updateSearchQuery({ query: { favoritesOnly } }));
+    } else {
+      this.router.navigateByUrl(targetPath).then();
+    }
   }
 
   protected readonly AssetEditPolicy = AssetEditPolicy;
diff --git a/libs/asset-editor/src/lib/components/asset-editor-launch/asset-editor-launch.component.ts b/libs/asset-editor/src/lib/components/asset-editor-launch/asset-editor-launch.component.ts
index 58f64089d..91f4e796f 100644
--- a/libs/asset-editor/src/lib/components/asset-editor-launch/asset-editor-launch.component.ts
+++ b/libs/asset-editor/src/lib/components/asset-editor-launch/asset-editor-launch.component.ts
@@ -9,15 +9,7 @@ import {
   ViewChild,
   ViewContainerRef,
 } from '@angular/core';
-import {
-  AppPortalService,
-  appSharedStateActions,
-  AppState,
-  CURRENT_LANG,
-  LifecycleHooks,
-  LifecycleHooksDirective,
-} from '@asset-sg/client-shared';
-import { Store } from '@ngrx/store';
+import { AppPortalService, CURRENT_LANG, LifecycleHooks, LifecycleHooksDirective } from '@asset-sg/client-shared';
 import { asyncScheduler, observeOn } from 'rxjs';
 
 @Component({
@@ -35,7 +27,6 @@ export class AssetEditorLaunchComponent implements OnDestroy {
   private readonly appPortalService = inject(AppPortalService);
   private readonly viewContainerRef = inject(ViewContainerRef);
   private readonly cd = inject(ChangeDetectorRef);
-  private readonly store = inject<Store<AppState>>(Store);
   public readonly currentLang$ = inject(CURRENT_LANG);
 
   constructor() {
@@ -45,7 +36,6 @@ export class AssetEditorLaunchComponent implements OnDestroy {
         new TemplatePortal(this._templateDrawerPortalContent, this.viewContainerRef)
       );
       this.cd.detectChanges();
-      this.store.dispatch(appSharedStateActions.openPanel());
     });
   }
 
diff --git a/libs/asset-viewer/src/lib/asset-viewer.module.ts b/libs/asset-viewer/src/lib/asset-viewer.module.ts
index b95a9b34f..8413d6151 100644
--- a/libs/asset-viewer/src/lib/asset-viewer.module.ts
+++ b/libs/asset-viewer/src/lib/asset-viewer.module.ts
@@ -44,7 +44,6 @@ import {
 } from '@asset-sg/client-shared';
 import { FavoritesModule } from '@asset-sg/favorites';
 import { SvgIconComponent } from '@ngneat/svg-icon';
-import { EffectsModule } from '@ngrx/effects';
 import { StoreModule } from '@ngrx/store';
 import { TranslateModule, TranslatePipe } from '@ngx-translate/core';
 import { ForModule } from '@rx-angular/template/for';
@@ -63,7 +62,6 @@ import { AssetViewerPageComponent } from './components/asset-viewer-page';
 import { MapComponent } from './components/map/map.component';
 import { MapControlsComponent } from './components/map-controls/map-controls.component';
 import { ViewerParamsService } from './services/viewer-params.service';
-import { AssetSearchEffects } from './state/asset-search/asset-search.effects';
 import { assetSearchReducer } from './state/asset-search/asset-search.reducer';
 import { mapControlReducer } from './state/map-control/map-control.reducer';
 
@@ -92,7 +90,6 @@ import { mapControlReducer } from './state/map-control/map-control.reducer';
     TranslateModule.forChild(),
     StoreModule.forFeature('assetSearch', assetSearchReducer),
     StoreModule.forFeature('mapControl', mapControlReducer),
-    EffectsModule.forFeature(AssetSearchEffects),
     ReactiveFormsModule,
 
     SvgIconComponent,
diff --git a/libs/asset-viewer/src/lib/components/asset-picker/asset-picker.component.ts b/libs/asset-viewer/src/lib/components/asset-picker/asset-picker.component.ts
index 45f279fbd..34bf84bae 100644
--- a/libs/asset-viewer/src/lib/components/asset-picker/asset-picker.component.ts
+++ b/libs/asset-viewer/src/lib/components/asset-picker/asset-picker.component.ts
@@ -28,8 +28,7 @@ import {
   Subject,
   switchMap,
 } from 'rxjs';
-
-import * as actions from '../../state/asset-search/asset-search.actions';
+import { ViewerControllerService } from '../../services/viewer-controller.service';
 
 interface AssetPickerState {
   assets: AssetEditDetail[];
@@ -52,6 +51,8 @@ const initialState: AssetPickerState = {
   changeDetection: ChangeDetectionStrategy.OnPush,
 })
 export class AssetPickerComponent extends RxState<AssetPickerState> {
+  private readonly viewerControllerService = inject(ViewerControllerService);
+
   public show$ = this.select('show');
   public assets$ = this.select('assets');
   public currentAssetId$ = this.select('currentAssetId');
@@ -160,7 +161,7 @@ export class AssetPickerComponent extends RxState<AssetPickerState> {
   }
 
   public selectAndClose(assetId: number) {
-    this._store.dispatch(actions.assetClicked({ assetId }));
+    this.viewerControllerService.selectAsset(assetId);
     this.closePicker$.next();
   }
 }
diff --git a/libs/asset-viewer/src/lib/components/asset-search-detail/asset-search-detail.component.html b/libs/asset-viewer/src/lib/components/asset-search-detail/asset-search-detail.component.html
index a0e0954f5..95d3fa840 100644
--- a/libs/asset-viewer/src/lib/components/asset-search-detail/asset-search-detail.component.html
+++ b/libs/asset-viewer/src/lib/components/asset-search-detail/asset-search-detail.component.html
@@ -1,176 +1,169 @@
-<div *ngIf="(loadingState | async) === LoadingState.Loading; else detail" class="loading-spinner">
-  <mat-progress-spinner mode="indeterminate" />
+@if (asset$ | async; as assetDetail) {
+<div class="header">
+  <span translate="search.detailedInformation"></span>
+  <button
+    (click)="clearSelectedAsset()"
+    asset-sg-icon-button
+    class="close-button"
+    [attr.alt]="'search.closeAssetDetails' | translate"
+  >
+    <svg-icon key="close"></svg-icon>
+  </button>
 </div>
-<ng-template #detail>
-  <div class="header">
-    <span translate="search.detailedInformation"></span>
-    <button
-      (click)="clearSelectedAsset()"
-      asset-sg-icon-button
-      class="close-button"
-      [attr.alt]="'search.closeAssetDetails' | translate"
-    >
-      <svg-icon key="close"></svg-icon>
-    </button>
-  </div>
-  <div class="asset-detail-scroll-container">
-    <div class="asset-detail">
-      <ng-container *ngIf="assetDetail$ | push as assetDetail">
-        <div class="flex flex-row justify-between mb-4">
-          <div class="text-dark-red font-bold mt-2.5">{{ assetDetail.titlePublic }}</div>
-          <ng-container *canUpdate="AssetEditPolicy; with: assetDetail">
-            <a asset-sg-icon-button [routerLink]="['/', currentLang$ | async, 'asset-admin', assetDetail.assetId]">
-              <svg-icon key="edit" data-testid="edit-asset-button"></svg-icon>
-            </a>
-          </ng-container>
-        </div>
-        <div class="asset-status">
-          <ng-container *ngIf="assetDetail.publicUse.isAvailable; else internalUse">
-            <svg-icon
-              [attr.aria-hidden]="false"
-              key="success-filled"
-              [attr.alt]="'search.usageCode.public' | translate"
-            />
-          </ng-container>
-          <ng-template #internalUse>
-            <ng-container *ngIf="assetDetail.internalUse.isAvailable; else onRequest">
-              <svg-icon
-                [attr.aria-hidden]="false"
-                key="warning-filled"
-                [attr.alt]="'search.usageCode.internal' | translate"
-              />
-            </ng-container>
-            <ng-template #onRequest>
-              <svg-icon
-                [attr.aria-hidden]="false"
-                key="error-filled"
-                [attr.alt]="'search.usageCode.useOnRequest' | translate"
-              />
-            </ng-template>
-          </ng-template>
-        </div>
-        <table class="asset-details" *rxLet="filesByType$; let files">
-          <tr *ngIf="files.Normal.length > 0">
-            <th translate="search.file"></th>
-            <td>
-              <ul asset-sg-asset-viewer-files [assetId]="assetDetail.assetId" [files]="files.Normal" type="Normal"></ul>
-            </td>
-          </tr>
-          <tr>
-            <th translate="search.originalTitle"></th>
-            <td>{{ assetDetail.titleOriginal }}</td>
-          </tr>
-          <tr>
-            <th translate="search.kind"></th>
-            <td>{{ assetDetail.assetKindItem | valueItemName }}</td>
-          </tr>
-          <tr>
-            <th translate="search.createdDate"></th>
-            <td>{{ assetDetail.createDate | assetSgDate }}</td>
-          </tr>
-          <tr>
-            <th translate="search.alternativeId"></th>
-            <td>
-              <ng-container *ngIf="assetDetail.ids.length === 0">&ndash;</ng-container>
-              <span class="line" *rxFor="let id of assetDetail.ids">{{ id.id + " [" + id.description + "]" }}</span>
-            </td>
-          </tr>
-          <tr>
-            <th translate="search.contacts"></th>
-            <td>
-              <ng-container *ngIf="assetDetail.assetContacts.length === 0">&ndash;</ng-container>
-              <ul [class.single-entry]="assetDetail.assetContacts.length === 1">
-                <li *rxFor="let contact of assetDetail.assetContacts">
-                  {{ "contactRoles." + contact.role | translate }}: <br />
-                  {{ contact.name }} <br />
-                  {{ contact.locality }} <br />
-                  {{ contact.contactKindItem | valueItemName }}
-                </li>
-              </ul>
-            </td>
-          </tr>
-          <tr *ngIf="files.Legal.length > 0">
-            <th translate="search.legalFile"></th>
-            <td>
-              <ul asset-sg-asset-viewer-files [assetId]="assetDetail.assetId" [files]="files.Legal" type="Legal"></ul>
-            </td>
-          </tr>
-          <tr>
-            <th translate="search.subject"></th>
-            <td>
-              <ng-container *ngIf="assetDetail.manCatLabels.length === 0">&ndash;</ng-container>
-              <ng-container *rxFor="let manCatLabel of assetDetail.manCatLabels; let last = last">
-                {{ (manCatLabel | valueItemName) + (last ? "" : ", ") }}
-              </ng-container>
-            </td>
-          </tr>
-          <tr>
-            <th translate="search.language"></th>
-            <td>
-              <ul class="languages">
-                <li *ngFor="let language of assetDetail.languages">{{ language | valueItemName }}</li>
-              </ul>
-            </td>
-          </tr>
-          <tr>
-            <th translate="search.format"></th>
-            <td>{{ assetDetail.assetFormatItem | valueItemName }}</td>
-          </tr>
-          <tr>
-            <th translate="search.content"></th>
-            <td>
-              <ng-container *ngIf="assetDetail.assetFormatCompositions.length === 0">&ndash;</ng-container>
-              <ng-container *rxFor="let assetFormatComposition of assetDetail.assetFormatCompositions; let last = last">
-                {{ (assetFormatComposition | valueItemName) + (last ? "" : ", ") }}
-              </ng-container>
-            </td>
-          </tr>
-          <tr>
-            <th translate="search.nationalInterest"></th>
-            <td>
-              <ng-container *ngIf="assetDetail.typeNatRels.length === 0">&ndash;</ng-container>
-              <span class="line" *rxFor="let typeNatRel of assetDetail.typeNatRels">{{
-                typeNatRel | valueItemName
-              }}</span>
-            </td>
-          </tr>
-          <tr>
-            <th translate="search.reference"></th>
-            <td>
-              <ng-container *ngIf="assetDetail.referenceAssets.length === 0">&ndash;</ng-container>
-              <ul [class.single-entry]="assetDetail.referenceAssets.length === 1">
-                <li *rxFor="let referenceAsset of assetDetail.referenceAssets">
-                  <a
-                    asset-sg-reset
-                    class="line"
-                    (click)="searchForReferenceAsset(referenceAsset.assetId)"
-                    queryParamsHandling="merge"
-                  >
-                    {{ referenceAsset.titlePublic }}
-                  </a>
-                </li>
-              </ul>
-            </td>
-          </tr>
-          <tr>
-            <th translate="search.lastProcessedDate"></th>
-            <td>{{ assetDetail.lastProcessedDate | assetSgDate }}</td>
-          </tr>
-          <tr>
-            <th translate="search.status"></th>
-            <td>
-              <ng-container *ngIf="assetDetail.statusWorks.length === 0">&ndash;</ng-container>
-              <table class="status-works">
-                <caption></caption>
-                <tr *rxFor="let statusWork of assetDetail.statusWorks">
-                  <th>{{ statusWork.statusWorkDate | assetSgDate }}</th>
-                  <td>{{ statusWork.statusWork | valueItemName }}</td>
-                </tr>
-              </table>
-            </td>
-          </tr>
-        </table>
+<div class="asset-detail-scroll-container">
+  <div class="asset-detail">
+    <div class="flex flex-row justify-between mb-4">
+      <div class="text-dark-red font-bold mt-2.5">{{ assetDetail.titlePublic }}</div>
+      <ng-container *canUpdate="AssetEditPolicy; with: assetDetail">
+        <a asset-sg-icon-button [routerLink]="['/', currentLang$ | async, 'asset-admin', assetDetail.assetId]">
+          <svg-icon key="edit" data-testid="edit-asset-button"></svg-icon>
+        </a>
+      </ng-container>
+    </div>
+    <div class="asset-status">
+      <ng-container *ngIf="assetDetail.publicUse.isAvailable; else internalUse">
+        <svg-icon [attr.aria-hidden]="false" key="success-filled" [attr.alt]="'search.usageCode.public' | translate" />
       </ng-container>
+      <ng-template #internalUse>
+        <ng-container *ngIf="assetDetail.internalUse.isAvailable; else onRequest">
+          <svg-icon
+            [attr.aria-hidden]="false"
+            key="warning-filled"
+            [attr.alt]="'search.usageCode.internal' | translate"
+          />
+        </ng-container>
+        <ng-template #onRequest>
+          <svg-icon
+            [attr.aria-hidden]="false"
+            key="error-filled"
+            [attr.alt]="'search.usageCode.useOnRequest' | translate"
+          />
+        </ng-template>
+      </ng-template>
     </div>
+    <table class="asset-details" *rxLet="filesByType$; let files">
+      <tr *ngIf="files.Normal.length > 0">
+        <th translate="search.file"></th>
+        <td>
+          <ul asset-sg-asset-viewer-files [assetId]="assetDetail.assetId" [files]="files.Normal" type="Normal"></ul>
+        </td>
+      </tr>
+      <tr>
+        <th translate="search.originalTitle"></th>
+        <td>{{ assetDetail.titleOriginal }}</td>
+      </tr>
+      <tr>
+        <th translate="search.kind"></th>
+        <td>{{ assetDetail.assetKindItem | valueItemName }}</td>
+      </tr>
+      <tr>
+        <th translate="search.createdDate"></th>
+        <td>{{ assetDetail.createDate | assetSgDate }}</td>
+      </tr>
+      <tr>
+        <th translate="search.alternativeId"></th>
+        <td>
+          <ng-container *ngIf="assetDetail.ids.length === 0">&ndash;</ng-container>
+          <span class="line" *rxFor="let id of assetDetail.ids">{{ id.id + " [" + id.description + "]" }}</span>
+        </td>
+      </tr>
+      <tr>
+        <th translate="search.contacts"></th>
+        <td>
+          <ng-container *ngIf="assetDetail.assetContacts.length === 0">&ndash;</ng-container>
+          <ul [class.single-entry]="assetDetail.assetContacts.length === 1">
+            <li *rxFor="let contact of assetDetail.assetContacts">
+              {{ "contactRoles." + contact.role | translate }}: <br />
+              {{ contact.name }} <br />
+              {{ contact.locality }} <br />
+              {{ contact.contactKindItem | valueItemName }}
+            </li>
+          </ul>
+        </td>
+      </tr>
+      <tr *ngIf="files.Legal.length > 0">
+        <th translate="search.legalFile"></th>
+        <td>
+          <ul asset-sg-asset-viewer-files [assetId]="assetDetail.assetId" [files]="files.Legal" type="Legal"></ul>
+        </td>
+      </tr>
+      <tr>
+        <th translate="search.subject"></th>
+        <td>
+          <ng-container *ngIf="assetDetail.manCatLabels.length === 0">&ndash;</ng-container>
+          <ng-container *rxFor="let manCatLabel of assetDetail.manCatLabels; let last = last">
+            {{ (manCatLabel | valueItemName) + (last ? "" : ", ") }}
+          </ng-container>
+        </td>
+      </tr>
+      <tr>
+        <th translate="search.language"></th>
+        <td>
+          <ul class="languages">
+            <li *ngFor="let language of assetDetail.languages">{{ language | valueItemName }}</li>
+          </ul>
+        </td>
+      </tr>
+      <tr>
+        <th translate="search.format"></th>
+        <td>{{ assetDetail.assetFormatItem | valueItemName }}</td>
+      </tr>
+      <tr>
+        <th translate="search.content"></th>
+        <td>
+          <ng-container *ngIf="assetDetail.assetFormatCompositions.length === 0">&ndash;</ng-container>
+          <ng-container *rxFor="let assetFormatComposition of assetDetail.assetFormatCompositions; let last = last">
+            {{ (assetFormatComposition | valueItemName) + (last ? "" : ", ") }}
+          </ng-container>
+        </td>
+      </tr>
+      <tr>
+        <th translate="search.nationalInterest"></th>
+        <td>
+          <ng-container *ngIf="assetDetail.typeNatRels.length === 0">&ndash;</ng-container>
+          <span class="line" *rxFor="let typeNatRel of assetDetail.typeNatRels">{{ typeNatRel | valueItemName }}</span>
+        </td>
+      </tr>
+      <tr>
+        <th translate="search.reference"></th>
+        <td>
+          <ng-container *ngIf="assetDetail.referenceAssets.length === 0">&ndash;</ng-container>
+          <ul [class.single-entry]="assetDetail.referenceAssets.length === 1">
+            <li *rxFor="let referenceAsset of assetDetail.referenceAssets">
+              <a
+                asset-sg-reset
+                class="line"
+                (click)="searchForReferenceAsset(referenceAsset.assetId)"
+                queryParamsHandling="merge"
+              >
+                {{ referenceAsset.titlePublic }}
+              </a>
+            </li>
+          </ul>
+        </td>
+      </tr>
+      <tr>
+        <th translate="search.lastProcessedDate"></th>
+        <td>{{ assetDetail.lastProcessedDate | assetSgDate }}</td>
+      </tr>
+      <tr>
+        <th translate="search.status"></th>
+        <td>
+          <ng-container *ngIf="assetDetail.statusWorks.length === 0">&ndash;</ng-container>
+          <table class="status-works">
+            <caption></caption>
+            <tr *rxFor="let statusWork of assetDetail.statusWorks">
+              <th>{{ statusWork.statusWorkDate | assetSgDate }}</th>
+              <td>{{ statusWork.statusWork | valueItemName }}</td>
+            </tr>
+          </table>
+        </td>
+      </tr>
+    </table>
   </div>
-</ng-template>
+</div>
+} @else {
+<div class="loading-spinner">
+  <mat-progress-spinner mode="indeterminate" />
+</div>
+}
diff --git a/libs/asset-viewer/src/lib/components/asset-search-detail/asset-search-detail.component.ts b/libs/asset-viewer/src/lib/components/asset-search-detail/asset-search-detail.component.ts
index ee3d75273..30c3b2bf0 100644
--- a/libs/asset-viewer/src/lib/components/asset-search-detail/asset-search-detail.component.ts
+++ b/libs/asset-viewer/src/lib/components/asset-search-detail/asset-search-detail.component.ts
@@ -1,16 +1,13 @@
 import { Component, inject } from '@angular/core';
-import { AppState, CURRENT_LANG } from '@asset-sg/client-shared';
+import { CURRENT_LANG } from '@asset-sg/client-shared';
 import { AssetFileType } from '@asset-sg/shared';
 import { AssetEditPolicy } from '@asset-sg/shared/v2';
 import { Store } from '@ngrx/store';
 import { map, Observable } from 'rxjs';
+import { ViewerControllerService } from '../../services/viewer-controller.service';
 import * as actions from '../../state/asset-search/asset-search.actions';
-import { LoadingState } from '../../state/asset-search/asset-search.reducer';
-import {
-  AssetDetailFileVM,
-  selectAssetDetailLoadingState,
-  selectCurrentAssetDetailVM,
-} from '../../state/asset-search/asset-search.selector';
+import { AppStateWithAssetSearch } from '../../state/asset-search/asset-search.reducer';
+import { AssetDetailFileVM, selectCurrentAssetDetailVM } from '../../state/asset-search/asset-search.selector';
 
 @Component({
   selector: 'asset-sg-asset-search-detail',
@@ -19,11 +16,12 @@ import {
   standalone: false,
 })
 export class AssetSearchDetailComponent {
-  private readonly store = inject(Store<AppState>);
+  private readonly store = inject(Store<AppStateWithAssetSearch>);
   public readonly currentLang$ = inject(CURRENT_LANG);
+  private readonly viewerControllerService = inject(ViewerControllerService);
 
-  public readonly assetDetail$ = this.store.select(selectCurrentAssetDetailVM);
-  public readonly filesByType$: Observable<Record<AssetFileType, AssetDetailFileVM[]>> = this.assetDetail$.pipe(
+  public readonly asset$ = this.store.select(selectCurrentAssetDetailVM);
+  public readonly filesByType$: Observable<Record<AssetFileType, AssetDetailFileVM[]>> = this.asset$.pipe(
     map((asset) => {
       const mapping: Record<AssetFileType, AssetDetailFileVM[]> = {
         Normal: [],
@@ -39,16 +37,13 @@ export class AssetSearchDetailComponent {
     })
   );
 
-  public loadingState = this.store.select(selectAssetDetailLoadingState);
-
   public clearSelectedAsset() {
-    this.store.dispatch(actions.clearSelectedAsset());
+    this.store.dispatch(actions.setCurrentAsset({ asset: null }));
   }
 
   public searchForReferenceAsset(assetId: number) {
-    this.store.dispatch(actions.assetClicked({ assetId }));
+    this.viewerControllerService.selectAsset(assetId);
   }
 
-  protected readonly LoadingState = LoadingState;
   protected readonly AssetEditPolicy = AssetEditPolicy;
 }
diff --git a/libs/asset-viewer/src/lib/components/asset-search-filter/asset-search-filter.component.ts b/libs/asset-viewer/src/lib/components/asset-search-filter/asset-search-filter.component.ts
index 57e44092d..b37ead9b3 100644
--- a/libs/asset-viewer/src/lib/components/asset-search-filter/asset-search-filter.component.ts
+++ b/libs/asset-viewer/src/lib/components/asset-search-filter/asset-search-filter.component.ts
@@ -24,7 +24,7 @@ export class AssetSearchFilterComponent<T> {
       activeValues.add(filter.value);
     }
     this.store.dispatch(
-      actions.mergeQuery({
+      actions.updateSearchQuery({
         query: { [filter.queryKey]: activeValues.size > 0 ? [...activeValues] : undefined },
       })
     );
diff --git a/libs/asset-viewer/src/lib/components/asset-search-refine/asset-search-refine.component.ts b/libs/asset-viewer/src/lib/components/asset-search-refine/asset-search-refine.component.ts
index 98fb43f71..ef3ad998a 100644
--- a/libs/asset-viewer/src/lib/components/asset-search-refine/asset-search-refine.component.ts
+++ b/libs/asset-viewer/src/lib/components/asset-search-refine/asset-search-refine.component.ts
@@ -1,24 +1,24 @@
 import { AfterViewInit, Component, inject, OnDestroy, OnInit } from '@angular/core';
 import { FormControl } from '@angular/forms';
 import { MatOptionSelectionChange } from '@angular/material/core';
-import { Router } from '@angular/router';
 import { AssetSearchQuery, DateRange } from '@asset-sg/shared';
 import { Store } from '@ngrx/store';
 import { map, startWith, Subscription } from 'rxjs';
 
 import * as actions from '../../state/asset-search/asset-search.actions';
+import { PanelState, resetSearch } from '../../state/asset-search/asset-search.actions';
 import {
   AvailableAuthor,
   Filter,
   selectActiveFilters,
   selectAssetKindFilters,
-  selectAssetSearchQuery,
   selectAvailableAuthors,
   selectCreateDate,
   selectGeometryFilters,
   selectIsFiltersOpen,
   selectLanguageFilters,
   selectManCatLabelFilters,
+  selectSearchQuery,
   selectUsageCodeFilters,
   selectWorkgroupFilters,
 } from '../../state/asset-search/asset-search.selector';
@@ -35,7 +35,6 @@ const MIN_CREATE_DATE = new Date(1800, 0, 1);
 })
 export class AssetSearchRefineComponent implements OnInit, OnDestroy, AfterViewInit {
   private readonly store = inject(Store);
-  private readonly router = inject(Router);
 
   public authorAutoCompleteControl = new FormControl('');
   public minDateControl = new FormControl();
@@ -104,7 +103,7 @@ export class AssetSearchRefineComponent implements OnInit, OnDestroy, AfterViewI
   }
 
   public removePolygon() {
-    this.store.dispatch(actions.clearPolygon());
+    this.store.dispatch(actions.updateSearchQuery({ query: { polygon: undefined } }));
   }
 
   public toggleDrawPolygon() {
@@ -120,7 +119,7 @@ export class AssetSearchRefineComponent implements OnInit, OnDestroy, AfterViewI
 
   public updateSearch(filterConfiguration: Partial<AssetSearchQuery>) {
     if (this.isFiltersOpen) {
-      this.store.dispatch(actions.mergeQuery({ query: filterConfiguration }));
+      this.store.dispatch(actions.updateSearchQuery({ query: filterConfiguration }));
     }
   }
 
@@ -144,11 +143,10 @@ export class AssetSearchRefineComponent implements OnInit, OnDestroy, AfterViewI
   }
 
   public resetSearch() {
-    void this.router.navigate([]);
     this.authorAutoCompleteControl.setValue('');
     this.maxDateControl.setValue(null);
     this.maxDateControl.setValue(null);
-    this.store.dispatch(actions.resetSearch());
+    this.store.dispatch(resetSearch());
   }
 
   public removeFilter(filterToRemove: Filter<string | number>) {
@@ -156,7 +154,7 @@ export class AssetSearchRefineComponent implements OnInit, OnDestroy, AfterViewI
       .filter((filter) => filter.queryKey === filterToRemove.queryKey && filter.value !== filterToRemove.value)
       .map((remainingFilter) => remainingFilter.value);
     this.store.dispatch(
-      actions.mergeQuery({
+      actions.updateSearchQuery({
         query: {
           [filterToRemove.queryKey]: remainingFiltersWithSameKey.length > 0 ? remainingFiltersWithSameKey : undefined,
         },
@@ -165,14 +163,14 @@ export class AssetSearchRefineComponent implements OnInit, OnDestroy, AfterViewI
   }
 
   public closeFilters() {
-    this.store.dispatch(actions.setFiltersOpen({ isOpen: false }));
+    this.store.dispatch(actions.setFiltersState({ state: PanelState.ClosedManually }));
   }
 
   private initSubscriptions() {
     this.subscriptions.add(this.isFiltersOpen$.subscribe((isOpen) => (this.isFiltersOpen = isOpen)));
     this.subscriptions.add(this.activeFilters$.subscribe((activeFilter) => (this.activeFilters = activeFilter)));
     this.subscriptions.add(
-      this.store.select(selectAssetSearchQuery).subscribe((query) => {
+      this.store.select(selectSearchQuery).subscribe((query) => {
         this.assetSearchQuery = query;
       })
     );
diff --git a/libs/asset-viewer/src/lib/components/asset-search-results/asset-search-results.component.ts b/libs/asset-viewer/src/lib/components/asset-search-results/asset-search-results.component.ts
index 0f8cc976f..55115b843 100644
--- a/libs/asset-viewer/src/lib/components/asset-search-results/asset-search-results.component.ts
+++ b/libs/asset-viewer/src/lib/components/asset-search-results/asset-search-results.component.ts
@@ -1,26 +1,18 @@
 import { Component, ElementRef, EventEmitter, inject, OnDestroy, OnInit, Output, ViewChild } from '@angular/core';
 import { sleep, tick } from '@asset-sg/shared/v2';
 import { Store } from '@ngrx/store';
-import {
-  BehaviorSubject,
-  combineLatestWith,
-  filter,
-  firstValueFrom,
-  Subject,
-  Subscription,
-  switchMap,
-  take,
-} from 'rxjs';
+import { BehaviorSubject, combineLatestWith, firstValueFrom, map, Subject, Subscription, switchMap, take } from 'rxjs';
+import { ViewerControllerService } from '../../services/viewer-controller.service';
 import * as actions from '../../state/asset-search/asset-search.actions';
-import { setScrollOffsetForResults } from '../../state/asset-search/asset-search.actions';
+import { PanelState, setScrollOffsetForResults } from '../../state/asset-search/asset-search.actions';
 import { AppStateWithAssetSearch } from '../../state/asset-search/asset-search.reducer';
 import {
   AssetEditDetailVM,
   selectAssetEditDetailVM,
-  selectAssetSearchTotalResults,
-  selectCurrentAssetDetail,
+  selectCurrentAsset,
   selectIsResultsOpen,
   selectScrollOffsetForResults,
+  selectSearchStats,
 } from '../../state/asset-search/asset-search.selector';
 
 @Component({
@@ -52,10 +44,11 @@ export class AssetSearchResultsComponent implements OnInit, OnDestroy {
   private readonly pageSize = 50;
 
   private readonly store = inject(Store<AppStateWithAssetSearch>);
+  private readonly viewerControllerService = inject(ViewerControllerService);
   public readonly isResultsOpen$ = this.store.select(selectIsResultsOpen);
   public readonly assets$ = this.store.select(selectAssetEditDetailVM);
-  public readonly total$ = this.store.select(selectAssetSearchTotalResults);
-  public readonly currentAssetDetail$ = this.store.select(selectCurrentAssetDetail);
+  public readonly total$ = this.store.select(selectSearchStats).pipe(map((stats) => stats.total));
+  public readonly currentAssetDetail$ = this.store.select(selectCurrentAsset);
   public readonly scrollOffset$ = this.store.select(selectScrollOffsetForResults);
 
   private readonly subscriptions: Subscription = new Subscription();
@@ -74,15 +67,17 @@ export class AssetSearchResultsComponent implements OnInit, OnDestroy {
   }
 
   public searchForAsset(assetId: number): void {
-    this.store.dispatch(actions.assetClicked({ assetId }));
+    this.viewerControllerService.selectAsset(assetId);
   }
 
   public async toggleResultsOpen(): Promise<void> {
     const isOpen = await firstValueFrom(this.isResultsOpen$);
     if (isOpen) {
       this.saveScrollToStore(0);
+      this.store.dispatch(actions.setResultsState({ state: PanelState.ClosedManually }));
+    } else {
+      this.store.dispatch(actions.setResultsState({ state: PanelState.OpenedManually }));
     }
-    this.store.dispatch(actions.setResultsOpen({ isOpen: 'toggle' }));
   }
 
   public onScroll(event: Event): void {
@@ -130,9 +125,9 @@ export class AssetSearchResultsComponent implements OnInit, OnDestroy {
 
   private initSubscriptions(): void {
     // Scroll to the offset stored in the store.
-    this.isTableReady$
+    this.viewerControllerService.viewerReady$
       .pipe(
-        combineLatestWith(this.resultsReady$),
+        combineLatestWith(this.isTableReady$, this.resultsReady$),
         take(1),
         switchMap(() => this.scrollOffset$),
         take(1),
@@ -142,12 +137,13 @@ export class AssetSearchResultsComponent implements OnInit, OnDestroy {
 
     // Subscribe to results.
     this.subscriptions.add(
-      this.assets$.pipe(filter((assets) => assets.length !== 0)).subscribe((assets) => {
+      this.viewerControllerService.viewerReady$.pipe(switchMap(() => this.assets$)).subscribe(async (assets) => {
         this.allResults$.next(assets);
         this.size = Math.min(this.pageSize, assets.length);
         this.resultsToDisplay = this.allResults.slice(0, this.size);
         const container = this.scrollContainer?.nativeElement;
         if (container !== undefined) {
+          await tick();
           container.scrollTo({ top: 0, behavior: 'smooth' });
         }
         this.resultsReady$.next();
diff --git a/libs/asset-viewer/src/lib/components/asset-viewer-page/asset-viewer-page.component.html b/libs/asset-viewer/src/lib/components/asset-viewer-page/asset-viewer-page.component.html
index fe212a099..4dfa4dedb 100644
--- a/libs/asset-viewer/src/lib/components/asset-viewer-page/asset-viewer-page.component.html
+++ b/libs/asset-viewer/src/lib/components/asset-viewer-page/asset-viewer-page.component.html
@@ -20,8 +20,8 @@
   </div>
 </ng-template>
 
-<asset-sg-asset-search-refine *ngIf="isFiltersOpen$ | push" class="mat-elevation-z4" [style.visibility]="'visible'" />
+<asset-sg-asset-search-refine *ngIf="isFiltersOpen$ | async" class="mat-elevation-z4" [style.visibility]="'visible'" />
 
 <asset-sg-asset-search-results class="mat-elevation-z2" (assetMouseOver)="highlightedAssetId = $event" />
 
-<asset-sg-asset-search-detail *ngIf="currentAsset$ | push" class="mat-elevation-z4" />
+<asset-sg-asset-search-detail *ngIf="hasCurrentAsset$ | async" class="mat-elevation-z4" />
diff --git a/libs/asset-viewer/src/lib/components/asset-viewer-page/asset-viewer-page.component.ts b/libs/asset-viewer/src/lib/components/asset-viewer-page/asset-viewer-page.component.ts
index 282f2156b..258817ff6 100644
--- a/libs/asset-viewer/src/lib/components/asset-viewer-page/asset-viewer-page.component.ts
+++ b/libs/asset-viewer/src/lib/components/asset-viewer-page/asset-viewer-page.component.ts
@@ -10,23 +10,15 @@ import {
   ViewChild,
   ViewContainerRef,
 } from '@angular/core';
-import {
-  AppPortalService,
-  AppState,
-  AuthService,
-  LifecycleHooks,
-  LifecycleHooksDirective,
-} from '@asset-sg/client-shared';
+import { AppPortalService, AuthService, LifecycleHooks, LifecycleHooksDirective } from '@asset-sg/client-shared';
 import { AssetEditDetail } from '@asset-sg/shared';
 import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy';
 import { Store } from '@ngrx/store';
 import * as A from 'fp-ts/Array';
-import { flow } from 'fp-ts/function';
 import { Eq as eqNumber } from 'fp-ts/number';
 import * as O from 'fp-ts/Option';
 import {
   asyncScheduler,
-  combineLatest,
   filter,
   identity,
   map,
@@ -40,17 +32,16 @@ import {
   withLatestFrom,
 } from 'rxjs';
 
-import { ViewerParamsService } from '../../services/viewer-params.service';
+import { ViewerControllerService } from '../../services/viewer-controller.service';
 import * as actions from '../../state/asset-search/asset-search.actions';
-import { LoadingState } from '../../state/asset-search/asset-search.reducer';
+import { PanelState } from '../../state/asset-search/asset-search.actions';
+import { AppStateWithAssetSearch, AssetSearchState } from '../../state/asset-search/asset-search.reducer';
 import {
-  selectAssetDetailLoadingState,
-  selectAssetSearchQuery,
-  selectAssetSearchResultData,
-  selectCurrentAssetDetail,
-  selectFilterLoadingState,
+  selectCurrentAsset,
+  selectHasCurrentAsset,
   selectIsFiltersOpen,
-  selectSearchLoadingState,
+  selectSearchQuery,
+  selectSearchResults,
 } from '../../state/asset-search/asset-search.selector';
 
 @UntilDestroy()
@@ -68,44 +59,44 @@ export class AssetViewerPageComponent implements OnInit, OnDestroy {
   private readonly _lc = inject(LifecycleHooks);
   private readonly _appPortalService = inject(AppPortalService);
   private readonly _viewContainerRef = inject(ViewContainerRef);
-  private readonly _store = inject(Store<AppState>);
+  private readonly store = inject(Store<AppStateWithAssetSearch>);
   private readonly _cd = inject(ChangeDetectorRef);
 
-  public isLoading$ = combineLatest(
-    [
-      this._store.select(selectFilterLoadingState),
-      this._store.select(selectSearchLoadingState),
-      this._store.select(selectAssetDetailLoadingState),
-    ],
-    (filterLoadingState, searchLoadingState, detailLoadingState) =>
-      filterLoadingState === LoadingState.Loading ||
-      searchLoadingState === LoadingState.Loading ||
-      detailLoadingState === LoadingState.Loading
+  private readonly authService = inject(AuthService);
+  private readonly viewerControllerService = inject(ViewerControllerService);
+
+  public isLoading$ = this.store.pipe(
+    map((store): AssetSearchState => store.assetSearch),
+    map(
+      (search) =>
+        search.isLoadingStudies ||
+        search.isLoadingResults ||
+        search.isLoadingStats ||
+        // Loading for the current asset is only shown on the map in case an asset is already being displayed.
+        // Otherwise, the detail panel shows a loader.
+        (search.isLoadingAsset && search.currentAsset !== null)
+    )
   );
-  public currentAssetId$ = this._store.select(selectCurrentAssetDetail).pipe(
+
+  public currentAssetId$ = this.store.select(selectCurrentAsset).pipe(
     map((currentAsset) => currentAsset?.assetId),
     map(O.fromNullable)
   );
-  public currentAsset$ = this._store.select(selectCurrentAssetDetail);
-  public isFiltersOpen$ = this._store.select(selectIsFiltersOpen);
+
+  public hasCurrentAsset$ = this.store.select(selectHasCurrentAsset);
+
+  public isFiltersOpen$ = this.store.select(selectIsFiltersOpen);
 
   public _searchTextKeyDown$ = new Subject<KeyboardEvent>();
-  private _searchTextChanged$ = this._searchTextKeyDown$.pipe(
-    filter((ev) => ev.key === 'Enter'),
-    map((ev) => {
-      const value = (ev.target as HTMLInputElement).value;
-      return value ? O.some(value) : O.none;
-    })
+  private readonly searchTextChanged$ = this._searchTextKeyDown$.pipe(
+    filter((event) => event.key === 'Enter'),
+    map((event) => (event.target as HTMLInputElement).value)
   );
 
   public assetClicked$ = new Subject<number[]>();
   public assetsForPicker$: Observable<AssetEditDetail[]>;
   public highlightedAssetId: number | null = null;
 
-  private readonly authService = inject(AuthService);
-
-  private readonly viewerParamsService = inject(ViewerParamsService);
-
   constructor() {
     const setupPortals$ = this._lc.afterViewInit$.pipe(
       observeOn(asyncScheduler),
@@ -128,7 +119,7 @@ export class AssetViewerPageComponent implements OnInit, OnDestroy {
 
     setupPortals$
       .pipe(
-        switchMap(() => this._store.select(selectAssetSearchQuery)),
+        switchMap(() => this.store.select(selectSearchQuery)),
         untilDestroyed(this)
       )
       .subscribe((searchQuery) => {
@@ -150,38 +141,30 @@ export class AssetViewerPageComponent implements OnInit, OnDestroy {
     );
 
     this.assetsForPicker$ = multipleStudiesClicked$.pipe(
-      withLatestFrom(this._store.select(selectAssetSearchResultData)),
-      map(([assetIds, searchAssets]) => searchAssets.filter((a) => assetIds.includes(a.assetId)))
+      withLatestFrom(this.store.select(selectSearchResults)),
+      map(([assetIds, searchAssets]) => searchAssets.data.filter((a) => assetIds.includes(a.assetId)))
     );
 
     singleStudyClicked$.pipe(untilDestroyed(this)).subscribe((assetIds) => {
-      this._store.dispatch(actions.assetClicked({ assetId: assetIds[0] }));
+      this.viewerControllerService.selectAsset(assetIds[0]);
     });
 
-    this._searchTextChanged$
-      .pipe(
-        map(
-          flow(
-            O.map((text) => actions.mergeQuery({ query: { text } })),
-            O.getOrElseW(() => actions.mergeQuery({ query: { text: '' } }))
-          )
-        )
-      )
-      .pipe(untilDestroyed(this))
-      .subscribe(this._store);
+    this.searchTextChanged$.pipe(untilDestroyed(this)).subscribe((text) => {
+      this.store.dispatch(actions.updateSearchQuery({ query: { text } }));
+    });
   }
 
   public ngOnInit() {
-    this._store.dispatch(actions.setFiltersOpen({ isOpen: true }));
+    this.store.dispatch(actions.setFiltersState({ state: PanelState.OpenedAutomatically }));
 
     this.authService.isInitialized$.pipe(filter(identity), take(1)).subscribe(() => {
-      this.viewerParamsService.start();
+      this.viewerControllerService.initialize();
     });
     this._appPortalService.setAppBarPortalContent(null);
   }
 
   ngOnDestroy() {
     this._appPortalService.setAppBarPortalContent(null);
-    this.viewerParamsService.stop();
+    this.viewerControllerService.reset();
   }
 }
diff --git a/libs/asset-viewer/src/lib/components/map/map-controller.ts b/libs/asset-viewer/src/lib/components/map/map-controller.ts
index c9348da08..16a8f0af9 100644
--- a/libs/asset-viewer/src/lib/components/map/map-controller.ts
+++ b/libs/asset-viewer/src/lib/components/map/map-controller.ts
@@ -1,6 +1,7 @@
 import { featureStyles, makeRhombusImage, olCoordsFromLV95, SWISS_CENTER, SWISS_EXTENT } from '@asset-sg/client-shared';
 import { isNotUndefined } from '@asset-sg/core';
 import { AssetEditDetail, getCoordsFromStudy, Study } from '@asset-sg/shared';
+import { extend } from '@asset-sg/shared/v2';
 import { Control } from 'ol/control';
 import { Coordinate } from 'ol/coordinate';
 import { easeOut } from 'ol/easing';
@@ -19,12 +20,10 @@ import { distinctUntilChanged, filter, fromEventPattern, map, Observable, Replay
 import { AllStudyDTO } from '../../models';
 import { wktToGeoJSON } from '../../state/asset-search/asset-search.selector';
 
-export const INITIAL_RESOLUTION = 500;
-
 export const DEFAULT_MAP_POSITION: MapPosition = {
   x: SWISS_CENTER[0],
   y: SWISS_CENTER[1],
-  z: INITIAL_RESOLUTION,
+  z: 8,
 };
 
 export class MapController {
@@ -35,7 +34,7 @@ export class MapController {
 
   readonly assetsClick$: Observable<number[]>;
   readonly assetsHover$: Observable<number[]>;
-  readonly positionChange$: Observable<[number, number, number]>;
+  readonly positionChange$: Observable<MapPosition>;
 
   /**
    * The id of all visible assets, mapped to their {@link AssetEditDetail} object.
@@ -70,10 +69,11 @@ export class MapController {
   constructor(element: HTMLElement, initialPosition: MapPosition) {
     const view = new View({
       projection: 'EPSG:3857',
-      minResolution: 0.1,
-      resolution: initialPosition.z,
+      zoom: initialPosition.z,
       center: [initialPosition.x, initialPosition.y],
       extent: SWISS_EXTENT,
+      maxZoom: 20,
+      minZoom: DEFAULT_MAP_POSITION.z,
       showFullExtent: true,
     });
 
@@ -110,13 +110,6 @@ export class MapController {
 
     this.map.once('loadend', () => {
       this.isInitialized = true;
-      if (this.activeAsset === null) {
-        const zoom = view.getZoom();
-        if (zoom != null) {
-          view.setMinZoom(zoom);
-        }
-      }
-
       this.requestedPosition$.subscribe(this.setPositionImmediately.bind(this));
     });
   }
@@ -289,20 +282,33 @@ export class MapController {
     });
   }
 
+  getPosition(): MapPosition | null {
+    const center = this.map.getView().getCenter();
+    const zoom = this.map.getView().getZoom();
+    if (center === undefined || zoom === undefined) {
+      return null;
+    }
+    return { x: center[0], y: center[1], z: zoom };
+  }
+
   setPosition(position: Partial<MapPosition>): void {
     this.requestedPosition$.next(position);
   }
 
   private setPositionImmediately(position: Partial<MapPosition>): void {
-    const view = this.map.getView();
-    const center = view.getCenter();
-    if (center === undefined) {
+    const oldPosition = this.getPosition();
+    if (oldPosition === null) {
       throw new Error("can't set position, view is not yet initialized.");
     }
-    view.setCenter([position.x ?? center[0], position.y ?? center[1]]);
-    if (position.z !== undefined) {
-      view.setResolution(position.z);
+    const newPosition = extend(oldPosition, position);
+    const hasChanged =
+      newPosition.x !== oldPosition.x || newPosition.y !== oldPosition.y || newPosition.z !== oldPosition.z;
+    if (!hasChanged) {
+      return;
     }
+    const view = this.map.getView();
+    view.setCenter([newPosition.x, newPosition.y]);
+    view.setZoom(newPosition.z);
     this.map.render();
   }
 
@@ -348,16 +354,9 @@ export class MapController {
     }) as MapLayer<Point>;
   }
 
-  private makePositionChange$(): Observable<[number, number, number]> {
+  private makePositionChange$(): Observable<MapPosition> {
     return fromEventPattern((h) => this.map.getView().on('change:center', h)).pipe(
-      map(() => {
-        const center = this.map.getView().getCenter();
-        const resolution = this.map.getView().getResolution();
-        if (center === undefined || resolution === undefined) {
-          return null;
-        }
-        return [...center, resolution] as [number, number, number];
-      }),
+      map(() => this.getPosition()),
       filter((it) => it !== null)
     );
   }
@@ -666,13 +665,13 @@ const zoomToCenter = (map: OlMap, { center, zoom }: { center: Coordinate; zoom:
 export const resetZoom = (view: View, options: { isAnimated?: boolean } = {}): void => {
   if (options.isAnimated) {
     view.animate({
-      resolution: INITIAL_RESOLUTION,
-      center: SWISS_CENTER,
+      zoom: DEFAULT_MAP_POSITION.z,
+      center: [DEFAULT_MAP_POSITION.x, DEFAULT_MAP_POSITION.y],
       duration: 250,
       easing: easeOut,
     });
   } else {
-    view.setResolution(INITIAL_RESOLUTION);
-    view.setCenter(SWISS_CENTER);
+    view.setZoom(DEFAULT_MAP_POSITION.z);
+    view.setCenter([DEFAULT_MAP_POSITION.x, DEFAULT_MAP_POSITION.y]);
   }
 };
diff --git a/libs/asset-viewer/src/lib/components/map/map.component.ts b/libs/asset-viewer/src/lib/components/map/map.component.ts
index d01733168..db016ae65 100644
--- a/libs/asset-viewer/src/lib/components/map/map.component.ts
+++ b/libs/asset-viewer/src/lib/components/map/map.component.ts
@@ -13,18 +13,19 @@ import {
   ViewChild,
 } from '@angular/core';
 import { arrayEqual, isNotNull } from '@asset-sg/core';
-import { extend, filterNullish } from '@asset-sg/shared/v2';
+import { isEmptySearchQuery } from '@asset-sg/shared';
+import { filterNullish } from '@asset-sg/shared/v2';
 import { Store } from '@ngrx/store';
-import { delay, filter, first, skip, Subscription, take, withLatestFrom } from 'rxjs';
+import { distinctUntilChanged, filter, first, map, skip, Subscription, switchMap, take, withLatestFrom } from 'rxjs';
+import { ViewerControllerService } from '../../services/viewer-controller.service';
 import * as searchActions from '../../state/asset-search/asset-search.actions';
 import { setMapPosition } from '../../state/asset-search/asset-search.actions';
 import {
-  selectAssetSearchPolygon,
-  selectAssetSearchResultData,
-  selectCurrentAssetDetail,
-  selectHasNoActiveFilters,
+  selectSearchResults,
+  selectCurrentAsset,
   selectMapPosition,
   selectStudies,
+  selectSearchQuery,
 } from '../../state/asset-search/asset-search.selector';
 import { AppStateWithMapControl } from '../../state/map-control/map-control.reducer';
 import { DrawControl } from '../map-controls/draw-controls';
@@ -76,6 +77,7 @@ export class MapComponent implements AfterViewInit, OnChanges, OnDestroy {
   controlsElement!: ElementRef<HTMLElement>;
 
   private readonly store = inject(Store<AppStateWithMapControl>);
+  private readonly viewerControllerService = inject(ViewerControllerService);
 
   private controller!: MapController;
 
@@ -90,6 +92,8 @@ export class MapComponent implements AfterViewInit, OnChanges, OnDestroy {
 
   private readonly subscription = new Subscription();
 
+  private publishedPosition = DEFAULT_MAP_POSITION;
+
   constructor() {
     this.initializeEnd.subscribe(() => {
       this.isInitialized = true;
@@ -98,16 +102,27 @@ export class MapComponent implements AfterViewInit, OnChanges, OnDestroy {
 
   ngAfterViewInit(): void {
     // Set the initial map position by its stored value.
-    this.store
-      .select(selectMapPosition)
-      .pipe(
-        filter((it) => Object.keys(it).length !== 0),
-        take(1),
-        delay(1)
-      )
-      .subscribe((position) => {
-        this.initializeMap(extend(DEFAULT_MAP_POSITION, position));
-      });
+    const storedPosition$ = this.viewerControllerService.viewerReady$.pipe(
+      switchMap(() => this.store.select(selectMapPosition))
+    );
+
+    storedPosition$.pipe(take(1)).subscribe((position) => {
+      this.publishedPosition = position;
+      this.initializeMap(position);
+    });
+
+    this.subscription.add(
+      storedPosition$.pipe(skip(1)).subscribe((position) => {
+        if (position === this.publishedPosition) {
+          return;
+        }
+        if (this.timeoutForSetPosition !== null) {
+          clearTimeout(this.timeoutForSetPosition);
+          this.timeoutForSetPosition = null;
+        }
+        this.controller.setPosition(position);
+      })
+    );
   }
 
   ngOnChanges(changes: SimpleChanges): void {
@@ -141,20 +156,6 @@ export class MapComponent implements AfterViewInit, OnChanges, OnDestroy {
     this.controller.addControl(this.controls.zoom);
     this.controller.addControl(this.controls.draw);
 
-    // Reset the map position when its stored value is cleared.
-    // Note that we do not want to react to any other changes to it,
-    // as these are always supposed to be made by ourselves.
-    this.subscription.add(
-      this.store
-        .select(selectMapPosition)
-        .pipe(skip(1))
-        .subscribe((position) => {
-          if (Object.values(position).every((it) => it === undefined)) {
-            this.controller.setPosition(DEFAULT_MAP_POSITION);
-          }
-        })
-    );
-
     this.controls.draw.isDrawing$.subscribe((isDrawing) => {
       this.controller.setClickEnabled(!isDrawing);
     });
@@ -175,16 +176,16 @@ export class MapComponent implements AfterViewInit, OnChanges, OnDestroy {
 
   private initializeStoreBindings() {
     this.subscription.add(
-      this.store.select(selectHasNoActiveFilters).subscribe((hasNoActiveFilters) => {
-        this.controller.setShowHeatmap(hasNoActiveFilters);
+      this.store.select(selectSearchQuery).subscribe((query) => {
+        this.controller.setShowHeatmap(isEmptySearchQuery(query));
       })
     );
     this.subscription.add(
-      this.store.select(selectAssetSearchResultData).subscribe((assets) => this.controller.setAssets(assets))
+      this.store.select(selectSearchResults).subscribe((results) => this.controller.setAssets(results.data))
     );
 
     this.subscription.add(
-      this.store.select(selectCurrentAssetDetail).subscribe((asset) => {
+      this.store.select(selectCurrentAsset).subscribe((asset) => {
         if (asset == null) {
           this.controller.clearActiveAsset();
         } else {
@@ -203,20 +204,26 @@ export class MapComponent implements AfterViewInit, OnChanges, OnDestroy {
 
   private initializePolygonBindings(): void {
     this.subscription.add(
-      this.store.select(selectAssetSearchPolygon).subscribe((polygon) => {
-        this.controls.draw.setPolygon(polygon ?? null);
-      })
+      this.store
+        .select(selectSearchQuery)
+        .pipe(
+          map((it) => it.polygon),
+          distinctUntilChanged()
+        )
+        .subscribe((polygon) => {
+          this.controls.draw.setPolygon(polygon ?? null);
+        })
     );
     this.subscription.add(
       this.controls.draw.polygon$
         .pipe(
           filterNullish(),
-          withLatestFrom(this.store.select(selectAssetSearchPolygon)),
+          withLatestFrom(this.store.select(selectSearchQuery).pipe(map((query) => query.polygon))),
           filter(([polygon, storePolygon]) => !arrayEqual(polygon, storePolygon))
         )
         .subscribe(([polygon, _]) =>
           this.store.dispatch(
-            searchActions.mergeQuery({
+            searchActions.updateSearchQuery({
               query: { polygon: polygon },
             })
           )
@@ -232,13 +239,14 @@ export class MapComponent implements AfterViewInit, OnChanges, OnDestroy {
     }
   }
 
-  private handlePositionChange([x, y, z]: [number, number, number]): void {
+  private handlePositionChange(position: MapPosition): void {
     if (this.timeoutForSetPosition !== null) {
       clearTimeout(this.timeoutForSetPosition);
     }
     this.timeoutForSetPosition = setTimeout(() => {
-      this.store.dispatch(setMapPosition({ position: { x, y, z } }));
-    }, 250);
+      this.publishedPosition = position;
+      this.store.dispatch(setMapPosition({ position }));
+    }, 50);
   }
 
   @HostBinding('class.is-loading')
diff --git a/libs/asset-viewer/src/lib/services/all-study.service.ts b/libs/asset-viewer/src/lib/services/all-study.service.ts
index 3ace3e766..1e550a04f 100644
--- a/libs/asset-viewer/src/lib/services/all-study.service.ts
+++ b/libs/asset-viewer/src/lib/services/all-study.service.ts
@@ -1,23 +1,19 @@
 import { HttpClient, HttpDownloadProgressEvent, HttpEvent, HttpEventType } from '@angular/common/http';
-import { Injectable } from '@angular/core';
-import { ApiError } from '@asset-sg/client-shared';
-import { ORD } from '@asset-sg/core';
+import { inject, Injectable } from '@angular/core';
 import { LV95 } from '@asset-sg/shared';
-import * as RD from '@devexperts/remote-data-ts';
-import * as E from 'fp-ts/Either';
-import { concatMap, filter, from, map, Observable, scan, share, toArray } from 'rxjs';
+import { concatMap, filter, from, map, Observable, scan, toArray } from 'rxjs';
 
 import { AllStudyDTO, AllStudyDTOs } from '../models';
 
 @Injectable({ providedIn: 'root' })
 export class AllStudyService {
-  constructor(private _httpClient: HttpClient) {}
+  private readonly _httpClient = inject(HttpClient);
 
-  getAllStudies(): ORD.ObservableRemoteData<ApiError, AllStudyDTOs> {
+  getAllStudies(): Observable<AllStudyDTOs> {
     return this.getAllStudiesFromApi();
   }
 
-  private getAllStudiesFromApi(): ORD.ObservableRemoteData<ApiError, AllStudyDTO[]> {
+  private getAllStudiesFromApi(): Observable<AllStudyDTOs> {
     return this._httpClient.get('/api/studies', { observe: 'events', responseType: 'text', reportProgress: true }).pipe(
       filter((event) => event.type === HttpEventType.DownloadProgress || event.type === HttpEventType.Response),
       map((event: HttpEvent<string>) => (event as HttpDownloadProgressEvent).partialText ?? ''),
@@ -32,10 +28,7 @@ export class AllStudyService {
           centroid: { x: parseFloat(x), y: parseFloat(y) } as LV95,
         } as AllStudyDTO;
       }),
-      toArray(),
-      map((it) => E.right<ApiError, AllStudyDTO[]>(it)),
-      map(RD.fromEither),
-      share()
+      toArray()
     );
   }
 }
diff --git a/libs/asset-viewer/src/lib/services/viewer-controller.service.ts b/libs/asset-viewer/src/lib/services/viewer-controller.service.ts
new file mode 100644
index 000000000..148c24f5d
--- /dev/null
+++ b/libs/asset-viewer/src/lib/services/viewer-controller.service.ts
@@ -0,0 +1,240 @@
+import { inject, Injectable } from '@angular/core';
+import { NavigationEnd, Router } from '@angular/router';
+import { AssetSearchQuery, isEmptySearchQuery, makeEmptyAssetSearchResults } from '@asset-sg/shared';
+import { AssetId } from '@asset-sg/shared/v2';
+import { Store } from '@ngrx/store';
+import {
+  distinctUntilChanged,
+  filter,
+  firstValueFrom,
+  identity,
+  map,
+  merge,
+  Observable,
+  ReplaySubject,
+  skip,
+  Subscription,
+  take,
+} from 'rxjs';
+import { DEFAULT_MAP_POSITION } from '../components/map/map-controller';
+import * as actions from '../state/asset-search/asset-search.actions';
+import {
+  isPanelAutomaticallyToggled,
+  isPanelOpen,
+  PanelState,
+  setCurrentAsset,
+  setFiltersState,
+  setMapPosition,
+  setQuery,
+  setResultsState,
+  setScrollOffsetForResults,
+} from '../state/asset-search/asset-search.actions';
+import { AppStateWithAssetSearch } from '../state/asset-search/asset-search.reducer';
+import {
+  selectCurrentAsset,
+  selectFiltersState,
+  selectIsResultsOpen,
+  selectMapPosition,
+  selectResultsState,
+  selectScrollOffsetForResults,
+  selectSearchQuery,
+  selectSearchResults,
+  selectStudies,
+} from '../state/asset-search/asset-search.selector';
+import { AllStudyService } from './all-study.service';
+import { AssetSearchService } from './asset-search.service';
+import { isEmptyViewerParams, ViewerParams, ViewerParamsService } from './viewer-params.service';
+
+@Injectable({ providedIn: 'root' })
+export class ViewerControllerService {
+  private readonly store = inject(Store<AppStateWithAssetSearch>);
+  private readonly viewerParamsService = inject(ViewerParamsService);
+  private readonly assetSearchService = inject(AssetSearchService);
+  private readonly allStudyService = inject(AllStudyService);
+  private readonly router = inject(Router);
+
+  private viewerReadySubject = new ReplaySubject<void>(1);
+  private isUpdatingUrl = false;
+  private isUpdatingStore = false;
+
+  private subscription = new Subscription();
+
+  /**
+   * Emits once when the viewer is ready, e.g. all data has been loaded and no more initial loads are taking place.
+   * Subscribing to the observable after the initial emission will emit immediately, once.
+   */
+  get viewerReady$(): Observable<void> {
+    return this.viewerReadySubject.pipe(take(1));
+  }
+
+  initialize(): void {
+    this.initializeFromStore().then();
+  }
+
+  reset(): void {
+    this.subscription.unsubscribe();
+    this.subscription = new Subscription();
+    this.viewerReadySubject = new ReplaySubject(1);
+    this.isUpdatingUrl = false;
+    this.isUpdatingStore = false;
+  }
+
+  selectAsset(id: AssetId): void {
+    this.loadAsset(id).then();
+  }
+
+  private async initializeFromStore(): Promise<void> {
+    const loads: Array<Promise<void>> = [];
+
+    const paramsFromUrl = await this.viewerParamsService.readParamsFromUrl();
+    const paramsFromStore = await this.viewerParamsService.readParamsFromStore();
+
+    let params: ViewerParams;
+    if (isEmptyViewerParams(paramsFromUrl) && !isEmptyViewerParams(paramsFromStore)) {
+      params = paramsFromStore;
+      await this.viewerParamsService.writeParamsToUrl(params);
+      loads.push(this.loadAsset(params.assetId));
+    } else {
+      params = paramsFromUrl;
+      loads.push(this.updateStoreByParams(paramsFromUrl));
+    }
+
+    const studies = await firstValueFrom(this.store.select(selectStudies));
+    if (studies.length === 0) {
+      loads.push(this.loadStudies());
+    }
+    loads.push(this.loadResults(params.query, { force: isPanelOpen(params.ui.resultsState) }));
+    loads.push(this.loadStats(params.query));
+    await Promise.all(loads);
+
+    this.subscription.add(this.syncUrlParams());
+    this.subscription.add(this.loadResultsWhenPanelIsOpened());
+    this.subscription.add(this.reactToNavigation());
+    this.subscription.add(this.store.select(selectSearchQuery).pipe(skip(1)).subscribe(this.updateByQuery.bind(this)));
+    this.viewerReadySubject.next();
+  }
+
+  private async loadStudies(): Promise<void> {
+    this.store.dispatch(actions.setStudies({ isLoading: true }));
+    const studies = await firstValueFrom(this.allStudyService.getAllStudies());
+    this.store.dispatch(actions.setStudies({ studies, isLoading: false }));
+  }
+
+  private async loadResults(query: AssetSearchQuery, options: { force?: boolean } = {}): Promise<void> {
+    if (!options.force && isEmptySearchQuery(query)) {
+      this.store.dispatch(actions.setResults({ results: makeEmptyAssetSearchResults(), isLoading: false }));
+      return;
+    }
+    this.store.dispatch(actions.setResults({ isLoading: true }));
+    const results = await firstValueFrom(this.assetSearchService.search(query));
+    this.store.dispatch(actions.setResults({ results, isLoading: false }));
+  }
+
+  private async loadStats(query: AssetSearchQuery): Promise<void> {
+    this.store.dispatch(actions.setStats({ isLoading: true }));
+    const stats = await firstValueFrom(this.assetSearchService.searchStats(query));
+    this.store.dispatch(actions.setStats({ stats, isLoading: false }));
+  }
+
+  private async loadAsset(id: AssetId | null): Promise<void> {
+    if (id === null) {
+      this.store.dispatch(setCurrentAsset({ asset: null }));
+      return;
+    }
+    this.store.dispatch(actions.setCurrentAsset({ isLoading: true }));
+    const asset = await firstValueFrom(this.assetSearchService.fetchAssetEditDetail(id));
+    this.store.dispatch(actions.setCurrentAsset({ asset, isLoading: false }));
+  }
+
+  private async updateByQuery(query: AssetSearchQuery): Promise<void> {
+    if (isEmptySearchQuery(query)) {
+      this.store.dispatch(actions.setMapPosition({ position: DEFAULT_MAP_POSITION }));
+    }
+    await Promise.all([this.loadResults(query), this.loadStats(query)]);
+    const results = await firstValueFrom(this.store.select(selectSearchResults));
+    this.store.dispatch(
+      actions.setResultsState({
+        state: results.page.total === 0 ? PanelState.ClosedAutomatically : PanelState.OpenedAutomatically,
+      })
+    );
+  }
+
+  private syncUrlParams(): Subscription {
+    const prepareEvent = <T>(
+      event$: Observable<T>,
+      shouldReplaceUrl: boolean | ((value: T) => boolean)
+    ): Observable<boolean> => {
+      const transform = typeof shouldReplaceUrl === 'boolean' ? () => shouldReplaceUrl : shouldReplaceUrl;
+      return event$.pipe(skip(1), distinctUntilChanged(), map(transform));
+    };
+
+    // Events that add a new history entry.
+    const foregroundEvents: Array<Observable<unknown>> = [
+      this.store.select(selectSearchQuery),
+      this.store.select(selectCurrentAsset),
+      this.store.select(selectFiltersState),
+      this.store.select(selectResultsState),
+    ];
+
+    // Events that replace the current history entry.
+    const backgroundEvents: Array<Observable<unknown>> = [
+      this.store.select(selectScrollOffsetForResults),
+      this.store.select(selectMapPosition),
+    ];
+
+    // Events that may or may not replace the current history entry, depending on their contents.
+    const dynamicEvents: Array<Observable<boolean>> = [
+      prepareEvent(this.store.select(selectFiltersState), isPanelAutomaticallyToggled),
+      prepareEvent(this.store.select(selectResultsState), isPanelAutomaticallyToggled),
+    ];
+
+    const foreground$ = merge(...foregroundEvents.map((event$) => prepareEvent(event$, false)));
+    const background$ = merge(...backgroundEvents.map((event$) => prepareEvent(event$, true)));
+    const dynamic$ = merge(...dynamicEvents);
+
+    return merge(foreground$, background$, dynamic$).subscribe(async (shouldReplaceUrl) => {
+      if (this.isUpdatingStore) {
+        return;
+      }
+      const params = await this.viewerParamsService.readParamsFromStore();
+      this.isUpdatingUrl = true;
+      await this.viewerParamsService.writeParamsToUrl(params, { shouldReplaceUrl });
+      this.isUpdatingUrl = false;
+    });
+  }
+
+  private loadResultsWhenPanelIsOpened(): Subscription {
+    return this.store
+      .select(selectIsResultsOpen)
+      .pipe(skip(1), filter(identity))
+      .subscribe(async () => {
+        const query = await firstValueFrom(this.store.select(selectSearchQuery));
+        const results = await firstValueFrom(this.store.select(selectSearchResults));
+        if (isEmptySearchQuery(query) && results.page.total === 0) {
+          await this.loadResults(query, { force: true });
+        }
+      });
+  }
+
+  private reactToNavigation(): Subscription {
+    return this.router.events.subscribe(async (event) => {
+      if (this.isUpdatingUrl || !(event instanceof NavigationEnd)) {
+        return;
+      }
+      const params = await this.viewerParamsService.readParamsFromUrl();
+      this.isUpdatingStore = true;
+      await this.updateStoreByParams(params);
+      this.isUpdatingStore = false;
+    });
+  }
+
+  private updateStoreByParams(params: ViewerParams): Promise<void> {
+    const { ui, query, assetId } = params;
+    this.store.dispatch(setScrollOffsetForResults({ offset: ui.scrollOffsetForResults }));
+    this.store.dispatch(setFiltersState({ state: ui.filtersState }));
+    this.store.dispatch(setResultsState({ state: ui.resultsState }));
+    this.store.dispatch(setMapPosition({ position: ui.map }));
+    this.store.dispatch(setQuery({ query }));
+    return this.loadAsset(assetId);
+  }
+}
diff --git a/libs/asset-viewer/src/lib/services/viewer-params.service.ts b/libs/asset-viewer/src/lib/services/viewer-params.service.ts
index f5f1cdcf0..44140abd9 100644
--- a/libs/asset-viewer/src/lib/services/viewer-params.service.ts
+++ b/libs/asset-viewer/src/lib/services/viewer-params.service.ts
@@ -1,156 +1,73 @@
 import { inject, Injectable } from '@angular/core';
-import { ActivatedRoute, NavigationEnd, Params, Router } from '@angular/router';
+import { Params, Router } from '@angular/router';
 import { isNotNull } from '@asset-sg/core';
-import { AssetSearchQuery, LV95, Polygon } from '@asset-sg/shared';
+import { AssetSearchQuery, isEmptySearchQuery, LV95, Polygon } from '@asset-sg/shared';
 import { AssetId } from '@asset-sg/shared/v2';
 import { Store } from '@ngrx/store';
-import { combineLatest, distinctUntilChanged, firstValueFrom, map, skip, Subscription } from 'rxjs';
+import { firstValueFrom, map, Observable } from 'rxjs';
 import { DEFAULT_MAP_POSITION } from '../components/map/map-controller';
+
+import { isPanelOpen, PanelState } from '../state/asset-search/asset-search.actions';
 import {
-  runCombinedSearch,
-  setFiltersOpen,
-  setMapPosition,
-  setResultsOpen,
-  setScrollOffsetForResults,
-} from '../state/asset-search/asset-search.actions';
-import { AssetSearchState, AssetSearchUiState } from '../state/asset-search/asset-search.reducer';
-import {
-  hasNoActiveFilters,
-  selectAssetSearchQuery,
-  selectCurrentAssetDetail,
-  selectIsFiltersOpen,
-  selectIsResultsOpen,
-  selectMapPosition,
-  selectScrollOffsetForResults,
-} from '../state/asset-search/asset-search.selector';
+  AppStateWithAssetSearch,
+  AssetSearchState,
+  AssetSearchUiState,
+} from '../state/asset-search/asset-search.reducer';
 
 @Injectable({ providedIn: 'root' })
 export class ViewerParamsService {
-  private readonly store = inject(Store<AssetSearchState>);
   private readonly router = inject(Router);
-  private readonly route = inject(ActivatedRoute);
-
-  private params!: ViewerParams;
-  private isUpdatingUrl = false;
-
-  private readonly paramsFromStore$ = combineLatest([
-    this.store.select(selectAssetSearchQuery).pipe(distinctUntilChanged()),
-    this.store.select(selectCurrentAssetDetail).pipe(distinctUntilChanged()),
-    combineLatest([
-      this.store.select(selectScrollOffsetForResults).pipe(distinctUntilChanged()),
-      this.store.select(selectIsFiltersOpen).pipe(distinctUntilChanged()),
-      this.store.select(selectIsResultsOpen).pipe(distinctUntilChanged()),
-      this.store.select(selectMapPosition).pipe(distinctUntilChanged()),
-    ]).pipe(
-      map(
-        ([scrollOffsetForResults, isFiltersOpen, isResultsOpen, map]): AssetSearchUiState => ({
-          scrollOffsetForResults,
-          isFiltersOpen,
-          isResultsOpen,
-          map,
-        })
-      )
-    ),
-  ]).pipe(
-    map(([query, asset, ui]) => ({
-      query,
-      ui,
-      assetId: asset?.assetId ?? null,
-    }))
-  );
-
-  private subscription = new Subscription();
-
-  start(): void {
-    this.initialize().then();
-  }
-
-  stop(): void {
-    this.params = undefined as unknown as ViewerParams;
-    this.subscription.unsubscribe();
-    this.subscription = new Subscription();
-  }
+  private readonly store = inject(Store<AppStateWithAssetSearch>);
 
-  private async initialize(): Promise<void> {
-    const paramsFromUrl = this.parseParamsFromUrl();
-
-    const paramsFromStore = await firstValueFrom(this.paramsFromStore$);
-
-    // We only use the values from the store if:
-    // - There are no URL query params
-    // - There are query or assetId values in the store
-    // - We always take the value for 'favoritesOnly' from the URL
-    if (isEmptyParams(paramsFromUrl) && !isEmptyParams(paramsFromStore)) {
-      this.params = paramsFromStore;
-      this.params.query = {
-        ...this.params.query,
-        favoritesOnly: paramsFromUrl.query.favoritesOnly,
-      };
-      this.writeParamsToUrl({ shouldReplaceUrl: true });
-      // When navigating from 'Favorites' to 'Create Asset' to 'Filter', we need to override the 'favoritesOnly' property in the state and trigger a new search
-      this.writeParamsToStore();
-    } else {
-      this.params = paramsFromUrl;
-      this.writeParamsToStore();
-    }
-    this.subscribe();
-  }
-
-  private subscribe(): void {
-    // Whether the next query params update should replace the current url instead of adding a new navigation entry.
-    // This is mainly set due to a navigation to a different path (e.g. `/de` to `/de/favorites`).
-    let shouldReplaceUrl = false;
-
-    // Write values that change in the store to the URL.
-    // Note that we skip the first value as we will already have synced with it.
-    this.subscription.add(
-      this.paramsFromStore$.pipe(skip(1)).subscribe((params) => {
-        this.params = params;
-        // We need the value from the URL for 'favoritesOnly' as it is removed from the state after resetting the search
-        const paramsFromUrl = this.parseParamsFromUrl();
-        this.params.query = {
-          ...this.params.query,
-          favoritesOnly: paramsFromUrl.query.favoritesOnly,
-        };
-        this.writeParamsToUrl({ shouldReplaceUrl });
-        shouldReplaceUrl = false;
-      })
+  async readParamsFromStore(): Promise<ViewerParams> {
+    const state = await firstValueFrom(
+      this.store.pipe(map((store) => store.assetSearch)) as Observable<AssetSearchState>
     );
+    return {
+      assetId: state.currentAsset?.assetId ?? null,
+      query: state.query,
+      ui: state.ui,
+    };
+  }
 
-    // Ensure that the params stay up-to-date when navigating.
-    let lastUrl = document.location.pathname.split('?', 2)[0];
-    this.subscription.add(
-      this.router.events.subscribe(async (event) => {
-        if (!(event instanceof NavigationEnd)) {
-          return;
-        }
-        const url = event.url.split('?', 2)[0];
-        if (this.isUpdatingUrl) {
-          lastUrl = url;
-          return;
-        }
-        if (url === lastUrl) {
-          this.params = this.parseParamsFromUrl();
-          this.writeParamsToStore();
-        } else {
-          const isFavoritesOnly = this.parseFavoritesOnlyFromUrl();
-          this.params = await firstValueFrom(this.paramsFromStore$);
-          this.params.query = {
-            ...this.params.query,
-            favoritesOnly: isFavoritesOnly,
-          };
-          this.params.ui.isResultsOpen = false;
-          this.params.ui.scrollOffsetForResults = 0;
-          shouldReplaceUrl = true;
-          this.writeParamsToStore();
-        }
-        lastUrl = url;
-      })
-    );
+  async readParamsFromUrl(): Promise<ViewerParams> {
+    const { queryParams: params } = this.router.routerState.snapshot.root;
+    const query: AssetSearchQuery = {};
+    const assetId = readNumberParam(params, QUERY_PARAM_MAPPING.assetId) ?? null;
+    query.text = readStringParam(params, QUERY_PARAM_MAPPING.text);
+    query.polygon = readPolygonParam(params, QUERY_PARAM_MAPPING.polygon);
+    query.authorId = readNumberParam(params, QUERY_PARAM_MAPPING.authorId);
+    const min = readDateParam(params, QUERY_PARAM_MAPPING.createDate.min);
+    const max = readDateParam(params, QUERY_PARAM_MAPPING.createDate.max);
+    query.createDate = min && max ? { min, max } : undefined;
+    query.manCatLabelItemCodes = readArrayParam(params, QUERY_PARAM_MAPPING.manCatLabelItemCodes);
+    query.assetKindItemCodes = readArrayParam(params, QUERY_PARAM_MAPPING.assetKindItemCodes);
+    query.usageCodes = readArrayParam(params, QUERY_PARAM_MAPPING.usageCodes);
+    query.geometryCodes = readArrayParam(params, QUERY_PARAM_MAPPING.geometryCodes);
+    query.languageItemCodes = readArrayParam(params, QUERY_PARAM_MAPPING.languageItemCodes);
+    query.workgroupIds = readArrayParam<number>(params, QUERY_PARAM_MAPPING.workgroupIds);
+    query.favoritesOnly = this.parseFavoritesOnlyFromUrl();
+    const ui: AssetSearchUiState = {
+      filtersState:
+        readBooleanParam(params, UI_PARAM_MAPPING.filtersState) ?? true
+          ? PanelState.OpenedAutomatically
+          : PanelState.ClosedAutomatically,
+      resultsState:
+        readBooleanParam(params, UI_PARAM_MAPPING.resultsState) ?? false
+          ? PanelState.OpenedAutomatically
+          : PanelState.ClosedAutomatically,
+      scrollOffsetForResults: readNumberParam(params, UI_PARAM_MAPPING.scrollOffsetForResults) ?? 0,
+      map: {
+        x: readNumberParam(params, UI_PARAM_MAPPING.map.x) ?? DEFAULT_MAP_POSITION.x,
+        y: readNumberParam(params, UI_PARAM_MAPPING.map.y) ?? DEFAULT_MAP_POSITION.y,
+        z: readNumberParam(params, UI_PARAM_MAPPING.map.z) ?? DEFAULT_MAP_POSITION.z,
+      },
+    };
+    return { query, ui, assetId };
   }
 
-  private writeParamsToUrl(options: { shouldReplaceUrl?: boolean } = {}): void {
-    const { query, ui, assetId } = this.params;
+  async writeParamsToUrl(viewerParams: ViewerParams, options: { shouldReplaceUrl?: boolean } = {}): Promise<void> {
+    const { query, ui, assetId } = viewerParams;
 
     const params: Params = {};
     updatePlainParam(params, QUERY_PARAM_MAPPING.text, query.text);
@@ -171,8 +88,8 @@ export class ViewerParamsService {
     updatePlainParam(params, QUERY_PARAM_MAPPING.assetId, assetId);
 
     updatePlainParam(params, UI_PARAM_MAPPING.scrollOffsetForResults, ui.scrollOffsetForResults, { defaultValue: 0 });
-    updatePlainParam(params, UI_PARAM_MAPPING.isFiltersOpen, ui.isFiltersOpen, { defaultValue: true });
-    updatePlainParam(params, UI_PARAM_MAPPING.isResultsOpen, ui.isResultsOpen, { defaultValue: false });
+    updatePlainParam(params, UI_PARAM_MAPPING.filtersState, isPanelOpen(ui.filtersState), { defaultValue: true });
+    updatePlainParam(params, UI_PARAM_MAPPING.resultsState, isPanelOpen(ui.resultsState), { defaultValue: false });
 
     updatePlainParam(params, UI_PARAM_MAPPING.map.x, ui.map.x, { defaultValue: DEFAULT_MAP_POSITION.x });
     updatePlainParam(params, UI_PARAM_MAPPING.map.y, ui.map.y, { defaultValue: DEFAULT_MAP_POSITION.y });
@@ -181,60 +98,11 @@ export class ViewerParamsService {
     const url = document.location.pathname.split('/', 3);
     const route = query.favoritesOnly ? ['favorites'] : [];
 
-    this.isUpdatingUrl = true;
-    this.router
-      .navigate([url[1], ...route], {
-        queryParams: params,
-        queryParamsHandling: 'merge',
-        replaceUrl: options.shouldReplaceUrl,
-      })
-      .finally(() => {
-        this.isUpdatingUrl = false;
-      });
-  }
-
-  private writeParamsToStore(): void {
-    const { assetId, query, ui } = this.params;
-    this.store.dispatch(
-      runCombinedSearch({
-        assetId: assetId ?? undefined,
-        query,
-      })
-    );
-    this.store.dispatch(setScrollOffsetForResults({ offset: ui.scrollOffsetForResults }));
-    this.store.dispatch(setFiltersOpen({ isOpen: ui.isFiltersOpen }));
-    this.store.dispatch(setResultsOpen({ isOpen: ui.isResultsOpen }));
-    this.store.dispatch(setMapPosition({ position: ui.map }));
-  }
-
-  private parseParamsFromUrl(): ViewerParams {
-    const { queryParams: params } = this.route.snapshot;
-    const query: AssetSearchQuery = {};
-    const assetId = readNumberParam(params, QUERY_PARAM_MAPPING.assetId) ?? null;
-    query.text = readStringParam(params, QUERY_PARAM_MAPPING.text);
-    query.polygon = readPolygonParam(params, QUERY_PARAM_MAPPING.polygon);
-    query.authorId = readNumberParam(params, QUERY_PARAM_MAPPING.authorId);
-    const min = readDateParam(params, QUERY_PARAM_MAPPING.createDate.min);
-    const max = readDateParam(params, QUERY_PARAM_MAPPING.createDate.max);
-    query.createDate = min && max ? { min, max } : undefined;
-    query.manCatLabelItemCodes = readArrayParam(params, QUERY_PARAM_MAPPING.manCatLabelItemCodes);
-    query.assetKindItemCodes = readArrayParam(params, QUERY_PARAM_MAPPING.assetKindItemCodes);
-    query.usageCodes = readArrayParam(params, QUERY_PARAM_MAPPING.usageCodes);
-    query.geometryCodes = readArrayParam(params, QUERY_PARAM_MAPPING.geometryCodes);
-    query.languageItemCodes = readArrayParam(params, QUERY_PARAM_MAPPING.languageItemCodes);
-    query.workgroupIds = readArrayParam<number>(params, QUERY_PARAM_MAPPING.workgroupIds);
-    query.favoritesOnly = this.parseFavoritesOnlyFromUrl();
-    const ui: AssetSearchUiState = {
-      isFiltersOpen: readBooleanParam(params, UI_PARAM_MAPPING.isFiltersOpen) ?? true,
-      isResultsOpen: readBooleanParam(params, UI_PARAM_MAPPING.isResultsOpen) ?? false,
-      scrollOffsetForResults: readNumberParam(params, UI_PARAM_MAPPING.scrollOffsetForResults) ?? 0,
-      map: {
-        x: readNumberParam(params, UI_PARAM_MAPPING.map.x),
-        y: readNumberParam(params, UI_PARAM_MAPPING.map.y),
-        z: readNumberParam(params, UI_PARAM_MAPPING.map.z),
-      },
-    };
-    return { query, ui, assetId };
+    await this.router.navigate([url[1], ...route], {
+      queryParams: params,
+      queryParamsHandling: 'merge',
+      replaceUrl: options.shouldReplaceUrl,
+    });
   }
 
   private parseFavoritesOnlyFromUrl(): boolean {
@@ -243,14 +111,15 @@ export class ViewerParamsService {
   }
 }
 
-interface ViewerParams {
+export const isEmptyViewerParams = (params: ViewerParams): boolean =>
+  params.assetId == null && isEmptySearchQuery(params.query);
+
+export interface ViewerParams {
   query: AssetSearchQuery;
   ui: AssetSearchUiState;
   assetId: AssetId | null;
 }
 
-const isEmptyParams = (params: ViewerParams): boolean => params.assetId == null && hasNoActiveFilters(params.query);
-
 const QUERY_PARAM_MAPPING = {
   text: 'search[text]',
   polygon: 'search[polygon]',
@@ -270,13 +139,13 @@ const QUERY_PARAM_MAPPING = {
 };
 
 type ParamMapping<T> = {
-  [K in keyof T]: T[K] extends Record<string, unknown> ? ParamMapping<Required<T[K]>> : string;
+  [K in keyof T]: T[K] extends Record<string, any> ? ParamMapping<Required<T[K]>> : string;
 };
 
 const UI_PARAM_MAPPING: ParamMapping<AssetSearchUiState> = {
   scrollOffsetForResults: 'results[offset]',
-  isResultsOpen: 'results[show]',
-  isFiltersOpen: 'search[show]',
+  resultsState: 'results[show]',
+  filtersState: 'search[show]',
   map: {
     x: 'map[x]',
     y: 'map[y]',
diff --git a/libs/asset-viewer/src/lib/state/asset-search/asset-search.actions.ts b/libs/asset-viewer/src/lib/state/asset-search/asset-search.actions.ts
index 17381063c..cbf8898ff 100644
--- a/libs/asset-viewer/src/lib/state/asset-search/asset-search.actions.ts
+++ b/libs/asset-viewer/src/lib/state/asset-search/asset-search.actions.ts
@@ -1,59 +1,88 @@
 import { AssetEditDetail, AssetSearchQuery, AssetSearchResult, AssetSearchStats } from '@asset-sg/shared';
-import { AssetId } from '@asset-sg/shared/v2';
 import { createAction, props } from '@ngrx/store';
+import { MapPosition } from '../../components/map/map-controller';
 import { AllStudyDTOs } from '../../models';
-import { AssetSearchUiState } from './asset-search.reducer';
 
-export const initialize = createAction('[Asset Search] Initialize');
-export const runCombinedSearch = createAction(
-  '[Asset Search] Run Combined Search',
+export const setQuery = createAction(
+  '[Asset Search] Set Query',
   props<{
-    assetId: number | undefined;
-    query: AssetSearchQuery;
+    query: Partial<AssetSearchQuery>;
   }>()
 );
-export const search = createAction(
-  '[Asset Search] Search',
+
+export const updateSearchQuery = createAction(
+  '[Asset Search] Update Query',
   props<{
-    query: AssetSearchQuery;
+    query: Partial<AssetSearchQuery>;
   }>()
 );
-export const mergeQuery = createAction(
-  '[Asset Search] Merge Query',
+
+export const setResults = createAction(
+  '[Asset Search] Set Results',
   props<{
-    query: AssetSearchQuery;
+    results?: AssetSearchResult;
+    isLoading?: boolean;
   }>()
 );
-export const executeSearch = createAction('[Asset Search] Execute Search', props<{ query: AssetSearchQuery }>());
-
-export const updateResults = createAction(
-  '[Asset Search] Update Results',
+export const setStats = createAction(
+  '[Asset Search] Set Stats',
   props<{
-    results: AssetSearchResult;
+    stats?: AssetSearchStats;
+    isLoading?: boolean;
   }>()
 );
-export const updateStats = createAction(
-  '[Asset Search] Update Stats',
+
+export const setStudies = createAction(
+  '[Asset Search] Set Studies',
   props<{
-    stats: AssetSearchStats;
+    studies?: AllStudyDTOs;
+    isLoading?: boolean;
   }>()
 );
-export const resetSearch = createAction('[Asset Search] Reset Search');
 
-export const assetClicked = createAction('[Asset Search] Asset Clicked', props<{ assetId: number }>());
-export const selectAsset = createAction('[Asset Search] Select Asset', props<{ assetId: AssetId }>());
-export const setSelectedAsset = createAction('[Asset Search] Set Selected Asset', props<{ asset: AssetEditDetail }>());
-export const clearSelectedAsset = createAction('[Asset Search] Clear Selected Asset');
-export const clearPolygon = createAction('[Asset Search] Clear Polygon');
-export const setStudies = createAction('[Asset Search] Set Studies', props<{ studies: AllStudyDTOs }>());
+export const setCurrentAsset = createAction(
+  '[Asset Search] Set Current Asset',
+  props<{
+    asset?: AssetEditDetail | null;
+    isLoading?: boolean;
+  }>()
+);
 
-export const setFiltersOpen = createAction('[Asset Search] Set Filters Open', props<{ isOpen: boolean | 'toggle' }>());
-export const setResultsOpen = createAction('[Asset Search] Set Results Open', props<{ isOpen: boolean | 'toggle' }>());
+export const setFiltersState = createAction('[Asset Search] Set Filters Open', props<{ state: PanelState }>());
+export const setResultsState = createAction('[Asset Search] Set Results Open', props<{ state: PanelState }>());
 export const setScrollOffsetForResults = createAction(
   '[Asset Search] Set Scroll Offset For Results',
   props<{ offset: number }>()
 );
-export const setMapPosition = createAction(
-  '[Asset Search] Set Map Position',
-  props<{ position: Partial<AssetSearchUiState['map']> }>()
-);
+export const setMapPosition = createAction('[Asset Search] Set Map Position', props<{ position: MapPosition }>());
+
+export const resetSearch = createAction('[Asset Search] Reset Search');
+
+export enum PanelState {
+  ClosedManually,
+  ClosedAutomatically,
+  OpenedManually,
+  OpenedAutomatically,
+}
+
+export const isPanelOpen = (state: PanelState): boolean => {
+  switch (state) {
+    case PanelState.ClosedManually:
+    case PanelState.ClosedAutomatically:
+      return false;
+    case PanelState.OpenedManually:
+    case PanelState.OpenedAutomatically:
+      return true;
+  }
+};
+
+export const isPanelAutomaticallyToggled = (state: PanelState): boolean => {
+  switch (state) {
+    case PanelState.ClosedManually:
+    case PanelState.OpenedManually:
+      return false;
+    case PanelState.ClosedAutomatically:
+    case PanelState.OpenedAutomatically:
+      return true;
+  }
+};
diff --git a/libs/asset-viewer/src/lib/state/asset-search/asset-search.effects.ts b/libs/asset-viewer/src/lib/state/asset-search/asset-search.effects.ts
index 293f06bf2..8cc514434 100644
--- a/libs/asset-viewer/src/lib/state/asset-search/asset-search.effects.ts
+++ b/libs/asset-viewer/src/lib/state/asset-search/asset-search.effects.ts
@@ -7,6 +7,7 @@ import { Action, Store } from '@ngrx/store';
 import { filter, map, of, skip, switchMap, take, withLatestFrom } from 'rxjs';
 import { AllStudyService } from '../../services/all-study.service';
 import { AssetSearchService } from '../../services/asset-search.service';
+
 import * as actions from './asset-search.actions';
 import { LoadingState } from './asset-search.reducer';
 import {
diff --git a/libs/asset-viewer/src/lib/state/asset-search/asset-search.reducer.ts b/libs/asset-viewer/src/lib/state/asset-search/asset-search.reducer.ts
index 3eeee10e0..6a4940ac6 100644
--- a/libs/asset-viewer/src/lib/state/asset-search/asset-search.reducer.ts
+++ b/libs/asset-viewer/src/lib/state/asset-search/asset-search.reducer.ts
@@ -7,6 +7,7 @@ import {
   GeomFromGeomText,
   LineString,
   LV95,
+  makeEmptyAssetSearchResults,
   Point,
   StudyPolygon,
 } from '@asset-sg/shared';
@@ -15,33 +16,30 @@ import * as E from 'fp-ts/Either';
 
 import { getCenter } from 'ol/extent';
 import { LineString as OlLineString, Polygon } from 'ol/geom';
-import { MapPosition } from '../../components/map/map-controller';
+import { DEFAULT_MAP_POSITION, MapPosition } from '../../components/map/map-controller';
 import { AllStudyDTO, AllStudyDTOs } from '../../models';
 import * as actions from './asset-search.actions';
-
-export enum LoadingState {
-  Initial = 'initial',
-  Loading = 'loading',
-  Loaded = 'loaded',
-}
+import { PanelState } from './asset-search.actions';
 
 export interface AssetSearchState {
   query: AssetSearchQuery;
   results: AssetSearchResult;
   stats: AssetSearchStats;
-  currentAsset: AssetEditDetail | undefined;
-  resultsLoadingState: LoadingState;
-  filterLoadingState: LoadingState;
-  assetDetailLoadingState: LoadingState;
-  studies: AllStudyDTOs | null;
+  studies: AllStudyDTOs;
+  currentAsset: AssetEditDetail | null;
   ui: AssetSearchUiState;
+
+  isLoadingStudies: boolean;
+  isLoadingResults: boolean;
+  isLoadingStats: boolean;
+  isLoadingAsset: boolean;
 }
 
 export interface AssetSearchUiState {
   scrollOffsetForResults: number;
-  isFiltersOpen: boolean;
-  isResultsOpen: boolean;
-  map: Partial<MapPosition>;
+  filtersState: PanelState;
+  resultsState: PanelState;
+  map: MapPosition;
 }
 
 export interface AppStateWithAssetSearch extends AppState {
@@ -50,14 +48,8 @@ export interface AppStateWithAssetSearch extends AppState {
 
 const initialState: AssetSearchState = {
   query: {},
-  results: {
-    page: {
-      size: 0,
-      offset: 0,
-      total: 0,
-    },
-    data: [],
-  },
+  studies: [],
+  results: makeEmptyAssetSearchResults(),
   stats: {
     total: 0,
     authorIds: [],
@@ -69,23 +61,31 @@ const initialState: AssetSearchState = {
     workgroupIds: [],
     createDate: null,
   },
-  resultsLoadingState: LoadingState.Initial,
-  assetDetailLoadingState: LoadingState.Initial,
-  filterLoadingState: LoadingState.Initial,
-  currentAsset: undefined,
-  studies: null,
+  currentAsset: null,
   ui: {
-    isFiltersOpen: true,
-    isResultsOpen: false,
+    filtersState: PanelState.OpenedAutomatically,
+    resultsState: PanelState.ClosedAutomatically,
     scrollOffsetForResults: 0,
-    map: {},
+    map: DEFAULT_MAP_POSITION,
   },
+
+  isLoadingStudies: false,
+  isLoadingResults: false,
+  isLoadingStats: false,
+  isLoadingAsset: false,
 };
 
 export const assetSearchReducer = createReducer(
   initialState,
   on(
-    actions.search,
+    actions.setQuery,
+    (state, { query }): AssetSearchState => ({
+      ...state,
+      query,
+    })
+  ),
+  on(
+    actions.updateSearchQuery,
     (state, { query }): AssetSearchState => ({
       ...state,
       query: {
@@ -95,57 +95,51 @@ export const assetSearchReducer = createReducer(
     })
   ),
   on(
-    actions.updateResults,
-    (state, { results }): AssetSearchState => ({
+    actions.setStudies,
+    (state, { studies, isLoading }): AssetSearchState => ({
       ...state,
-      results,
-      resultsLoadingState: LoadingState.Loaded,
+      studies: studies ?? state.studies,
+      isLoadingStudies: isLoading ?? state.isLoadingStudies,
     })
   ),
   on(
-    actions.updateStats,
-    (state, { stats }): AssetSearchState => ({
+    actions.setResults,
+    (state, { results, isLoading }): AssetSearchState => ({
       ...state,
-      stats,
-      filterLoadingState: LoadingState.Loaded,
+      results: results ?? state.results,
+      isLoadingResults: isLoading ?? state.isLoadingResults,
     })
   ),
   on(
-    actions.selectAsset,
-    (state): AssetSearchState => ({
+    actions.setStats,
+    (state, { stats, isLoading }): AssetSearchState => ({
       ...state,
-      assetDetailLoadingState: LoadingState.Loading,
+      stats: stats ?? state.stats,
+      isLoadingStats: isLoading ?? state.isLoadingStats,
     })
   ),
   on(
-    actions.setSelectedAsset,
-    (state, { asset }): AssetSearchState => ({
+    actions.setCurrentAsset,
+    (state, { asset, isLoading }): AssetSearchState => ({
       ...state,
-      currentAsset: asset,
-      assetDetailLoadingState: LoadingState.Loaded,
+      currentAsset: asset === undefined ? state.currentAsset : asset,
+      isLoadingAsset: isLoading ?? state.isLoadingAsset,
     })
   ),
   on(
-    actions.clearSelectedAsset,
-    (state): AssetSearchState => ({
+    actions.setFiltersState,
+    (state, { state: filtersState }): AssetSearchState => ({
       ...state,
-      currentAsset: initialState.currentAsset,
-      assetDetailLoadingState: initialState.assetDetailLoadingState,
+      ui: { ...state.ui, filtersState },
     })
   ),
   on(
-    actions.setFiltersOpen,
-    (state, { isOpen }): AssetSearchState => ({
+    actions.setResultsState,
+    (state, { state: resultsState }): AssetSearchState => ({
       ...state,
-      ui: { ...state.ui, isFiltersOpen: isOpen === 'toggle' ? !state.ui.isFiltersOpen : isOpen },
+      ui: { ...state.ui, resultsState },
     })
   ),
-  on(actions.setResultsOpen, (state, { isOpen }): AssetSearchState => {
-    return {
-      ...state,
-      ui: { ...state.ui, isResultsOpen: isOpen === 'toggle' ? !state.ui.isResultsOpen : isOpen },
-    };
-  }),
   on(actions.setScrollOffsetForResults, (state, { offset }): AssetSearchState => {
     return {
       ...state,
@@ -155,33 +149,26 @@ export const assetSearchReducer = createReducer(
   on(actions.setMapPosition, (state, { position }): AssetSearchState => {
     return {
       ...state,
-      ui: { ...state.ui, map: { ...state.ui.map, ...position } },
+      ui: { ...state.ui, map: position },
     };
   }),
-  on(actions.setStudies, (state, { studies }): AssetSearchState => ({ ...state, studies })),
   on(
-    actions.clearPolygon,
+    actions.resetSearch,
     (state): AssetSearchState => ({
       ...state,
-      query: {
-        ...state.query,
-        polygon: undefined,
+      query: {},
+      currentAsset: null,
+      ui: {
+        ...state.ui,
+        resultsState: PanelState.OpenedManually,
       },
     })
   ),
-  on(
-    actions.resetSearch,
-    (state): AssetSearchState => ({
-      ...initialState,
-      stats: state.stats,
-    })
-  ),
-  on(appSharedStateActions.openPanel, (state): AssetSearchState => ({ ...state })),
   on(
     appSharedStateActions.removeAssetFromSearch,
     (state, { assetId }): AssetSearchState => ({
       ...state,
-      currentAsset: state.currentAsset?.assetId === assetId ? undefined : state.currentAsset,
+      currentAsset: state.currentAsset?.assetId === assetId ? null : state.currentAsset,
       results: {
         ...state.results,
         data: state.results.data.filter((it) => it.assetId !== assetId),
@@ -194,7 +181,7 @@ export const assetSearchReducer = createReducer(
     const mapAsset = (it: AssetEditDetail): AssetEditDetail => (it.assetId === asset.assetId ? asset : it);
     return {
       ...state,
-      currentAsset: state.currentAsset == undefined ? undefined : mapAsset(state.currentAsset),
+      currentAsset: state.currentAsset === null ? null : mapAsset(state.currentAsset),
       results: {
         ...state.results,
         data: state.results.data.map(mapAsset),
diff --git a/libs/asset-viewer/src/lib/state/asset-search/asset-search.selector.ts b/libs/asset-viewer/src/lib/state/asset-search/asset-search.selector.ts
index bbb173ab6..1a4bd296e 100644
--- a/libs/asset-viewer/src/lib/state/asset-search/asset-search.selector.ts
+++ b/libs/asset-viewer/src/lib/state/asset-search/asset-search.selector.ts
@@ -28,16 +28,19 @@ import * as O from 'fp-ts/Option';
 
 import { AssetDetail } from '../../models';
 
+import { isPanelOpen } from './asset-search.actions';
 import { AppStateWithAssetSearch } from './asset-search.reducer';
 
 const assetSearchFeature = (state: AppStateWithAssetSearch) => state.assetSearch;
 
-export const selectSearchLoadingState = createSelector(assetSearchFeature, (state) => state.resultsLoadingState);
-export const selectFilterLoadingState = createSelector(assetSearchFeature, (state) => state.filterLoadingState);
+export const selectFiltersState = createSelector(assetSearchFeature, (state) => state.ui.filtersState);
 
-export const selectIsFiltersOpen = createSelector(assetSearchFeature, (state) => state.ui.isFiltersOpen);
+export const selectResultsState = createSelector(assetSearchFeature, (state) => state.ui.resultsState);
+
+export const selectIsFiltersOpen = createSelector(selectFiltersState, isPanelOpen);
+
+export const selectIsResultsOpen = createSelector(selectResultsState, isPanelOpen);
 
-export const selectIsResultsOpen = createSelector(assetSearchFeature, (state) => state.ui.isResultsOpen);
 export const selectMapPosition = createSelector(assetSearchFeature, (state) => state.ui.map);
 
 export const selectScrollOffsetForResults = createSelector(
@@ -45,37 +48,24 @@ export const selectScrollOffsetForResults = createSelector(
   (state) => state.ui.scrollOffsetForResults
 );
 
-export const selectAssetDetailLoadingState = createSelector(
-  assetSearchFeature,
-  (state) => state.assetDetailLoadingState
-);
-
-export const selectAssetSearchQuery = createSelector(assetSearchFeature, (state) => state.query);
+export const selectSearchQuery = createSelector(assetSearchFeature, (state) => state.query);
 
-export const selectAssetSearchResultData = createSelector(assetSearchFeature, (state) => state.results.data);
+export const selectSearchResults = createSelector(assetSearchFeature, (state) => state.results);
 
-export const selectAssetSearchPolygon = createSelector(assetSearchFeature, (state) => state.query.polygon);
+export const selectSearchStats = createSelector(assetSearchFeature, (state) => state.stats);
 
-export const selectAssetsSearchStats = createSelector(assetSearchFeature, (state) => state.stats);
+export const selectCurrentAsset = createSelector(assetSearchFeature, (state) => state.currentAsset);
 
-export const selectAssetSearchTotalResults = createSelector(assetSearchFeature, (state) => state.stats.total);
-
-export const selectCurrentAssetDetail = createSelector(assetSearchFeature, (state) => state.currentAsset);
-export const selectStudies = createSelector(assetSearchFeature, (state) => state.studies);
-
-export const selectHasNoActiveFilters = createSelector(assetSearchFeature, ({ query }) => hasNoActiveFilters(query));
-
-export const selectIsSearchQueryEmpty = createSelector(
+export const selectHasCurrentAsset = createSelector(
   assetSearchFeature,
-  ({ query, currentAsset }) => currentAsset == null && hasNoActiveFilters(query)
+  (state) => state.currentAsset !== null || state.isLoadingAsset
 );
 
-export const hasNoActiveFilters = (query: AssetSearchQuery): boolean =>
-  Object.values(query).every((value) => value === undefined || value == false);
+export const selectStudies = createSelector(assetSearchFeature, (state) => state.studies);
 
 export const selectCurrentAssetDetailVM = createSelector(
   fromAppShared.selectRDReferenceData,
-  selectCurrentAssetDetail,
+  selectCurrentAsset,
   (referenceData, currentAssetDetail) => {
     if (RD.isSuccess(referenceData) && !!currentAssetDetail) {
       return makeAssetDetailVMNew(referenceData.value, currentAssetDetail);
@@ -86,12 +76,12 @@ export const selectCurrentAssetDetailVM = createSelector(
 
 export const selectAssetEditDetailVM = createSelector(
   fromAppShared.selectRDReferenceData,
-  selectAssetSearchResultData,
+  selectSearchResults,
   (referenceData, assets): AssetEditDetailVM[] => {
     if (!RD.isSuccess(referenceData) || !assets) {
       return [];
     }
-    return assets.map((asset) => {
+    return assets.data.map((asset) => {
       const manCatLabelItems: ValueItem[] = asset.manCatLabelRefs.map(
         (manCatLabelItemCode) => referenceData.value.manCatLabelItems[manCatLabelItemCode]
       );
@@ -120,7 +110,7 @@ export const selectAssetEditDetailVM = createSelector(
 
 export const selectAvailableAuthors = createSelector(
   fromAppShared.selectRDReferenceData,
-  selectAssetsSearchStats,
+  selectSearchStats,
   (referenceData, stats): AvailableAuthor[] | null => {
     if (RD.isSuccess(referenceData)) {
       return stats.authorIds.map((authorId) => {
@@ -135,7 +125,7 @@ export const selectAvailableAuthors = createSelector(
   }
 );
 
-export const selectCreateDate = createSelector(selectAssetsSearchStats, (stats): DateRange | null => stats.createDate);
+export const selectCreateDate = createSelector(selectSearchStats, (stats): DateRange | null => stats.createDate);
 
 const makeFilters = <T>(
   configs: Array<FilterConfig<T>>,
@@ -171,8 +161,8 @@ export const selectFilters = <T extends string>(
 ) =>
   createSelector(
     fromAppShared.selectRDReferenceData,
-    selectAssetSearchQuery,
-    selectAssetsSearchStats,
+    selectSearchQuery,
+    selectSearchStats,
     (referenceData, query, stats): Array<Filter<T>> => {
       if (!RD.isSuccess(referenceData)) {
         return [];
@@ -196,8 +186,8 @@ export const selectFilters = <T extends string>(
 
 export const selectWorkgroupFilters = createSelector(
   fromAppShared.selectWorkgroups,
-  selectAssetSearchQuery,
-  selectAssetsSearchStats,
+  selectSearchQuery,
+  selectSearchStats,
   (workgroups, query, stats) => {
     // Create a mapping of workgroups by their id for easier and more performant lookup.
     const workgroupsById = new Map<WorkgroupId, SimpleWorkgroup>();
diff --git a/libs/client-shared/src/lib/directives/base-policy.directive.ts b/libs/client-shared/src/lib/directives/base-policy.directive.ts
index f7261ac83..d22e24f65 100644
--- a/libs/client-shared/src/lib/directives/base-policy.directive.ts
+++ b/libs/client-shared/src/lib/directives/base-policy.directive.ts
@@ -27,7 +27,9 @@ export abstract class BasePolicyDirective<T> implements OnInit, OnChanges, OnDes
 
   private user: User | null = null;
 
-  private ref = inject(ChangeDetectorRef);
+  private readonly ref = inject(ChangeDetectorRef);
+
+  private hasRendered = false;
 
   private readonly subscription = new Subscription();
 
@@ -56,11 +58,16 @@ export abstract class BasePolicyDirective<T> implements OnInit, OnChanges, OnDes
 
   private render(): void {
     const policyInstance = this.user == null ? null : new this.policy(this.user);
-    if (policyInstance != null && this.check(policyInstance)) {
+    const shouldRender = policyInstance != null && this.check(policyInstance);
+    if (shouldRender === this.hasRendered) {
+      return;
+    }
+    if (shouldRender) {
       this.viewContainer.createEmbeddedView(this.templateRef);
     } else {
       this.viewContainer.clear();
     }
+    this.hasRendered = shouldRender;
   }
 
   protected abstract check(policy: Policy<T>): boolean;
diff --git a/libs/client-shared/src/lib/state/app-shared-state.actions.ts b/libs/client-shared/src/lib/state/app-shared-state.actions.ts
index 9485343ae..bb57dc739 100644
--- a/libs/client-shared/src/lib/state/app-shared-state.actions.ts
+++ b/libs/client-shared/src/lib/state/app-shared-state.actions.ts
@@ -40,8 +40,6 @@ export const loadWorkgroupsResult = createAction(
   props<{ workgroups: SimpleWorkgroup[] }>()
 );
 
-export const openPanel = createAction('[App Shared State] Open Panel');
-
 export const logout = createAction('[App Shared State] Logout');
 
 export const setLang = createAction('[App Shared State] Set Lang', props<{ lang: Lang }>());
diff --git a/libs/shared/src/lib/models/asset-search/asset-search-query.ts b/libs/shared/src/lib/models/asset-search/asset-search-query.ts
index 4e55d4a83..afa0ba399 100644
--- a/libs/shared/src/lib/models/asset-search/asset-search-query.ts
+++ b/libs/shared/src/lib/models/asset-search/asset-search-query.ts
@@ -23,3 +23,6 @@ export enum GeometryCode {
 }
 
 export type Polygon = LV95[];
+
+export const isEmptySearchQuery = (query: AssetSearchQuery): boolean =>
+  Object.values(query).every((value) => value === undefined || value == false);
diff --git a/libs/shared/src/lib/models/asset-search/asset-search-result.ts b/libs/shared/src/lib/models/asset-search/asset-search-result.ts
index 0501ceeb9..5c1ae5ec7 100644
--- a/libs/shared/src/lib/models/asset-search/asset-search-result.ts
+++ b/libs/shared/src/lib/models/asset-search/asset-search-result.ts
@@ -31,3 +31,12 @@ export interface ValueCount<T> {
   value: T;
   count: number;
 }
+
+export const makeEmptyAssetSearchResults = (): AssetSearchResult => ({
+  page: {
+    size: 0,
+    offset: 0,
+    total: 0,
+  },
+  data: [],
+});


Reviews:
user: login: Tugark
body: @daniel-va This is no review, but I just merged `develop` into my `feature/legend-style-switch` release branch to resolve existing conflicts, and only realized afterwards that you're refactoring more things in this branch here :D

I just checked the conflicts between this branch and my release branch, and most conflicts are resolveable. There are two issues however, so I took the liberty of adding these questions without reviewing the whole PR (that's @TIL-EBP 's work ;))
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-26T16:01:54Z
user: login: TIL-EBP
body: Something is off with the studies on the map. I have to some way in to be able to see a single study. The heaetmap is dispalyed way longer than before, even when there is only a single study there. Not sure if this was caused by your PR though.

Setting filters and then navigating back updates the url but not the ui. Search needs to be triggered again after browser navigation. 
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-26T16:15:01Z
user: login: daniel-va
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-27T10:10:37Z
user: login: daniel-va
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-27T10:15:20Z
user: login: daniel-va
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-27T10:43:59Z
user: login: TIL-EBP
body: There are still two minor things that don't work properly, but all in all this looks very nice and clean and is way easier to follow
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-27T13:09:21Z
user: login: daniel-va
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-27T13:25:27Z
user: login: daniel-va
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-27T13:36:17Z
user: login: Tugark
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-27T14:05:11Z
user: login: Tugark
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-27T14:08:23Z
user: login: daniel-va
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-27T14:08:48Z
user: login: TIL-EBP
body: I think we need to discuss the exact behaviour that we want when navigating back and forward, because i am not sure which is the best way of handling that.


state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-27T15:46:54Z
user: login: daniel-va
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-27T16:25:55Z
user: login: TIL-EBP
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-31T07:29:56Z

Review comments:
diff_hunk: @@ -133,11 +137,14 @@ export class AssetSearchRefineComponent implements OnInit, OnDestroy, AfterViewI
   }
 
   public resetSearch() {
-    void this.router.navigate([]);
     this.authorAutoCompleteControl.setValue('');
     this.maxDateControl.setValue(null);
     this.maxDateControl.setValue(null);
-    this.store.dispatch(actions.resetSearch());
+    this.store.dispatch(setScrollOffsetForResults({ offset: 0 }));
+    this.store.dispatch(setResultsOpen({ isOpen: false }));
+    this.store.dispatch(setMapPosition({ position: DEFAULT_MAP_POSITION }));
+    this.store.dispatch(actions.setQuery({ query: {} }));
+    this.store.dispatch(actions.setCurrentAsset({ asset: null }));
path: libs/asset-viewer/src/lib/components/asset-search-refine/asset-search-refine.component.ts
user: login: TIL-EBP
body: Not sure if this applies here because we no longer have any effects, but usually it is discouraged to dispatch multiple actions at once. 
created_at: 2025-03-26T15:42:37Z
author_association: CONTRIBUTOR
diff_hunk: @@ -45,37 +42,24 @@ export const selectScrollOffsetForResults = createSelector(
   (state) => state.ui.scrollOffsetForResults
 );
 
-export const selectAssetDetailLoadingState = createSelector(
-  assetSearchFeature,
-  (state) => state.assetDetailLoadingState
-);
-
-export const selectAssetSearchQuery = createSelector(assetSearchFeature, (state) => state.query);
-
-export const selectAssetSearchResultData = createSelector(assetSearchFeature, (state) => state.results.data);
+export const selectSearchQuery = createSelector(assetSearchFeature, (state) => state.query);
 
-export const selectAssetSearchPolygon = createSelector(assetSearchFeature, (state) => state.query.polygon);
+export const selectSearchResults = createSelector(assetSearchFeature, (state) => state.results);
 
-export const selectAssetsSearchStats = createSelector(assetSearchFeature, (state) => state.stats);
+export const selectSearchStats = createSelector(assetSearchFeature, (state) => state.stats);
 
-export const selectAssetSearchTotalResults = createSelector(assetSearchFeature, (state) => state.stats.total);
+export const selectCurrentAsset = createSelector(assetSearchFeature, (state) => state.currentAsset);
 
-export const selectCurrentAssetDetail = createSelector(assetSearchFeature, (state) => state.currentAsset);
-export const selectStudies = createSelector(assetSearchFeature, (state) => state.studies);
-
-export const selectHasNoActiveFilters = createSelector(assetSearchFeature, ({ query }) => hasNoActiveFilters(query));
path: libs/asset-viewer/src/lib/state/asset-search/asset-search.selector.ts
user: login: Tugark
body: Can we leave this in? I rely on this selector for toggling between the different icons for point/actual geometries.

Or is there an alternative?
created_at: 2025-03-26T15:56:58Z
author_association: CONTRIBUTOR
diff_hunk: @@ -15,33 +16,29 @@ import * as E from 'fp-ts/Either';
 
 import { getCenter } from 'ol/extent';
 import { LineString as OlLineString, Polygon } from 'ol/geom';
-import { MapPosition } from '../../components/map/map-controller';
+import { DEFAULT_MAP_POSITION, MapPosition } from '../../components/map/map-controller';
 import { AllStudyDTO, AllStudyDTOs } from '../../models';
 import * as actions from './asset-search.actions';
 
-export enum LoadingState {
-  Initial = 'initial',
-  Loading = 'loading',
-  Loaded = 'loaded',
-}
-
 export interface AssetSearchState {
   query: AssetSearchQuery;
   results: AssetSearchResult;
   stats: AssetSearchStats;
-  currentAsset: AssetEditDetail | undefined;
-  resultsLoadingState: LoadingState;
-  filterLoadingState: LoadingState;
-  assetDetailLoadingState: LoadingState;
-  studies: AllStudyDTOs | null;
+  studies: AllStudyDTOs;
path: libs/asset-viewer/src/lib/state/asset-search/asset-search.reducer.ts
user: login: Tugark
body: Just one question - in my legend refactor, I removed this `AllStudyDTOs` by extracting a simple `AllStudyDTO` interface and using it as `AllStudyDTO[]`.

Do you see any issues with that? I'm mainly asking because it is no longer nullable, so I guess some things changed.
created_at: 2025-03-26T15:59:47Z
author_association: CONTRIBUTOR
diff_hunk: @@ -15,33 +16,29 @@ import * as E from 'fp-ts/Either';
 
 import { getCenter } from 'ol/extent';
 import { LineString as OlLineString, Polygon } from 'ol/geom';
-import { MapPosition } from '../../components/map/map-controller';
+import { DEFAULT_MAP_POSITION, MapPosition } from '../../components/map/map-controller';
 import { AllStudyDTO, AllStudyDTOs } from '../../models';
 import * as actions from './asset-search.actions';
 
-export enum LoadingState {
-  Initial = 'initial',
-  Loading = 'loading',
-  Loaded = 'loaded',
-}
-
 export interface AssetSearchState {
   query: AssetSearchQuery;
   results: AssetSearchResult;
   stats: AssetSearchStats;
-  currentAsset: AssetEditDetail | undefined;
-  resultsLoadingState: LoadingState;
-  filterLoadingState: LoadingState;
-  assetDetailLoadingState: LoadingState;
-  studies: AllStudyDTOs | null;
+  studies: AllStudyDTOs;
path: libs/asset-viewer/src/lib/state/asset-search/asset-search.reducer.ts
user: login: daniel-va
body: I'm just using an empty array as the default, as the `null` value does not really have a purpose anymore. I don't see any issues with your change, besides the merge we'll have to go through either way 😒 
created_at: 2025-03-27T10:10:37Z
author_association: CONTRIBUTOR
diff_hunk: @@ -45,37 +42,24 @@ export const selectScrollOffsetForResults = createSelector(
   (state) => state.ui.scrollOffsetForResults
 );
 
-export const selectAssetDetailLoadingState = createSelector(
-  assetSearchFeature,
-  (state) => state.assetDetailLoadingState
-);
-
-export const selectAssetSearchQuery = createSelector(assetSearchFeature, (state) => state.query);
-
-export const selectAssetSearchResultData = createSelector(assetSearchFeature, (state) => state.results.data);
+export const selectSearchQuery = createSelector(assetSearchFeature, (state) => state.query);
 
-export const selectAssetSearchPolygon = createSelector(assetSearchFeature, (state) => state.query.polygon);
+export const selectSearchResults = createSelector(assetSearchFeature, (state) => state.results);
 
-export const selectAssetsSearchStats = createSelector(assetSearchFeature, (state) => state.stats);
+export const selectSearchStats = createSelector(assetSearchFeature, (state) => state.stats);
 
-export const selectAssetSearchTotalResults = createSelector(assetSearchFeature, (state) => state.stats.total);
+export const selectCurrentAsset = createSelector(assetSearchFeature, (state) => state.currentAsset);
 
-export const selectCurrentAssetDetail = createSelector(assetSearchFeature, (state) => state.currentAsset);
-export const selectStudies = createSelector(assetSearchFeature, (state) => state.studies);
-
-export const selectHasNoActiveFilters = createSelector(assetSearchFeature, ({ query }) => hasNoActiveFilters(query));
path: libs/asset-viewer/src/lib/state/asset-search/asset-search.selector.ts
user: login: daniel-va
body: You could achieve the same result with this:
```ts
store.select(selectSearchQuery).pipe(map(isEmptySearchQuery))
```
I'm also fine with adding a new selector if you prefer. I just didn't have a use for it anymore and didn't want to have an unused selector lying around.
created_at: 2025-03-27T10:15:20Z
author_association: CONTRIBUTOR
diff_hunk: @@ -133,11 +137,14 @@ export class AssetSearchRefineComponent implements OnInit, OnDestroy, AfterViewI
   }
 
   public resetSearch() {
-    void this.router.navigate([]);
     this.authorAutoCompleteControl.setValue('');
     this.maxDateControl.setValue(null);
     this.maxDateControl.setValue(null);
-    this.store.dispatch(actions.resetSearch());
+    this.store.dispatch(setScrollOffsetForResults({ offset: 0 }));
+    this.store.dispatch(setResultsOpen({ isOpen: false }));
+    this.store.dispatch(setMapPosition({ position: DEFAULT_MAP_POSITION }));
+    this.store.dispatch(actions.setQuery({ query: {} }));
+    this.store.dispatch(actions.setCurrentAsset({ asset: null }));
path: libs/asset-viewer/src/lib/components/asset-search-refine/asset-search-refine.component.ts
user: login: daniel-va
body: Still applies, generally, as this can effectively trigger some observables 5 times in a row. I moved this into the new `resetSearch` action, which makes sense to have anyways.
created_at: 2025-03-27T10:43:59Z
author_association: CONTRIBUTOR
diff_hunk: @@ -186,4 +190,32 @@ export class ViewerControllerService {
         }
       });
   }
+
+  private reactToNavigation(): Subscription {
+    return this.router.events.subscribe(async (event) => {
+      if (this.isUpdatingUrl || !(event instanceof NavigationEnd)) {
+        return;
+      }
+      const params = this.viewerParamsService.readParamsFromUrl();
+      this.isUpdatingStore = true;
+      await this.updateStoreByParams(params);
+      this.isUpdatingStore = false;
+    });
+  }
+
+  private updateStoreByParams(params: ViewerParams): Promise<void> {
+    const { ui, query, assetId } = params;
+    this.store.dispatch(setScrollOffsetForResults({ offset: ui.scrollOffsetForResults }));
path: libs/asset-viewer/src/lib/services/viewer-controller.service.ts
user: login: TIL-EBP
body: again here, multiple disaptches in a row are discouraged
created_at: 2025-03-27T12:25:52Z
author_association: CONTRIBUTOR
diff_hunk: @@ -186,4 +190,32 @@ export class ViewerControllerService {
         }
       });
   }
+
+  private reactToNavigation(): Subscription {
path: libs/asset-viewer/src/lib/services/viewer-controller.service.ts
user: login: TIL-EBP
body: when i add just one filter and navigate back, i am stuck, becuase after adding the filter to the url, the state of the (open panel) is written to the url. So when navigating back, this will be removed and added back. ONly when I click "back" twice in quick succession can i remove the filter.


created_at: 2025-03-27T13:04:42Z
author_association: CONTRIBUTOR
diff_hunk: @@ -66,40 +71,29 @@ export class ViewerControllerService {
     this.subscription.unsubscribe();
     this.subscription = new Subscription();
     this.viewerReadySubject = new ReplaySubject(1);
+    this.isUpdatingUrl = false;
+    this.isUpdatingStore = false;
   }
 
   selectAsset(id: AssetId): void {
     this.loadAsset(id).then();
   }
 
   private async initializeFromStore(): Promise<void> {
-    const params = await this.viewerParamsService.readParamsFromUrl();
-
-    const { ui, query, assetId } = params;
-    this.store.dispatch(setScrollOffsetForResults({ offset: ui.scrollOffsetForResults }));
-    this.store.dispatch(setFiltersOpen({ isOpen: ui.isFiltersOpen }));
-    this.store.dispatch(setResultsOpen({ isOpen: ui.isResultsOpen }));
-    this.store.dispatch(setMapPosition({ position: ui.map }));
-    this.store.dispatch(setQuery({ query }));
-
-    const loads: Array<Promise<void>> = [];
+    const params = this.viewerParamsService.readParamsFromUrl();
path: libs/asset-viewer/src/lib/services/viewer-controller.service.ts
user: login: TIL-EBP
body: when moving away from `/` or `/favorites` and then back again, I lose the state from the url
created_at: 2025-03-27T13:08:28Z
author_association: CONTRIBUTOR
diff_hunk: @@ -186,4 +190,32 @@ export class ViewerControllerService {
         }
       });
   }
+
+  private reactToNavigation(): Subscription {
+    return this.router.events.subscribe(async (event) => {
+      if (this.isUpdatingUrl || !(event instanceof NavigationEnd)) {
+        return;
+      }
+      const params = this.viewerParamsService.readParamsFromUrl();
+      this.isUpdatingStore = true;
+      await this.updateStoreByParams(params);
+      this.isUpdatingStore = false;
+    });
+  }
+
+  private updateStoreByParams(params: ViewerParams): Promise<void> {
+    const { ui, query, assetId } = params;
+    this.store.dispatch(setScrollOffsetForResults({ offset: ui.scrollOffsetForResults }));
path: libs/asset-viewer/src/lib/services/viewer-controller.service.ts
user: login: daniel-va
body: I considered creating an action for this, but these actions are mostly unrelated and would only complicate the state. I would prefer to leave this as-is for now.
created_at: 2025-03-27T13:25:27Z
author_association: CONTRIBUTOR
diff_hunk: @@ -66,40 +71,29 @@ export class ViewerControllerService {
     this.subscription.unsubscribe();
     this.subscription = new Subscription();
     this.viewerReadySubject = new ReplaySubject(1);
+    this.isUpdatingUrl = false;
+    this.isUpdatingStore = false;
   }
 
   selectAsset(id: AssetId): void {
     this.loadAsset(id).then();
   }
 
   private async initializeFromStore(): Promise<void> {
-    const params = await this.viewerParamsService.readParamsFromUrl();
-
-    const { ui, query, assetId } = params;
-    this.store.dispatch(setScrollOffsetForResults({ offset: ui.scrollOffsetForResults }));
-    this.store.dispatch(setFiltersOpen({ isOpen: ui.isFiltersOpen }));
-    this.store.dispatch(setResultsOpen({ isOpen: ui.isResultsOpen }));
-    this.store.dispatch(setMapPosition({ position: ui.map }));
-    this.store.dispatch(setQuery({ query }));
-
-    const loads: Array<Promise<void>> = [];
+    const params = this.viewerParamsService.readParamsFromUrl();
path: libs/asset-viewer/src/lib/services/viewer-controller.service.ts
user: login: daniel-va
body: Forgot about that behavior, it's back now.
created_at: 2025-03-27T13:36:16Z
author_association: CONTRIBUTOR
diff_hunk: @@ -15,33 +16,29 @@ import * as E from 'fp-ts/Either';
 
 import { getCenter } from 'ol/extent';
 import { LineString as OlLineString, Polygon } from 'ol/geom';
-import { MapPosition } from '../../components/map/map-controller';
+import { DEFAULT_MAP_POSITION, MapPosition } from '../../components/map/map-controller';
 import { AllStudyDTO, AllStudyDTOs } from '../../models';
 import * as actions from './asset-search.actions';
 
-export enum LoadingState {
-  Initial = 'initial',
-  Loading = 'loading',
-  Loaded = 'loaded',
-}
-
 export interface AssetSearchState {
   query: AssetSearchQuery;
   results: AssetSearchResult;
   stats: AssetSearchStats;
-  currentAsset: AssetEditDetail | undefined;
-  resultsLoadingState: LoadingState;
-  filterLoadingState: LoadingState;
-  assetDetailLoadingState: LoadingState;
-  studies: AllStudyDTOs | null;
+  studies: AllStudyDTOs;
path: libs/asset-viewer/src/lib/state/asset-search/asset-search.reducer.ts
user: login: Tugark
body: Perfect, that's cleaner anyway 👍
created_at: 2025-03-27T14:05:11Z
author_association: CONTRIBUTOR
diff_hunk: @@ -45,37 +42,24 @@ export const selectScrollOffsetForResults = createSelector(
   (state) => state.ui.scrollOffsetForResults
 );
 
-export const selectAssetDetailLoadingState = createSelector(
-  assetSearchFeature,
-  (state) => state.assetDetailLoadingState
-);
-
-export const selectAssetSearchQuery = createSelector(assetSearchFeature, (state) => state.query);
-
-export const selectAssetSearchResultData = createSelector(assetSearchFeature, (state) => state.results.data);
+export const selectSearchQuery = createSelector(assetSearchFeature, (state) => state.query);
 
-export const selectAssetSearchPolygon = createSelector(assetSearchFeature, (state) => state.query.polygon);
+export const selectSearchResults = createSelector(assetSearchFeature, (state) => state.results);
 
-export const selectAssetsSearchStats = createSelector(assetSearchFeature, (state) => state.stats);
+export const selectSearchStats = createSelector(assetSearchFeature, (state) => state.stats);
 
-export const selectAssetSearchTotalResults = createSelector(assetSearchFeature, (state) => state.stats.total);
+export const selectCurrentAsset = createSelector(assetSearchFeature, (state) => state.currentAsset);
 
-export const selectCurrentAssetDetail = createSelector(assetSearchFeature, (state) => state.currentAsset);
-export const selectStudies = createSelector(assetSearchFeature, (state) => state.studies);
-
-export const selectHasNoActiveFilters = createSelector(assetSearchFeature, ({ query }) => hasNoActiveFilters(query));
path: libs/asset-viewer/src/lib/state/asset-search/asset-search.selector.ts
user: login: Tugark
body: Yeah, of course, but I'll just have to re-add your removed selector again, because it's more readable thatn having the logic in the component.

Bad coincidence with you working on that and me working on the other part without having it merged ;)
created_at: 2025-03-27T14:08:23Z
author_association: CONTRIBUTOR
diff_hunk: @@ -186,4 +190,32 @@ export class ViewerControllerService {
         }
       });
   }
+
+  private reactToNavigation(): Subscription {
path: libs/asset-viewer/src/lib/services/viewer-controller.service.ts
user: login: daniel-va
body: Solved by differentiating between the panel states stemming from the user or the application.
created_at: 2025-03-27T14:08:47Z
author_association: CONTRIBUTOR
diff_hunk: @@ -59,8 +64,13 @@ export class MenuBarComponent {
     startWith('home' as const)
   );
 
-  toggleAssetDrawer(): void {
-    this.store.dispatch(setFiltersOpen({ isOpen: 'toggle' }));
+  async toggleAssetDrawer(): Promise<void> {
+    const isOpen = await firstValueFrom(this.store.select(selectIsFiltersOpen));
path: apps/client-asset-sg/src/app/components/menu-bar/menu-bar.component.ts
user: login: TIL-EBP
body: now i can no longer manually open the panel
created_at: 2025-03-27T15:39:50Z
author_association: CONTRIBUTOR
diff_hunk: @@ -59,8 +64,13 @@ export class MenuBarComponent {
     startWith('home' as const)
   );
 
-  toggleAssetDrawer(): void {
-    this.store.dispatch(setFiltersOpen({ isOpen: 'toggle' }));
+  async toggleAssetDrawer(): Promise<void> {
+    const isOpen = await firstValueFrom(this.store.select(selectIsFiltersOpen));
path: apps/client-asset-sg/src/app/components/menu-bar/menu-bar.component.ts
user: login: daniel-va
body: I used the wrong value for opening, it's working now.
created_at: 2025-03-27T16:25:55Z
author_association: CONTRIBUTOR

PR comments:
user: login: daniel-va
created_at: 2025-03-27T10:34:38Z
body: @TIL-EBP The issue with the studies was caused by the new `maxResolution` setting effectively overwriting the layer's individual zoom levels. This is now fixed by using zoom instead of resolution for the `z` axis.
author_association: CONTRIBUTOR
user: login: sonarqubecloud[bot]
created_at: 2025-03-31T09:20:24Z
body: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=swisstopo_swissgeol-assets-suite&pullRequest=471) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 New issues](https://sonarcloud.io/project/issues?id=swisstopo_swissgeol-assets-suite&pullRequest=471&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [1 Accepted issue](https://sonarcloud.io/project/issues?id=swisstopo_swissgeol-assets-suite&pullRequest=471&issueStatuses=ACCEPTED)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=swisstopo_swissgeol-assets-suite&pullRequest=471&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Coverage on New Code](https://sonarcloud.io/component_measures?id=swisstopo_swissgeol-assets-suite&pullRequest=471&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=swisstopo_swissgeol-assets-suite&pullRequest=471&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=swisstopo_swissgeol-assets-suite&pullRequest=471)


author_association: NONE