Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
giswqs
GitHub Repository: giswqs/geemap
Path: blob/master/js/raster_layer_editor.ts
2313 views
1
import {
2
css,
3
html,
4
nothing,
5
LitElement,
6
PropertyValues,
7
TemplateResult,
8
} from "lit";
9
import { property, query, queryAll } from "lit/decorators.js";
10
11
import { legacyStyles } from "./ipywidgets_styles";
12
import { LegendCustomization } from "./legend_customization";
13
import { materialStyles, flexStyles } from "./styles";
14
import { SelectOption, renderSelect } from "./utils";
15
import { PaletteEditor } from "./palette_editor";
16
17
import "./legend_customization";
18
import "./palette_editor";
19
20
enum ColorModel {
21
RGB = "rgb",
22
Gray = "gray",
23
}
24
25
enum ColorRamp {
26
Palette = "palette",
27
Gamma = "gamma",
28
}
29
30
export class RasterLayerEditor extends LitElement {
31
static get componentName() {
32
return `raster-layer-editor`;
33
}
34
35
static override styles = [
36
flexStyles,
37
legacyStyles,
38
materialStyles,
39
css`
40
.horizontal-flex > button {
41
height: 28px;
42
width: 28px;
43
}
44
`,
45
];
46
47
@property({ type: Array }) stretchOptions: Array<SelectOption> = [
48
{ label: "Custom", value: "custom" },
49
{ label: "1σ", value: "sigma-1" },
50
{ label: "2σ", value: "sigma-2" },
51
{ label: "3σ", value: "sigma-3" },
52
{ label: "90%", value: "percent-90" },
53
{ label: "98%", value: "percent-98" },
54
{ label: "100%", value: "percent-100" },
55
];
56
@property({ type: Array }) colorModels: Array<SelectOption> = [
57
{ label: "1 band (Grayscale)", value: ColorModel.Gray },
58
{ label: "3 bands (RGB)", value: ColorModel.RGB },
59
];
60
@property({ type: Array }) colorRamps: Array<SelectOption> = [
61
{ label: "Gamma", value: ColorRamp.Gamma },
62
{ label: "Palette", value: ColorRamp.Palette },
63
];
64
65
@property({ type: String }) colorModel: string = "";
66
@property({ type: Array }) bandNames: Array<string> = [];
67
@property({ type: Array }) selectedBands: Array<string> = [];
68
@property({ type: String }) stretch: string = this.stretchOptions[0].value;
69
@property({ type: Number }) minValue: number | undefined = 0.0; // undefined is loading state.
70
@property({ type: Number }) maxValue: number | undefined = 1.0; // undefined is loading state.
71
@property({ type: Boolean }) minAndMaxValuesLocked: boolean = false;
72
@property({ type: Number }) opacity: number = 1.0;
73
@property({ type: String }) colorRamp: string = ColorRamp.Gamma;
74
@property({ type: Number }) gamma: number = 1.0;
75
@property({ type: Array }) colormaps: Array<string> = [];
76
77
@query("palette-editor") paletteEditor?: PaletteEditor;
78
@query("legend-customization") legendCustomization?: LegendCustomization;
79
@queryAll("#band-selection select") bandSelects!: NodeListOf<HTMLInputElement>;
80
81
@query("#min") private minInput!: HTMLInputElement;
82
@query("#max") private maxInput!: HTMLInputElement;
83
84
override connectedCallback() {
85
super.connectedCallback();
86
this.colorModel =
87
this.bandNames.length > 1 ? ColorModel.RGB : ColorModel.Gray;
88
}
89
90
getVisualizationOptions(): any {
91
const visOptions = {
92
bands: this.selectedBands,
93
min: this.minValue,
94
max: this.maxValue,
95
opacity: this.opacity,
96
} as any;
97
if (this.colorModel === ColorModel.Gray) {
98
if (this.colorRamp === ColorRamp.Palette) {
99
visOptions.palette = this.paletteEditor?.paletteTokens ?? [];
100
} else if (this.colorRamp === ColorRamp.Gamma) {
101
visOptions.gamma = this.gamma;
102
}
103
} else if (this.colorModel === ColorModel.RGB) {
104
visOptions.gamma = this.gamma;
105
}
106
if (this.legendCustomization) {
107
visOptions.legend = this.legendCustomization.getLegendData();
108
}
109
return visOptions;
110
}
111
112
override render(): TemplateResult {
113
return html`
114
<div class="vertical-flex">
115
<div class="horizontal-flex">
116
${this.colorModels.map((model) =>
117
this.renderColorModelRadio(model)
118
)}
119
</div>
120
<div id="band-selection" class="horizontal-flex">
121
${this.selectedBands.map((band) =>
122
this.renderBandSelection(band)
123
)}
124
</div>
125
<div class="horizontal-flex">
126
<span class="legacy-text">Stretch:</span>
127
${renderSelect(
128
this.stretchOptions,
129
this.stretch,
130
this.onStretchChanged
131
)}
132
<button
133
class="legacy-button"
134
@click="${this.onRefreshButtonClicked}"
135
>
136
<span class="material-symbols-outlined">refresh</span>
137
</button>
138
</div>
139
<div class="horizontal-flex">
140
<span class="legacy-text">Range:</span>
141
<input
142
type="text"
143
class="legacy-text-input"
144
id="min"
145
name="min"
146
.value="${this.minValue ?? "Loading..."}"
147
@keydown="${this.onInputKeyDown}"
148
@change="${this.onMinTextChanged}"
149
?disabled="${this.minAndMaxValuesLocked}"
150
/>
151
<span class="legacy-text">to</span>
152
<input
153
type="text"
154
class="legacy-text-input"
155
id="max"
156
name="max"
157
.value="${this.maxValue ?? "Loading..."}"
158
@keydown="${this.onInputKeyDown}"
159
@change="${this.onMaxTextChanged}"
160
?disabled="${this.minAndMaxValuesLocked}"
161
/>
162
</div>
163
<div class="horizontal-flex">
164
<span class="legacy-text">Opacity:</span>
165
<input
166
type="range"
167
class="legacy-slider"
168
id="opacity"
169
name="opacity"
170
min="0"
171
max="1.0"
172
step="0.01"
173
.value=${this.opacity}
174
@input=${this.onOpacityChanged}
175
/>
176
<span class="legacy-text">${this.opacity.toFixed(2)}</span>
177
</div>
178
${this.renderPaletteGammaSelector()}
179
${this.renderPaletteEditor()}
180
${this.renderGammaSlider()}
181
${this.renderLegendCustomization()}
182
</div>
183
`;
184
}
185
186
private renderPaletteGammaSelector(): TemplateResult | typeof nothing {
187
if (this.colorModel === ColorModel.Gray) {
188
return html`
189
<div class="horizontal-flex">
190
${this.colorRamps.map((model) =>
191
this.renderColorRampRadio(model)
192
)}
193
</div>
194
`;
195
}
196
return nothing;
197
}
198
199
private renderPaletteEditor(): TemplateResult | typeof nothing {
200
if (
201
this.colorRamp === ColorRamp.Palette &&
202
this.colorModel === ColorModel.Gray
203
) {
204
return html`
205
<palette-editor .colormaps="${this.colormaps}">
206
<slot></slot>
207
</palette-editor>
208
`;
209
}
210
return nothing;
211
}
212
213
private renderLegendCustomization(): TemplateResult | typeof nothing {
214
if (
215
this.colorRamp === ColorRamp.Palette &&
216
this.colorModel === ColorModel.Gray
217
) {
218
return html`<legend-customization></legend-customization>`;
219
}
220
return nothing;
221
}
222
223
private renderGammaSlider(): TemplateResult | typeof nothing {
224
if (
225
this.colorRamp === ColorRamp.Gamma ||
226
this.colorModel === ColorModel.RGB
227
) {
228
return html`
229
<div class="horizontal-flex">
230
<span class="legacy-text">Gamma:</span>
231
<input
232
type="range"
233
class="legacy-slider"
234
id="gamma"
235
name="gamma"
236
min="0.1"
237
max="10"
238
step="0.01"
239
.value=${this.gamma}
240
@input=${this.onGammaChanged}
241
/>
242
<span class="legacy-text">${this.gamma.toFixed(2)}</span>
243
</div>
244
`;
245
}
246
return nothing;
247
}
248
249
private renderColorModelRadio(option: SelectOption): TemplateResult {
250
return html`
251
<span>
252
<input
253
type="radio"
254
class="legacy-radio"
255
id="${option.value}"
256
name="color-model"
257
value="${option.value}"
258
@click="${this.onColorModelChanged}"
259
?checked="${this.colorModel === option.value}"
260
/>
261
<label class="legacy-text">${option.label}</label>
262
</span>
263
`;
264
}
265
266
private renderColorRampRadio(option: SelectOption): TemplateResult {
267
return html`
268
<span>
269
<input
270
type="radio"
271
class="legacy-radio"
272
id="${option.value}"
273
name="color-ramp"
274
value="${option.value}"
275
@click="${this.onColorRampChanged}"
276
?checked="${this.colorRamp === option.value}"
277
/>
278
<label class="legacy-text">${option.label}</label>
279
</span>
280
`;
281
}
282
283
private onColorRampChanged(event: Event): void {
284
this.colorRamp = (event.target as HTMLInputElement).value;
285
}
286
287
private renderBandSelection(value: string): TemplateResult {
288
return renderSelect(this.bandNames, value, this.onBandSelectionChanged);
289
}
290
291
private onRefreshButtonClicked(event: Event): void {
292
event.stopImmediatePropagation();
293
this.calculateBandStats();
294
}
295
296
private onOpacityChanged(event: Event): void {
297
this.opacity = (event.target as HTMLInputElement).valueAsNumber;
298
}
299
300
private onGammaChanged(event: Event): void {
301
this.gamma = (event.target as HTMLInputElement).valueAsNumber;
302
}
303
304
private onInputKeyDown(event: KeyboardEvent): void {
305
event.stopPropagation(); // Prevent the event from bubbling up to the document.
306
}
307
308
private onMinTextChanged(_event: Event): void {
309
this.minValue = +this.minInput.value; // Convert input string to number.
310
}
311
312
private onMaxTextChanged(_event: Event): void {
313
this.maxValue = +this.maxInput.value; // Convert input string to number.
314
}
315
316
private calculateBandStats(): void {
317
if (this.stretch === "custom") {
318
return;
319
}
320
this.minValue = undefined;
321
this.maxValue = undefined;
322
this.dispatchEvent(
323
new CustomEvent("calculate-band-stats", {
324
bubbles: true,
325
composed: true,
326
})
327
);
328
}
329
330
private onStretchChanged(event: Event): void {
331
this.stretch = (event.target as HTMLInputElement).value;
332
this.minAndMaxValuesLocked = this.stretch !== "custom";
333
}
334
335
private onBandSelectionChanged(_event: Event): void {
336
this.selectedBands = this.getSelectedBands();
337
}
338
339
private onColorModelChanged(event: Event): void {
340
this.colorModel = (event.target as HTMLInputElement).value;
341
}
342
343
override updated(changedProperties: PropertyValues<RasterLayerEditor>): void {
344
super.updated(changedProperties);
345
346
if (changedProperties.has("colorModel")) {
347
if (this.colorModel === ColorModel.Gray) {
348
this.selectedBands = [this.bandNames[0]];
349
} else if (this.colorModel == ColorModel.RGB) {
350
this.selectedBands = [
351
this.bandNames[0],
352
this.bandNames[0],
353
this.bandNames[0],
354
];
355
}
356
}
357
358
if (
359
changedProperties.has("selectedBands") ||
360
changedProperties.has("stretch")
361
) {
362
this.calculateBandStats();
363
}
364
}
365
366
private getSelectedBands(): Array<string> {
367
return Array.from(this.bandSelects).map(input => input.value);
368
}
369
}
370
371
// Without this check, there's a component registry issue when developing locally.
372
if (!customElements.get(RasterLayerEditor.componentName)) {
373
customElements.define(RasterLayerEditor.componentName, RasterLayerEditor);
374
}
375
376