From 3cfa0ea7dfca7f007adb58235458b15475518c7b Mon Sep 17 00:00:00 2001 From: Christian Beilschmidt Date: Thu, 7 Nov 2024 10:44:16 +0100 Subject: [PATCH 01/13] signals --- .../raster-symbology-editor.component.html | 10 +- .../raster-symbology-editor.component.ts | 92 +++++++++++-------- 2 files changed, 58 insertions(+), 44 deletions(-) diff --git a/projects/common/src/lib/symbology/raster-symbology-editor/raster-symbology-editor.component.html b/projects/common/src/lib/symbology/raster-symbology-editor/raster-symbology-editor.component.html index 47ada748c..57329c06b 100644 --- a/projects/common/src/lib/symbology/raster-symbology-editor/raster-symbology-editor.component.html +++ b/projects/common/src/lib/symbology/raster-symbology-editor/raster-symbology-editor.component.html @@ -20,8 +20,8 @@

Raster band:

- - {{ band.name }} + + {{ band.name }}
@@ -48,9 +48,9 @@ diff --git a/projects/common/src/lib/symbology/raster-symbology-editor/raster-symbology-editor.component.ts b/projects/common/src/lib/symbology/raster-symbology-editor/raster-symbology-editor.component.ts index 274999c96..4c1af62b3 100644 --- a/projects/common/src/lib/symbology/raster-symbology-editor/raster-symbology-editor.component.ts +++ b/projects/common/src/lib/symbology/raster-symbology-editor/raster-symbology-editor.component.ts @@ -9,6 +9,13 @@ import { ChangeDetectorRef, Output, EventEmitter, + input, + output, + computed, + signal, + effect, + untracked, + inject, } from '@angular/core'; import {RasterSymbology, SingleBandRasterColorizer, SymbologyQueryParams, SymbologyWorkflow} from '../symbology.model'; import {Colorizer, ColorizerType, LinearGradient, LogarithmicGradient, PaletteColorizer, RgbaColorizer} from '../../colors/colorizer.model'; @@ -23,6 +30,11 @@ import { } from '@geoengine/openapi-client'; import {WorkflowsService} from '../../workflows/workflows.service'; +const FAUX_RASTER_SYMBOLOGY = new RasterSymbology( + 1.0, + new SingleBandRasterColorizer(0, new LinearGradient([new ColorBreakpoint(0, WHITE)], TRANSPARENT, TRANSPARENT, TRANSPARENT)), +); + /** * An editor for generating raster symbologies. */ @@ -32,13 +44,15 @@ import {WorkflowsService} from '../../workflows/workflows.service'; styleUrls: ['raster-symbology-editor.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush, }) -export class RasterSymbologyEditorComponent implements OnChanges { - @Input({required: true}) symbologyWorkflow!: SymbologyWorkflow; - @Input() queryParams?: SymbologyQueryParams; +export class RasterSymbologyEditorComponent { + private readonly workflowsService = inject(WorkflowsService); + + symbologyWorkflow = input.required>(); + queryParams = input(); - @Output() changedSymbology: EventEmitter = new EventEmitter(); + changedSymbology = output(); - symbology!: RasterSymbology; + symbology = signal(FAUX_RASTER_SYMBOLOGY); readonly linearGradientColorizerType = LinearGradient.TYPE_NAME; readonly logarithmicGradientColorizerType = LogarithmicGradient.TYPE_NAME; @@ -46,25 +60,23 @@ export class RasterSymbologyEditorComponent implements OnChanges { readonly rgbaColorizerType = RgbaColorizer.TYPE_NAME; readonly loading$ = new BehaviorSubject(false); - readonly bands$ = new BehaviorSubject>([]); - selectedBand?: RasterBandDescriptor; - - constructor( - private readonly workflowsService: WorkflowsService, - private readonly changeDetectorRef: ChangeDetectorRef, - ) {} + readonly bands = signal>([]); + readonly selectedBand = signal(undefined); - ngOnChanges(changes: SimpleChanges): void { - if (changes.symbologyWorkflow) { - this.setUp(); - } + constructor() { + effect(() => { + this.symbologyWorkflow(); + untracked(() => { + this.setUp(); + }); + }); } /** * Get the opacity in the range [0, 100] */ getOpacity(): number { - return this.symbology.opacity * 100; + return this.symbology().opacity * 100; } /** @@ -73,15 +85,15 @@ export class RasterSymbologyEditorComponent implements OnChanges { updateOpacity(value: number): void { const opacity = value / 100; - this.symbology = this.symbology.cloneWith({opacity}); + this.symbology.set(this.symbology().cloneWith({opacity})); - this.changedSymbology.emit(this.symbology); + this.changedSymbology.emit(this.symbology()); } updateColorizer(colorizer: Colorizer): void { const rasterColorizer = new SingleBandRasterColorizer(this.getSelectedBandIndex(), colorizer); - this.symbology = this.symbology.cloneWith({colorizer: rasterColorizer}); - this.changedSymbology.emit(this.symbology); + this.symbology.set(this.symbology().cloneWith({colorizer: rasterColorizer})); + this.changedSymbology.emit(this.symbology()); } resetChanges(): void { @@ -111,15 +123,15 @@ export class RasterSymbologyEditorComponent implements OnChanges { } setSelectedBand(band: RasterBandDescriptor): void { - this.selectedBand = band; - if (this.symbology.rasterColorizer instanceof SingleBandRasterColorizer) { + this.selectedBand.set(band); + const symbology = this.symbology(); + if (symbology.rasterColorizer instanceof SingleBandRasterColorizer) { const index = this.getSelectedBandIndex(); - this.symbology = new RasterSymbology( - this.getOpacity(), - new SingleBandRasterColorizer(index, this.symbology.rasterColorizer.bandColorizer), + this.symbology.set( + new RasterSymbology(this.getOpacity(), new SingleBandRasterColorizer(index, symbology.rasterColorizer.bandColorizer)), ); - this.symbologyWorkflow = {symbology: this.symbology, workflowId: this.symbologyWorkflow.workflowId}; - this.changedSymbology.emit(this.symbology); + // this.symbologyWorkflow = {symbology: this.symbology(), workflowId: this.symbologyWorkflow().workflowId}; + this.changedSymbology.emit(this.symbology()); } } @@ -173,35 +185,37 @@ export class RasterSymbologyEditorComponent implements OnChanges { const rasterColorizer = new SingleBandRasterColorizer(this.getSelectedBandIndex(), colorizer); - this.symbology = this.symbology.cloneWith({colorizer: rasterColorizer}); - this.changedSymbology.emit(this.symbology); + this.symbology.set(this.symbology().cloneWith({colorizer: rasterColorizer})); + this.changedSymbology.emit(this.symbology()); } private setUp() { - this.symbology = this.symbologyWorkflow.symbology.clone(); - const bandIndex = (this.symbology.rasterColorizer as SingleBandRasterColorizer).band; - this.workflowsService.getMetadata(this.symbologyWorkflow.workflowId).then((resultDescriptor) => { + this.symbology.set(this.symbologyWorkflow().symbology.clone()); + const bandIndex = (this.symbology().rasterColorizer as SingleBandRasterColorizer).band; + this.workflowsService.getMetadata(this.symbologyWorkflow().workflowId).then((resultDescriptor) => { if (resultDescriptor.type === 'raster') { const rd = resultDescriptor as RasterResultDescriptorDict; - this.bands$.next(rd.bands); - this.selectedBand = rd.bands[bandIndex]; + this.bands.set(rd.bands); + this.selectedBand.set(rd.bands[bandIndex]); } }); } protected getSelectedBandIndex(): number { - if (this.selectedBand) { - return this.bands$.value.indexOf(this.selectedBand); + const selectedBand = this.selectedBand(); + if (selectedBand) { + return this.bands().indexOf(selectedBand); } return 0; } protected getActualColorizer(): Colorizer { - if (!(this.symbology.rasterColorizer instanceof SingleBandRasterColorizer)) { + const symbology = this.symbology(); + if (!(symbology.rasterColorizer instanceof SingleBandRasterColorizer)) { throw Error('Symbology editor only supports single band raster colorizers'); } - return this.symbology.rasterColorizer.bandColorizer; + return symbology.rasterColorizer.bandColorizer; } protected createGradientColorizer( From 8193e78facd9ce62a2990f26a3bac736dcf634c9 Mon Sep 17 00:00:00 2001 From: Christian Beilschmidt Date: Sun, 10 Nov 2024 11:22:39 +0100 Subject: [PATCH 02/13] multiband symbology v1 --- package-lock.json | 8 +- package.json | 2 +- .../common/src/lib/colors/colorizer.model.ts | 66 +-- projects/common/src/lib/common.module.ts | 22 +- .../raster-icon/raster-icon.component.ts | 9 +- ...-multiband-symbology-editor.component.html | 53 +++ ...multiband-symbology-editor.component.scss} | 0 ...er-multiband-symbology-editor.component.ts | 270 ++++++++++++ .../raster-symbology-editor.component.html | 65 ++- .../raster-symbology-editor.component.scss | 4 + .../raster-symbology-editor.component.ts | 205 ++++++--- .../src/lib/symbology/symbology.model.ts | 167 ++++++- projects/common/src/public-api.ts | 7 +- projects/core/src/lib/core.module.ts | 2 - .../lib/datasets/upload/upload.component.ts | 6 +- .../layer-collection.service.ts | 3 +- .../symbology-editor.component.html | 102 ++--- .../symbology-editor.component.scss | 33 +- .../operator-list/operator-list.component.ts | 9 - .../rgb-composite.component.html | 87 ---- .../rgb-composite/rgb-composite.component.ts | 414 ------------------ projects/core/src/public-api.ts | 1 - proxy-geoengine.conf.json | 2 +- 23 files changed, 789 insertions(+), 748 deletions(-) create mode 100644 projects/common/src/lib/symbology/raster-multiband-symbology-editor/raster-multiband-symbology-editor.component.html rename projects/{core/src/lib/operators/dialogs/rgb-composite/rgb-composite.component.scss => common/src/lib/symbology/raster-multiband-symbology-editor/raster-multiband-symbology-editor.component.scss} (100%) create mode 100644 projects/common/src/lib/symbology/raster-multiband-symbology-editor/raster-multiband-symbology-editor.component.ts delete mode 100644 projects/core/src/lib/operators/dialogs/rgb-composite/rgb-composite.component.html delete mode 100644 projects/core/src/lib/operators/dialogs/rgb-composite/rgb-composite.component.ts diff --git a/package-lock.json b/package-lock.json index c3c324f6d..12aab2cf5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,7 +20,7 @@ "@angular/platform-browser-dynamic": "^18.2.4", "@angular/router": "^18.2.4", "@egjs/hammerjs": "^2.0.17", - "@geoengine/openapi-client": "0.0.13", + "@geoengine/openapi-client": "0.0.16", "codemirror": "~5.65.16", "d3": "~7.9.0", "dagre": "~0.8.5", @@ -3055,9 +3055,9 @@ } }, "node_modules/@geoengine/openapi-client": { - "version": "0.0.13", - "resolved": "https://registry.npmjs.org/@geoengine/openapi-client/-/openapi-client-0.0.13.tgz", - "integrity": "sha512-/3/IVCwzinm4K00TRlK2JnB9jy1MwrSkuyG9rX9sREjHlNgEJchMAdNm15aql+59ITbD66CsJQ75IkNMEfoFDQ==" + "version": "0.0.16", + "resolved": "https://registry.npmjs.org/@geoengine/openapi-client/-/openapi-client-0.0.16.tgz", + "integrity": "sha512-dP79oF9wHQdats0Rd0sm4dKonlZrhxOafA2irCk1qunTw8pdNgXlyiqEmnim0HYwJLD57Km69v33UQq15V1MOw==" }, "node_modules/@humanwhocodes/config-array": { "version": "0.11.14", diff --git a/package.json b/package.json index 32f332da7..6c644f3db 100644 --- a/package.json +++ b/package.json @@ -56,7 +56,7 @@ "@angular/platform-browser-dynamic": "^18.2.4", "@angular/router": "^18.2.4", "@egjs/hammerjs": "^2.0.17", - "@geoengine/openapi-client": "0.0.13", + "@geoengine/openapi-client": "0.0.16", "codemirror": "~5.65.16", "d3": "~7.9.0", "dagre": "~0.8.5", diff --git a/projects/common/src/lib/colors/colorizer.model.ts b/projects/common/src/lib/colors/colorizer.model.ts index 2b113df50..a884582f2 100644 --- a/projects/common/src/lib/colors/colorizer.model.ts +++ b/projects/common/src/lib/colors/colorizer.model.ts @@ -5,10 +5,9 @@ import { LinearGradient as LinearGradientDict, LogarithmicGradient as LogarithmicGradientDict, PaletteColorizer as PaletteColorizerDict, - RgbaColorizer as RgbaColorizerDict, } from '@geoengine/openapi-client'; -export type ColorizerType = 'linearGradient' | 'logarithmicGradient' | 'palette' | 'rgba'; +export type ColorizerType = 'linearGradient' | 'logarithmicGradient' | 'palette'; export abstract class Colorizer { abstract readonly noDataColor: Color; @@ -20,8 +19,6 @@ export abstract class Colorizer { return LogarithmicGradient.fromLogarithmicGradientDict(dict); } else if (dict.type === PaletteColorizer.TYPE_NAME) { return PaletteColorizer.fromPaletteDict(dict); - } else if (dict.type === RgbaColorizer.TYPE_NAME) { - return RgbaColorizer.fromRgbaColorDict(dict); } throw new Error('Unimplemented or invalid colorizer'); @@ -443,64 +440,3 @@ export class PaletteColorizer extends Colorizer { return this.colors.size; } } - -export class RgbaColorizer extends Colorizer { - static readonly TYPE_NAME = 'rgba'; - - override noDataColor: Color = TRANSPARENT; - - static fromRgbaColorDict(_dict: RgbaColorizerDict): RgbaColorizer { - return new RgbaColorizer(); - } - - override getColor(value: number | undefined): Color { - if (value === undefined) { - return this.noDataColor; - } - - // `>>>` is unsigned u32 right shift - const valueU32 = value >>> 0; - - const red = (valueU32 & 0xff_00_00_00) >>> 24; - const green = (valueU32 & 0x00_ff_00_00) >>> 16; - const blue = (valueU32 & 0x00_00_ff_00) >>> 8; - const alpha = (valueU32 & 0x00_00_00_ff) >>> 0; - - return new Color({ - r: red, - g: green, - b: blue, - a: alpha, - }); - } - override getBreakpoints(): ColorBreakpoint[] { - return []; - } - override equals(other: Colorizer): boolean { - if (other instanceof RgbaColorizer) { - return true; - } - - return false; - } - override clone(): Colorizer { - return new RgbaColorizer(); - } - override toDict(): ColorizerDict { - return { - type: RgbaColorizer.TYPE_NAME, - } as RgbaColorizerDict; - } - override isGradient(): boolean { - return false; - } - override isDiscrete(): boolean { - return false; - } - override getColorAtIndex(_index: number): Color { - return this.noDataColor; - } - override getNumberOfColors(): number { - return 0; - } -} diff --git a/projects/common/src/lib/common.module.ts b/projects/common/src/lib/common.module.ts index f4095cdec..1866074d8 100644 --- a/projects/common/src/lib/common.module.ts +++ b/projects/common/src/lib/common.module.ts @@ -62,6 +62,7 @@ import {TimeIntervalInputComponent} from './time/time-interval-input/time-interv import {PercentileBreakpointSelectorComponent} from './colors/percentile-breakpoint-selector/percentile-breakpoint-selector.component'; import {OgrDatasetComponent} from './datasets/ogr-dataset/ogr-dataset.component'; import {CommonConfig} from './config.service'; +import {RasterMultibandSymbologyEditorComponent} from './symbology/raster-multiband-symbology-editor/raster-multiband-symbology-editor.component'; export const MATERIAL_MODULES = [ MatAutocompleteModule, @@ -99,23 +100,24 @@ const COMMON_COMPONENTS = [ ColorAttributeInputComponent, ColorBreakpointInputComponent, ColorMapSelectorComponent, - ColorTableEditorComponent, ColorParamEditorComponent, + ColorTableEditorComponent, ConfirmationComponent, + LineIconComponent, MeasurementComponent, - RasterGradientSymbologyEditorComponent, - RasterPaletteSymbologyEditorComponent, - RasterSymbologyEditorComponent, - VectorSymbologyEditorComponent, NumberParamEditorComponent, + PercentileBreakpointSelectorComponent, PointIconComponent, - LineIconComponent, PolygonIconComponent, + RasterGradientSymbologyEditorComponent, RasterIconComponent, - VegaViewerComponent, + RasterMultibandSymbologyEditorComponent, + RasterPaletteSymbologyEditorComponent, + RasterSymbologyEditorComponent, TimeInputComponent, TimeIntervalInputComponent, - PercentileBreakpointSelectorComponent, + VectorSymbologyEditorComponent, + VegaViewerComponent, ]; const COMMON_PIPES = [ @@ -123,12 +125,12 @@ const COMMON_PIPES = [ AsyncStringSanitizer, AsyncValueDefault, BreakpointToCssStringPipe, + BreakpointToCssStringPipe, ColorBreakpointsCssGradientPipe, ColorizerCssGradientPipe, + ColorizerCssGradientPipe, RasterColorizerCssGradientPipe, RgbaArrayCssGradientPipe, - BreakpointToCssStringPipe, - ColorizerCssGradientPipe, ]; const FXFLEX_LEGACY_DIRECTIVES = [FxFlexDirective, FxLayoutDirective, FxLayoutGapDirective, FxLayoutAlignDirective]; diff --git a/projects/common/src/lib/layer-icons/raster-icon/raster-icon.component.ts b/projects/common/src/lib/layer-icons/raster-icon/raster-icon.component.ts index 26562c62f..dac39a7de 100644 --- a/projects/common/src/lib/layer-icons/raster-icon/raster-icon.component.ts +++ b/projects/common/src/lib/layer-icons/raster-icon/raster-icon.component.ts @@ -1,6 +1,6 @@ import {ChangeDetectionStrategy, Component, Input, OnChanges, OnInit, SimpleChange} from '@angular/core'; import {Color, TRANSPARENT} from '../../colors/color'; -import {Colorizer, LinearGradient, RgbaColorizer} from '../../colors/colorizer.model'; +import {Colorizer, LinearGradient} from '../../colors/colorizer.model'; import {ColorBreakpoint} from '../../colors/color-breakpoint.model'; /** @@ -84,9 +84,10 @@ export class RasterIconComponent implements OnInit, OnChanges { private cellColor(x: number, y: number): Color { let colorizer = this.colorizer; - if (this.colorizer instanceof RgbaColorizer) { - colorizer = RasterIconComponent.RAINBOW_COLORIZER; - } + // TODO: multiband + // if (this.colorizer instanceof RgbaColorizer) { + // colorizer = RasterIconComponent.RAINBOW_COLORIZER; + // } const validSymbology = colorizer && colorizer.getNumberOfColors() > 0; if (!validSymbology) { diff --git a/projects/common/src/lib/symbology/raster-multiband-symbology-editor/raster-multiband-symbology-editor.component.html b/projects/common/src/lib/symbology/raster-multiband-symbology-editor/raster-multiband-symbology-editor.component.html new file mode 100644 index 000000000..deb54a07e --- /dev/null +++ b/projects/common/src/lib/symbology/raster-multiband-symbology-editor/raster-multiband-symbology-editor.component.html @@ -0,0 +1,53 @@ + + + + RGB Settings + Specify min and max values for scaling + looks + + + +
+ +

{{ channel.color | titlecase }} Channel ({{ channel.label }})

+
+ + Min + + + + Max + + + + Scaling Factor + + + + The min value must be smaller than the max value. + + + The scaling factor must be between 0 and 1. + +
+
+
+ +
+

Calculate channel min/max values from current viewport:

+ @if (isLoadingRasterStats()) { + + } @else { + + } +
+
+
diff --git a/projects/core/src/lib/operators/dialogs/rgb-composite/rgb-composite.component.scss b/projects/common/src/lib/symbology/raster-multiband-symbology-editor/raster-multiband-symbology-editor.component.scss similarity index 100% rename from projects/core/src/lib/operators/dialogs/rgb-composite/rgb-composite.component.scss rename to projects/common/src/lib/symbology/raster-multiband-symbology-editor/raster-multiband-symbology-editor.component.scss diff --git a/projects/common/src/lib/symbology/raster-multiband-symbology-editor/raster-multiband-symbology-editor.component.ts b/projects/common/src/lib/symbology/raster-multiband-symbology-editor/raster-multiband-symbology-editor.component.ts new file mode 100644 index 000000000..f59e23da9 --- /dev/null +++ b/projects/common/src/lib/symbology/raster-multiband-symbology-editor/raster-multiband-symbology-editor.component.ts @@ -0,0 +1,270 @@ +import {map, mergeMap, tap} from 'rxjs/operators'; +import {BehaviorSubject, Observable, combineLatest, firstValueFrom, from} from 'rxjs'; +import {AfterViewInit, ChangeDetectionStrategy, Component, effect, inject, input, Input, output, signal} from '@angular/core'; +import {FormBuilder, FormControl, FormGroup, Validators} from '@angular/forms'; + +import {TypedOperatorOperator, Workflow as WorkflowDict} from '@geoengine/openapi-client'; +import {RasterLayer} from '../../layers/layer.model'; +import {ResultTypes} from '../../operators/result-type.model'; +import {RasterResultDescriptor, UUID} from '../../datasets/dataset.model'; +import {SrsString} from '../../spatial-references/spatial-reference.model'; +import {extentToBboxDict} from '../../util/conversions'; +import {geoengineValidators} from '../../util/form.validators'; +import {SymbologyQueryParams, MultiBandRasterColorizer} from '../symbology.model'; +import {Color, TRANSPARENT} from '../../colors/color'; + +interface RgbSettingsForm { + red: FormGroup<{ + min: FormControl; + max: FormControl; + scale: FormControl; + }>; + green: FormGroup<{ + min: FormControl; + max: FormControl; + scale: FormControl; + }>; + blue: FormGroup<{ + min: FormControl; + max: FormControl; + scale: FormControl; + }>; + noDataColor: FormControl; +} + +interface RgbRasterStats { + red: { + min: number; + max: number; + }; + green: { + min: number; + max: number; + }; + blue: { + min: number; + max: number; + }; +} + +type RgbColorName = 'red' | 'green' | 'blue'; + +/** + * This dialog allows calculations on (one or more) raster layers. + */ +@Component({ + selector: 'geoengine-raster-multiband-symbology-editor', + templateUrl: './raster-multiband-symbology-editor.component.html', + styleUrls: ['./raster-multiband-symbology-editor.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class RasterMultibandSymbologyEditorComponent { + private readonly formBuilder = inject(FormBuilder); + + readonly band = input.required(); + readonly workflowId = input.required(); + readonly queryParams = input(); + readonly colorizer = input.required(); + readonly colorizerChange = output(); + + readonly RASTER_TYPE = [ResultTypes.RASTER]; + readonly CHANNELS: Array<{color: RgbColorName; label: string}> = [ + {color: 'red', label: 'A'}, + {color: 'green', label: 'B'}, + {color: 'blue', label: 'C'}, + ]; + readonly form: FormGroup; + + readonly isLoadingRasterStats = signal(false); + + /** + * Set up the form and disable it if the raster stats are loading + */ + constructor() { + const formBuilder = this.formBuilder.nonNullable; + this.form = new FormGroup({ + red: formBuilder.group( + { + min: formBuilder.control(0, Validators.required), + max: formBuilder.control(255, Validators.required), + scale: formBuilder.control(1, [Validators.required, Validators.min(0), Validators.max(1)]), + }, + { + validators: geoengineValidators.minAndMax('min', 'max', { + checkBothExist: true, + mustNotEqual: true, + }), + }, + ), + green: formBuilder.group( + { + min: formBuilder.control(0, Validators.required), + max: formBuilder.control(255, Validators.required), + scale: formBuilder.control(1, [Validators.required, Validators.min(0), Validators.max(1)]), + }, + { + validators: geoengineValidators.minAndMax('min', 'max', { + checkBothExist: true, + mustNotEqual: true, + }), + }, + ), + blue: formBuilder.group( + { + min: formBuilder.control(0, Validators.required), + max: formBuilder.control(255, Validators.required), + scale: formBuilder.control(1, [Validators.required, Validators.min(0), Validators.max(1)]), + }, + { + validators: geoengineValidators.minAndMax('min', 'max', { + checkBothExist: true, + mustNotEqual: true, + }), + }, + ), + noDataColor: formBuilder.control(TRANSPARENT, Validators.required), + }); + + effect(() => { + if (this.isLoadingRasterStats()) { + this.form.disable(); + } else { + this.form.enable(); + } + }); + } + + calculateRasterStats(): void { + if (this.isLoadingRasterStats()) { + return; // checked by form validator + } + + // const rasterLayers: Array | undefined = this.form.controls.rasterLayers.value; + + // if (!rasterLayers || rasterLayers.length !== 3) { + // return; // checked by form validator + // } + + // this.isRasterStatsNotLoading$.next(false); + + // from(this.queryRasterStats(rasterLayers[0], rasterLayers[1], rasterLayers[2])).subscribe({ + // next: (plotData) => { + // this.isRasterStatsNotLoading$.next(true); + + // const colors: Array = ['red', 'green', 'blue']; + + // for (const color of colors) { + // this.form.controls[color].controls.min.setValue(plotData[color].min); + // this.form.controls[color].controls.max.setValue(plotData[color].max); + // } + + // this.form.updateValueAndValidity(); + // }, + // error: (error) => { + // const errorMsg = error.error.message; + // this.notificationService.error(errorMsg); + + // this.isRasterStatsNotLoading$.next(true); + // }, + // }); + } + + // protected async queryRasterStats(layerRed: RasterLayer, layerGreen: RasterLayer, layerBlue: RasterLayer): Promise { + // const [redOperator, greenOperator, blueOperator] = await firstValueFrom( + // this.projectService.getAutomaticallyProjectedOperatorsFromLayers([layerRed, layerGreen, layerBlue]), + // ); + + // const workflow: WorkflowDict = { + // type: 'Plot', + // operator: { + // type: 'Statistics', + // params: {columnNames: ['red', 'green', 'blue']}, + // sources: { + // source: [redOperator, greenOperator, blueOperator], + // }, + // } as StatisticsDict, + // }; + + // const [workflowId, sessionToken, queryParams] = await firstValueFrom( + // combineLatest([ + // this.projectService.registerWorkflow(workflow), + // this.userService.getSessionTokenForRequest(), + // this.estimateQueryParams(redOperator), // we use the red operator to estimate the query params + // ]), + // ); + + // const plot = await firstValueFrom(this.backend.getPlot(workflowId, queryParams, sessionToken)); + // const plotData = plot.data as { + // [name: string]: { + // valueCount: number; + // validCount: number; + // min: number; + // max: number; + // mean: number; + // stddev: number; + // }; + // }; + + // return { + // red: { + // min: plotData.red.min, + // max: plotData.red.max, + // }, + // green: { + // min: plotData.green.min, + // max: plotData.green.max, + // }, + // blue: { + // min: plotData.blue.min, + // max: plotData.blue.max, + // }, + // }; + // } + + /** + * TODO: put function to util or service? + * A similar function is used in the symbology component + // */ + // protected async estimateQueryParams(rasterOperator: TypedOperatorOperator): Promise<{ + // bbox: BBoxDict; + // crs: SrsString; + // time: TimeIntervalDict; + // spatialResolution: [number, number]; + // }> { + // const rasterWorkflowId = await firstValueFrom( + // this.projectService.registerWorkflow({ + // type: 'Raster', + // operator: rasterOperator as TypedOperatorOperator, + // }), + // ); + + // const resultDescriptorDict = await firstValueFrom(this.projectService.getWorkflowMetaData(rasterWorkflowId)); + + // const resultDescriptor = RasterResultDescriptor.fromDict(resultDescriptorDict as RasterResultDescriptorDict); + + // let bbox = resultDescriptor.bbox; + + // if (!bbox) { + // // if we don't know the bbox of the dataset, we use the projection's whole bbox for guessing the symbology + // // TODO: better use the screen extent? + // bbox = await firstValueFrom( + // this.spatialReferenceService + // .getSpatialReferenceSpecification(resultDescriptor.spatialReference) + // .pipe(map((spatialReferenceSpecification) => extentToBboxDict(spatialReferenceSpecification.extent))), + // ); + // } + + // const time = await this.projectService.getTimeOnce(); + + // // for sampling, we choose a reasonable resolution + // const NUM_PIXELS = 1024; + // const xResolution = (bbox.upperRightCoordinate.x - bbox.lowerLeftCoordinate.x) / NUM_PIXELS; + // const yResolution = (bbox.upperRightCoordinate.y - bbox.lowerLeftCoordinate.y) / NUM_PIXELS; + // return { + // bbox, + // crs: resultDescriptor.spatialReference, + // time: time.toDict(), + // spatialResolution: [xResolution, yResolution], + // }; + // } +} diff --git a/projects/common/src/lib/symbology/raster-symbology-editor/raster-symbology-editor.component.html b/projects/common/src/lib/symbology/raster-symbology-editor/raster-symbology-editor.component.html index 57329c06b..8de703476 100644 --- a/projects/common/src/lib/symbology/raster-symbology-editor/raster-symbology-editor.component.html +++ b/projects/common/src/lib/symbology/raster-symbology-editor/raster-symbology-editor.component.html @@ -17,31 +17,47 @@ {{ opacitySliderInput.value }} % -
-

Raster band:

+

Colorizer:

- - {{ band.name }} - -
+ + Linear Gradient + Logarithmic Gradient + Palette + Multi Band RGB + -

Colorizer:

+ @if (rasterSymbologyType() === 'singleBand') { +

Raster band:

-
- - Linear Gradient - Logarithmic Gradient - Palette - RGBA - -
+ + Band + + {{ band.name }} + + + } @else if (rasterSymbologyType() === 'multiBand') { +

RGB Bands:

+ + @for (band of ['Red', 'Green', 'Blue']; track $index; let i = $index) { + + {{ band }} Band + + {{ band.name }} + + + } + } @@ -60,5 +76,14 @@ (colorizerChange)="updateColorizer($event)" > + + diff --git a/projects/common/src/lib/symbology/raster-symbology-editor/raster-symbology-editor.component.scss b/projects/common/src/lib/symbology/raster-symbology-editor/raster-symbology-editor.component.scss index ec225eba8..1be62b213 100644 --- a/projects/common/src/lib/symbology/raster-symbology-editor/raster-symbology-editor.component.scss +++ b/projects/common/src/lib/symbology/raster-symbology-editor/raster-symbology-editor.component.scss @@ -30,3 +30,7 @@ mat-divider { flex: 1; } } + +mat-form-field.band { + display: block; +} diff --git a/projects/common/src/lib/symbology/raster-symbology-editor/raster-symbology-editor.component.ts b/projects/common/src/lib/symbology/raster-symbology-editor/raster-symbology-editor.component.ts index 4c1af62b3..25f15d0c6 100644 --- a/projects/common/src/lib/symbology/raster-symbology-editor/raster-symbology-editor.component.ts +++ b/projects/common/src/lib/symbology/raster-symbology-editor/raster-symbology-editor.component.ts @@ -16,10 +16,18 @@ import { effect, untracked, inject, + Signal, + WritableSignal, } from '@angular/core'; -import {RasterSymbology, SingleBandRasterColorizer, SymbologyQueryParams, SymbologyWorkflow} from '../symbology.model'; -import {Colorizer, ColorizerType, LinearGradient, LogarithmicGradient, PaletteColorizer, RgbaColorizer} from '../../colors/colorizer.model'; -import {Color, TRANSPARENT, WHITE} from '../../colors/color'; +import { + MultiBandRasterColorizer, + RasterSymbology, + SingleBandRasterColorizer, + SymbologyQueryParams, + SymbologyWorkflow, +} from '../symbology.model'; +import {Colorizer, ColorizerType, LinearGradient, LogarithmicGradient, PaletteColorizer} from '../../colors/colorizer.model'; +import {BLACK, Color, TRANSPARENT, WHITE} from '../../colors/color'; import {ColorBreakpoint} from '../../colors/color-breakpoint.model'; import {BehaviorSubject, Subscription, map} from 'rxjs'; import { @@ -30,6 +38,11 @@ import { } from '@geoengine/openapi-client'; import {WorkflowsService} from '../../workflows/workflows.service'; +type RasterSymbologyType = 'singleBand' | 'multiBand'; + +/** + * A faux raster symbology to use as a default value. + */ const FAUX_RASTER_SYMBOLOGY = new RasterSymbology( 1.0, new SingleBandRasterColorizer(0, new LinearGradient([new ColorBreakpoint(0, WHITE)], TRANSPARENT, TRANSPARENT, TRANSPARENT)), @@ -57,11 +70,24 @@ export class RasterSymbologyEditorComponent { readonly linearGradientColorizerType = LinearGradient.TYPE_NAME; readonly logarithmicGradientColorizerType = LogarithmicGradient.TYPE_NAME; readonly paletteColorizerType = PaletteColorizer.TYPE_NAME; - readonly rgbaColorizerType = RgbaColorizer.TYPE_NAME; + readonly multiBandType = 'multiBand'; readonly loading$ = new BehaviorSubject(false); readonly bands = signal>([]); readonly selectedBand = signal(undefined); + readonly selectedBand2 = signal(undefined); + readonly selectedBand3 = signal(undefined); + + readonly rasterSymbologyType = computed(() => { + const symbology = this.symbology(); + if (symbology.rasterColorizer instanceof SingleBandRasterColorizer) { + return 'singleBand'; + } + if (symbology.rasterColorizer instanceof MultiBandRasterColorizer) { + return 'multiBand'; + } + throw Error('unknown raster symbology type'); + }); constructor() { effect(() => { @@ -96,12 +122,21 @@ export class RasterSymbologyEditorComponent { this.changedSymbology.emit(this.symbology()); } + updateMultiBandColorizer(colorizer: MultiBandRasterColorizer): void { + this.symbology.set(this.symbology().cloneWith({colorizer})); + this.changedSymbology.emit(this.symbology()); + } + resetChanges(): void { this.setUp(); } - getColorizerType(): ColorizerType { - const colorizer = this.getActualColorizer(); + getColorizerType(): ColorizerType | 'multiBand' { + if (this.symbology().rasterColorizer instanceof MultiBandRasterColorizer) { + return 'multiBand'; + } + + const colorizer = this.getSingleBandColorizer(); if (colorizer instanceof LinearGradient) { return LinearGradient.TYPE_NAME; @@ -115,28 +150,48 @@ export class RasterSymbologyEditorComponent { return LogarithmicGradient.TYPE_NAME; } - if (colorizer instanceof RgbaColorizer) { - return RgbaColorizer.TYPE_NAME; - } - throw Error('unknown colorizer type'); } - setSelectedBand(band: RasterBandDescriptor): void { - this.selectedBand.set(band); + getSelectedBand(i: number): WritableSignal { + switch (i) { + case 0: + return this.selectedBand; + case 1: + return this.selectedBand2; + case 2: + return this.selectedBand3; + default: + throw Error('unknown band index'); + } + } + + setSelectedBand(band: RasterBandDescriptor, i = 0): void { + this.getSelectedBand(i).set(band); const symbology = this.symbology(); if (symbology.rasterColorizer instanceof SingleBandRasterColorizer) { const index = this.getSelectedBandIndex(); this.symbology.set( new RasterSymbology(this.getOpacity(), new SingleBandRasterColorizer(index, symbology.rasterColorizer.bandColorizer)), ); - // this.symbologyWorkflow = {symbology: this.symbology(), workflowId: this.symbologyWorkflow().workflowId}; + this.changedSymbology.emit(this.symbology()); + } else if (symbology.rasterColorizer instanceof MultiBandRasterColorizer) { + this.symbology.set( + new RasterSymbology( + this.getOpacity(), + symbology.rasterColorizer.withBands( + this.getSelectedBandIndex(0), + this.getSelectedBandIndex(1), + this.getSelectedBandIndex(2), + ), + ), + ); this.changedSymbology.emit(this.symbology()); } } get paletteColorizer(): PaletteColorizer | undefined { - const colorizer = this.getActualColorizer(); + const colorizer = this.getSingleBandColorizer(); if (colorizer instanceof PaletteColorizer) { return colorizer; @@ -145,7 +200,7 @@ export class RasterSymbologyEditorComponent { } get gradientColorizer(): LinearGradient | LogarithmicGradient | undefined { - const colorizer = this.getActualColorizer(); + const colorizer = this.getSingleBandColorizer(); if (colorizer instanceof LinearGradient || colorizer instanceof LogarithmicGradient) { return colorizer; @@ -153,37 +208,66 @@ export class RasterSymbologyEditorComponent { return undefined; } + get multibandColorizer(): MultiBandRasterColorizer | undefined { + const colorizer = this.symbology().rasterColorizer; + + if (colorizer instanceof MultiBandRasterColorizer) { + return colorizer; + } + return undefined; + } + /** * Conversion between different colorizer types */ - updateColorizerType(colorizerType: ColorizerType): void { + updateColorizerType(colorizerType: ColorizerType | 'multiBand'): void { if (colorizerType === this.getColorizerType()) { return; } - let colorizer = this.getActualColorizer(); - - switch (colorizerType) { - case 'linearGradient': - colorizer = this.createGradientColorizer( - (breakpoints: Array, noDataColor: Color, overColor: Color, underColor: Color) => - new LinearGradient(breakpoints, noDataColor, overColor, underColor), - ); - break; - case 'logarithmicGradient': - colorizer = this.createGradientColorizer( - (breakpoints: Array, noDataColor: Color, overColor: Color, underColor: Color) => - new LogarithmicGradient(breakpoints, noDataColor, overColor, underColor), - ); - break; - case 'palette': - colorizer = this.createPaletteColorizer(); - break; - case 'rgba': - colorizer = new RgbaColorizer(); - } + let rasterColorizer: SingleBandRasterColorizer | MultiBandRasterColorizer; + + if (colorizerType === 'multiBand') { + rasterColorizer = new MultiBandRasterColorizer( + this.getSelectedBandIndex(0), + this.getSelectedBandIndex(1), + this.getSelectedBandIndex(2), + 0, + 255, + 1, + 0, + 255, + 1, + 0, + 255, + 1, + TRANSPARENT, + ); + } else { + // single band + + let colorizer = this.getSingleBandColorizer(); + + switch (colorizerType) { + case 'linearGradient': + colorizer = this.createGradientColorizer( + (breakpoints: Array, noDataColor: Color, overColor: Color, underColor: Color) => + new LinearGradient(breakpoints, noDataColor, overColor, underColor), + ); + break; + case 'logarithmicGradient': + colorizer = this.createGradientColorizer( + (breakpoints: Array, noDataColor: Color, overColor: Color, underColor: Color) => + new LogarithmicGradient(breakpoints, noDataColor, overColor, underColor), + ); + break; + case 'palette': + colorizer = this.createPaletteColorizer(); + break; + } - const rasterColorizer = new SingleBandRasterColorizer(this.getSelectedBandIndex(), colorizer); + rasterColorizer = new SingleBandRasterColorizer(this.getSelectedBandIndex(), colorizer); + } this.symbology.set(this.symbology().cloneWith({colorizer: rasterColorizer})); this.changedSymbology.emit(this.symbology()); @@ -201,18 +285,22 @@ export class RasterSymbologyEditorComponent { }); } - protected getSelectedBandIndex(): number { - const selectedBand = this.selectedBand(); + updateRasterSymbologyType($event: any): void { + throw new Error('Method not implemented.'); + } + + protected getSelectedBandIndex(i = 0): number { + const selectedBand = this.getSelectedBand(i)(); if (selectedBand) { return this.bands().indexOf(selectedBand); } return 0; } - protected getActualColorizer(): Colorizer { + protected getSingleBandColorizer(): Colorizer | undefined { const symbology = this.symbology(); if (!(symbology.rasterColorizer instanceof SingleBandRasterColorizer)) { - throw Error('Symbology editor only supports single band raster colorizers'); + return undefined; } return symbology.rasterColorizer.bandColorizer; @@ -221,19 +309,15 @@ export class RasterSymbologyEditorComponent { protected createGradientColorizer( constructorFn: (breakpoints: Array, noDataColor: Color, overColor: Color, underColor: Color) => G, ): G { - const colorizer = this.getActualColorizer(); + const colorizer = this.getSingleBandColorizer(); - if (colorizer instanceof RgbaColorizer) { - // TODO: derive some reasonable default values - return constructorFn([new ColorBreakpoint(0, WHITE)], TRANSPARENT, TRANSPARENT, TRANSPARENT); - } - - const breakpoints = colorizer.getBreakpoints(); + let breakpoints: Array; let noDataColor: Color; let overColor: Color; let underColor: Color; if (colorizer instanceof LogarithmicGradient || colorizer instanceof LinearGradient) { + breakpoints = colorizer.getBreakpoints(); noDataColor = colorizer.noDataColor; overColor = colorizer.overColor; underColor = colorizer.underColor; @@ -242,29 +326,30 @@ export class RasterSymbologyEditorComponent { const paletteColorizer = colorizer as PaletteColorizer; const defaultColor: Color = paletteColorizer.defaultColor ? paletteColorizer.defaultColor : TRANSPARENT; + breakpoints = paletteColorizer.getBreakpoints(); noDataColor = paletteColorizer.noDataColor ? paletteColorizer.noDataColor : TRANSPARENT; overColor = defaultColor; underColor = defaultColor; } else { - throw Error('unknown colorizer type'); + // create a palette colorizer without any previous information + breakpoints = [new ColorBreakpoint(0, BLACK), new ColorBreakpoint(255, WHITE)]; + noDataColor = TRANSPARENT; + overColor = BLACK; + underColor = WHITE; } return constructorFn(breakpoints, noDataColor, overColor, underColor); } protected createPaletteColorizer(): PaletteColorizer { - const colorizer = this.getActualColorizer(); + const colorizer = this.getSingleBandColorizer(); - if (colorizer instanceof RgbaColorizer) { - // TODO: derive some reasonable default values - return new PaletteColorizer(new Map([[0, WHITE]]), TRANSPARENT, TRANSPARENT); - } - - const breakpoints = colorizer.getBreakpoints(); + let breakpoints: Array; let noDataColor: Color; let defaultColor: Color; if (colorizer instanceof LogarithmicGradient || colorizer instanceof LinearGradient) { + breakpoints = colorizer.getBreakpoints(); noDataColor = colorizer.noDataColor; // we can neither use the over nor the under color @@ -273,10 +358,14 @@ export class RasterSymbologyEditorComponent { // Must be a palette then, so use values from the color selectors or RGBA 0, 0, 0, 0 as a fallback const paletteColorizer = colorizer as PaletteColorizer; + breakpoints = paletteColorizer.getBreakpoints(); noDataColor = paletteColorizer.noDataColor ? paletteColorizer.noDataColor : TRANSPARENT; defaultColor = paletteColorizer.defaultColor ? paletteColorizer.defaultColor : TRANSPARENT; } else { - throw Error('unknown colorizer type'); + // create a palette colorizer without any previous information + breakpoints = [new ColorBreakpoint(0, BLACK), new ColorBreakpoint(255, WHITE)]; + noDataColor = TRANSPARENT; + defaultColor = TRANSPARENT; } return new PaletteColorizer(this.createColorMap(breakpoints), noDataColor, defaultColor); diff --git a/projects/common/src/lib/symbology/symbology.model.ts b/projects/common/src/lib/symbology/symbology.model.ts index b292ff0af..54f932ec8 100644 --- a/projects/common/src/lib/symbology/symbology.model.ts +++ b/projects/common/src/lib/symbology/symbology.model.ts @@ -1,4 +1,4 @@ -import {Color, RgbaTuple, WHITE, BLACK, rgbaColorFromDict, colorToDict} from '../colors/color'; +import {Color, RgbaTuple, WHITE, BLACK, rgbaColorFromDict, colorToDict, TRANSPARENT} from '../colors/color'; import {Feature as OlFeature} from 'ol'; import {Circle as OlStyleCircle, Fill as OlStyleFill, Stroke as OlStyleStroke, Style as OlStyle, Text as OlStyleText} from 'ol/style'; import {StyleFunction as OlStyleFunction} from 'ol/style/Style'; @@ -20,6 +20,7 @@ import { RasterSymbology as RasterSymbologyDict, RasterColorizer as RasterColorizerDict, SpatialResolution, + MultiBandRasterColorizer as MultiBandRasterColorizerDict, } from '@geoengine/openapi-client'; import {PointIconStyle} from '../layer-icons/point-icon/point-icon.component'; import {LineIconStyle} from '../layer-icons/line-icon/line-icon.component'; @@ -652,12 +653,27 @@ export class RasterSymbology extends Symbology { export abstract class RasterColorizer { static fromDict(dict: RasterColorizerDict): RasterColorizer { - // TODO: multi band if (dict.type === 'singleBand') { return new SingleBandRasterColorizer(dict.band, Colorizer.fromDict(dict.bandColorizer)); - } else { - throw new Error('unable to deserialize `RasterColorizer`'); } + if (dict.type === 'multiBand') { + return new MultiBandRasterColorizer( + dict.redBand, + dict.greenBand, + dict.blueBand, + dict.redMin, + dict.redMax, + dict.redScale ?? 1, + dict.greenMin, + dict.greenMax, + dict.greenScale ?? 1, + dict.blueMin, + dict.blueMax, + dict.blueScale ?? 1, + dict.noDataColor ? Color.fromRgbaLike(rgbaColorFromDict(dict.noDataColor)) : TRANSPARENT, + ); + } + throw new Error('unable to deserialize `RasterColorizer`'); } abstract equals(other: RasterColorizer): boolean; @@ -733,6 +749,149 @@ export class SingleBandRasterColorizer extends RasterColorizer { } } +export class MultiBandRasterColorizer extends RasterColorizer { + readonly redBand: number; + readonly greenBand: number; + readonly blueBand: number; + readonly redMin: number; + readonly redMax: number; + readonly redScale: number; + readonly greenMin: number; + readonly greenMax: number; + readonly greenScale: number; + readonly blueMin: number; + readonly blueMax: number; + readonly blueScale: number; + readonly noDataColor: Color; + + constructor( + redBand: number, + greenBand: number, + blueBand: number, + redMin: number, + redMax: number, + redScale: number, + greenMin: number, + greenMax: number, + greenScale: number, + blueMin: number, + blueMax: number, + blueScale: number, + noDataColor: Color, + ) { + super(); + this.redBand = redBand; + this.greenBand = greenBand; + this.blueBand = blueBand; + this.redMin = redMin; + this.redMax = redMax; + this.redScale = redScale; + this.greenMin = greenMin; + this.greenMax = greenMax; + this.greenScale = greenScale; + this.blueMin = blueMin; + this.blueMax = blueMax; + this.blueScale = blueScale; + this.noDataColor = noDataColor; + } + + override equals(other: RasterColorizer): boolean { + if (!(other instanceof MultiBandRasterColorizer)) { + return false; + } + + return ( + this.redBand === other.redBand && + this.greenBand === other.greenBand && + this.blueBand === other.blueBand && + this.redMin === other.redMin && + this.redMax === other.redMax && + this.redScale === other.redScale && + this.greenMin === other.greenMin && + this.greenMax === other.greenMax && + this.greenScale === other.greenScale && + this.blueMin === other.blueMin && + this.blueMax === other.blueMax && + this.blueScale === other.blueScale && + this.noDataColor.equals(other.noDataColor) + ); + } + override clone(): MultiBandRasterColorizer { + return new MultiBandRasterColorizer( + this.redBand, + this.greenBand, + this.blueBand, + this.redMin, + this.redMax, + this.redScale, + this.greenMin, + this.greenMax, + this.greenScale, + this.blueMin, + this.blueMax, + this.blueScale, + this.noDataColor, + ); + } + + withBands(redBand: number, greenBand: number, blueBand: number): MultiBandRasterColorizer { + return new MultiBandRasterColorizer( + redBand, + greenBand, + blueBand, + this.redMin, + this.redMax, + this.redScale, + this.greenMin, + this.greenMax, + this.greenScale, + this.blueMin, + this.blueMax, + this.blueScale, + this.noDataColor, + ); + } + + override toDict(): MultiBandRasterColorizerDict { + return { + type: 'multiBand', + redBand: this.redBand, + greenBand: this.greenBand, + blueBand: this.blueBand, + redMin: this.redMin, + redMax: this.redMax, + redScale: this.redScale, + greenMin: this.greenMin, + greenMax: this.greenMax, + greenScale: this.greenScale, + blueMin: this.blueMin, + blueMax: this.blueMax, + blueScale: this.blueScale, + noDataColor: colorToDict(this.noDataColor), + }; + } + + override getBreakpoints(): Array { + return []; + } + + override isDiscrete(): boolean { + return false; + } + + override getNumberOfColors(): number { + return 0; + } + + override isGradient(): boolean { + return false; + } + + override getColorAtIndex(index: number): Color { + return TRANSPARENT; + } +} + export abstract class ColorParam { static fromDict(dict: ColorParamDict): ColorParam { if (dict.type === 'static') { diff --git a/projects/common/src/public-api.ts b/projects/common/src/public-api.ts index f270dc485..1df2ce2c5 100644 --- a/projects/common/src/public-api.ts +++ b/projects/common/src/public-api.ts @@ -19,22 +19,23 @@ export * from './lib/colors/color-breakpoint-input/color-breakpoint-input.compon export * from './lib/colors/color-map-selector/color-map-selector.component'; export * from './lib/colors/color-table-editor/color-table-editor.component'; export * from './lib/colors/percentile-breakpoint-selector/percentile-breakpoint-selector.component'; +export * from './lib/datasets/ogr-dataset/ogr-dataset.component'; export * from './lib/dialogs/confirmation/confirmation.component'; export * from './lib/layer-icons/line-icon/line-icon.component'; export * from './lib/layer-icons/point-icon/point-icon.component'; export * from './lib/layer-icons/polygon-icon/polygon-icon.component'; export * from './lib/layer-icons/raster-icon/raster-icon.component'; export * from './lib/measurement/measurement.component'; -export * from './lib/datasets/ogr-dataset/ogr-dataset.component'; +export * from './lib/plots/vega-viewer/vega-viewer.component'; export * from './lib/symbology/color-param-editor/color-param-editor.component'; export * from './lib/symbology/number-param-editor/number-param-editor.component'; export * from './lib/symbology/raster-gradient-symbology-editor/raster-gradient-symbology-editor.component'; +export * from './lib/symbology/raster-multiband-symbology-editor/raster-multiband-symbology-editor.component'; export * from './lib/symbology/raster-palette-symbology-editor/raster-palette-symbology-editor.component'; export * from './lib/symbology/raster-symbology-editor/raster-symbology-editor.component'; export * from './lib/symbology/vector-symbology-editor/vector-symbology-editor.component'; -export * from './lib/time/time-interval-input/time-interval-input.component'; export * from './lib/time/time-input/time-input.component'; -export * from './lib/plots/vega-viewer/vega-viewer.component'; +export * from './lib/time/time-interval-input/time-interval-input.component'; // Models export * from './lib/colors/color-breakpoint.model'; diff --git a/projects/core/src/lib/core.module.ts b/projects/core/src/lib/core.module.ts index 3a2b236dc..c1fa3e264 100644 --- a/projects/core/src/lib/core.module.ts +++ b/projects/core/src/lib/core.module.ts @@ -139,7 +139,6 @@ import {UserSessionComponent} from './users/user-session/user-session.component' import {QuotaInfoComponent} from './users/quota/quota-info/quota-info.component'; import {LineSimplificationComponent} from './operators/dialogs/line-simplification/line-simplification.component'; import {TaskListComponent} from './tasks/task-list/task-list.component'; -import {RgbaCompositeComponent} from './operators/dialogs/rgb-composite/rgb-composite.component'; import {RolesComponent} from './users/roles/roles.component'; import {VectorExpressionComponent} from './operators/dialogs/vector-expression/vector-expression.component'; import {CommonConfig, CommonModule} from '@geoengine/common'; @@ -270,7 +269,6 @@ const CORE_COMPONENTS = [ RasterTypeConversionComponent, RasterVectorJoinComponent, RenameLayerComponent, - RgbaCompositeComponent, RolesComponent, SaveProjectAsComponent, ScatterplotOperatorComponent, diff --git a/projects/core/src/lib/datasets/upload/upload.component.ts b/projects/core/src/lib/datasets/upload/upload.component.ts index f055c01de..06db7062e 100644 --- a/projects/core/src/lib/datasets/upload/upload.component.ts +++ b/projects/core/src/lib/datasets/upload/upload.component.ts @@ -4,13 +4,13 @@ import {FormControl, FormGroup, Validators} from '@angular/forms'; import {MatStepper} from '@angular/material/stepper'; import {Subject, Subscription} from 'rxjs'; import {mergeMap} from 'rxjs/operators'; -import {AddDatasetDict, UUID} from '../../backend/backend.model'; +import {UUID} from '../../backend/backend.model'; import {NotificationService} from '../../notification.service'; import {ProjectService} from '../../project/project.service'; import {UserService} from '../../users/user.service'; import {DatasetsService, OgrDatasetComponent, UploadsService, timeStepGranularityOptions} from '@geoengine/common'; import {DatasetService} from '../dataset.service'; -import {DatasetDefinition, MetaDataDefinition, MetaDataSuggestion, TimeGranularity} from '@geoengine/openapi-client'; +import {AddDataset, DatasetDefinition, MetaDataDefinition, MetaDataSuggestion, TimeGranularity} from '@geoengine/openapi-client'; interface NameDescription { name: FormControl; @@ -158,7 +158,7 @@ export class UploadComponent implements OnDestroy { const metaData: MetaDataDefinition = this.ogrDatasetComponent.getMetaData(); - const addData: AddDatasetDict = { + const addData: AddDataset = { name: this.userNamePrefix + ':' + formDataset.name.value, displayName: formDataset.displayName.value, description: formDataset.description.value, diff --git a/projects/core/src/lib/layer-collections/layer-collection.service.ts b/projects/core/src/lib/layer-collections/layer-collection.service.ts index 806998fa2..aa3a84b0f 100644 --- a/projects/core/src/lib/layer-collections/layer-collection.service.ts +++ b/projects/core/src/lib/layer-collections/layer-collection.service.ts @@ -8,7 +8,6 @@ import { LayerCollectionItemDict, LayerCollectionLayerDict, ProviderLayerIdDict, - RasterSymbologyDict, UUID, VectorResultDescriptorDict, } from '../backend/backend.model'; @@ -187,7 +186,7 @@ export class LayerCollectionService { isVisible: true, isLegendVisible: false, symbology: layer.symbology - ? RasterSymbology.fromRasterSymbologyDict(layer.symbology as RasterSymbologyDict) + ? (layer.symbology as unknown as RasterSymbology) : RasterSymbology.fromRasterSymbologyDict({ type: 'raster', opacity: 1.0, diff --git a/projects/core/src/lib/layers/symbology/symbology-editor/symbology-editor.component.html b/projects/core/src/lib/layers/symbology/symbology-editor/symbology-editor.component.html index f286a7789..dc16896cf 100644 --- a/projects/core/src/lib/layers/symbology/symbology-editor/symbology-editor.component.html +++ b/projects/core/src/lib/layers/symbology/symbology-editor/symbology-editor.component.html @@ -1,27 +1,28 @@ Symbology of {{ layer.name }} - -
+
@if (rasterSymbologyWorkflow$ | async; as symbologyWorkflow) { - -

The symbology editor enables customization of the style for vector layers.

-

- Vector features are shown in the map as points, lines or polygons. Points and polygons have a fill color. All vector - features have a stroke. Points have additionally a radius since they are displayed as circles. You can always specify - default visualization parameters. You can adapt the stroke width and color. Points and Polygons have a fill color. As all - other global settings, this is overwritten by other style rules. -

-

- Each setting has a default value, which is used if no additional rule applies. Additional rules are derived values. If you - specify a derived attribute name, your style is based on the feature values. For instance, you can specify a numeric column - to be used as a radius for a point feature. -

-
+
+ +

The symbology editor enables customization of the style for vector layers.

+

+ Vector features are shown in the map as points, lines or polygons. Points and polygons have a fill color. All vector + features have a stroke. Points have additionally a radius since they are displayed as circles. You can always + specify default visualization parameters. You can adapt the stroke width and color. Points and Polygons have a fill + color. As all other global settings, this is overwritten by other style rules. +

+

+ Each setting has a default value, which is used if no additional rule applies. Additional rules are derived values. If + you specify a derived attribute name, your style is based on the feature values. For instance, you can specify a numeric + column to be used as a radius for a point feature. +

+
- + +
@@ -30,35 +31,36 @@ } @if (vectorSymbologyWorkflow$ | async; as symbologyWorkflow) { - -

The Symbology Editor enables customization of the style for raster layers.

-

- The Global Layer Properties define the default visualization parameters. The layer Opacity is adjustable in a - range from 0 to 100 %. You can choose a NoData color for pixels with the nodata value. Use the picker tool to select - the desired RGB color and opacity. This also applies to the Overflow color, which indicates the pixels with values - without coloring rules. -

-

- The Color Map section provides an overview of the pixel values with a frequency plot, which also allows to adapt the - color with respect to the raster values. The plot refers to the field of view shown in the map. If - Sync map and histogram is turned on, the histogram updates if the map view changes. To specify the value range of - interest, you can set a minimum and maximum pixel value. You can choose a color ramp from a variety of color schemes (Colormap name) and reverse it, if desired. Additionally, different functions for the step distribution can be selected (linear, - logarithmic, square root function, square function). Consider that the logarithmic function requires positive values (>0). - The number of Color steps is also kept flexible and can be set to a number between 2 and 16. Click - Create color table to apply your adjustments. -

-

- The Color Table section allows fine grained changes to colors. The gradient defines the interpolation between - values. You can dynamically add and remove color steps by clicking the minus and plus symbols or select distinct RGBA values - for a specific color step value. -

-
+
+ +

The Symbology Editor enables customization of the style for raster layers.

+

+ The Global Layer Properties define the default visualization parameters. The layer Opacity is adjustable + in a range from 0 to 100 %. You can choose a NoData color for pixels with the nodata value. Use the picker tool + to select the desired RGB color and opacity. This also applies to the Overflow color, which indicates the pixels + with values without coloring rules. +

+

+ The Color Map section provides an overview of the pixel values with a frequency plot, which also allows to adapt + the color with respect to the raster values. The plot refers to the field of view shown in the map. If + Sync map and histogram is turned on, the histogram updates if the map view changes. To specify the value range + of interest, you can set a minimum and maximum pixel value. You can choose a color ramp from a variety of color schemes + (Colormap name) and reverse it, if desired. Additionally, different functions for the step distribution can be + selected (linear, logarithmic, square root function, square function). Consider that the logarithmic function requires + positive values (>0). The number of Color steps is also kept flexible and can be set to a number between 2 and 16. Click + Create color table to apply your adjustments. +

+

+ The Color Table section allows fine grained changes to colors. The gradient defines the interpolation between + values. You can dynamically add and remove color steps by clicking the minus and plus symbols or select distinct RGBA + values for a specific color step value. +

+
- + +
}
diff --git a/projects/core/src/lib/layers/symbology/symbology-editor/symbology-editor.component.scss b/projects/core/src/lib/layers/symbology/symbology-editor/symbology-editor.component.scss index 55f7cf56f..a6449fa78 100644 --- a/projects/core/src/lib/layers/symbology/symbology-editor/symbology-editor.component.scss +++ b/projects/core/src/lib/layers/symbology/symbology-editor/symbology-editor.component.scss @@ -1,20 +1,33 @@ :host { - display: flex; - flex-direction: column; height: 100%; } .container { height: 100%; - padding: 0rem 1rem 1rem 1rem; -} + width: 100%; + box-sizing: border-box; + + display: flex; + flex-direction: column; -.actions { - text-align: right; - height: 3rem; - padding: 1rem; + .specification { + flex: 1 1 0%; - button { - margin: 0.5rem; + overflow-y: auto; + + padding: 0rem 1rem 1rem 1rem; + } + + .actions { + text-align: right; + padding: 1rem; + + button { + margin-left: 0.5rem; + } } } + +geoengine-raster-symbology-editor { + display: block; +} diff --git a/projects/core/src/lib/operators/dialogs/operator-list/operator-list.component.ts b/projects/core/src/lib/operators/dialogs/operator-list/operator-list.component.ts index 1424f10a3..7e9f233d2 100644 --- a/projects/core/src/lib/operators/dialogs/operator-list/operator-list.component.ts +++ b/projects/core/src/lib/operators/dialogs/operator-list/operator-list.component.ts @@ -25,7 +25,6 @@ import {TimeShiftComponent} from '../time-shift/time-shift.component'; import {PieChartComponent} from '../pie-chart/pie-chart.component'; import {RasterizationComponent} from '../rasterization/rasterization.component'; import {LineSimplificationComponent} from '../line-simplification/line-simplification.component'; -import {RgbaCompositeComponent as RgbaCompositeComponent} from '../rgb-composite/rgb-composite.component'; import {RasterStackerComponent} from '../raster-stacker/raster-stacker.component'; import {VectorExpressionComponent} from '../vector-expression/vector-expression.component'; import {BandwiseExpressionOperatorComponent} from '../bandwise-expression-operator/bandwise-expression-operator.component'; @@ -249,14 +248,6 @@ export class OperatorListComponent implements OnInit, OnChanges { // }, // description: 'Apply a mask to a raster', // }, - { - component: RgbaCompositeComponent, - type: { - NAME: 'RGBA Composite', - ICON_URL: createIconDataUrl('RGBA Composite'), - }, - description: 'Create an RGB composite from a set of rasters', - }, ]; static readonly DEFAULT_VECTOR_OPERATOR_DIALOGS: Array = [ diff --git a/projects/core/src/lib/operators/dialogs/rgb-composite/rgb-composite.component.html b/projects/core/src/lib/operators/dialogs/rgb-composite/rgb-composite.component.html deleted file mode 100644 index af94ec6d2..000000000 --- a/projects/core/src/lib/operators/dialogs/rgb-composite/rgb-composite.component.html +++ /dev/null @@ -1,87 +0,0 @@ -Calculate RGB Composite on Rasters - -
- - - help_center - - - - - - - - help_center - - - -

Calculate channel min/max values from current viewport:

- - - - - -

{{ channel.color | titlecase }} Channel ({{ channel.label }})

-
- - Min - - - - Max - - - - Scaling Factor - - - - The min value must be smaller than the max value. - - - The scaling factor must be between 0 and 1. - -
-
-
- - - - - The name must be non-empty. - - - - - - - -
-
- - -

- no raster input available -
- -

-
diff --git a/projects/core/src/lib/operators/dialogs/rgb-composite/rgb-composite.component.ts b/projects/core/src/lib/operators/dialogs/rgb-composite/rgb-composite.component.ts deleted file mode 100644 index 0a01da37b..000000000 --- a/projects/core/src/lib/operators/dialogs/rgb-composite/rgb-composite.component.ts +++ /dev/null @@ -1,414 +0,0 @@ -import {map, mergeMap, tap} from 'rxjs/operators'; -import {BehaviorSubject, Observable, combineLatest, firstValueFrom, from} from 'rxjs'; -import {AfterViewInit, ChangeDetectionStrategy, Component, Input} from '@angular/core'; -import {FormBuilder, FormControl, FormGroup, Validators} from '@angular/forms'; -import {ProjectService} from '../../../project/project.service'; -import {BBoxDict, RasterResultDescriptorDict, SrsString, TimeIntervalDict, UUID} from '../../../backend/backend.model'; -import {LayoutService, SidenavConfig} from '../../../layout.service'; -import {NotificationService} from '../../../notification.service'; -import {BackendService} from '../../../backend/backend.service'; -import {UserService} from '../../../users/user.service'; -import {SpatialReferenceService} from '../../../spatial-references/spatial-reference.service'; -import { - Layer, - RasterLayer, - RasterResultDescriptor, - RasterSymbology, - ResultTypes, - RgbDict, - RgbaColorizer, - SingleBandRasterColorizer, - StatisticsDict, - extentToBboxDict, - geoengineValidators, -} from '@geoengine/common'; - -import {TypedOperatorOperator, Workflow as WorkflowDict} from '@geoengine/openapi-client'; - -interface RgbCompositeForm { - rasterLayers: FormControl | undefined>; - - name: FormControl; - - red: FormGroup<{ - min: FormControl; - max: FormControl; - scale: FormControl; - }>; - green: FormGroup<{ - min: FormControl; - max: FormControl; - scale: FormControl; - }>; - blue: FormGroup<{ - min: FormControl; - max: FormControl; - scale: FormControl; - }>; -} - -interface RgbRasterStats { - red: { - min: number; - max: number; - }; - green: { - min: number; - max: number; - }; - blue: { - min: number; - max: number; - }; -} - -type RgbColorName = 'red' | 'green' | 'blue'; - -/** - * This dialog allows calculations on (one or more) raster layers. - */ -@Component({ - selector: 'geoengine-rgb-composite', - templateUrl: './rgb-composite.component.html', - styleUrls: ['./rgb-composite.component.scss'], - changeDetection: ChangeDetectionStrategy.OnPush, -}) -export class RgbaCompositeComponent implements AfterViewInit { - /** - * If the list is empty, show the following button. - */ - @Input() dataListConfig?: SidenavConfig; - - readonly RASTER_TYPE = [ResultTypes.RASTER]; - readonly CHANNELS: Array<{color: RgbColorName; label: string}> = [ - {color: 'red', label: 'A'}, - {color: 'green', label: 'B'}, - {color: 'blue', label: 'C'}, - ]; - readonly form: FormGroup; - - readonly projectHasRasterLayers$: Observable; - readonly notAllLayersSet$: Observable; - - readonly isRasterStatsNotLoading$ = new BehaviorSubject(true); - - readonly loading$ = new BehaviorSubject(false); - - /** - * DI of services and setup of observables for the template - */ - constructor( - protected readonly projectService: ProjectService, - protected readonly layoutService: LayoutService, - protected readonly notificationService: NotificationService, - protected readonly spatialReferenceService: SpatialReferenceService, - protected readonly backend: BackendService, - protected readonly userService: UserService, - readonly _formBuilder: FormBuilder, - ) { - const formBuilder = _formBuilder.nonNullable; - this.form = new FormGroup({ - rasterLayers: new FormControl | undefined>(undefined, { - nonNullable: true, - validators: [Validators.required, Validators.minLength(3), Validators.maxLength(3)], - }), - name: new FormControl('RGBA', { - nonNullable: true, - validators: [Validators.required, geoengineValidators.notOnlyWhitespace], - }), - red: formBuilder.group( - { - min: formBuilder.control(0, Validators.required), - max: formBuilder.control(255, Validators.required), - scale: formBuilder.control(1, [Validators.required, Validators.min(0), Validators.max(1)]), - }, - { - validators: geoengineValidators.minAndMax('min', 'max', { - checkBothExist: true, - mustNotEqual: true, - }), - }, - ), - green: formBuilder.group( - { - min: formBuilder.control(0, Validators.required), - max: formBuilder.control(255, Validators.required), - scale: formBuilder.control(1, [Validators.required, Validators.min(0), Validators.max(1)]), - }, - { - validators: geoengineValidators.minAndMax('min', 'max', { - checkBothExist: true, - mustNotEqual: true, - }), - }, - ), - blue: formBuilder.group( - { - min: formBuilder.control(0, Validators.required), - max: formBuilder.control(255, Validators.required), - scale: formBuilder.control(1, [Validators.required, Validators.min(0), Validators.max(1)]), - }, - { - validators: geoengineValidators.minAndMax('min', 'max', { - checkBothExist: true, - mustNotEqual: true, - }), - }, - ), - }); - - this.projectHasRasterLayers$ = this.projectService - .getLayerStream() - .pipe(map((layers: Array) => layers.filter((layer) => layer.layerType === 'raster').length > 0)); - - this.notAllLayersSet$ = this.form.controls.rasterLayers.valueChanges.pipe( - map((rasterLayers: Array | undefined) => { - if (!rasterLayers) { - return true; - } - - return rasterLayers.length !== 3; - }), - ); - } - - ngAfterViewInit(): void { - setTimeout(() => - this.form.controls['rasterLayers'].updateValueAndValidity({ - onlySelf: false, - emitEvent: true, - }), - ); - } - - /** - * Uses the user input and creates a new rgb composite operator. - * The resulting layer is added to the map. - */ - add(): void { - if (this.loading$.value) { - return; // don't add while loading - } - - const rasterLayers: Array | undefined = this.form.controls.rasterLayers.value; - - if (!rasterLayers || rasterLayers.length !== 3) { - return; // checked by form validator - } - - const name: string = this.form.controls.name.value; - - const redMin: number = this.form.controls.red.controls.min.value; - const redMax: number = this.form.controls.red.controls.max.value; - const redScale: number = this.form.controls.red.controls.scale.value; - - const greenMin: number = this.form.controls.green.controls.min.value; - const greenMax: number = this.form.controls.green.controls.max.value; - const greenScale: number = this.form.controls.green.controls.scale.value; - - const blueMin: number = this.form.controls.blue.controls.min.value; - const blueMax: number = this.form.controls.blue.controls.max.value; - const blueScale: number = this.form.controls.blue.controls.scale.value; - - const sourceOperators = this.projectService.getAutomaticallyProjectedOperatorsFromLayers(rasterLayers); - - const symbology = new RasterSymbology(1, new SingleBandRasterColorizer(0, new RgbaColorizer())); - - sourceOperators - .pipe( - tap({next: () => this.loading$.next(true)}), - mergeMap((operators: Array) => { - const workflow: WorkflowDict = { - type: 'Raster', - operator: { - type: 'Rgb', - params: { - redMin, - redMax, - redScale, - greenMin, - greenMax, - greenScale, - blueMin, - blueMax, - blueScale, - }, - sources: { - red: operators[0], - green: operators[1], - blue: operators[2], - }, - } as RgbDict, - }; - - return this.projectService.registerWorkflow(workflow); - }), - mergeMap((workflowId: UUID) => - this.projectService.addLayer( - new RasterLayer({ - workflowId, - name, - symbology, - isLegendVisible: false, - isVisible: true, - }), - ), - ), - ) - .subscribe({ - next: () => { - // everything worked well - - this.loading$.next(false); - }, - error: (error) => { - const errorMsg = error.error.message; - this.notificationService.error(errorMsg); - - this.loading$.next(false); - }, - }); - } - - goToAddDataTab(): void { - if (!this.dataListConfig) { - return; - } - - this.layoutService.setSidenavContentComponent(this.dataListConfig); - } - - calculateRasterStats(): void { - if (this.loading$.value) { - return; // checked by form validator - } - - const rasterLayers: Array | undefined = this.form.controls.rasterLayers.value; - - if (!rasterLayers || rasterLayers.length !== 3) { - return; // checked by form validator - } - - this.isRasterStatsNotLoading$.next(false); - - from(this.queryRasterStats(rasterLayers[0], rasterLayers[1], rasterLayers[2])).subscribe({ - next: (plotData) => { - this.isRasterStatsNotLoading$.next(true); - - const colors: Array = ['red', 'green', 'blue']; - - for (const color of colors) { - this.form.controls[color].controls.min.setValue(plotData[color].min); - this.form.controls[color].controls.max.setValue(plotData[color].max); - } - - this.form.updateValueAndValidity(); - }, - error: (error) => { - const errorMsg = error.error.message; - this.notificationService.error(errorMsg); - - this.isRasterStatsNotLoading$.next(true); - }, - }); - } - - protected async queryRasterStats(layerRed: RasterLayer, layerGreen: RasterLayer, layerBlue: RasterLayer): Promise { - const [redOperator, greenOperator, blueOperator] = await firstValueFrom( - this.projectService.getAutomaticallyProjectedOperatorsFromLayers([layerRed, layerGreen, layerBlue]), - ); - - const workflow: WorkflowDict = { - type: 'Plot', - operator: { - type: 'Statistics', - params: {columnNames: ['red', 'green', 'blue']}, - sources: { - source: [redOperator, greenOperator, blueOperator], - }, - } as StatisticsDict, - }; - - const [workflowId, sessionToken, queryParams] = await firstValueFrom( - combineLatest([ - this.projectService.registerWorkflow(workflow), - this.userService.getSessionTokenForRequest(), - this.estimateQueryParams(redOperator), // we use the red operator to estimate the query params - ]), - ); - - const plot = await firstValueFrom(this.backend.getPlot(workflowId, queryParams, sessionToken)); - const plotData = plot.data as { - [name: string]: { - valueCount: number; - validCount: number; - min: number; - max: number; - mean: number; - stddev: number; - }; - }; - - return { - red: { - min: plotData.red.min, - max: plotData.red.max, - }, - green: { - min: plotData.green.min, - max: plotData.green.max, - }, - blue: { - min: plotData.blue.min, - max: plotData.blue.max, - }, - }; - } - - /** - * TODO: put function to util or service? - * A similar function is used in the symbology component - */ - protected async estimateQueryParams(rasterOperator: TypedOperatorOperator): Promise<{ - bbox: BBoxDict; - crs: SrsString; - time: TimeIntervalDict; - spatialResolution: [number, number]; - }> { - const rasterWorkflowId = await firstValueFrom( - this.projectService.registerWorkflow({ - type: 'Raster', - operator: rasterOperator as TypedOperatorOperator, - }), - ); - - const resultDescriptorDict = await firstValueFrom(this.projectService.getWorkflowMetaData(rasterWorkflowId)); - - const resultDescriptor = RasterResultDescriptor.fromDict(resultDescriptorDict as RasterResultDescriptorDict); - - let bbox = resultDescriptor.bbox; - - if (!bbox) { - // if we don't know the bbox of the dataset, we use the projection's whole bbox for guessing the symbology - // TODO: better use the screen extent? - bbox = await firstValueFrom( - this.spatialReferenceService - .getSpatialReferenceSpecification(resultDescriptor.spatialReference) - .pipe(map((spatialReferenceSpecification) => extentToBboxDict(spatialReferenceSpecification.extent))), - ); - } - - const time = await this.projectService.getTimeOnce(); - - // for sampling, we choose a reasonable resolution - const NUM_PIXELS = 1024; - const xResolution = (bbox.upperRightCoordinate.x - bbox.lowerLeftCoordinate.x) / NUM_PIXELS; - const yResolution = (bbox.upperRightCoordinate.y - bbox.lowerLeftCoordinate.y) / NUM_PIXELS; - return { - bbox, - crs: resultDescriptor.spatialReference, - time: time.toDict(), - spatialResolution: [xResolution, yResolution], - }; - } -} diff --git a/projects/core/src/public-api.ts b/projects/core/src/public-api.ts index 06a79cb63..1c4a38a6f 100644 --- a/projects/core/src/public-api.ts +++ b/projects/core/src/public-api.ts @@ -80,7 +80,6 @@ export * from './lib/operators/dialogs/raster-stacker/raster-stacker.component'; export * from './lib/operators/dialogs/raster-type-conversion/raster-type-conversion.component'; export * from './lib/operators/dialogs/raster-vector-join/raster-vector-join.component'; export * from './lib/operators/dialogs/rasterization/rasterization.component'; -export * from './lib/operators/dialogs/rgb-composite/rgb-composite.component'; export * from './lib/operators/dialogs/scatterplot-operator/scatterplot-operator.component'; export * from './lib/operators/dialogs/statistics-plot/statistics-plot.component'; export * from './lib/operators/dialogs/temporal-raster-aggregation/temporal-raster-aggregation.component'; diff --git a/proxy-geoengine.conf.json b/proxy-geoengine.conf.json index 8f7963247..57ec40132 100644 --- a/proxy-geoengine.conf.json +++ b/proxy-geoengine.conf.json @@ -1,6 +1,6 @@ { "/api/**": { - "target": "https://nightly.peter.geoengine.io/", + "target": "https://zentrale.app.geoengine.io/", "changeOrigin": true, "secure": false, "logLevel": "debug" From 024b89704ec22141f52c4a43906c272166ed647c Mon Sep 17 00:00:00 2001 From: Christian Beilschmidt Date: Sun, 10 Nov 2024 11:26:09 +0100 Subject: [PATCH 03/13] openapi 0.0.17 --- package-lock.json | 8 ++++---- package.json | 2 +- projects/common/src/public-api.ts | 14 +++++++------- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/package-lock.json b/package-lock.json index 12aab2cf5..71f5a0231 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,7 +20,7 @@ "@angular/platform-browser-dynamic": "^18.2.4", "@angular/router": "^18.2.4", "@egjs/hammerjs": "^2.0.17", - "@geoengine/openapi-client": "0.0.16", + "@geoengine/openapi-client": "0.0.14", "codemirror": "~5.65.16", "d3": "~7.9.0", "dagre": "~0.8.5", @@ -3055,9 +3055,9 @@ } }, "node_modules/@geoengine/openapi-client": { - "version": "0.0.16", - "resolved": "https://registry.npmjs.org/@geoengine/openapi-client/-/openapi-client-0.0.16.tgz", - "integrity": "sha512-dP79oF9wHQdats0Rd0sm4dKonlZrhxOafA2irCk1qunTw8pdNgXlyiqEmnim0HYwJLD57Km69v33UQq15V1MOw==" + "version": "0.0.14", + "resolved": "https://registry.npmjs.org/@geoengine/openapi-client/-/openapi-client-0.0.14.tgz", + "integrity": "sha512-VzT/jDP6XBAfZ+9lBX69XCjhsr3mwkfuD9EEx4GfuHi9i947UHYWMh1wjw6lUlgsSJYKR7/EDGHtJQxQq/iw4Q==" }, "node_modules/@humanwhocodes/config-array": { "version": "0.11.14", diff --git a/package.json b/package.json index 9d3b81676..636176aec 100644 --- a/package.json +++ b/package.json @@ -56,7 +56,7 @@ "@angular/platform-browser-dynamic": "^18.2.4", "@angular/router": "^18.2.4", "@egjs/hammerjs": "^2.0.17", - "@geoengine/openapi-client": "0.0.17", + "@geoengine/openapi-client": "0.0.14", "codemirror": "~5.65.16", "d3": "~7.9.0", "dagre": "~0.8.5", diff --git a/projects/common/src/public-api.ts b/projects/common/src/public-api.ts index 007212e65..008899ff3 100644 --- a/projects/common/src/public-api.ts +++ b/projects/common/src/public-api.ts @@ -24,17 +24,18 @@ export * from './lib/colors/color-table-editor/color-table-editor.component'; export * from './lib/colors/percentile-breakpoint-selector/percentile-breakpoint-selector.component'; export * from './lib/datasets/ogr-dataset/ogr-dataset.component'; export * from './lib/dialogs/confirmation/confirmation.component'; -export * from './lib/layer-icons/line-icon/line-icon.component'; -export * from './lib/layer-icons/point-icon/point-icon.component'; -export * from './lib/layer-icons/polygon-icon/polygon-icon.component'; -export * from './lib/layer-icons/raster-icon/raster-icon.component'; export * from './lib/layer-collections/layer-collection-dropdown/layer-collection-dropdown.component'; -export * from './lib/layer-collections/layer-collection-layer/layer-collection-layer.component'; export * from './lib/layer-collections/layer-collection-layer-details/layer-collection-layer-details.component'; +export * from './lib/layer-collections/layer-collection-layer/layer-collection-layer.component'; export * from './lib/layer-collections/layer-collection-list/layer-collection-list.component'; export * from './lib/layer-collections/layer-collection-navigation/layer-collection-navigation.component'; +export * from './lib/layer-icons/line-icon/line-icon.component'; +export * from './lib/layer-icons/point-icon/point-icon.component'; +export * from './lib/layer-icons/polygon-icon/polygon-icon.component'; +export * from './lib/layer-icons/raster-icon/raster-icon.component'; export * from './lib/measurement/measurement.component'; export * from './lib/plots/vega-viewer/vega-viewer.component'; +export * from './lib/plots/vega-viewer/vega-viewer.component'; export * from './lib/symbology/color-param-editor/color-param-editor.component'; export * from './lib/symbology/number-param-editor/number-param-editor.component'; export * from './lib/symbology/raster-gradient-symbology-editor/raster-gradient-symbology-editor.component'; @@ -43,10 +44,9 @@ export * from './lib/symbology/raster-palette-symbology-editor/raster-palette-sy export * from './lib/symbology/raster-symbology-editor/raster-symbology-editor.component'; export * from './lib/symbology/vector-symbology-editor/vector-symbology-editor.component'; export * from './lib/time/time-input/time-input.component'; -export * from './lib/time/time-interval-input/time-interval-input.component'; export * from './lib/time/time-input/time-input.component'; +export * from './lib/time/time-interval-input/time-interval-input.component'; export * from './lib/util/components/code-editor.component'; -export * from './lib/plots/vega-viewer/vega-viewer.component'; // Models export * from './lib/colors/color-breakpoint.model'; From e317697c4eb86dbb40b0e301c4e47a94b4ad6dc6 Mon Sep 17 00:00:00 2001 From: Christian Beilschmidt Date: Sun, 10 Nov 2024 11:31:23 +0100 Subject: [PATCH 04/13] v0.0.17 --- package-lock.json | 8 ++++---- package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 71f5a0231..d78bab244 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,7 +20,7 @@ "@angular/platform-browser-dynamic": "^18.2.4", "@angular/router": "^18.2.4", "@egjs/hammerjs": "^2.0.17", - "@geoengine/openapi-client": "0.0.14", + "@geoengine/openapi-client": "0.0.17", "codemirror": "~5.65.16", "d3": "~7.9.0", "dagre": "~0.8.5", @@ -3055,9 +3055,9 @@ } }, "node_modules/@geoengine/openapi-client": { - "version": "0.0.14", - "resolved": "https://registry.npmjs.org/@geoengine/openapi-client/-/openapi-client-0.0.14.tgz", - "integrity": "sha512-VzT/jDP6XBAfZ+9lBX69XCjhsr3mwkfuD9EEx4GfuHi9i947UHYWMh1wjw6lUlgsSJYKR7/EDGHtJQxQq/iw4Q==" + "version": "0.0.17", + "resolved": "https://registry.npmjs.org/@geoengine/openapi-client/-/openapi-client-0.0.17.tgz", + "integrity": "sha512-6qMiMoypsYYmFiGx1l3MEG/bBjYmZcS3x42bAE1nwDmUjjIKw863uVHHyDLcZFQxhMxgGsb86Ox+cQF38Y3ERw==" }, "node_modules/@humanwhocodes/config-array": { "version": "0.11.14", diff --git a/package.json b/package.json index 636176aec..9d3b81676 100644 --- a/package.json +++ b/package.json @@ -56,7 +56,7 @@ "@angular/platform-browser-dynamic": "^18.2.4", "@angular/router": "^18.2.4", "@egjs/hammerjs": "^2.0.17", - "@geoengine/openapi-client": "0.0.14", + "@geoengine/openapi-client": "0.0.17", "codemirror": "~5.65.16", "d3": "~7.9.0", "dagre": "~0.8.5", From 3c62f8badebe04fba356ae5d8eceae3bcf3a2c22 Mon Sep 17 00:00:00 2001 From: Christian Beilschmidt Date: Sun, 10 Nov 2024 11:35:44 +0100 Subject: [PATCH 05/13] fix missing imports --- projects/common/src/lib/common.module.ts | 11 ++++++++++- proxy-geoengine.conf.json | 2 +- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/projects/common/src/lib/common.module.ts b/projects/common/src/lib/common.module.ts index 8832cda9c..637d2c474 100644 --- a/projects/common/src/lib/common.module.ts +++ b/projects/common/src/lib/common.module.ts @@ -104,6 +104,7 @@ export const MATERIAL_MODULES = [ ]; const COMMON_COMPONENTS = [ + AutocompleteSelectDirective, CodeEditorComponent, ColorAttributeInputComponent, ColorBreakpointInputComponent, @@ -111,10 +112,17 @@ const COMMON_COMPONENTS = [ ColorParamEditorComponent, ColorTableEditorComponent, ConfirmationComponent, + LayerCollectionDropdownComponent, + LayerCollectionLayerComponent, + LayerCollectionLayerDetailsComponent, + LayerCollectionListComponent, + LayerCollectionNavigationComponent, + LineIconComponent, LineIconComponent, MeasurementComponent, NumberParamEditorComponent, PercentileBreakpointSelectorComponent, + PercentileBreakpointSelectorComponent, PointIconComponent, PolygonIconComponent, RasterGradientSymbologyEditorComponent, @@ -124,7 +132,8 @@ const COMMON_COMPONENTS = [ RasterSymbologyEditorComponent, TimeInputComponent, TimeIntervalInputComponent, - PercentileBreakpointSelectorComponent, + VectorSymbologyEditorComponent, + VegaViewerComponent, ]; const COMMON_PIPES = [ diff --git a/proxy-geoengine.conf.json b/proxy-geoengine.conf.json index 57ec40132..8f7963247 100644 --- a/proxy-geoengine.conf.json +++ b/proxy-geoengine.conf.json @@ -1,6 +1,6 @@ { "/api/**": { - "target": "https://zentrale.app.geoengine.io/", + "target": "https://nightly.peter.geoengine.io/", "changeOrigin": true, "secure": false, "logLevel": "debug" From b3a33151fd2f3f6c827f9d4698204521e4a1e608 Mon Sep 17 00:00:00 2001 From: Christian Beilschmidt Date: Mon, 11 Nov 2024 11:04:47 +0100 Subject: [PATCH 06/13] remove console log --- projects/common/src/lib/layer-collections/layers.service.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/projects/common/src/lib/layer-collections/layers.service.ts b/projects/common/src/lib/layer-collections/layers.service.ts index 9d6e89da3..12067f373 100644 --- a/projects/common/src/lib/layer-collections/layers.service.ts +++ b/projects/common/src/lib/layer-collections/layers.service.ts @@ -129,8 +129,6 @@ export class LayersService { const metadata = await this.getWorkflowIdMetadata(workflowId); - console.log(metadata); - if (metadata instanceof VectorLayerMetadata) { return new VectorLayer({ name: layer.name, From 2d7f05132ac0d07a8c64741ac6dc085499071698 Mon Sep 17 00:00:00 2001 From: Christian Beilschmidt Date: Mon, 11 Nov 2024 11:05:08 +0100 Subject: [PATCH 07/13] multiband colorizer --- ...-multiband-symbology-editor.component.html | 47 ++- ...-multiband-symbology-editor.component.scss | 4 + ...er-multiband-symbology-editor.component.ts | 290 +++++++++--------- .../raster-symbology-editor.component.html | 13 +- .../raster-symbology-editor.component.ts | 60 ++-- .../src/lib/symbology/symbology.model.ts | 29 ++ 6 files changed, 253 insertions(+), 190 deletions(-) diff --git a/projects/common/src/lib/symbology/raster-multiband-symbology-editor/raster-multiband-symbology-editor.component.html b/projects/common/src/lib/symbology/raster-multiband-symbology-editor/raster-multiband-symbology-editor.component.html index deb54a07e..99d3a1f76 100644 --- a/projects/common/src/lib/symbology/raster-multiband-symbology-editor/raster-multiband-symbology-editor.component.html +++ b/projects/common/src/lib/symbology/raster-multiband-symbology-editor/raster-multiband-symbology-editor.component.html @@ -8,7 +8,7 @@
- + @for (channel of channels(); track channel.label) {

{{ channel.color | titlecase }} Channel ({{ channel.label }})

@@ -23,31 +23,30 @@

{{ channel.color | titlecase }} Channel ({{ channel.label }})

Scaling Factor
- - The min value must be smaller than the max value. - - - The scaling factor must be between 0 and 1. - + @if (form.controls[channel.color].errors?.minOverMax || form.controls[channel.color].errors?.minEqualsMax) { + The min value must be smaller than the max value. + } + @if ( + form.controls[channel.color].controls.scale.errors?.min || form.controls[channel.color].controls.scale.errors?.max + ) { + The scaling factor must be between 0 and 1. + }
-
+ }
-
-

Calculate channel min/max values from current viewport:

- @if (isLoadingRasterStats()) { - - } @else { - - } -
+ @if (queryParams()) { +
+

Calculate channel min/max values from current viewport:

+ @if (isLoadingRasterStats()) { + + } @else { + + } +
+ }
diff --git a/projects/common/src/lib/symbology/raster-multiband-symbology-editor/raster-multiband-symbology-editor.component.scss b/projects/common/src/lib/symbology/raster-multiband-symbology-editor/raster-multiband-symbology-editor.component.scss index 5c8bc9d06..f590d4cdd 100644 --- a/projects/common/src/lib/symbology/raster-multiband-symbology-editor/raster-multiband-symbology-editor.component.scss +++ b/projects/common/src/lib/symbology/raster-multiband-symbology-editor/raster-multiband-symbology-editor.component.scss @@ -23,3 +23,7 @@ mat-spinner { margin-top: 50%; text-align: center; } + +mat-card { + margin-top: 1rem; +} diff --git a/projects/common/src/lib/symbology/raster-multiband-symbology-editor/raster-multiband-symbology-editor.component.ts b/projects/common/src/lib/symbology/raster-multiband-symbology-editor/raster-multiband-symbology-editor.component.ts index f59e23da9..7d529b0c3 100644 --- a/projects/common/src/lib/symbology/raster-multiband-symbology-editor/raster-multiband-symbology-editor.component.ts +++ b/projects/common/src/lib/symbology/raster-multiband-symbology-editor/raster-multiband-symbology-editor.component.ts @@ -1,17 +1,13 @@ -import {map, mergeMap, tap} from 'rxjs/operators'; -import {BehaviorSubject, Observable, combineLatest, firstValueFrom, from} from 'rxjs'; -import {AfterViewInit, ChangeDetectionStrategy, Component, effect, inject, input, Input, output, signal} from '@angular/core'; +import {Subscription} from 'rxjs'; +import {ChangeDetectionStrategy, Component, computed, effect, inject, input, OnDestroy, output, signal} from '@angular/core'; import {FormBuilder, FormControl, FormGroup, Validators} from '@angular/forms'; - -import {TypedOperatorOperator, Workflow as WorkflowDict} from '@geoengine/openapi-client'; -import {RasterLayer} from '../../layers/layer.model'; -import {ResultTypes} from '../../operators/result-type.model'; -import {RasterResultDescriptor, UUID} from '../../datasets/dataset.model'; -import {SrsString} from '../../spatial-references/spatial-reference.model'; -import {extentToBboxDict} from '../../util/conversions'; import {geoengineValidators} from '../../util/form.validators'; import {SymbologyQueryParams, MultiBandRasterColorizer} from '../symbology.model'; import {Color, TRANSPARENT} from '../../colors/color'; +import {WorkflowsService} from '../../workflows/workflows.service'; +import {ExpressionDict, StatisticsDict, StatisticsParams} from '../../operators/operator.model'; +import {PlotsService} from '../../plots/plots.service'; +import {UUID} from '../../datasets/dataset.model'; interface RgbSettingsForm { red: FormGroup<{ @@ -58,22 +54,42 @@ type RgbColorName = 'red' | 'green' | 'blue'; styleUrls: ['./raster-multiband-symbology-editor.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush, }) -export class RasterMultibandSymbologyEditorComponent { +export class RasterMultibandSymbologyEditorComponent implements OnDestroy { private readonly formBuilder = inject(FormBuilder); + private readonly workflowsService = inject(WorkflowsService); + private readonly plotsService = inject(PlotsService); - readonly band = input.required(); readonly workflowId = input.required(); readonly queryParams = input(); + readonly band1 = input.required<{ + name: string; + index: number; + }>(); + readonly band2 = input.required<{ + name: string; + index: number; + }>(); + readonly band3 = input.required<{ + name: string; + index: number; + }>(); readonly colorizer = input.required(); readonly colorizerChange = output(); - readonly RASTER_TYPE = [ResultTypes.RASTER]; - readonly CHANNELS: Array<{color: RgbColorName; label: string}> = [ - {color: 'red', label: 'A'}, - {color: 'green', label: 'B'}, - {color: 'blue', label: 'C'}, - ]; + readonly channels = computed>(() => { + let band1 = this.band1(); + let band2 = this.band2(); + let band3 = this.band3(); + + return [ + {color: 'red', label: band1.name}, + {color: 'green', label: band2.name}, + {color: 'blue', label: band3.name}, + ]; + }); + readonly form: FormGroup; + readonly formSubscription: Subscription; readonly isLoadingRasterStats = signal(false); @@ -125,6 +141,55 @@ export class RasterMultibandSymbologyEditorComponent { noDataColor: formBuilder.control(TRANSPARENT, Validators.required), }); + this.formSubscription = this.form.valueChanges.subscribe({ + next: (_value) => { + if (this.form.invalid) { + return; + } + + const value = this.form.getRawValue(); + const colorizer = this.colorizer().withParams( + value.red.min, + value.red.max, + value.red.scale, + value.green.min, + value.green.max, + value.green.scale, + value.blue.min, + value.blue.max, + value.blue.scale, + value.noDataColor, + ); + + if (!this.colorizer().equals(colorizer)) { + this.colorizerChange.emit(colorizer); + } + }, + }); + + effect(() => { + const colorizer = this.colorizer(); + + this.form.setValue({ + red: { + min: colorizer.redMin, + max: colorizer.redMax, + scale: colorizer.redScale, + }, + green: { + min: colorizer.greenMin, + max: colorizer.greenMax, + scale: colorizer.greenScale, + }, + blue: { + min: colorizer.blueMin, + max: colorizer.blueMax, + scale: colorizer.blueScale, + }, + noDataColor: colorizer.noDataColor, + }); + }); + effect(() => { if (this.isLoadingRasterStats()) { this.form.disable(); @@ -134,137 +199,84 @@ export class RasterMultibandSymbologyEditorComponent { }); } - calculateRasterStats(): void { + ngOnDestroy(): void { + this.formSubscription.unsubscribe(); + } + + async calculateRasterStats(): Promise { if (this.isLoadingRasterStats()) { return; // checked by form validator } + const queryParams = this.queryParams(); + if (!queryParams) { + return; + } - // const rasterLayers: Array | undefined = this.form.controls.rasterLayers.value; - - // if (!rasterLayers || rasterLayers.length !== 3) { - // return; // checked by form validator - // } - - // this.isRasterStatsNotLoading$.next(false); - - // from(this.queryRasterStats(rasterLayers[0], rasterLayers[1], rasterLayers[2])).subscribe({ - // next: (plotData) => { - // this.isRasterStatsNotLoading$.next(true); - - // const colors: Array = ['red', 'green', 'blue']; - - // for (const color of colors) { - // this.form.controls[color].controls.min.setValue(plotData[color].min); - // this.form.controls[color].controls.max.setValue(plotData[color].max); - // } - - // this.form.updateValueAndValidity(); - // }, - // error: (error) => { - // const errorMsg = error.error.message; - // this.notificationService.error(errorMsg); - - // this.isRasterStatsNotLoading$.next(true); - // }, - // }); - } - - // protected async queryRasterStats(layerRed: RasterLayer, layerGreen: RasterLayer, layerBlue: RasterLayer): Promise { - // const [redOperator, greenOperator, blueOperator] = await firstValueFrom( - // this.projectService.getAutomaticallyProjectedOperatorsFromLayers([layerRed, layerGreen, layerBlue]), - // ); - - // const workflow: WorkflowDict = { - // type: 'Plot', - // operator: { - // type: 'Statistics', - // params: {columnNames: ['red', 'green', 'blue']}, - // sources: { - // source: [redOperator, greenOperator, blueOperator], - // }, - // } as StatisticsDict, - // }; - - // const [workflowId, sessionToken, queryParams] = await firstValueFrom( - // combineLatest([ - // this.projectService.registerWorkflow(workflow), - // this.userService.getSessionTokenForRequest(), - // this.estimateQueryParams(redOperator), // we use the red operator to estimate the query params - // ]), - // ); - - // const plot = await firstValueFrom(this.backend.getPlot(workflowId, queryParams, sessionToken)); - // const plotData = plot.data as { - // [name: string]: { - // valueCount: number; - // validCount: number; - // min: number; - // max: number; - // mean: number; - // stddev: number; - // }; - // }; + this.isLoadingRasterStats.set(true); - // return { - // red: { - // min: plotData.red.min, - // max: plotData.red.max, - // }, - // green: { - // min: plotData.green.min, - // max: plotData.green.max, - // }, - // blue: { - // min: plotData.blue.min, - // max: plotData.blue.max, - // }, - // }; - // } + const workflow = await this.workflowsService.getWorkflow(this.workflowId()); - /** - * TODO: put function to util or service? - * A similar function is used in the symbology component - // */ - // protected async estimateQueryParams(rasterOperator: TypedOperatorOperator): Promise<{ - // bbox: BBoxDict; - // crs: SrsString; - // time: TimeIntervalDict; - // spatialResolution: [number, number]; - // }> { - // const rasterWorkflowId = await firstValueFrom( - // this.projectService.registerWorkflow({ - // type: 'Raster', - // operator: rasterOperator as TypedOperatorOperator, - // }), - // ); + // TODO: remove expressions when Statistics Operator supports multiple bands + const subExpression = ({name, index}: {name: string; index: number}): ExpressionDict => { + return { + type: 'Expression', + params: { + expression: String.fromCharCode('A'.charCodeAt(0) + index), + outputType: 'F64', + outputBand: { + name, + measurement: {type: 'unitless'}, + }, + mapNoData: false, + }, + sources: { + raster: workflow.operator, + }, + } as ExpressionDict; + }; - // const resultDescriptorDict = await firstValueFrom(this.projectService.getWorkflowMetaData(rasterWorkflowId)); + const statsWorkflowId = await this.workflowsService.registerWorkflow({ + type: 'Plot', + operator: { + type: 'Statistics', + params: {columnNames: ['red', 'green', 'blue']} as StatisticsParams, + sources: { + source: [subExpression(this.band1()), subExpression(this.band2()), subExpression(this.band3())], + }, + } as StatisticsDict, + }); - // const resultDescriptor = RasterResultDescriptor.fromDict(resultDescriptorDict as RasterResultDescriptorDict); + const plot = await this.plotsService.getPlot( + statsWorkflowId, + queryParams.bbox, + queryParams.time, + queryParams.resolution, + queryParams.spatialReference, + ); - // let bbox = resultDescriptor.bbox; + const plotData = plot.data as { + red: { + min: number; + max: number; + }; + green: { + min: number; + max: number; + }; + blue: { + min: number; + max: number; + }; + }; - // if (!bbox) { - // // if we don't know the bbox of the dataset, we use the projection's whole bbox for guessing the symbology - // // TODO: better use the screen extent? - // bbox = await firstValueFrom( - // this.spatialReferenceService - // .getSpatialReferenceSpecification(resultDescriptor.spatialReference) - // .pipe(map((spatialReferenceSpecification) => extentToBboxDict(spatialReferenceSpecification.extent))), - // ); - // } + const colors: Array = ['red', 'green', 'blue']; + for (const color of colors) { + this.form.controls[color].controls.min.setValue(plotData[color].min); + this.form.controls[color].controls.max.setValue(plotData[color].max); + } - // const time = await this.projectService.getTimeOnce(); + this.form.updateValueAndValidity(); - // // for sampling, we choose a reasonable resolution - // const NUM_PIXELS = 1024; - // const xResolution = (bbox.upperRightCoordinate.x - bbox.lowerLeftCoordinate.x) / NUM_PIXELS; - // const yResolution = (bbox.upperRightCoordinate.y - bbox.lowerLeftCoordinate.y) / NUM_PIXELS; - // return { - // bbox, - // crs: resultDescriptor.spatialReference, - // time: time.toDict(), - // spatialResolution: [xResolution, yResolution], - // }; - // } + this.isLoadingRasterStats.set(false); + } } diff --git a/projects/common/src/lib/symbology/raster-symbology-editor/raster-symbology-editor.component.html b/projects/common/src/lib/symbology/raster-symbology-editor/raster-symbology-editor.component.html index 8de703476..c3de7b315 100644 --- a/projects/common/src/lib/symbology/raster-symbology-editor/raster-symbology-editor.component.html +++ b/projects/common/src/lib/symbology/raster-symbology-editor/raster-symbology-editor.component.html @@ -83,7 +83,18 @@ (colorizerChange)="updateMultiBandColorizer($event)" [queryParams]="queryParams()" [workflowId]="symbologyWorkflow().workflowId" - [band]="selectedBand()?.name | valueDefault: 'band'" + [band1]="{ + name: getSelectedBand(0)()?.name | valueDefault: '', + index: getSelectedBandIndex(0), + }" + [band2]="{ + name: getSelectedBand(1)()?.name | valueDefault: '', + index: getSelectedBandIndex(1), + }" + [band3]="{ + name: getSelectedBand(2)()?.name | valueDefault: '', + index: getSelectedBandIndex(2), + }" >
diff --git a/projects/common/src/lib/symbology/raster-symbology-editor/raster-symbology-editor.component.ts b/projects/common/src/lib/symbology/raster-symbology-editor/raster-symbology-editor.component.ts index 25f15d0c6..838724c7c 100644 --- a/projects/common/src/lib/symbology/raster-symbology-editor/raster-symbology-editor.component.ts +++ b/projects/common/src/lib/symbology/raster-symbology-editor/raster-symbology-editor.component.ts @@ -1,14 +1,6 @@ import { Component, - Input, ChangeDetectionStrategy, - OnInit, - OnDestroy, - OnChanges, - SimpleChanges, - ChangeDetectorRef, - Output, - EventEmitter, input, output, computed, @@ -16,7 +8,6 @@ import { effect, untracked, inject, - Signal, WritableSignal, } from '@angular/core'; import { @@ -29,13 +20,8 @@ import { import {Colorizer, ColorizerType, LinearGradient, LogarithmicGradient, PaletteColorizer} from '../../colors/colorizer.model'; import {BLACK, Color, TRANSPARENT, WHITE} from '../../colors/color'; import {ColorBreakpoint} from '../../colors/color-breakpoint.model'; -import {BehaviorSubject, Subscription, map} from 'rxjs'; -import { - BoundingBox2D, - RasterBandDescriptor, - TypedRasterResultDescriptor as RasterResultDescriptorDict, - SpatialResolution, -} from '@geoengine/openapi-client'; +import {BehaviorSubject} from 'rxjs'; +import {RasterBandDescriptor, TypedRasterResultDescriptor as RasterResultDescriptorDict} from '@geoengine/openapi-client'; import {WorkflowsService} from '../../workflows/workflows.service'; type RasterSymbologyType = 'singleBand' | 'multiBand'; @@ -273,16 +259,38 @@ export class RasterSymbologyEditorComponent { this.changedSymbology.emit(this.symbology()); } - private setUp() { - this.symbology.set(this.symbologyWorkflow().symbology.clone()); - const bandIndex = (this.symbology().rasterColorizer as SingleBandRasterColorizer).band; - this.workflowsService.getMetadata(this.symbologyWorkflow().workflowId).then((resultDescriptor) => { - if (resultDescriptor.type === 'raster') { - const rd = resultDescriptor as RasterResultDescriptorDict; - this.bands.set(rd.bands); - this.selectedBand.set(rd.bands[bandIndex]); - } - }); + protected async setUp() { + const symbologyWorkflow = this.symbologyWorkflow(); + const symbology = symbologyWorkflow.symbology.clone(); + + // TODO: loading indicator + const _resultDescriptor = await this.workflowsService.getMetadata(symbologyWorkflow.workflowId); + + if (_resultDescriptor.type !== 'raster') { + throw Error('expected raster result descriptor'); + } + + const resultDescriptor = _resultDescriptor as RasterResultDescriptorDict; + + let selectedBand = undefined; + let selectedBand2 = undefined; + let selectedBand3 = undefined; + if (symbology.rasterColorizer instanceof SingleBandRasterColorizer) { + selectedBand = resultDescriptor.bands[symbology.rasterColorizer.band]; + // just select some bands + selectedBand2 = resultDescriptor.bands[(symbology.rasterColorizer.band + 1) % resultDescriptor.bands.length]; + selectedBand3 = resultDescriptor.bands[(symbology.rasterColorizer.band + 2) % resultDescriptor.bands.length]; + } else if (symbology.rasterColorizer instanceof MultiBandRasterColorizer) { + selectedBand = resultDescriptor.bands[symbology.rasterColorizer.redBand]; + selectedBand2 = resultDescriptor.bands[symbology.rasterColorizer.greenBand]; + selectedBand3 = resultDescriptor.bands[symbology.rasterColorizer.blueBand]; + } + + this.symbology.set(symbology); + this.bands.set(resultDescriptor.bands); + this.selectedBand.set(selectedBand); + this.selectedBand2.set(selectedBand2); + this.selectedBand3.set(selectedBand3); } updateRasterSymbologyType($event: any): void { diff --git a/projects/common/src/lib/symbology/symbology.model.ts b/projects/common/src/lib/symbology/symbology.model.ts index 54f932ec8..d41ec8fd7 100644 --- a/projects/common/src/lib/symbology/symbology.model.ts +++ b/projects/common/src/lib/symbology/symbology.model.ts @@ -852,6 +852,35 @@ export class MultiBandRasterColorizer extends RasterColorizer { ); } + withParams( + redMin: number, + redMax: number, + redScale: number, + greenMin: number, + greenMax: number, + greenScale: number, + blueMin: number, + blueMax: number, + blueScale: number, + noDataColor: Color, + ): MultiBandRasterColorizer { + return new MultiBandRasterColorizer( + this.redBand, + this.greenBand, + this.blueBand, + redMin, + redMax, + redScale, + greenMin, + greenMax, + greenScale, + blueMin, + blueMax, + blueScale, + noDataColor, + ); + } + override toDict(): MultiBandRasterColorizerDict { return { type: 'multiBand', From 4fa2f70ede92c85643285b622f9b3c0d50fcabcb Mon Sep 17 00:00:00 2001 From: Christian Beilschmidt Date: Mon, 11 Nov 2024 11:12:15 +0100 Subject: [PATCH 08/13] dict fix --- .../accordion-vector-entry.component.ts | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/projects/dashboards/data-atlas/src/app/accordion-vector-entry/accordion-vector-entry.component.ts b/projects/dashboards/data-atlas/src/app/accordion-vector-entry/accordion-vector-entry.component.ts index d61e72ad1..efdb1e287 100644 --- a/projects/dashboards/data-atlas/src/app/accordion-vector-entry/accordion-vector-entry.component.ts +++ b/projects/dashboards/data-atlas/src/app/accordion-vector-entry/accordion-vector-entry.component.ts @@ -1,17 +1,18 @@ import {Component, OnInit, ChangeDetectionStrategy, Input, ChangeDetectorRef} from '@angular/core'; import {mergeMap, BehaviorSubject, of, forkJoin, from} from 'rxjs'; -import { - LayerCollectionLayerDict, - ProjectService, - ProviderLayerCollectionIdDict, - UUID, - VectorResultDescriptorDict, - VectorSymbologyDict, -} from '@geoengine/core'; +import {LayerCollectionLayerDict, ProjectService, ProviderLayerCollectionIdDict, UUID, VectorResultDescriptorDict} from '@geoengine/core'; import {DataSelectionService} from '../data-selection.service'; import moment from 'moment'; import {Layer as LayerDict} from '@geoengine/openapi-client'; -import {LayersService, RandomColorService, Time, VectorLayer, VectorSymbology, createVectorSymbology} from '@geoengine/common'; +import { + LayersService, + RandomColorService, + Time, + VectorLayer, + VectorSymbology, + VectorSymbologyDict, + createVectorSymbology, +} from '@geoengine/common'; @Component({ selector: 'geoengine-accordion-vector-entry', From 4fc82afeb12b86a5e4e2405c0212cee14ebfb5ef Mon Sep 17 00:00:00 2001 From: Christian Beilschmidt Date: Mon, 11 Nov 2024 11:36:03 +0100 Subject: [PATCH 09/13] multiband symbology icon --- ...ayer-collection-layer-details.component.ts | 24 ++-- .../raster-icon/raster-icon.component.svg | 2 +- .../raster-icon/raster-icon.component.ts | 112 +++++++++--------- .../layer-list-element.component.html | 2 +- .../layer-list-element.component.ts | 6 +- 5 files changed, 77 insertions(+), 69 deletions(-) diff --git a/projects/common/src/lib/layer-collections/layer-collection-layer-details/layer-collection-layer-details.component.ts b/projects/common/src/lib/layer-collections/layer-collection-layer-details/layer-collection-layer-details.component.ts index 6d7dd320b..44320e780 100644 --- a/projects/common/src/lib/layer-collections/layer-collection-layer-details/layer-collection-layer-details.component.ts +++ b/projects/common/src/lib/layer-collections/layer-collection-layer-details/layer-collection-layer-details.component.ts @@ -2,6 +2,7 @@ import {ChangeDetectionStrategy, ChangeDetectorRef, Component, Input} from '@ang import {LayerMetadata, RasterLayerMetadata, VectorLayerMetadata} from '../../layers/layer-metadata.model'; import {VectorDataTypes} from '../../operators/datatype.model'; import {Colorizer} from '../../colors/colorizer.model'; +import {SingleBandRasterColorizer} from '../../symbology/symbology.model'; @Component({ selector: 'geoengine-layer-collection-layer-details', @@ -15,16 +16,19 @@ export class LayerCollectionLayerDetailsComponent { readonly VectorDataTypes = VectorDataTypes; - readonly rasterColorizer = Colorizer.fromDict({ - type: 'linearGradient', - breakpoints: [ - {value: 0, color: [122, 122, 122, 255]}, - {value: 1, color: [255, 255, 255, 255]}, - ], - overColor: [255, 255, 255, 127], - underColor: [122, 122, 122, 127], - noDataColor: [0, 0, 0, 0], - }); + readonly rasterColorizer = new SingleBandRasterColorizer( + 0, + Colorizer.fromDict({ + type: 'linearGradient', + breakpoints: [ + {value: 0, color: [122, 122, 122, 255]}, + {value: 1, color: [255, 255, 255, 255]}, + ], + overColor: [255, 255, 255, 127], + underColor: [122, 122, 122, 127], + noDataColor: [0, 0, 0, 0], + }), + ); constructor() {} diff --git a/projects/common/src/lib/layer-icons/raster-icon/raster-icon.component.svg b/projects/common/src/lib/layer-icons/raster-icon/raster-icon.component.svg index dfef26564..658ff8294 100644 --- a/projects/common/src/lib/layer-icons/raster-icon/raster-icon.component.svg +++ b/projects/common/src/lib/layer-icons/raster-icon/raster-icon.component.svg @@ -1,6 +1,6 @@ - (); + /** number of cells in y direction */ + readonly yCells = input.required(); + /** the raster style used to color the icon */ + readonly colorizer = input.required(); + /** This is the number of pixels used for the icon */ + readonly cellSpace = input(24); /** * the array of generated (colored and positioned) cells. */ - cells: Array = []; - /** - * This is the number of pixels used for the icon - */ - cellSpace = 24; - - static RAINBOW_COLORIZER: Colorizer = new LinearGradient( - [ - new ColorBreakpoint(0, Color.fromRgbaLike('#ff0000')), - new ColorBreakpoint(0, Color.fromRgbaLike('#ffa500')), - new ColorBreakpoint(0, Color.fromRgbaLike('#ffff00')), - new ColorBreakpoint(0, Color.fromRgbaLike('#008000')), - new ColorBreakpoint(0, Color.fromRgbaLike('#0000ff')), - new ColorBreakpoint(0, Color.fromRgbaLike('#4b0082')), - new ColorBreakpoint(0, Color.fromRgbaLike('#ee82ee')), - ], - TRANSPARENT, - TRANSPARENT, - TRANSPARENT, - ); + cells = computed>(() => { + const rasterColorizer = this.colorizer(); + const cellSpace = this.cellSpace(); + const xCells = this.xCells(); + const yCells = this.yCells(); - ngOnChanges(_changes: {[propertyName: string]: SimpleChange}): void { - this.generateCells(this.xCells, this.yCells); - } + let colorizer: Colorizer; + if (rasterColorizer instanceof SingleBandRasterColorizer) { + colorizer = rasterColorizer.bandColorizer; + } else { + colorizer = RAINBOW_COLORIZER; + } - ngOnInit(): void { - this.generateCells(this.xCells, this.yCells); - } + return RasterIconComponent.generateCells(colorizer, cellSpace, xCells, yCells); + }); /** * generates an array of cell descriptors which are used by the template */ - generateCells(xCells: number, yCells: number): void { - this.cells = new Array(xCells * yCells); - for (let y = 0; y < this.yCells; y++) { - for (let x = 0; x < this.xCells; x++) { - const idx = this.xCells * y + x; - this.cells[idx] = { - xStart: (x * this.cellSpace) / this.xCells, - yStart: (y * this.cellSpace) / this.yCells, - xSize: this.cellSpace / this.xCells, - ySize: this.cellSpace / this.yCells, - colorString: Color.rgbaToCssString(this.cellColor(x, y)), + static generateCells(colorizer: Colorizer, cellSpace: number, xCells: number, yCells: number): Array { + const cells = new Array(xCells * yCells); + for (let y = 0; y < yCells; y++) { + for (let x = 0; x < xCells; x++) { + const idx = xCells * y + x; + cells[idx] = { + xStart: (x * cellSpace) / xCells, + yStart: (y * cellSpace) / yCells, + xSize: cellSpace / xCells, + ySize: cellSpace / yCells, + colorString: Color.rgbaToCssString(RasterIconComponent.cellColor(colorizer, xCells, yCells, x, y)), }; } } + return cells; } - private cellColor(x: number, y: number): Color { - let colorizer = this.colorizer; - - // TODO: multiband - // if (this.colorizer instanceof RgbaColorizer) { - // colorizer = RasterIconComponent.RAINBOW_COLORIZER; - // } - + private static cellColor(colorizer: Colorizer, xCells: number, yCells: number, x: number, y: number): Color { const validSymbology = colorizer && colorizer.getNumberOfColors() > 0; if (!validSymbology) { return Color.fromRgbaLike('#ff0000'); } - const numberOfCells = this.xCells * this.yCells; + const numberOfCells = xCells * yCells; const numberOfColors = colorizer.getNumberOfColors(); const isGradient = colorizer.isGradient(); - const scale = isGradient ? numberOfColors / (this.xCells + this.yCells - 1) : numberOfColors / numberOfCells; - const idx = y * this.xCells + x; + const scale = isGradient ? numberOfColors / (xCells + yCells - 1) : numberOfColors / numberOfCells; + const idx = y * xCells + x; let colorIdx = 0; if (numberOfColors === 2) { colorIdx = y % 2 === 0 ? x % 2 : (x + 1) % 2; } else { - const uidx = isGradient ? this.xCells - 1 - x + y : idx; + const uidx = isGradient ? xCells - 1 - x + y : idx; colorIdx = Math.trunc(uidx * scale) % numberOfColors; } return colorizer.getColorAtIndex(colorIdx); } } + +export const RAINBOW_COLORIZER: Colorizer = new LinearGradient( + [ + new ColorBreakpoint(0, Color.fromRgbaLike('#ff0000')), + new ColorBreakpoint(0, Color.fromRgbaLike('#ffa500')), + new ColorBreakpoint(0, Color.fromRgbaLike('#ffff00')), + new ColorBreakpoint(0, Color.fromRgbaLike('#008000')), + new ColorBreakpoint(0, Color.fromRgbaLike('#0000ff')), + new ColorBreakpoint(0, Color.fromRgbaLike('#4b0082')), + new ColorBreakpoint(0, Color.fromRgbaLike('#ee82ee')), + ], + TRANSPARENT, + TRANSPARENT, + TRANSPARENT, +); diff --git a/projects/core/src/lib/layers/layer-list/layer-list-element/layer-list-element.component.html b/projects/core/src/lib/layers/layer-list/layer-list-element/layer-list-element.component.html index f8a5e46c2..8281b7364 100644 --- a/projects/core/src/lib/layers/layer-list/layer-list-element/layer-list-element.component.html +++ b/projects/core/src/lib/layers/layer-list/layer-list-element/layer-list-element.component.html @@ -99,7 +99,7 @@ diff --git a/projects/core/src/lib/layers/layer-list/layer-list-element/layer-list-element.component.ts b/projects/core/src/lib/layers/layer-list/layer-list-element/layer-list-element.component.ts index d730f82f8..00d4fae21 100644 --- a/projects/core/src/lib/layers/layer-list/layer-list-element/layer-list-element.component.ts +++ b/projects/core/src/lib/layers/layer-list/layer-list-element/layer-list-element.component.ts @@ -17,7 +17,7 @@ import {BackendService} from '../../../backend/backend.service'; import {UserService} from '../../../users/user.service'; import {HttpEventType} from '@angular/common/http'; import {filenameFromHttpHeaders} from '../../../util/http'; -import {IconStyle, Layer, RasterLayerMetadata, Symbology, SymbologyType} from '@geoengine/common'; +import {IconStyle, Layer, RasterLayerMetadata, RasterSymbology, Symbology, SymbologyType} from '@geoengine/common'; import {RasterBandDescriptor} from '@geoengine/openapi-client'; import {SymbologyEditorComponent} from '../../symbology/symbology-editor/symbology-editor.component'; import {DownloadLayerComponent} from '../../../download-layer/download-layer.component'; @@ -184,4 +184,8 @@ export class LayerListElementComponent { showDownload(layer: Layer): void { this.layoutService.setSidenavContentComponent({component: DownloadLayerComponent, config: {layer}}); } + + rasterSymbology(layer: Layer): RasterSymbology { + return layer.symbology as RasterSymbology; + } } From 956b717dea8a94e442b158762e47fdc8c538075b Mon Sep 17 00:00:00 2001 From: Christian Beilschmidt Date: Thu, 14 Nov 2024 12:26:54 +0100 Subject: [PATCH 10/13] rgb legend --- projects/core/src/lib/core.module.ts | 20 +- .../raster-legend.component.html | 126 ++++++------ .../legend-raster/raster-legend.component.ts | 180 ++++++++++-------- 3 files changed, 172 insertions(+), 154 deletions(-) diff --git a/projects/core/src/lib/core.module.ts b/projects/core/src/lib/core.module.ts index 73556ed7e..3ddd22855 100644 --- a/projects/core/src/lib/core.module.ts +++ b/projects/core/src/lib/core.module.ts @@ -54,11 +54,7 @@ import {RenameLayerComponent} from './layers/rename-layer/rename-layer.component import {VectorLegendComponent} from './layers/legend/legend-vector/vector-legend.component'; import {LayerListComponent} from './layers/layer-list/layer-list.component'; import {LayerListElementComponent} from './layers/layer-list/layer-list-element/layer-list-element.component'; -import { - CastMeasurementToClassificationPipe, - CastMeasurementToContinuousPipe, - RasterLegendComponent, -} from './layers/legend/legend-raster/raster-legend.component'; +import {RasterLegendComponent} from './layers/legend/legend-raster/raster-legend.component'; import {SafeStylePipe} from './util/pipes/safe-style.pipe'; import {SmallTimeInteractionComponent} from './time/small-time-interaction/small-time-interaction.component'; import {TimeConfigComponent} from './time/time-config/time-config.component'; @@ -174,16 +170,7 @@ export const MATERIAL_MODULES = [ MatTooltipModule, ]; -const CORE_PIPES = [ - CastMeasurementToClassificationPipe, - CastMeasurementToContinuousPipe, - CssStringToRgbaPipe, - HighlightPipe, - RgbaToCssStringPipe, - SafeHtmlPipe, - SafeStylePipe, - TrimPipe, -]; +const CORE_PIPES = [CssStringToRgbaPipe, HighlightPipe, RgbaToCssStringPipe, SafeHtmlPipe, SafeStylePipe, TrimPipe]; const CORE_COMPONENTS = [ AddDataComponent, @@ -249,8 +236,6 @@ const CORE_COMPONENTS = [ ProvenanceTableComponent, QuotaInfoComponent, RasterizationComponent, - RasterLegendComponent, - RasterLegendComponent, RasterScalingComponent, RasterStackerComponent, RasterTypeConversionComponent, @@ -307,6 +292,7 @@ const CORE_COMPONENTS = [ PortalModule, ReactiveFormsModule, ScrollingModule, + RasterLegendComponent, ], providers: [ {provide: MAT_FORM_FIELD_DEFAULT_OPTIONS, useValue: {appearance: 'fill'}}, diff --git a/projects/core/src/lib/layers/legend/legend-raster/raster-legend.component.html b/projects/core/src/lib/layers/legend/legend-raster/raster-legend.component.html index 16802c49c..b86a764d9 100644 --- a/projects/core/src/lib/layers/legend/legend-raster/raster-legend.component.html +++ b/projects/core/src/lib/layers/legend/legend-raster/raster-legend.component.html @@ -1,59 +1,77 @@ - - - - -
- @if (showSingleBandNames) { - - Bandname: {{ band.name }} - } +@if (selectedBands(); as bands) { + + + + + - - - - - - - - + @if (bandsHaveUnits()) { + Measurements: + @for (band of bands; track band.name) { + @if (band.measurement | continuousMeasurement; as measurement) { + + {{ measurement.measurement }} (in {{ measurement.unit }}), + } + @if (band.measurement | classificationMeasurement; as measurement) { + {{ measurement.measurement }}, + } + @if (band.measurement | unitlessMeasurement; as measurement) { + unitless, + } + } + } + - - - - - - - + + + + + + + - + - - - - - - + + + + + + + + + + + + + + + - - -
+ @if (showBandNames()) { + + Bandnames: + + @for (band of bands; track band.name) { + {{ band.name }}, + } + + + } - - Measurement: - {{ measurement.measurement }} (in {{ measurement.unit }}) - - - Measurement {{ measurement.measurement }} - -
{{ breakpoint }}
-
-
{{ breakpoint.value }}
{{ breakpoint }}
-
-
{{ breakpoint.value }}{{ measurement.classes[breakpoint.value] }}
+
+
{{ breakpoint.value }}
+
+
{{ breakpoint.value }}{{ measurement.classes[breakpoint.value] }}
+ +
+} @else { + + +} diff --git a/projects/core/src/lib/layers/legend/legend-raster/raster-legend.component.ts b/projects/core/src/lib/layers/legend/legend-raster/raster-legend.component.ts index 469bb0a33..7e05eb745 100644 --- a/projects/core/src/lib/layers/legend/legend-raster/raster-legend.component.ts +++ b/projects/core/src/lib/layers/legend/legend-raster/raster-legend.component.ts @@ -1,20 +1,16 @@ -import { - AfterViewInit, - ChangeDetectionStrategy, - ChangeDetectorRef, - Component, - Input, - OnChanges, - OnDestroy, - OnInit, - Pipe, - PipeTransform, - SimpleChanges, -} from '@angular/core'; +import {ChangeDetectionStrategy, Component, computed, effect, inject, input, Pipe, PipeTransform, signal, untracked} from '@angular/core'; import {ProjectService} from '../../../project/project.service'; -import {Subject, Subscription, first} from 'rxjs'; -import {ColorBreakpoint, RasterLayer, RasterLayerMetadata, SingleBandRasterColorizer} from '@geoengine/common'; -import {RasterBandDescriptor, Measurement, ContinuousMeasurement, ClassificationMeasurement} from '@geoengine/openapi-client'; +import {firstValueFrom} from 'rxjs'; +import {ColorBreakpoint, CommonModule, MultiBandRasterColorizer, RasterLayer, SingleBandRasterColorizer} from '@geoengine/common'; +import { + RasterBandDescriptor, + Measurement, + ContinuousMeasurement, + ClassificationMeasurement, + UnitlessMeasurement, +} from '@geoengine/openapi-client'; +import {MatProgressSpinner} from '@angular/material/progress-spinner'; +import {CommonModule as AngularCommonModule} from '@angular/common'; /** * calculate the decimal places for the legend of raster data @@ -40,6 +36,7 @@ export function calculateNumberPipeParameters(breakpoints: Array Math.floor(x)).sort((a, b) => a - b))) { @@ -122,80 +135,81 @@ export function oneApart(values: number[]): boolean { templateUrl: 'raster-legend.component.html', styleUrls: ['raster-legend.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush, + imports: [ + AngularCommonModule, + CastMeasurementToUnitlessPipe, + CastMeasurementToClassificationPipe, + CastMeasurementToContinuousPipe, + MatProgressSpinner, + CommonModule, + ], + standalone: true, }) -export class RasterLegendComponent implements OnInit, OnChanges, OnDestroy, AfterViewInit { - @Input() layer!: RasterLayer; - selectedBand$ = new Subject(); - displayedBreakpoints: number[] = []; - - @Input() - orderValuesDescending = false; - - @Input() - showSingleBandNames = true; - - @Input() - detectChanges = false; // workaround to force change detection - - private layerMetaDataSubscription?: Subscription; - - constructor( - public projectService: ProjectService, - private changeDetectorRef: ChangeDetectorRef, - ) {} - - ngOnInit(): void { - this.calculateDisplayedBreakpoints(); - } - - ngAfterViewInit(): void { - this.layerMetaDataSubscription = this.projectService - .getLayerMetadata(this.layer) - .pipe(first()) - .subscribe((m) => { - const bands = (m as RasterLayerMetadata).bands; - const bandIndex = (this.layer.symbology.rasterColorizer as SingleBandRasterColorizer).band; - this.selectedBand$.next(bands[bandIndex]); - - if (this.detectChanges) { - this.changeDetectorRef.detectChanges(); - } - }); - } - - ngOnChanges(changes: SimpleChanges): void { - if (changes.layer || changes.orderValuesDescending) { - this.calculateDisplayedBreakpoints(); +export class RasterLegendComponent { + private readonly projectService = inject(ProjectService); + + readonly layer = input.required(); + readonly orderValuesDescending = input(false); + readonly showBandNames = input(true); + + readonly selectedBands = signal | undefined>(undefined); + readonly displayedBreakpoints = computed>(() => + calculateDisplayedBreakpoints(this.layer(), this.orderValuesDescending()), + ); + readonly colorizerBreakpoints = computed>(() => { + const layer = this.layer(); + if (this.orderValuesDescending()) { + return layer.symbology.rasterColorizer.getBreakpoints().slice().reverse(); + } else { + return layer.symbology.rasterColorizer.getBreakpoints(); } - if (!changes.showSingleBandNames == undefined) { - this.changeDetectorRef.markForCheck(); + }); + readonly gradientAngle = computed(() => (this.orderValuesDescending() ? 0 : 180)); + readonly bandsHaveUnits = computed(() => { + const selectedBands = this.selectedBands(); + if (!selectedBands) { + return false; } - } + return selectedBands.some((band) => band.measurement.type !== 'unitless'); + }); - ngOnDestroy(): void { - if (this.layerMetaDataSubscription) { - this.layerMetaDataSubscription.unsubscribe(); - } + constructor() { + effect(() => { + const layer = this.layer(); + untracked(() => { + firstValueFrom(this.projectService.getRasterLayerMetadata(layer)).then((metadata) => { + const bands = metadata.bands; + const rasterColorizer = layer.symbology.rasterColorizer; + + let bandDescriptors: Array; + if (rasterColorizer instanceof SingleBandRasterColorizer) { + bandDescriptors = [bands[rasterColorizer.band]]; + } else if (rasterColorizer instanceof MultiBandRasterColorizer) { + bandDescriptors = [ + bands[rasterColorizer.redBand], + bands[rasterColorizer.greenBand], + bands[rasterColorizer.blueBand], + ]; + } else { + throw new Error('Unknown raster colorizer'); + } + this.selectedBands.set(bandDescriptors); + }); + }); + }); } +} - protected calculateDisplayedBreakpoints(): void { - this.displayedBreakpoints = this.layer.symbology.rasterColorizer.getBreakpoints().map((x) => x.value); - this.displayedBreakpoints = unifyDecimals(this.displayedBreakpoints); - - if (this.orderValuesDescending) { - this.displayedBreakpoints = this.displayedBreakpoints.reverse(); - } - } +/** + * Calculate the displayed breakpoints for the legend + */ +function calculateDisplayedBreakpoints(layer: RasterLayer, orderValuesDescending: boolean): Array { + let displayedBreakpoints = layer.symbology.rasterColorizer.getBreakpoints().map((x) => x.value); + displayedBreakpoints = unifyDecimals(displayedBreakpoints); - get gradientAngle(): number { - return this.orderValuesDescending ? 0 : 180; + if (orderValuesDescending) { + displayedBreakpoints = displayedBreakpoints.reverse(); } - get colorizerBreakpoints(): Array { - if (this.orderValuesDescending) { - return this.layer.symbology.rasterColorizer.getBreakpoints().slice().reverse(); - } else { - return this.layer.symbology.rasterColorizer.getBreakpoints(); - } - } + return displayedBreakpoints; } From 9838243a6f6992aa9d0f4ee1007567b92b7b4e20 Mon Sep 17 00:00:00 2001 From: Christian Beilschmidt Date: Thu, 14 Nov 2024 12:32:53 +0100 Subject: [PATCH 11/13] imports --- projects/dashboards/data-atlas/src/app/app.module.ts | 2 ++ .../dashboards/ebv-analyzer/src/app/app.module.ts | 2 ++ .../src/app/legend/legend.component.html | 7 +------ .../ecometrics/src/app/legend/legend.component.html | 7 +------ .../nfdi-portal-demo/src/app/app.module.ts | 12 +++++++++++- 5 files changed, 17 insertions(+), 13 deletions(-) diff --git a/projects/dashboards/data-atlas/src/app/app.module.ts b/projects/dashboards/data-atlas/src/app/app.module.ts index 5e30f0fac..354f89f3a 100644 --- a/projects/dashboards/data-atlas/src/app/app.module.ts +++ b/projects/dashboards/data-atlas/src/app/app.module.ts @@ -15,6 +15,7 @@ import { UserService, CoreModule, CoreConfig, + RasterLegendComponent, } from '@geoengine/core'; import {AppConfig} from './app-config.service'; import {PortalModule} from '@angular/cdk/portal'; @@ -53,6 +54,7 @@ import {CommonConfig} from '@geoengine/common'; PortalModule, AppRoutingModule, NgxMatSelectSearchModule, + RasterLegendComponent, ], providers: [ AppConfig, diff --git a/projects/dashboards/ebv-analyzer/src/app/app.module.ts b/projects/dashboards/ebv-analyzer/src/app/app.module.ts index e8ee0a13d..1596b50f0 100644 --- a/projects/dashboards/ebv-analyzer/src/app/app.module.ts +++ b/projects/dashboards/ebv-analyzer/src/app/app.module.ts @@ -14,6 +14,7 @@ import { UserService, CoreModule, CoreConfig, + RasterLegendComponent, } from '@geoengine/core'; import {AppConfig} from './app-config.service'; import {PortalModule} from '@angular/cdk/portal'; @@ -38,6 +39,7 @@ import {CommonConfig} from '@geoengine/common'; PortalModule, NgxMatSelectSearchModule, LayoutModule, + RasterLegendComponent, ], providers: [ AppConfig, diff --git a/projects/dashboards/ebv-analyzer/src/app/legend/legend.component.html b/projects/dashboards/ebv-analyzer/src/app/legend/legend.component.html index facbd96e2..571e93eac 100644 --- a/projects/dashboards/ebv-analyzer/src/app/legend/legend.component.html +++ b/projects/dashboards/ebv-analyzer/src/app/legend/legend.component.html @@ -1,10 +1,5 @@

Legend

- +
diff --git a/projects/dashboards/ecometrics/src/app/legend/legend.component.html b/projects/dashboards/ecometrics/src/app/legend/legend.component.html index c31a90cba..b18e169fa 100644 --- a/projects/dashboards/ecometrics/src/app/legend/legend.component.html +++ b/projects/dashboards/ecometrics/src/app/legend/legend.component.html @@ -7,12 +7,7 @@

Legend

@if (layer(); as layer) { - + } } @else {