diff --git a/ui/angular.json b/ui/angular.json index d90c6cd6ff..70319fecac 100644 --- a/ui/angular.json +++ b/ui/angular.json @@ -167,6 +167,7 @@ "test": { "builder": "@angular/build:unit-test", "options": { + "runnerConfig": "vitest.config.ts", "coverage": true, "coverageReporters": ["html", "lcovonly"] } @@ -295,6 +296,7 @@ "test": { "builder": "@angular/build:unit-test", "options": { + "runnerConfig": "vitest.config.ts", "coverage": true, "coverageReporters": ["html", "lcovonly"] } @@ -422,6 +424,7 @@ "test": { "builder": "@angular/build:unit-test", "options": { + "runnerConfig": "vitest.config.ts", "coverage": true, "coverageReporters": ["html", "lcovonly"] } @@ -549,6 +552,7 @@ "test": { "builder": "@angular/build:unit-test", "options": { + "runnerConfig": "vitest.config.ts", "coverage": true, "coverageReporters": ["html", "lcovonly"] } @@ -676,6 +680,7 @@ "test": { "builder": "@angular/build:unit-test", "options": { + "runnerConfig": "vitest.config.ts", "coverage": true, "coverageReporters": ["html", "lcovonly"] } @@ -806,6 +811,7 @@ "test": { "builder": "@angular/build:unit-test", "options": { + "runnerConfig": "vitest.config.ts", "coverage": true, "coverageReporters": ["html", "lcovonly"] } @@ -919,6 +925,7 @@ "test": { "builder": "@angular/build:unit-test", "options": { + "runnerConfig": "vitest.config.ts", "coverage": true, "coverageReporters": ["html", "lcovonly"] } @@ -966,6 +973,7 @@ "test": { "builder": "@angular/build:unit-test", "options": { + "runnerConfig": "vitest.config.ts", "coverage": true, "coverageReporters": ["html", "lcovonly"] } @@ -1085,6 +1093,7 @@ "test": { "builder": "@angular/build:unit-test", "options": { + "runnerConfig": "vitest.config.ts", "coverage": true, "coverageReporters": ["html", "lcovonly"] } @@ -1204,6 +1213,7 @@ "test": { "builder": "@angular/build:unit-test", "options": { + "runnerConfig": "vitest.config.ts", "coverage": true, "coverageReporters": ["html", "lcovonly"] } diff --git a/ui/empty-module.js b/ui/empty-module.js deleted file mode 100644 index cb0ff5c3b5..0000000000 --- a/ui/empty-module.js +++ /dev/null @@ -1 +0,0 @@ -export {}; diff --git a/ui/justfile b/ui/justfile index 50aa9c7051..0b016f7941 100644 --- a/ui/justfile +++ b/ui/justfile @@ -34,11 +34,13 @@ build project="gis": common::_clear run project="gis" nightly="false": common::_clear npm run serve:{{ project }}:{{ if nightly == "true" { "nightly" } else { "local" } }} +[arg("app", long="app", help="Whether to only run tests for a specific app, e.g. `--app=gis`.")] [arg("include", long="include", help="Whether to include only specific test files, e.g. `--include=`.")] [arg("ci", long="ci", value="true", help="Whether to run tests in CI mode.")] -test ci="false" include="" filter="": common::_clear +test ci="false" app="" include="" filter="": common::_clear npm run test{{ if ci == "true" { ":browser:ci" } else { "" } }} \ -- \ + {{ app }} \ {{ if include == "" { "" } else { "--include=" + include } }} \ {{ if filter == "" { "" } else { '--filter="' + filter + '"' } }} diff --git a/ui/projects/common/src/index.ts b/ui/projects/common/src/index.ts index 171032c52b..1b8cc33766 100644 --- a/ui/projects/common/src/index.ts +++ b/ui/projects/common/src/index.ts @@ -89,13 +89,14 @@ export * from './lib/util/guards/can-register.guard'; export * from './lib/util/guards/log-in.guard'; // Misc -export * from './lib/colors/colormaps/mpl-colormaps'; export * from './lib/colors/colormaps/colormaps'; +export * from './lib/colors/colormaps/mpl-colormaps'; +export * from './lib/util/assertions'; export * from './lib/util/constants'; export * from './lib/util/conversions'; -export * from './lib/util/directives/flexbox-legacy.directive'; export * from './lib/util/directives/autocomplete-select.directive'; +export * from './lib/util/directives/flexbox-legacy.directive'; export * from './lib/util/errors'; export * from './lib/util/form.validators'; -export * from './lib/util/symbologies'; export * from './lib/util/icons'; +export * from './lib/util/symbologies'; diff --git a/ui/projects/common/src/lib/util/assertions.spec.ts b/ui/projects/common/src/lib/util/assertions.spec.ts new file mode 100644 index 0000000000..7783e0541b --- /dev/null +++ b/ui/projects/common/src/lib/util/assertions.spec.ts @@ -0,0 +1,15 @@ +import {assertNever, isNullOrUndefined} from './assertions'; + +describe('assertions', () => { + it('should handle never assertions', () => { + expect(() => assertNever(undefined as never)).toThrow(); + }); + + it('should handle null or undefined assertions', () => { + expect(isNullOrUndefined(null)).toBe(true); + expect(isNullOrUndefined(undefined)).toBe(true); + expect(isNullOrUndefined(0)).toBe(false); + expect(isNullOrUndefined('')).toBe(false); + expect(isNullOrUndefined(false)).toBe(false); + }); +}); diff --git a/ui/projects/common/src/lib/util/assertions.ts b/ui/projects/common/src/lib/util/assertions.ts new file mode 100644 index 0000000000..419b6a3de9 --- /dev/null +++ b/ui/projects/common/src/lib/util/assertions.ts @@ -0,0 +1,9 @@ +/** + * A helper function that provides both compile-time type checking and runtime error throwing if invalid data bypasses TypeScript (e.g., untyped API responses). + * @param x The value that should never occur + */ +export function assertNever(x: never): never { + throw new Error(`Unexpected value: ${JSON.stringify(x)}`); +} + +export const isNullOrUndefined = (value: T | null | undefined): boolean => value === null || value === undefined; diff --git a/ui/projects/core/src/lib/config.service.ts b/ui/projects/core/src/lib/config.service.ts index c084464bd9..c112d1194b 100644 --- a/ui/projects/core/src/lib/config.service.ts +++ b/ui/projects/core/src/lib/config.service.ts @@ -29,11 +29,26 @@ export interface ConfigDefaults { readonly FOCUS_EXTENT: [number, number, number, number]; } +export interface DrawSettings { + readonly DRAW_STYLE: DrawSettingsStyle; + readonly AFTER_DRAW_STYLE: DrawSettingsStyle; +} + +export interface DrawSettingsStyle { + readonly STROKE_COLOR: string; + readonly STROKE_CONTRAST_COLOR: string; + readonly FILL_COLOR: string; + readonly WIDTH: number; + readonly IMAGE_WIDTH: number; + readonly DASH_PATTERN: number[] | undefined; +} + export interface ConfigMap { readonly BASEMAPS: Basemaps; readonly DEFAULT_BASEMAP: keyof Basemaps; readonly REFRESH_LAYERS_ON_CHANGE: boolean; readonly VALID_CRS: Array; + readonly DRAWING: DrawSettings; } export type Basemaps = Record; @@ -149,6 +164,24 @@ export const DEFAULT_CORE_CONFIG: CoreConfigStructure = { }, REFRESH_LAYERS_ON_CHANGE: false, VALID_CRS: ['EPSG:4326', 'EPSG:3857'], + DRAWING: { + DRAW_STYLE: { + FILL_COLOR: 'rgba(25, 118, 210, 0.3)', + STROKE_COLOR: '#1976d2', + STROKE_CONTRAST_COLOR: '#FFFFFF', + WIDTH: 4, + IMAGE_WIDTH: 5, + DASH_PATTERN: [8, 8], + }, + AFTER_DRAW_STYLE: { + FILL_COLOR: 'rgba(25, 118, 210, 0.3)', + STROKE_COLOR: '#1976d2', + STROKE_CONTRAST_COLOR: '#FFFFFF', + WIDTH: 4, + IMAGE_WIDTH: 5, + DASH_PATTERN: [8, 8], + }, + }, }, API_URL: '/api', TIME: { diff --git a/ui/projects/core/src/lib/map/map-container/map-container.component.ts b/ui/projects/core/src/lib/map/map-container/map-container.component.ts index 4f246b40c7..ee82936914 100644 --- a/ui/projects/core/src/lib/map/map-container/map-container.component.ts +++ b/ui/projects/core/src/lib/map/map-container/map-container.component.ts @@ -17,6 +17,7 @@ import { input, viewChild, viewChildren, + signal, } from '@angular/core'; import OlMap from 'ol/Map'; @@ -42,6 +43,7 @@ import {ol as flatgeobuf} from 'flatgeobuf'; import OlStyleFill from 'ol/style/Fill'; import OlStyleStroke from 'ol/style/Stroke'; import OlStyleStyle, {StyleLike as OlStyleLike} from 'ol/style/Style'; +import OlCircleStyle from 'ol/style/Circle'; import OlInteractionDraw, {GeometryFunction} from 'ol/interaction/Draw'; import OlInteractionSelect from 'ol/interaction/Select'; @@ -53,7 +55,7 @@ import {MapLayerComponent} from '../map-layer.component'; import {FeatureSelection, ProjectService} from '../../project/project.service'; import {Extent, MapService} from '../map.service'; -import {Basemap, CoreConfig, VectorTiles, Wms} from '../../config.service'; +import {Basemap, CoreConfig, DrawSettingsStyle, VectorTiles, Wms} from '../../config.service'; import {MatGridList, MatGridListModule, MatGridTile} from '@angular/material/grid-list'; import {SpatialReferenceService, WGS_84} from '../../spatial-references/spatial-reference.service'; import {containsCoordinate, getCenter} from 'ol/extent'; @@ -115,6 +117,8 @@ export class MapContainerComponent implements AfterViewInit, OnChanges, OnDestro // eslint-disable-next-line @typescript-eslint/no-explicit-any private backgroundLayers: Array> = []; + readonly overlayLayer = signal> | undefined>(undefined); + private userSelect?: OlInteractionSelect; private selectedFeature?: OlFeature = undefined; @@ -157,6 +161,14 @@ export class MapContainerComponent implements AfterViewInit, OnChanges, OnDestro this.redrawLayers(projection); }); }); + + effect(() => { + this.overlayLayer(); + this.projection$.pipe(first()).subscribe((projection) => { + this.backgroundLayerSource = undefined; // reset source to force recreation + this.redrawLayers(projection); + }); + }); } ngOnDestroy(): void { @@ -293,10 +305,17 @@ export class MapContainerComponent implements AfterViewInit, OnChanges, OnDestro this.projection$.pipe(first()).subscribe((projection) => this.redrawLayers(projection)); } + public getMap(index = 0): OlMap { + return this.maps[index]; + } + private createDrawInteractionLayer(): OlLayerVector> { - return new OlLayerVector({ + const layer = new OlLayerVector({ source: this.drawInteractionSource, + style: [drawContrastStrokeStyle(this.config.MAP.DRAWING.AFTER_DRAW_STYLE), drawStyle(this.config.MAP.DRAWING.AFTER_DRAW_STYLE)], }); + + return layer; } private createDrawInteraction(): OlInteractionDraw { @@ -304,6 +323,8 @@ export class MapContainerComponent implements AfterViewInit, OnChanges, OnDestro source: this.drawInteractionSource, type: this.drawType, geometryFunction: this.drawGeometryFunction, + maxPoints: this.drawSingleFeature ? 2 : undefined, + style: [drawContrastStrokeStyle(this.config.MAP.DRAWING.DRAW_STYLE), drawStyle(this.config.MAP.DRAWING.DRAW_STYLE)], }); } @@ -527,6 +548,11 @@ export class MapContainerComponent implements AfterViewInit, OnChanges, OnDestro } else { this.mapLayers.forEach((layerComponent) => map.addLayer(layerComponent.mapLayer)); } + + const overlayLayer = this.overlayLayer(); + if (overlayLayer) { + map.getLayers().push(overlayLayer); + } }); this.reattachDrawInteractions(); @@ -808,3 +834,43 @@ export class MapContainerComponent implements AfterViewInit, OnChanges, OnDestro return mapCanvas.toDataURL('image/png'); } } + +/** + * Returns an OpenLayers style for drawing features based on the provided draw settings. + */ +export function drawStyle(style: DrawSettingsStyle): OlStyleStyle { + const {STROKE_COLOR, WIDTH, DASH_PATTERN, FILL_COLOR, IMAGE_WIDTH} = style; + return new OlStyleStyle({ + fill: new OlStyleFill({ + color: FILL_COLOR, + }), + stroke: new OlStyleStroke({ + color: STROKE_COLOR, + width: WIDTH + 2, + lineDash: DASH_PATTERN, + }), + image: new OlCircleStyle({ + radius: IMAGE_WIDTH, + fill: new OlStyleFill({ + color: FILL_COLOR, + }), + stroke: new OlStyleStroke({ + color: STROKE_COLOR, + }), + }), + }); +} + +/** + * Returns an OpenLayers style for drawing features with a contrasting stroke based on the provided draw settings. + */ +export function drawContrastStrokeStyle(style: DrawSettingsStyle): OlStyleStyle { + const {WIDTH, DASH_PATTERN, STROKE_CONTRAST_COLOR: STROKE_CONSTRAST_COLOR} = style; + return new OlStyleStyle({ + stroke: new OlStyleStroke({ + color: STROKE_CONSTRAST_COLOR, + width: WIDTH + 2, + lineDash: DASH_PATTERN, + }), + }); +} diff --git a/ui/projects/core/src/lib/map/map.service.ts b/ui/projects/core/src/lib/map/map.service.ts index 6e7bf380d8..ca1b1baded 100644 --- a/ui/projects/core/src/lib/map/map.service.ts +++ b/ui/projects/core/src/lib/map/map.service.ts @@ -4,13 +4,13 @@ import {BehaviorSubject, Observable} from 'rxjs'; import {Injectable} from '@angular/core'; import {containsExtent as olExtentContainsExtent, getIntersection as olExtentGetIntersection} from 'ol/extent'; import OlGeometry from 'ol/geom/Geometry'; -import {Vector as OlSourceVector} from 'ol/source'; import {Type as OlGeometryType} from 'ol/geom/Geometry'; +import {Vector as OlSourceVector} from 'ol/source'; import OlView from 'ol/View'; import OlFeature from 'ol/Feature'; import {MapContainerComponent} from './map-container/map-container.component'; -import {createBox} from 'ol/interaction/Draw'; +import {createBox, GeometryFunction} from 'ol/interaction/Draw'; import {olExtentToTuple} from '@geoengine/common'; /** @@ -86,11 +86,16 @@ export class MapService { this.mapComponent = mapComponent; } - public startDrawInteraction(drawType: OlGeometryType): void { + public startDrawInteraction( + drawType: OlGeometryType, + drawSingleFeature = false, + geometryFunction?: GeometryFunction, + endDrawCallback?: (feature: OlFeature) => void, + ): void { if (!this.mapComponent) { throw new Error('no MapComponent registered'); } - this.mapComponent.startDrawInteraction(drawType); + this.mapComponent.startDrawInteraction(drawType, drawSingleFeature, geometryFunction, endDrawCallback); } public startBoxDrawInteraction(endDrawCallback?: (feature: OlFeature) => void): void { diff --git a/ui/projects/enhanced-data-viewer/src/app-theme.scss b/ui/projects/enhanced-data-viewer/src/app-theme.scss index 8e9c262821..e3c5928a99 100644 --- a/ui/projects/enhanced-data-viewer/src/app-theme.scss +++ b/ui/projects/enhanced-data-viewer/src/app-theme.scss @@ -5,45 +5,56 @@ @include mat.elevation-classes(); @include mat.app-background(); -// Define Colors -$geoengine-main-green: ( - 50: #e7f5e7, - 100: #c6e5c3, - 200: #a1d49d, - 300: #7ac476, - 400: #5db858, - // main color - 500: #3fab39, - 600: #359c31, - 700: #298a26, - 800: #1c7a1b, - 900: #005b04, +// Base: #14387F / R 20 G 56 B 127 +$code-de-darkblue: ( + 50: #dce1ec, + 100: #b9c3d9, + 200: #8a9cbf, + 300: #5b74a5, + 400: #375692, + 500: #14387f, + 600: #11306c, + 700: #0e2759, + 800: #0a1c40, + 900: #07142c, + A100: #0048fa, + A200: #003fdd, + A400: #0037bf, + A700: #002ea2, contrast: ( 50: #000000, 100: #000000, 200: #000000, - 300: #000000, - 400: #000000, - 500: #000000, + 300: #ffffff, + 400: #ffffff, + 500: #ffffff, 600: #ffffff, 700: #ffffff, 800: #ffffff, 900: #ffffff, + A100: #ffffff, + A200: #ffffff, + A400: #ffffff, + A700: #ffffff, ), ); -$geoengine-mid-blue: ( - 50: #e1f5fa, - 100: #b3e4f3, - 200: #83d3eb, - 300: #56c1e4, - 400: #36b4e0, - 500: #18a7dd, - 600: #0e99cf, - // main color - 700: #0087bc, - 800: #0176a9, - 900: #005687, +// Base: #3EA3DC / R 62 G 163 B 220 +$code-de-lightblue: ( + 50: #e2f1fa, + 100: #c5e3f5, + 200: #9fd1ee, + 300: #78bfe7, + 400: #5bb1e1, + 500: #3ea3dc, + 600: #358bbb, + 700: #2b729a, + 800: #1f526e, + 900: #16394d, + A100: #e0f2ff, + A200: #a8dbff, + A400: #70c4ff, + A700: #37adff, contrast: ( 50: #000000, 100: #000000, @@ -51,16 +62,20 @@ $geoengine-mid-blue: ( 300: #000000, 400: #000000, 500: #000000, - 600: #000000, - 700: #000000, + 600: #ffffff, + 700: #ffffff, 800: #ffffff, 900: #ffffff, + A100: #000000, + A200: #000000, + A400: #000000, + A700: #000000, ), ); // Define App Theme -$geoengine-primary: mat.m2-define-palette($geoengine-main-green, 500, 300, 900); -$geoengine-accent: mat.m2-define-palette($geoengine-mid-blue, 700, 300, 900); +$geoengine-primary: mat.m2-define-palette($code-de-darkblue, 500, 300, 900); +$geoengine-accent: mat.m2-define-palette($code-de-lightblue, 700, 300, 900); $geoengine-warn: mat.m2-define-palette(mat.$m2-red-palette); $geoengine-typography: mat.m2-define-typography-config( diff --git a/ui/projects/enhanced-data-viewer/src/app/app-config.service.ts b/ui/projects/enhanced-data-viewer/src/app/app-config.service.ts index ed5c568385..2d3437c71e 100644 --- a/ui/projects/enhanced-data-viewer/src/app/app-config.service.ts +++ b/ui/projects/enhanced-data-viewer/src/app/app-config.service.ts @@ -7,9 +7,9 @@ interface AppConfigStructure extends CoreConfigStructure {} const APP_CONFIG_DEFAULTS = mergeDeepOverrideLists(DEFAULT_CORE_CONFIG, { BRANDING: { - LOGO_URL: 'assets/logo_code_copernicus_blue.png', - LOGO_ICON_URL: 'assets/geoengine-favicon-white.svg', - LOGO_ALT_URL: 'assets/geoengine-white.svg', + LOGO_URL: 'assets/CODE-DE-Lab_RGB.svg', + LOGO_ICON_URL: 'favicon.ico', + LOGO_ALT_URL: 'assets/CODE-DE-Lab_white_RGB.svg', PAGE_TITLE: 'Enhanced Data Viewer | CODE-DE Lab', }, DEFAULTS: { @@ -21,6 +21,26 @@ const APP_CONFIG_DEFAULTS = mergeDeepOverrideLists(DEFAULT_CORE_CONFIG, { }, FOCUS_EXTENT: [6.98865807458, 47.3024876979, 15.0169958839, 54.983104153], }, + MAP: { + DRAWING: { + DRAW_STYLE: { + STROKE_COLOR: 'rgba(62, 163, 220, 0.8)', + STROKE_CONTRAST_COLOR: '#FFFFFF', + FILL_COLOR: 'rgba(62, 163, 220, 0.1)', + WIDTH: 2, + IMAGE_WIDTH: 4, + DASH_PATTERN: [8, 8], + }, + AFTER_DRAW_STYLE: { + STROKE_COLOR: 'rgba(62, 163, 220, 1)', + STROKE_CONTRAST_COLOR: '#FFFFFF', + FILL_COLOR: 'rgba(62, 163, 220, 0.2)', + WIDTH: 2, + IMAGE_WIDTH: 4, + DASH_PATTERN: [8, 8], + }, + }, + }, }); @Injectable() diff --git a/ui/projects/enhanced-data-viewer/src/app/main/main.component.html b/ui/projects/enhanced-data-viewer/src/app/main/main.component.html index 49a56d9697..d90b6dcaeb 100644 --- a/ui/projects/enhanced-data-viewer/src/app/main/main.component.html +++ b/ui/projects/enhanced-data-viewer/src/app/main/main.component.html @@ -112,7 +112,7 @@

Time Selection

-

Vizualization Presets

+

Visualization Presets

RGB @@ -207,13 +207,31 @@

Vizualization Presets

- - +