Repository: gisaia/ARLAS-wui
Pull request #989: feat: add cog legend on hover of features layers
Author: login: QuCMGisaia
Created: 2025-05-16T14:10:29Z | Merged: 2025-05-30T08:14:47Z

- Fix #943 
- Needs gisaia/ARLAS-web-components#998

Linked issue #943: [COG Visualisation] Display the visualisation legend

Diff:
diff --git a/package-lock.json b/package-lock.json
index 841426ed..79e0e94b 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -7598,11 +7598,11 @@
       }
     },
     "node_modules/arlas-mapbox": {
-      "version": "27.1.0-beta.1",
-      "resolved": "https://registry.npmjs.org/arlas-mapbox/-/arlas-mapbox-27.1.0-beta.1.tgz",
-      "integrity": "sha512-qLUuOiMgka4xbm4HP5dh8FPx1Uy299mGrRQWZa0TipBT9PyFOQxoKDdj/mjECuxIQhDQOSdcdmnN+CpbkdfYVQ==",
+      "version": "27.1.0-beta.2",
+      "resolved": "https://registry.npmjs.org/arlas-mapbox/-/arlas-mapbox-27.1.0-beta.2.tgz",
+      "integrity": "sha512-ykfwbB9hVEF2YOrRwiQY6obSsQVjgo80xtzRvGdMbcR3KtKKHpjttPiqfswhPFlnoue2IHFYRS0Rv1IIAva8vQ==",
       "dependencies": {
-        "arlas-map": "27.1.0-beta.1",
+        "arlas-map": "27.1.0-beta.2",
         "mapbox-gl": "^1.6.1",
         "rxjs": "^7.4.0",
         "tslib": "^2.3.0"
@@ -7623,9 +7623,9 @@
       }
     },
     "node_modules/arlas-mapbox/node_modules/arlas-map": {
-      "version": "27.1.0-beta.1",
-      "resolved": "https://registry.npmjs.org/arlas-map/-/arlas-map-27.1.0-beta.1.tgz",
-      "integrity": "sha512-YzNus1fVdXBuEO52hwSGxHVU2QHw3tmhtQNnR65M6hYjQLnEPIqE1kr3nT9g4vABx9tvOplj4Wz8VBZt64B0oA==",
+      "version": "27.1.0-beta.2",
+      "resolved": "https://registry.npmjs.org/arlas-map/-/arlas-map-27.1.0-beta.2.tgz",
+      "integrity": "sha512-Fd1iPHbM3MAd/1SeDP0NtUNax9bhn9c7tVX265bGTkJM1HXKbhskTh8eYqy1ysdNcE5D/Pb6Z6t8qyb7gMruAA==",
       "dependencies": {
         "@mapbox/mapbox-gl-draw": "^1.4.3",
         "@mapbox/mapbox-gl-draw-static-mode": "^1.0.1",
@@ -7643,7 +7643,7 @@
         "@turf/rhumb-destination": "^6.5.0",
         "@turf/transform-rotate": "^6.5.0",
         "@turf/transform-translate": "^6.5.0",
-        "arlas-web-components": "27.1.0-beta.1",
+        "arlas-web-components": "27.1.0-beta.2",
         "geojson-polygon-self-intersections": "1.2.1",
         "geojson-validation": "0.2.1",
         "jsts": "2.1.0",
@@ -7672,11 +7672,11 @@
       }
     },
     "node_modules/arlas-maplibre": {
-      "version": "27.1.0-beta.1",
-      "resolved": "https://registry.npmjs.org/arlas-maplibre/-/arlas-maplibre-27.1.0-beta.1.tgz",
-      "integrity": "sha512-a2x+OFgGvxXLNiwJMVBOzZS6Uco+Nmziyq4XJp7zq4/WWH4hgKCKpXCKzpsHJuoTNKHdGg/s0Tfc4QRmY6wjSw==",
+      "version": "27.1.0-beta.2",
+      "resolved": "https://registry.npmjs.org/arlas-maplibre/-/arlas-maplibre-27.1.0-beta.2.tgz",
+      "integrity": "sha512-9mEogeuYh3XIslML3rnDkP5ZSxggnuQC54JML7xBw479wZUaKmiQ+8Wtf6M0bsgIT54fmwAwJ+fRqc9zP25AWQ==",
       "dependencies": {
-        "arlas-map": "27.1.0-beta.1",
+        "arlas-map": "27.1.0-beta.2",
         "maplibre-gl": "^5.2.0",
         "rxjs": "^7.4.0",
         "tslib": "^2.3.0"
@@ -7697,9 +7697,9 @@
       }
     },
     "node_modules/arlas-maplibre/node_modules/arlas-map": {
-      "version": "27.1.0-beta.1",
-      "resolved": "https://registry.npmjs.org/arlas-map/-/arlas-map-27.1.0-beta.1.tgz",
-      "integrity": "sha512-YzNus1fVdXBuEO52hwSGxHVU2QHw3tmhtQNnR65M6hYjQLnEPIqE1kr3nT9g4vABx9tvOplj4Wz8VBZt64B0oA==",
+      "version": "27.1.0-beta.2",
+      "resolved": "https://registry.npmjs.org/arlas-map/-/arlas-map-27.1.0-beta.2.tgz",
+      "integrity": "sha512-Fd1iPHbM3MAd/1SeDP0NtUNax9bhn9c7tVX265bGTkJM1HXKbhskTh8eYqy1ysdNcE5D/Pb6Z6t8qyb7gMruAA==",
       "dependencies": {
         "@mapbox/mapbox-gl-draw": "^1.4.3",
         "@mapbox/mapbox-gl-draw-static-mode": "^1.0.1",
@@ -7717,7 +7717,7 @@
         "@turf/rhumb-destination": "^6.5.0",
         "@turf/transform-rotate": "^6.5.0",
         "@turf/transform-translate": "^6.5.0",
-        "arlas-web-components": "27.1.0-beta.1",
+        "arlas-web-components": "27.1.0-beta.2",
         "geojson-polygon-self-intersections": "1.2.1",
         "geojson-validation": "0.2.1",
         "jsts": "2.1.0",
@@ -7773,9 +7773,9 @@
       }
     },
     "node_modules/arlas-web-components": {
-      "version": "27.1.0-beta.1",
-      "resolved": "https://registry.npmjs.org/arlas-web-components/-/arlas-web-components-27.1.0-beta.1.tgz",
-      "integrity": "sha512-dVz658MYba755ftjEK8eWaMJra2Av5NvGr34PvSlvqWaM3qSTuJlj1eiNEqgBk801d+FfEXNfYtDc5hq4hN7Og==",
+      "version": "27.1.0-beta.2",
+      "resolved": "https://registry.npmjs.org/arlas-web-components/-/arlas-web-components-27.1.0-beta.2.tgz",
+      "integrity": "sha512-L8TPLL6nDm02KAa15gm5n26PtpU4wu8jjCjWGhkuAHhKE06tUNarOqTA2KNQV0F/0szQ70HBhVpcdt7U4ALKjg==",
       "dependencies": {
         "arlas-d3": "~12.0.1",
         "iv-viewer": "^2.2.1",
@@ -20947,14 +20947,14 @@
       "name": "arlas-wui-cloud",
       "license": "Apache-2.0",
       "dependencies": {
-        "arlas-mapbox": "27.1.0-beta.1"
+        "arlas-mapbox": "27.1.0-beta.2"
       }
     },
     "packages/opensource": {
       "name": "arlas-wui",
       "license": "Apache-2.0",
       "dependencies": {
-        "arlas-maplibre": "27.1.0-beta.1"
+        "arlas-maplibre": "27.1.0-beta.2"
       }
     }
   }
diff --git a/package.json b/package.json
index 0ffd1f34..99b08544 100644
--- a/package.json
+++ b/package.json
@@ -83,6 +83,6 @@
   "overrides": {
     "esbuild": "~0.25.1",
     "arlas-web-contributors": "27.1.0-beta.1",
-    "arlas-web-components": "27.1.0-beta.1"
+    "arlas-web-components": "27.1.0-beta.2"
   }
 }
diff --git a/packages/cloud/package.json b/packages/cloud/package.json
index cd584b3b..46d9ecf4 100644
--- a/packages/cloud/package.json
+++ b/packages/cloud/package.json
@@ -3,6 +3,6 @@
   "author": "gisaia",
   "license": "Apache-2.0",
   "dependencies": {
-    "arlas-mapbox": "27.1.0-beta.1"
+    "arlas-mapbox": "27.1.0-beta.2"
   }
 }
diff --git a/packages/opensource/package.json b/packages/opensource/package.json
index de99cb48..e992eb3a 100644
--- a/packages/opensource/package.json
+++ b/packages/opensource/package.json
@@ -3,6 +3,6 @@
   "author": "gisaia",
   "license": "Apache-2.0",
   "dependencies": {
-    "arlas-maplibre": "27.1.0-beta.1"
+    "arlas-maplibre": "27.1.0-beta.2"
   }
 }
diff --git a/src/app/app.component.spec.ts b/src/app/app.component.spec.ts
index 50e64547..fe90f6ac 100644
--- a/src/app/app.component.spec.ts
+++ b/src/app/app.component.spec.ts
@@ -18,7 +18,7 @@
  */
 
 import { Dialog, DIALOG_SCROLL_STRATEGY } from '@angular/cdk/dialog';
-import { HttpClientModule } from '@angular/common/http';
+import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http';
 import { ComponentFixture, TestBed } from '@angular/core/testing';
 import { MAT_DIALOG_SCROLL_STRATEGY, MatDialog } from '@angular/material/dialog';
 import { MatSnackBar } from '@angular/material/snack-bar';
@@ -62,8 +62,7 @@ describe('ArlasWuiComponent', () => {
         }),
         MatTooltipModule,
         /** Needed for ResultlistService */
-        RouterTestingModule,
-        HttpClientModule
+        RouterTestingModule
         /** End */
       ],
       providers: [
@@ -97,6 +96,7 @@ describe('ArlasWuiComponent', () => {
           provide: DIALOG_SCROLL_STRATEGY,
           useValue: {}
         },
+        provideHttpClient(withInterceptorsFromDi()),
         /** End */
         {
           provide: ColorGeneratorLoader,
diff --git a/src/app/app.module.ts b/src/app/app.module.ts
index 48a63724..fc5dab1f 100644
--- a/src/app/app.module.ts
+++ b/src/app/app.module.ts
@@ -77,6 +77,7 @@ import {
   CogVisualisationManagerComponent
 } from './components/map/cog-visualisation-manager/cog-visualisation-manager.component';
 import { RastersManagerComponent } from './components/map/raster-layers-manager/rasters-manager.component';
+import { VisualisationLegendComponent } from './components/map/visualisation-legend/visualisation-legend.component';
 import { GetResultlistConfigPipe } from './pipes/get-resultlist-config.pipe';
 import { CogService } from './services/cog.service';
 import { ContributorService } from './services/contributors.service';
@@ -168,7 +169,8 @@ import { LazyLoadImageHooks } from './tools/lazy-loader';
     LoginModule,
     LazyLoadImageModule,
     ArlasMapModule,
-    CogVisualisationManagerComponent
+    CogVisualisationManagerComponent,
+    VisualisationLegendComponent
   ],
   providers: [
     VisualizeService,
diff --git a/src/app/components/arlas-analytics/arlas-analytics.component.spec.ts b/src/app/components/arlas-analytics/arlas-analytics.component.spec.ts
index 8230aac5..903a4134 100644
--- a/src/app/components/arlas-analytics/arlas-analytics.component.spec.ts
+++ b/src/app/components/arlas-analytics/arlas-analytics.component.spec.ts
@@ -1,6 +1,6 @@
 import { Dialog, DIALOG_SCROLL_STRATEGY } from '@angular/cdk/dialog';
 import { Overlay } from '@angular/cdk/overlay';
-import { HttpClientModule } from '@angular/common/http';
+import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http';
 import { ComponentFixture, TestBed } from '@angular/core/testing';
 import { MAT_DIALOG_SCROLL_STRATEGY, MatDialog } from '@angular/material/dialog';
 import { MatSnackBar } from '@angular/material/snack-bar';
@@ -21,7 +21,6 @@ describe('ArlasAnalyticsComponent', () => {
       declarations: [ ArlasAnalyticsComponent ],
       imports: [
         RouterTestingModule,
-        HttpClientModule,
         TranslateModule.forRoot({ loader: { provide: TranslateLoader, useClass: TranslateFakeLoader } })
       ],
       providers: [
@@ -44,7 +43,8 @@ describe('ArlasAnalyticsComponent', () => {
         },
         Overlay,
         ArlasCollectionService,
-        ContributorService
+        ContributorService,
+        provideHttpClient(withInterceptorsFromDi())
       ]
     })
       .compileComponents();
diff --git a/src/app/components/arlas-map/aoi-dimensions/aoi-dimensions.pipes.ts b/src/app/components/arlas-map/aoi-dimensions/aoi-dimensions.pipes.ts
index cee77764..0d41e375 100644
--- a/src/app/components/arlas-map/aoi-dimensions/aoi-dimensions.pipes.ts
+++ b/src/app/components/arlas-map/aoi-dimensions/aoi-dimensions.pipes.ts
@@ -26,11 +26,10 @@ import { Pipe, PipeTransform } from '@angular/core';
 })
 export class SquareKilometer implements PipeTransform {
 
-  public transform(a: number): any {
+  public transform(a: number): number {
     const oneSquareKM = 1000000;
     return Math.round(a / oneSquareKM * 10) / 10;
   }
-
 }
 
 @Pipe({
@@ -39,8 +38,7 @@ export class SquareKilometer implements PipeTransform {
 })
 export class RoundKilometer implements PipeTransform {
 
-  public transform(a: number): any {
+  public transform(a: number): number {
     return Math.round(a * 10) / 10;
   }
-
 }
diff --git a/src/app/components/arlas-map/arlas-map.component.html b/src/app/components/arlas-map/arlas-map.component.html
index 5e770ee7..8ee11357 100644
--- a/src/app/components/arlas-map/arlas-map.component.html
+++ b/src/app/components/arlas-map/arlas-map.component.html
@@ -48,12 +48,13 @@
       [class.arlas-cog-visualisation-manager--tight]="!wuiMapService.coordinatesHaveSpace && !wuiMapService.timeLineIsOpen"
       [class.arlas-cog-visualisation-manager--timeline-open]="wuiMapService.timeLineIsOpen && wuiMapService.coordinatesHaveSpace"
       [class.arlas-cog-visualisation-manager--full]="wuiMapService.timeLineIsOpen && !wuiMapService.coordinatesHaveSpace">
-    @if (visualizeService.isRasterOnMap) {
-      <arlas-rasters-manager></arlas-rasters-manager>
-    }
     @if (cogVisualisation()?.visualisation){
+      <arlas-visualisation-legend></arlas-visualisation-legend>
       <arlas-cog-visualisation-manager [visualisation]="cogVisualisation().visualisation" [preview]="cogVisualisation().preview"></arlas-cog-visualisation-manager>
     }
+    @if (visualizeService.isRasterOnMap) {
+      <arlas-rasters-manager></arlas-rasters-manager>
+    }
   </div>
 
     <arlas-map [id]="mapId" #map [dataSources]="mapDataSources" [redrawSource]="mapRedrawSources"
diff --git a/src/app/components/arlas-map/arlas-map.component.scss b/src/app/components/arlas-map/arlas-map.component.scss
index c12610d9..73134e07 100644
--- a/src/app/components/arlas-map/arlas-map.component.scss
+++ b/src/app/components/arlas-map/arlas-map.component.scss
@@ -71,7 +71,7 @@
     right: $sm-spacing;
     z-index: 2;
     display: flex;
-    align-items: center;
+    align-items: flex-end;
     gap: 5px;
 
     &--tight {
diff --git a/src/app/components/arlas-map/arlas-map.component.ts b/src/app/components/arlas-map/arlas-map.component.ts
index 810e63a7..666a10ec 100644
--- a/src/app/components/arlas-map/arlas-map.component.ts
+++ b/src/app/components/arlas-map/arlas-map.component.ts
@@ -16,6 +16,7 @@
  * specific language governing permissions and limitations
  * under the License.
  */
+
 import { Component, OnInit, signal, ViewChild } from '@angular/core';
 import { MatDialog } from '@angular/material/dialog';
 import { MatIconRegistry } from '@angular/material/icon';
@@ -27,6 +28,7 @@ import * as helpers from '@turf/helpers';
 import { VisualisationPreview } from 'app/tools/cog';
 import {
   AoiEdition,
+  ArlasDataLayer,
   ArlasLngLat,
   ArlasLngLatBounds,
   ArlasMapComponent,
@@ -124,7 +126,7 @@ export class ArlasWuiMapComponent<L, S, M> implements OnInit {
   /** Destroy subscriptions */
   private readonly _onDestroy$ = new Subject<boolean>();
 
-  /** show cog visualisation **/
+  /** Cog visualisation **/
   protected cogVisualisation = signal<VisualisationPreview | null>(null);
 
   @ViewChild('map', { static: false }) public mapglComponent: ArlasMapComponent<L, S, M>;
@@ -370,6 +372,8 @@ export class ArlasWuiMapComponent<L, S, M> implements OnInit {
           .filter(l => this.mapFrameworkService.getLayer(this.mapglComponent.map, l)).length > 0) {
         this.resultlistService.updateVisibleItems();
       }
+
+      this.notifyHoveredCogs();
     }
   }
 
@@ -597,4 +601,38 @@ export class ArlasWuiMapComponent<L, S, M> implements OnInit {
       .pipe(takeUntil(this._onDestroy$))
       .subscribe(v => this.cogVisualisation.set(v));
   }
+
+  /**
+   * If there is a COG visualisation, notify the CogService of all features of the visualised collection that are hovered
+   */
+  private notifyHoveredCogs() {
+    this.mapComponentConfig.mapLayers.layers
+      .filter((l: ArlasDataLayer) => l.source.startsWith('feature'))
+      .forEach((l: ArlasDataLayer) => {
+        // Multiple layers will send their values that are stored by the CogService and consumed by the VisualisationLegendComponent
+        this.mapFrameworkService.onLayerEvent('mousemove', this.mapglComponent.map, l.id, (e) => {
+          const collection = this.collaborativeService.registry.get(this.cogService.contributorId).collection;
+          // If the collection does not match the one of the vurrent viusalisation, skip the layer
+          // Also skip if there is no current COG visualisation
+          if (l.metadata?.collection !== collection || !this.cogService.getCurrentVisualisation()) {
+            return;
+          }
+
+          const hoveredIds = e.features.map(f => f.properties.id).filter(id => this.cogService.visualisedCogs.has(id));
+          // Notify the CogService of the visualized rasters that are hovered
+          this.cogService.hoverCogs(l.id, hoveredIds);
+        });
+        this.mapFrameworkService.onLayerEvent('mouseleave', this.mapglComponent.map, l.id, (e) => {
+          // If the collection does not match the one of the vurrent viusalisation, skip the layer
+          // Also skip if there is no current COG visualisation
+          if (l.metadata?.collection !== this.collaborativeService.registry.get(this.cogService.contributorId).collection
+              || !this.cogService.getCurrentVisualisation()) {
+            return;
+          }
+
+          // Notify the CogService that no visualized rasters are hovered
+          this.cogService.hoverCogs(l.id, []);
+        });
+      });
+  }
 }
diff --git a/src/app/components/export-data-dialog/export-data-dialog.component.ts b/src/app/components/export-data-dialog/export-data-dialog.component.ts
index b0575680..d47b015b 100644
--- a/src/app/components/export-data-dialog/export-data-dialog.component.ts
+++ b/src/app/components/export-data-dialog/export-data-dialog.component.ts
@@ -52,8 +52,7 @@ export interface ExportDataDialogConfiguration {
 export class ExportDataDialogComponent implements  OnInit {
 
   protected dialogData = inject<Array<ExportDataDialogConfiguration>>(MAT_DIALOG_DATA);
-  protected componentsConf = signal<{key: string;enabled: boolean;
-    component: any;title: string;injector: any;}[]>([]);
+  protected componentsConf = signal<{key: string; enabled: boolean; component: any; title: string; injector: any;}[]>([]);
   protected selectedIndex = computed( () => this.componentsConf().findIndex(e => e.enabled));
   public ngOnInit() {
     const tabs = Object.keys(this.dialogData)
diff --git a/src/app/components/geocoding/geocoding.component.spec.ts b/src/app/components/geocoding/geocoding.component.spec.ts
index 6232d238..e363acb6 100644
--- a/src/app/components/geocoding/geocoding.component.spec.ts
+++ b/src/app/components/geocoding/geocoding.component.spec.ts
@@ -1,14 +1,13 @@
-import {ComponentFixture, TestBed} from '@angular/core/testing';
-
-import {GeocodingComponent} from './geocoding.component';
-import {HttpClientModule} from '@angular/common/http';
-import {MatInputModule} from '@angular/material/input';
-import {ReactiveFormsModule} from '@angular/forms';
-import {MatTableModule} from '@angular/material/table';
-import {MatFormFieldModule} from '@angular/material/form-field';
-import {TranslateFakeLoader, TranslateLoader, TranslateModule} from '@ngx-translate/core';
-import {MatIconModule} from '@angular/material/icon';
-import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
+import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http';
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { ReactiveFormsModule } from '@angular/forms';
+import { MatFormFieldModule } from '@angular/material/form-field';
+import { MatIconModule } from '@angular/material/icon';
+import { MatInputModule } from '@angular/material/input';
+import { MatTableModule } from '@angular/material/table';
+import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
+import { TranslateFakeLoader, TranslateLoader, TranslateModule } from '@ngx-translate/core';
+import { GeocodingComponent } from './geocoding.component';
 
 describe('GeocodingComponent', () => {
   let component: GeocodingComponent;
@@ -16,10 +15,19 @@ describe('GeocodingComponent', () => {
 
   beforeEach(async () => {
     await TestBed.configureTestingModule({
-      imports: [HttpClientModule,  MatInputModule, MatIconModule, BrowserAnimationsModule,
+      imports: [
+        MatInputModule,
+        MatIconModule,
+        BrowserAnimationsModule,
         TranslateModule.forRoot({ loader: { provide: TranslateLoader, useClass: TranslateFakeLoader } }),
-        MatFormFieldModule, ReactiveFormsModule, MatTableModule],
-      declarations: [ GeocodingComponent ]
+        MatFormFieldModule,
+        ReactiveFormsModule,
+        MatTableModule
+      ],
+      declarations: [ GeocodingComponent ],
+      providers: [
+        provideHttpClient(withInterceptorsFromDi())
+      ]
     })
       .compileComponents();
 
diff --git a/src/app/components/geocoding/geocoding.component.ts b/src/app/components/geocoding/geocoding.component.ts
index 1ab42b1e..86e975cc 100644
--- a/src/app/components/geocoding/geocoding.component.ts
+++ b/src/app/components/geocoding/geocoding.component.ts
@@ -49,7 +49,7 @@ export class GeocodingComponent implements AfterViewInit {
   protected hasSearched = false;
   protected hasError = false;
   protected loading = false;
-  protected geocodingResult: MatTableDataSource<any>;
+  protected geocodingResult: MatTableDataSource<GeocodingResult>;
   protected searchFormControl = new FormControl('');
 
   private previousSearch: string;
diff --git a/src/app/components/map/cog-visualisation-manager/cog-visualisation-manager.component.spec.ts b/src/app/components/map/cog-visualisation-manager/cog-visualisation-manager.component.spec.ts
index 1353acb5..4d3788ba 100644
--- a/src/app/components/map/cog-visualisation-manager/cog-visualisation-manager.component.spec.ts
+++ b/src/app/components/map/cog-visualisation-manager/cog-visualisation-manager.component.spec.ts
@@ -1,4 +1,4 @@
-import { HttpClientModule } from '@angular/common/http';
+import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http';
 import { ComponentFixture, TestBed } from '@angular/core/testing';
 import { RouterTestingModule } from '@angular/router/testing';
 import { TranslateFakeLoader, TranslateLoader, TranslateModule } from '@ngx-translate/core';
@@ -17,7 +17,6 @@ describe('CogVisualisationManagerComponent', () => {
       imports: [
         CogVisualisationManagerComponent,
         RouterTestingModule,
-        HttpClientModule,
         TranslateModule.forRoot({
           loader: { provide: TranslateLoader, useClass: TranslateFakeLoader }
         })
@@ -29,7 +28,8 @@ describe('CogVisualisationManagerComponent', () => {
           useClass: MockArlasStartupService
         },
         VisualizeService,
-        ContributorService
+        ContributorService,
+        provideHttpClient(withInterceptorsFromDi())
       ]
     })
       .compileComponents();
diff --git a/src/app/components/map/visualisation-legend/visualisation-legend.component.html b/src/app/components/map/visualisation-legend/visualisation-legend.component.html
new file mode 100644
index 00000000..67dff89a
--- /dev/null
+++ b/src/app/components/map/visualisation-legend/visualisation-legend.component.html
@@ -0,0 +1,6 @@
+<div class="legend">
+    @for (raster of rasterHovered | keyvalue; track raster.key) {
+        <arlas-cog-legend class="legend__item" [colormapImage]="raster.value.url" [name]="raster.value.name"
+            [minimum]="raster.value.minimum" [maximum]="raster.value.maximum" [colormapWidth]="100"></arlas-cog-legend>
+    }
+</div>
diff --git a/src/app/components/map/visualisation-legend/visualisation-legend.component.scss b/src/app/components/map/visualisation-legend/visualisation-legend.component.scss
new file mode 100644
index 00000000..77bf88a5
--- /dev/null
+++ b/src/app/components/map/visualisation-legend/visualisation-legend.component.scss
@@ -0,0 +1,40 @@
+/*
+ * Licensed to Gisaïa under one or more contributor
+ * license agreements. See the NOTICE.txt file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Gisaïa licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+@import "../../../../styles/variables.scss";
+@import "../../../../styles/app/sizes.scss";
+@import "../../../../styles/app/mixin.scss";
+
+.legend {
+    display: flex;
+    flex-direction: column;
+    gap: 5px;
+
+    &__item {
+        background-color: white;
+        border-radius: 4px;
+
+        display: flex;
+        align-items: center;
+
+        ::ng-deep.legend {
+            height: $map-actions-width;
+        }
+    }
+}
diff --git a/src/app/components/map/visualisation-legend/visualisation-legend.component.spec.ts b/src/app/components/map/visualisation-legend/visualisation-legend.component.spec.ts
new file mode 100644
index 00000000..d887ff66
--- /dev/null
+++ b/src/app/components/map/visualisation-legend/visualisation-legend.component.spec.ts
@@ -0,0 +1,42 @@
+import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http';
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { TranslateFakeLoader, TranslateLoader, TranslateModule } from '@ngx-translate/core';
+import { ArlasStartupService } from 'arlas-wui-toolkit';
+import { ContributorService } from '../../../services/contributors.service';
+import { VisualizeService } from '../../../services/visualize.service';
+import { MockArlasStartupService } from '../../../tools/test';
+import { VisualisationLegendComponent } from './visualisation-legend.component';
+
+describe('VisualisationLegendComponent', () => {
+  let component: VisualisationLegendComponent;
+  let fixture: ComponentFixture<VisualisationLegendComponent>;
+
+  beforeEach(async () => {
+    await TestBed.configureTestingModule({
+      imports: [
+        VisualisationLegendComponent,
+        TranslateModule.forRoot({
+          loader: { provide: TranslateLoader, useClass: TranslateFakeLoader }
+        })
+      ],
+      providers: [
+        VisualizeService,
+        ContributorService,
+        {
+          provide: ArlasStartupService,
+          useClass: MockArlasStartupService
+        },
+        provideHttpClient(withInterceptorsFromDi())
+      ]
+    })
+      .compileComponents();
+
+    fixture = TestBed.createComponent(VisualisationLegendComponent);
+    component = fixture.componentInstance;
+    fixture.detectChanges();
+  });
+
+  it('should create', () => {
+    expect(component).toBeTruthy();
+  });
+});
diff --git a/src/app/components/map/visualisation-legend/visualisation-legend.component.ts b/src/app/components/map/visualisation-legend/visualisation-legend.component.ts
new file mode 100644
index 00000000..06257591
--- /dev/null
+++ b/src/app/components/map/visualisation-legend/visualisation-legend.component.ts
@@ -0,0 +1,111 @@
+/*
+ * Licensed to Gisaïa under one or more contributor
+ * license agreements. See the NOTICE.txt file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Gisaïa licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { KeyValuePipe } from '@angular/common';
+import { HttpClient } from '@angular/common/http';
+import { Component, inject, input } from '@angular/core';
+import { CogLegendComponent } from 'arlas-web-components';
+import { debounceTime } from 'rxjs';
+import { CogService } from '../../../services/cog.service';
+
+export interface CogLegendData {
+  url: string;
+  name: string;
+  minimum?: number;
+  maximum?: string;
+}
+
+
+@Component({
+  selector: 'arlas-visualisation-legend',
+  standalone: true,
+  imports: [
+    KeyValuePipe,
+    CogLegendComponent
+  ],
+  templateUrl: './visualisation-legend.component.html',
+  styleUrl: './visualisation-legend.component.scss'
+})
+export class VisualisationLegendComponent {
+  public rasterHovered = new Map<string, CogLegendData>();
+
+  /** Width of the colormap for the legend component */
+  public colormapWidth = input<number>(100);
+
+  private readonly cogService = inject(CogService);
+  private readonly http = inject(HttpClient);
+
+  public constructor() {
+    this.cogService.hoverCogChange.pipe(debounceTime(100)).subscribe(() => {
+      const uniqueIds = this.cogService.getHoveredCogs();
+
+      // Reset raster hovered if none were hovered
+      if (uniqueIds.size === 0) {
+        this.rasterHovered.clear();
+      }
+
+      // Remove excess keys
+      Array.from(this.rasterHovered.keys()).forEach(k => {
+        if (!uniqueIds.has(k)) {
+          this.rasterHovered.delete(k);
+        }
+      });
+
+      // For new keys, process them to get legend url
+      Array.from(uniqueIds.values()).filter(id => !this.rasterHovered.has(id))
+        .forEach(id => {
+          // Currently, we only generate a legend for the titler protocol
+          const dataGroup = this.cogService.visualisedCogs.get(id);
+          if (dataGroup.protocol === 'titiler') {
+            // Transform visualisation url into a legend url
+            const queryParams = dataGroup.visualisationUrl.split('?', 2)[1];
+            if (queryParams) {
+              const colorMap = queryParams.split('&').find(p => p.startsWith('colormap_name'))?.split('=')[1];
+
+              // If there is no colorMap, then we can't plot the legend
+              if (colorMap) {
+                const legendUrl = dataGroup.visualisationUrl.split('/cog/tiles/')[0] + '/colorMaps/'
+                  + colorMap + '?format=png&width=' + this.colormapWidth();
+                this.rasterHovered.set(id, {
+                  url: legendUrl,
+                  name: dataGroup.name
+                });
+
+                const statUrl = dataGroup.visualisationUrl.split('/tiles/')[0] + '/statistics?' + queryParams;
+                this.http.get(statUrl).subscribe((r: any) => {
+                  const bands: Array<[string, any]> = Object.entries(r);
+                  // If there is one band => either an expression OR just one band in the TIF
+                  if (bands.length === 1) {
+                    this.rasterHovered.set(id, {
+                      url: legendUrl,
+                      name: dataGroup.name,
+                      minimum: bands[0][1].min,
+                      maximum: bands[0][1].max
+                    });
+                  }
+                  // If there are multiple bands (for example TCI), titiler throws an internal error with a colormap
+                  // Because it needs to map numeric values and not an array to a color
+                });
+              }
+            }
+          }
+        });
+    });
+  }
+}
diff --git a/src/app/pipes/get-resultlist-config.pipe.ts b/src/app/pipes/get-resultlist-config.pipe.ts
index f313e098..779e5c3e 100644
--- a/src/app/pipes/get-resultlist-config.pipe.ts
+++ b/src/app/pipes/get-resultlist-config.pipe.ts
@@ -33,7 +33,7 @@ export class GetResultlistConfigPipe<L, S, M> implements PipeTransform {
 
   public constructor(private readonly resultlistService: ResultlistService<L, S, M>) { }
 
-  public transform(resultlistContributor: ResultListContributor): any {
+  public transform(resultlistContributor: ResultListContributor) {
     return this.resultlistService.resultlistConfigPerContId.get(resultlistContributor?.identifier);
   }
 
diff --git a/src/app/services/cog.service.ts b/src/app/services/cog.service.ts
index 915bb628..163f585f 100644
--- a/src/app/services/cog.service.ts
+++ b/src/app/services/cog.service.ts
@@ -17,7 +17,7 @@
  * under the License.
  */
 
-import { Injectable } from '@angular/core';
+import { EventEmitter, Injectable } from '@angular/core';
 import { MatDialog } from '@angular/material/dialog';
 import { getTitilerPreviewUrl, VisualisationPreview } from 'app/tools/cog';
 import { getItem } from 'app/tools/utils';
@@ -58,6 +58,14 @@ export class CogService<L, S, M> {
   protected firstCogSelection  = true;
   /** Previews for each of the visualisations defined. Allows to query only once for default preview */
   private defaultPreviews = new Array<string>();
+  /** Map containing for each visualised COG its DataGroup */
+  public visualisedCogs = new Map<string, DataGroup>();
+
+  /** --- Hovered state for legend */
+  /** Map containing per layer id the list of item's id that are visualised and hovered */
+  private readonly hoveredCogs = new Map<string, Array<string>>();
+  /** Emits whenever hoveredCogs has a value change */
+  public hoverCogChange = new EventEmitter<void>();
 
   public constructor(
     private readonly collaborativeService: ArlasCollaborativesearchService,
@@ -261,6 +269,7 @@ export class CogService<L, S, M> {
     if (!visualisation) {
       // If no visualisation, clean up the rasters
       this.visualizeService.removeRasters();
+      this.visualisedCogs.clear();
       this.actionManager.removeContributorAction(contributorId, 'visualize');
     } else if (previousVisualisation) {
       // If there is a visualisation and there are already visualisations, update them if they can be
@@ -273,6 +282,7 @@ export class CogService<L, S, M> {
             next: values => {
               action.matched = values.matched;
               this.visualizeService.removeRasters(item);
+              this.visualisedCogs.delete(item);
 
               // If we find a visualisationUrl, it means that we can continue viewing it, and we should switch to it
               if (this.getVisualisationUrl(action)) {
@@ -359,10 +369,16 @@ export class CogService<L, S, M> {
 
       if (!data.action.activated) {
         const collectionDescription = this.contributorsService.collectionToDescription.get(listContributor.collection);
-        this.visualizeService.getVisuInfo(data.elementidentifier, listContributor.collection, urlVisualisationTemplate).subscribe(url => {
-          this.visualizeService.displayDataOnMap(url, data.elementidentifier, collectionDescription.geometry_path,
-            collectionDescription.centroid_path, listContributor.collection, fitBounds);
-        });
+        this.visualizeService.getVisuInfo(data.elementidentifier, listContributor.collection, urlVisualisationTemplate)
+          .subscribe(url => {
+            this.visualizeService.displayDataOnMap(url, data.elementidentifier, collectionDescription.geometry_path,
+              collectionDescription.centroid_path, listContributor.collection, fitBounds);
+
+            // Edit visualisationUrl to be the one used by the product
+            const dataGroup = {...this.getDataGroup(data.action)};
+            dataGroup.visualisationUrl = url;
+            this.visualisedCogs.set(data.elementidentifier.idValue, dataGroup);
+          });
         this.actionManager.addAction(listContributor.identifier, data.elementidentifier.idValue, data.action);
       } else {
         this.visualizeService.removeRasters(data.elementidentifier.idValue);
@@ -371,23 +387,45 @@ export class CogService<L, S, M> {
     }
   }
 
+  public hoverCogs(layerId: string, ids: Array<string>) {
+    this.hoveredCogs.set(layerId, ids);
+    this.hoverCogChange.next();
+  }
+
+  public getHoveredCogs() {
+    const uniqueIds = new Set<string>();
+    Array.from(this.hoveredCogs.values()).forEach(ids => {
+      ids.forEach(id => uniqueIds.add(id));
+    });
+    return uniqueIds;
+  }
+
   private getVisualisationUrl(action: Action) {
     if (action.matched) {
-      const v = this.getCurrentVisualisation();
-
-      // Find the start of the selected visualisation in the array of matches of the action
-      // Only needed if there are more matches in the action than there are dataGroups.
-      // It happens when this item is not the first one visualized
-      let firstVisuElement = 0;
-      if (action.matched.length > v.visualisation.dataGroups.length) {
-        for (let i= 0; i < v.idx; i++) {
-          firstVisuElement += this.currentCogVisualisationConfig[i].dataGroups.length;
-        }
-      }
       // The url is the one of the first dataGroup for which the item matched the condition
-      return v.visualisation.dataGroups.find((_, i) => action.matched[firstVisuElement + i])?.visualisationUrl;
+      return this.getDataGroup(action)?.visualisationUrl;
     } else {
       return this.contributorConfig.visualisationLink;
     }
   }
+
+  /**
+   * @param action Action for the item containing the matches for the visualisations
+   * @returns The first matching data group in the current visualisation
+   */
+  private getDataGroup(action: Action) {
+    const v = this.getCurrentVisualisation();
+
+    // Find the start of the selected visualisation in the array of matches of the action
+    // Only needed if there are more matches in the action than there are dataGroups.
+    // It happens when this item is not the first one visualized
+    let firstVisuElement = 0;
+    if (action.matched.length > v.visualisation.dataGroups.length) {
+      for (let i= 0; i < v.idx; i++) {
+        firstVisuElement += this.currentCogVisualisationConfig[i].dataGroups.length;
+      }
+    }
+    // The url is the one of the first dataGroup for which the item matched the condition
+    return v.visualisation.dataGroups.find((_, i) => action.matched[firstVisuElement + i]);
+  }
 }
diff --git a/src/app/services/map.service.ts b/src/app/services/map.service.ts
index 02db0ca6..82299e4e 100644
--- a/src/app/services/map.service.ts
+++ b/src/app/services/map.service.ts
@@ -45,8 +45,11 @@ export class ArlasWuiMapService<L, S, M> {
   public coordinatesHaveSpace: boolean;
   public timeLineIsOpen: boolean;
 
-  public constructor(public mapService: ArlasMapFrameworkService<L, S, M>,
-    public mapLogicService: AbstractArlasMapService<L, S, M>) { }
+  public constructor(
+    private readonly mapService: ArlasMapFrameworkService<L, S, M>,
+    private readonly mapLogicService: AbstractArlasMapService<L, S, M>
+  ) {
+  }
 
   public setContributors(mapContributors: Array<MapContributor>) {
     this.mapContributors = mapContributors;
@@ -74,9 +77,9 @@ export class ArlasWuiMapService<L, S, M> {
 
   public getFeatureToHover(id: ElementIdentifier, mapContributor: MapContributor): FeatureHover {
     const f = mapContributor.getFeatureToHightLight(id);
-    if (mapContributor) {
-      f.elementidentifier.idFieldName = f.elementidentifier.idFieldName.replace(/\./g, '_');
-    }
+    f.elementidentifier.idFieldName = f.elementidentifier.idFieldName.replace(/\./g, '_');
+
+    this.featureToHightLight = f;
     return f;
   }
 
diff --git a/src/app/services/resultlist.service.spec.ts b/src/app/services/resultlist.service.spec.ts
index 4e807fdb..cdcb3642 100644
--- a/src/app/services/resultlist.service.spec.ts
+++ b/src/app/services/resultlist.service.spec.ts
@@ -1,5 +1,5 @@
 import { Overlay, OverlayModule } from '@angular/cdk/overlay';
-import { HttpClientModule } from '@angular/common/http';
+import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http';
 import { TestBed } from '@angular/core/testing';
 import { MatDialog, MatDialogModule } from '@angular/material/dialog';
 import { MatSnackBar } from '@angular/material/snack-bar';
@@ -23,7 +23,6 @@ describe('ResultlistService', () => {
     TestBed.configureTestingModule({
       imports: [
         RouterTestingModule,
-        HttpClientModule,
         TranslateModule.forRoot({ loader: { provide: TranslateLoader, useClass: TranslateFakeLoader } }),
         OverlayModule,
         MatDialogModule
@@ -42,7 +41,8 @@ describe('ResultlistService', () => {
         {
           provide: ArlasStartupService,
           useClass: MockArlasStartupService
-        }
+        },
+        provideHttpClient(withInterceptorsFromDi())
       ],
       teardown: { destroyAfterEach: false }
     });
diff --git a/src/app/services/resultlist.service.ts b/src/app/services/resultlist.service.ts
index ff3d0f76..2b76923f 100644
--- a/src/app/services/resultlist.service.ts
+++ b/src/app/services/resultlist.service.ts
@@ -340,11 +340,7 @@ export class ResultlistService<L, S, M> {
         break;
       case 'consultedItemEvent':
         if (mapContributor) {
-          const f = mapContributor.getFeatureToHightLight(event.data);
-          if (f) {
-            f.elementidentifier.idFieldName = f.elementidentifier.idFieldName.replace(/\./g, '_');
-          }
-          this.mapService.featureToHightLight = f;
+          mapContributor.getFeatureToHightLight(event.data);
         }
         break;
       case 'selectedItemsEvent': {
@@ -373,7 +369,6 @@ export class ResultlistService<L, S, M> {
               error: (e) => this.snackbar.open(marker('An error occured exporting the list'))
             });
         } else if (event.data.id === 'visualize') {
-          console.log('in');
           this.selectedItems.forEach(e => {
             // For each element, check if the necessary fields for the visualisation are present
             this.listComponent.detailedDataRetriever.getValues(e.idValue, event.data.fields).pipe(take(1)).subscribe({
@@ -472,7 +467,7 @@ export class ResultlistService<L, S, M> {
             ids,
             collection
           ).subscribe({
-            next: (item: any) => {
+            next: item => {
               const data = { ids, collection, nbProducts: ids.length, itemDetail: item, ...additionalData };
 
               this.dialog
@@ -544,7 +539,6 @@ export class ResultlistService<L, S, M> {
               id: 'remove', label: marker('Remove from map'), cssClass: '', tooltip: marker('Remove from map'), icon: 'visibility_off'
             }
           };
-          (action as any).hide = true;
           if (config.visualisationLink) {
             action.fields = this.visualizeService.getVisuFields(config.visualisationLink);
             c.addAction(action);
diff --git a/src/app/services/visualize.service.ts b/src/app/services/visualize.service.ts
index 89fea8a7..f62ee13e 100644
--- a/src/app/services/visualize.service.ts
+++ b/src/app/services/visualize.service.ts
@@ -41,7 +41,7 @@ const GEOCODING_PREVIEW_ID = 'geojson-geocoding-preview';
  */
 @Injectable()
 export class VisualizeService<L, S, M> {
-  public mapInstance: AbstractArlasMapGL;
+  private mapInstance: AbstractArlasMapGL;
   public fitbounds: Array<Array<number>> = [];
   /**  @deprecated. Use isRasterOnMap instead. */
   public isWMTSOnMap = false;
@@ -51,6 +51,9 @@ export class VisualizeService<L, S, M> {
   private readonly rasterRemovedSource = new Subject<string>();
   public rasterRemoved$ = this.rasterRemovedSource.asObservable();
 
+  /** Map containing the (item id, url) of visualised products to be able to remove the listening events */
+  private readonly visualizedRasters = new Map<string, string>();
+
   public constructor(
     private readonly collaborativeService: ArlasCollaborativesearchService,
     private readonly translateService: TranslateService,
@@ -101,9 +104,13 @@ export class VisualizeService<L, S, M> {
 
   public removeRasters(id?: string) {
     if (id) {
+      this.visualizedRasters.delete(id);
+
       this.mapFrameworkService.removeLayer(this.mapInstance, 'raster-source-' + id);
       this.mapFrameworkService.removeLayer(this.mapInstance, CROSS_LAYER_PREFIX + id);
     } else {
+      this.visualizedRasters.clear();
+
       this.mapFrameworkService.removeLayersFromPattern(this.mapInstance, 'raster-source-');
       this.mapFrameworkService.removeLayersFromPattern(this.mapInstance, CROSS_LAYER_PREFIX);
     }
@@ -112,7 +119,7 @@ export class VisualizeService<L, S, M> {
   }
 
   /** @deprecated Use add raster instead. */
-  public addWMTS(urlWmts, maxZoom, bounds: Array<number>, id: string, beforeId?: string) {
+  public addWMTS(urlWmts: string, maxZoom: number, bounds: Array<number>, id: string, beforeId?: string) {
     this.addRaster(urlWmts, maxZoom, bounds, id, beforeId);
   }
 
@@ -124,16 +131,17 @@ export class VisualizeService<L, S, M> {
    * @param id Identifier of the raster.
    * @param beforeId Insert before a given raster id.
    */
-  public addRaster(url, maxZoom, bounds: Array<number>, id: string, beforeId?: string) {
-    this.mapFrameworkService.removeLayer(this.mapInstance, 'raster-source-' + id);
+  public addRaster(url: string, maxZoom: number, bounds: Array<number>, id: string, beforeId?: string) {
+    const layerId = 'raster-source-' + id;
+    this.mapFrameworkService.removeLayer(this.mapInstance, layerId);
     this.mapFrameworkService.removeLayer(this.mapInstance, CROSS_LAYER_PREFIX + id);
-    this.mapFrameworkService.addRasterLayer(this.mapInstance, 'raster-source-' + id, url, bounds, maxZoom,
+    this.mapFrameworkService.addRasterLayer(this.mapInstance, layerId, url, bounds, maxZoom,
       /** tilesize */ 256, beforeId);
     if (id !== 'external') {
       this.isWMTSOnMap = true;
       this.isRasterOnMap = true;
     }
-    this.addcrossToRemove(bounds[2], bounds[3], id);
+    this.addCrossToRemove(bounds[2], bounds[3], id);
   }
 
   public displayDataOnMap(url: string, elementidentifier: ElementIdentifier,
@@ -159,7 +167,7 @@ export class VisualizeService<L, S, M> {
     }
   }
 
-  public addcrossToRemove(lat, lng, id) {
+  public addCrossToRemove(lat: number, lng: number, id: string) {
     const crossPosition = {
       type: 'FeatureCollection',
       'features': [
@@ -190,7 +198,7 @@ export class VisualizeService<L, S, M> {
     this.rasterRemovedSource.next(id);
   }
 
-  public handlePopup(lat, lng, id) {
+  public handlePopup(lat: number, lng: number, id: string) {
     const tooltipMsg = this.translateService.instant('Remove visualisation');
     const popup = this.mapFrameworkService.createPopup(lng, lat, tooltipMsg);
     this.mapFrameworkService.onLayerEvent('mouseenter', this.mapInstance, CROSS_LAYER_PREFIX + id, () => {


Reviews:
user: login: MohamedHamouGisaia
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-16T15:41:18Z
user: login: QuCMGisaia
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-19T08:01:06Z
user: login: MAudelGisaia
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-19T13:16:02Z
user: login: MohamedHamouGisaia
state: APPROVED
author_association: MEMBER
submitted_at: 2025-05-30T08:14:34Z

Review comments:
diff_hunk: @@ -26,11 +26,10 @@ import { Pipe, PipeTransform } from '@angular/core';
 })
 export class SquareKilometer implements PipeTransform {
 
-  public transform(a: number): any {
+  public transform(a: number) {
path: src/app/components/arlas-map/aoi-dimensions/aoi-dimensions.pipes.ts
user: login: MohamedHamouGisaia
body: returns a number ?
created_at: 2025-05-16T15:36:37Z
author_association: MEMBER
diff_hunk: @@ -39,8 +38,7 @@ export class SquareKilometer implements PipeTransform {
 })
 export class RoundKilometer implements PipeTransform {
 
-  public transform(a: number): any {
+  public transform(a: number) {
path: src/app/components/arlas-map/aoi-dimensions/aoi-dimensions.pipes.ts
user: login: MohamedHamouGisaia
body: returns a number ?
created_at: 2025-05-16T15:36:47Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,6 @@
+<div class="legend">
+    @for (raster of rasterHovered | keyvalue; track raster.key) {
+        <arlas-cog-legend class="legend__item" [colormapImage]="raster.value.url" [name]="raster.value.name"
+            [minimum]="raster.value.minimum" [maximum]="raster.value.maximum" [colormapWidth]="100"></arlas-cog-legend>
+    }
+</div>
path: src/app/components/map/visualisation-legend/visualisation-legend.component.html
user: login: MohamedHamouGisaia
body: can you add a retour à la ligne for end of file 
created_at: 2025-05-16T15:38:20Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,110 @@
+/*
+ * Licensed to Gisaïa under one or more contributor
+ * license agreements. See the NOTICE.txt file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Gisaïa licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { KeyValuePipe } from '@angular/common';
+import { HttpClient } from '@angular/common/http';
+import { Component, inject, input } from '@angular/core';
+import { CogLegendComponent } from 'arlas-web-components';
+import { debounceTime } from 'rxjs';
+import { CogService } from '../../../services/cog.service';
+
+export interface CogLegendData {
+  url: string;
+  name: string;
+  minimum?: number;
+  maximum?: string;
+}
+
+
+@Component({
+  selector: 'arlas-visualisation-legend',
+  standalone: true,
+  imports: [
+    KeyValuePipe,
+    CogLegendComponent
+  ],
+  templateUrl: './visualisation-legend.component.html',
+  styleUrl: './visualisation-legend.component.scss'
+})
+export class VisualisationLegendComponent {
+  public rasterHovered = new Map<string, CogLegendData>();
+
+  /** Width of the colormap for the legend component */
+  public colormapWidth = input<number>(100);
+
+  private readonly cogService = inject(CogService);
+  private readonly http = inject(HttpClient);
+
+  public constructor() {
+    this.cogService.hoverCogChange.pipe(debounceTime(100)).subscribe(() => {
+      const uniqueIds = this.cogService.getHoveredCogs();
+
+      // Reset raster hovered if none were hovered
+      if (uniqueIds.size === 0) {
+        this.rasterHovered.clear();
+      }
+
+      // Remove excess keys
+      Array.from(this.rasterHovered.keys()).forEach(k => {
+        if (!uniqueIds.has(k)) {
+          this.rasterHovered.delete(k);
+        }
+      });
+
+      // For new keys, process them to get legend url
+      Array.from(uniqueIds.values()).filter(id => !this.rasterHovered.has(id))
+        .forEach(id => {
+          // Currently, we only generate a legend for the titler protocol
+          const dataGroup = this.cogService.visualisedCogs.get(id);
+          if (dataGroup.protocol === 'titiler') {
+            // Transform visualisation url into a legend url
+            const queryParams = dataGroup.visualisationUrl.split('?', 2)[1];
+            if (queryParams) {
+              const colorMap = queryParams.split('&').find(p => p.startsWith('colormap_name'))?.split('=')[1];
+
+              // If there is no colorMap, then we can't plot the legend
+              if (colorMap) {
+                const legendUrl = dataGroup.visualisationUrl.split('/cog/tiles/')[0] + '/colorMaps/'
+                  + colorMap + '?format=png&width=' + this.colormapWidth();
+                this.rasterHovered.set(id, {
+                  url: legendUrl,
+                  name: dataGroup.name
+                });
+
+                const statUrl = dataGroup.visualisationUrl.split('/tiles/')[0] + '/statistics?' + queryParams;
+                this.http.get(statUrl).subscribe((r: any) => {
+                  const bands: Array<[string, any]> = Object.entries(r);
+                  // If there is one band => either an expression OR just one band in the TIF
+                  if (bands.length === 1) {
+                    this.rasterHovered.set(id, {
+                      url: legendUrl,
+                      name: dataGroup.name,
+                      minimum: bands[0][1].min,
+                      maximum: bands[0][1].max
+                    });
+                  }
+                  // If there are multiple bands, can't have a colormap => internal error for titiler
path: src/app/components/map/visualisation-legend/visualisation-legend.component.ts
user: login: MohamedHamouGisaia
body: whaaaat ?
created_at: 2025-05-16T15:39:32Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,110 @@
+/*
+ * Licensed to Gisaïa under one or more contributor
+ * license agreements. See the NOTICE.txt file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Gisaïa licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { KeyValuePipe } from '@angular/common';
+import { HttpClient } from '@angular/common/http';
+import { Component, inject, input } from '@angular/core';
+import { CogLegendComponent } from 'arlas-web-components';
+import { debounceTime } from 'rxjs';
+import { CogService } from '../../../services/cog.service';
+
+export interface CogLegendData {
+  url: string;
+  name: string;
+  minimum?: number;
+  maximum?: string;
+}
+
+
+@Component({
+  selector: 'arlas-visualisation-legend',
+  standalone: true,
+  imports: [
+    KeyValuePipe,
+    CogLegendComponent
+  ],
+  templateUrl: './visualisation-legend.component.html',
+  styleUrl: './visualisation-legend.component.scss'
+})
+export class VisualisationLegendComponent {
+  public rasterHovered = new Map<string, CogLegendData>();
+
+  /** Width of the colormap for the legend component */
+  public colormapWidth = input<number>(100);
+
+  private readonly cogService = inject(CogService);
+  private readonly http = inject(HttpClient);
+
+  public constructor() {
+    this.cogService.hoverCogChange.pipe(debounceTime(100)).subscribe(() => {
+      const uniqueIds = this.cogService.getHoveredCogs();
+
+      // Reset raster hovered if none were hovered
+      if (uniqueIds.size === 0) {
+        this.rasterHovered.clear();
+      }
+
+      // Remove excess keys
+      Array.from(this.rasterHovered.keys()).forEach(k => {
+        if (!uniqueIds.has(k)) {
+          this.rasterHovered.delete(k);
+        }
+      });
+
+      // For new keys, process them to get legend url
+      Array.from(uniqueIds.values()).filter(id => !this.rasterHovered.has(id))
+        .forEach(id => {
+          // Currently, we only generate a legend for the titler protocol
+          const dataGroup = this.cogService.visualisedCogs.get(id);
+          if (dataGroup.protocol === 'titiler') {
+            // Transform visualisation url into a legend url
+            const queryParams = dataGroup.visualisationUrl.split('?', 2)[1];
+            if (queryParams) {
+              const colorMap = queryParams.split('&').find(p => p.startsWith('colormap_name'))?.split('=')[1];
+
+              // If there is no colorMap, then we can't plot the legend
+              if (colorMap) {
+                const legendUrl = dataGroup.visualisationUrl.split('/cog/tiles/')[0] + '/colorMaps/'
+                  + colorMap + '?format=png&width=' + this.colormapWidth();
+                this.rasterHovered.set(id, {
+                  url: legendUrl,
+                  name: dataGroup.name
+                });
+
+                const statUrl = dataGroup.visualisationUrl.split('/tiles/')[0] + '/statistics?' + queryParams;
+                this.http.get(statUrl).subscribe((r: any) => {
+                  const bands: Array<[string, any]> = Object.entries(r);
+                  // If there is one band => either an expression OR just one band in the TIF
+                  if (bands.length === 1) {
+                    this.rasterHovered.set(id, {
+                      url: legendUrl,
+                      name: dataGroup.name,
+                      minimum: bands[0][1].min,
+                      maximum: bands[0][1].max
+                    });
+                  }
+                  // If there are multiple bands, can't have a colormap => internal error for titiler
path: src/app/components/map/visualisation-legend/visualisation-legend.component.ts
user: login: QuCMGisaia
body: With titiler, on a true colors tiff for example, it can't give a preview if you add a colormap to the query. Because it can't map the value [R, G, B] to a single color the same way as it can map a numerical value to the color
created_at: 2025-05-19T08:01:06Z
author_association: CONTRIBUTOR

PR comments:
user: login: sonarqubecloud[bot]
created_at: 2025-05-19T08:22:12Z
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=gisaia_ARLAS-wui&pullRequest=989) **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=gisaia_ARLAS-wui&pullRequest=989&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=gisaia_ARLAS-wui&pullRequest=989&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=gisaia_ARLAS-wui&pullRequest=989&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=gisaia_ARLAS-wui&pullRequest=989&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=gisaia_ARLAS-wui&pullRequest=989&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=gisaia_ARLAS-wui&pullRequest=989)


author_association: NONE