Skip to content

Commit 7493bb2

Browse files
authored
Merge pull request #649 from SlideRuleEarth/carlos-dev
Fix scaling of 3-D view so that the North and East axes have the corr…
2 parents 6c10ff2 + 36d0682 commit 7493bb2

File tree

7 files changed

+168
-192
lines changed

7 files changed

+168
-192
lines changed

web-client/src/components/SrAtl03Classification.vue

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
<script setup lang="ts">
2-
import SrMultiSelectNumber from '@/components/SrMultiSelectNumber.vue';
32
import SrCheckbox from '@/components/SrCheckbox.vue';
43
import { useReqParamsStore } from '@/stores/reqParamsStore';
54
import SrSurfaceRefType from '@/components/SrSurfaceRefType.vue';
@@ -18,7 +17,7 @@ const reqParamsStore = useReqParamsStore();
1817
label="Atl03 Classification"
1918
labelFontSize="large"
2019
tooltipText="A set of photon classification values that are designed to identify signal photons for different surface types with specified confidence"
21-
tooltipUrl="https://slideruleearth.io/web/rtd/user_guide/dataframe.html#icesat-2-request-fields"
20+
tooltipUrl="https://slideruleearth.io/web/rtd/user_guide/icesat2.html#native-atl03-photon-classification"
2221
v-model="reqParamsStore.enableAtl03Classification"
2322
:defaultValue="reqParamsStore.enableAtl03Classification"
2423
/>

web-client/src/components/SrAtl08Cnf.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ const reqParamsStore = useReqParamsStore();
1515
labelFontSize="large"
1616
v-model="reqParamsStore.enableAtl08Classification"
1717
tooltipText="If ATL08 classification parameters are specified, the ATL08 (vegetation height) files corresponding to the ATL03 files are queried for the more advanced classification scheme available in those files. Photons are then selected based on the classification values specified. Note that srt=0 (land) and cnf=0 (no native filtering) should be specified to allow all ATL08 photons to be used."
18-
tooltipUrl="https://slideruleearth.io/web/rtd/user_guide/dataframe.html#icesat-2-request-fields"
18+
tooltipUrl="https://slideruleearth.io/web/rtd/user_guide/icesat2.html#atl08-classification"
1919
/>
2020
</div>
2121
<SrMultiSelectText

web-client/src/components/SrGranuleSelection.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ const GtsSelection = (gts:SrListNumberItem[]) => {
8080
label="Granule Selection"
8181
labelFontSize="large"
8282
tooltipText="Granules are the smallest unit of data that can be independently accessed, processed, and analyzed."
83-
tooltipUrl="https://slideruleearth.io/web/rtd/user_guide/dataframe.html#granule"
83+
tooltipUrl="https://slideruleearth.io/web/rtd/user_guide/xseries.html#granule"
8484
/>
8585
</div>
8686
<div class="sr-granule-tracks-beams-div">

web-client/src/components/SrSurfaceElevation.vue

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import SrSwitchedSliderInput from '@/components/SrSwitchedSliderInput.vue';
44
import { useReqParamsStore } from '@/stores/reqParamsStore';
55
import { onMounted } from 'vue';
66
import { useSlideruleDefaults } from '@/stores/defaultsStore';
7+
import SrLabelInfoIconButton from '@/components/SrLabelInfoIconButton.vue';
78
89
const reqParamsStore = useReqParamsStore();
910
@@ -37,7 +38,11 @@ onMounted(async () => {
3738
<template>
3839
<div class="sr-surface-elevation-container">
3940
<div class="sr-surface-elevation-header">
40-
<span class="sr-surface-elevation-hdr">Surface Elevation Algorithm</span>
41+
<SrLabelInfoIconButton
42+
label="Surface Elevation Algorithm"
43+
tooltipText="Surface Fit parameters for the algorithm"
44+
tooltipUrl="https://slideruleearth.io/web/rtd/user_guide/xseries.html#surface-fit"
45+
/>
4146
</div>
4247
<div class="sr-surface-elevation-body">
4348
<SrSwitchedSliderInput
@@ -55,7 +60,6 @@ onMounted(async () => {
5560
:sliderMax="10"
5661
:decimalPlaces="0"
5762
tooltipText="maxi: The maximum number of iterations, not including initial least-squares-fit selection"
58-
tooltipUrl="https://slideruleearth.io/web/rtd/user_guide/dataframe.html#surface-fit"
5963
/>
6064
<SrSwitchedSliderInput
6165
v-model="reqParamsStore.minWindowHeight"
@@ -72,7 +76,6 @@ onMounted(async () => {
7276
:sliderMax="20"
7377
:decimalPlaces="0"
7478
tooltipText="H_min_win: The minimum height to which the refined photon-selection window is allowed to shrink, in meters"
75-
tooltipUrl="https://slideruleearth.io/web/rtd/user_guide/dataframe.html#surface-fit"
7679
/>
7780
<SrSwitchedSliderInput
7881
v-model="reqParamsStore.maxRobustDispersion"
@@ -87,7 +90,6 @@ onMounted(async () => {
8790
:max="200"
8891
:decimalPlaces="0"
8992
tooltipText="sigma_r_max: The maximum robust dispersion in meters"
90-
tooltipUrl="https://slideruleearth.io/web/rtd/user_guide/dataframe.html#surface-fit"
9193
/>
9294
</div>
9395
</div>
@@ -107,9 +109,5 @@ onMounted(async () => {
107109
background-color: transparent;
108110
margin-bottom: 1rem;
109111
}
110-
.sr-surface-elevation-hdr {
111-
font-size: large;
112-
font-weight: bold;
113-
color: var(--p-color-text);
114-
}
112+
115113
</style>

web-client/src/components/SrYAPC.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ onMounted(async () => {
3333
:defaultValue="defaultEnableYAPC()"
3434
label="YAPC"
3535
tooltipText="The experimental YAPC (Yet Another Photon Classifier) photon-classification scheme."
36-
tooltipUrl="https://slideruleearth.io/web/rtd/user_guide/dataframe.html#yapc"
36+
tooltipUrl="https://slideruleearth.io/web/rtd/user_guide/icesat2.html#yapc-classification"
3737
labelFontSize="large"
3838
/>
3939
</div>

web-client/src/utils/deck3DPlotUtils.ts

Lines changed: 112 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -8,18 +8,17 @@ import { useFieldNameStore } from '@/stores/fieldNameStore';
88
import { useSrToastStore } from '@/stores/srToastStore';
99
import { useDeck3DConfigStore } from '@/stores/deck3DConfigStore';
1010
import { useElevationColorMapStore } from '@/stores/elevationColorMapStore';
11-
import { getVerticalScaleRatio } from '@/utils/deckAxes';
1211
import { OrbitView, OrbitController } from '@deck.gl/core';
1312
import { ref,type Ref } from 'vue';
1413
import { Deck } from '@deck.gl/core';
14+
import proj4 from 'proj4';
1515

1616
// import log from '@probe.gl/log';
1717
// log.level = 1; // 0 = silent, 1 = minimal, 2 = verbose
1818

1919
import { toRaw, isProxy } from 'vue';
2020
import { formatTime } from '@/utils/formatUtils';
2121

22-
const toast = useSrToastStore();
2322
const deck3DConfigStore = useDeck3DConfigStore();
2423
const elevationStore = useElevationColorMapStore();
2524
const fieldStore = useFieldNameStore();
@@ -36,6 +35,18 @@ let cachedRawData: any[] = [];
3635
let lastLoadedReqId: number | null = null;
3736

3837

38+
// helper: pick a local metric CRS (UTM or polar)
39+
function pickLocalMetricCRS(lat: number, lon: number): string {
40+
const absLat = Math.abs(lat);
41+
if (absLat >= 83) {
42+
// near poles: use polar stereographic
43+
return lat >= 0 ? 'EPSG:3413' : 'EPSG:3031';
44+
}
45+
const zone = Math.floor((lon + 180) / 6) + 1;
46+
return lat >= 0 ? `EPSG:326${String(zone).padStart(2, '0')}` // WGS84 / UTM N
47+
: `EPSG:327${String(zone).padStart(2, '0')}`; // WGS84 / UTM S
48+
}
49+
3950

4051
/**
4152
* Strips Vue reactivity from Deck.gl-compatible data to prevent runtime Proxy errors.
@@ -77,8 +88,6 @@ function computeCentroid(position: [number, number, number][]) {
7788
}
7889

7990

80-
81-
8291
function initDeckIfNeeded(deckContainer: Ref<HTMLDivElement | null>): boolean {
8392
const container = deckContainer.value;
8493
if (!container) {
@@ -254,23 +263,6 @@ export async function loadAndCachePointCloudData(reqId: number) {
254263
elevMax = Math.max(elevMax, elev);
255264
}
256265
}
257-
258-
// Only call if we have valid ranges
259-
if (
260-
elevMin !== Infinity && elevMax !== -Infinity &&
261-
latMin !== Infinity && latMax !== -Infinity &&
262-
lonMin !== Infinity && lonMax !== -Infinity
263-
) {
264-
const vsr = getVerticalScaleRatio(
265-
elevMin, elevMax, latMin, latMax, lonMin, lonMax
266-
);
267-
console.log(
268-
`Vertical scale ratio for reqId ${reqId}: ${vsr.toFixed(2)}x`
269-
);
270-
deck3DConfigStore.verticalScaleRatio = vsr;
271-
} else {
272-
console.warn('Unable to compute vertical scale ratio: invalid bounds');
273-
}
274266
}
275267

276268

@@ -292,12 +284,11 @@ export async function loadAndCachePointCloudData(reqId: number) {
292284
* This is safe to call frequently (e.g., from debounced UI handlers).
293285
*/
294286
export function renderCachedData(deckContainer: Ref<HTMLDivElement | null>) {
295-
if(!deckContainer || !deckContainer.value) {
287+
if (!deckContainer || !deckContainer.value) {
296288
console.warn('Deck container is null or undefined');
297289
return;
298290
}
299291
if (!initDeckIfNeeded(deckContainer)) return;
300-
301292

302293
const deck3DConfigStore = useDeck3DConfigStore();
303294
const elevationStore = useElevationColorMapStore();
@@ -309,11 +300,21 @@ export function renderCachedData(deckContainer: Ref<HTMLDivElement | null>) {
309300
return;
310301
}
311302

312-
// --- All your fast, in-memory data processing logic ---
303+
// --- helpers ---
304+
const pickLocalMetricCRS = (lat: number, lon: number): string => {
305+
const absLat = Math.abs(lat);
306+
if (absLat >= 83) return lat >= 0 ? 'EPSG:3413' : 'EPSG:3031'; // polar stereo
307+
const zone = Math.floor((lon + 180) / 6) + 1;
308+
return lat >= 0
309+
? `EPSG:326${String(zone).padStart(2, '0')}` // UTM north
310+
: `EPSG:327${String(zone).padStart(2, '0')}`; // UTM south
311+
};
312+
313+
// --- fast, in-memory processing ---
313314
const heightField = fieldStore.getHFieldName(lastLoadedReqId);
314315

315316
const elevations = cachedRawData.map(d => d[heightField]).sort((a, b) => a - b);
316-
// ... all your min/max/range/percentile calculations using `cachedRawData` ...
317+
317318
const colorMin = getPercentile(elevations, deck3DConfigStore.minColorPercent);
318319
const colorMax = getPercentile(elevations, deck3DConfigStore.maxColorPercent);
319320
const colorRange = Math.max(1e-6, colorMax - colorMin);
@@ -327,91 +328,117 @@ export function renderCachedData(deckContainer: Ref<HTMLDivElement | null>) {
327328
const elevMinData = getPercentile(elevations, minElDataPercent);
328329
const elevMaxData = getPercentile(elevations, maxElDataPercent);
329330

331+
// geographic bounds (degrees)
330332
const lonMin = Math.min(...cachedRawData.map(d => d[lonField]));
331333
const lonMax = Math.max(...cachedRawData.map(d => d[lonField]));
332-
333334
const latMin = Math.min(...cachedRawData.map(d => d[latField]));
334335
const latMax = Math.max(...cachedRawData.map(d => d[latField]));
336+
const lonMid = 0.5 * (lonMin + lonMax);
337+
const latMid = 0.5 * (latMin + latMax);
338+
339+
// choose a local metric CRS
340+
const dstCrs = pickLocalMetricCRS(latMid, lonMid);
341+
342+
// project SW/NE to meters → extents
343+
const [Emin, Nmin] = proj4('EPSG:4326', dstCrs, [lonMin, latMin]);
344+
const [Emax, Nmax] = proj4('EPSG:4326', dstCrs, [lonMax, latMax]);
345+
346+
const Erange = Math.max(1e-6, Emax - Emin);
347+
const Nrange = Math.max(1e-6, Nmax - Nmin);
348+
349+
// ---- Longest-axis scaling ----
350+
// The longest ground span maps to deck3DConfigStore.scale; the other axis shrinks proportionally.
351+
const targetScale = deck3DConfigStore.scale; // your "max side" length in world units
352+
const longestRange = Math.max(Erange, Nrange);
353+
const metersToWorld = targetScale / longestRange;
335354

336-
const lonRange = Math.max(1e-6, lonMax - lonMin);
337-
const latRange = Math.max(1e-6, latMax - latMin);
355+
const scaleX = Erange * metersToWorld; // <= targetScale
356+
const scaleY = Nrange * metersToWorld; // <= targetScale
357+
const scaleZ = Math.max(scaleX, scaleY); // keep Z axis length comparable
338358

339359
elevationStore.updateElevationColorMapValues();
340360
const rgbaArray = elevationStore.elevationColorMap;
341361

342-
// Map cached data to point cloud format
362+
// Map cached data → world coords
343363
const pointCloudData = cachedRawData
344-
.filter(d => {
345-
const height = d[heightField];
346-
return height >= elevMinData && height <= elevMaxData;
347-
})
348-
.map(d => {
349-
const x = deck3DConfigStore.scale * (d[lonField] - lonMin) / lonRange;
350-
const y = deck3DConfigStore.scale * (d[latField] - latMin) / latRange;
351-
352-
const z = (deck3DConfigStore.verticalExaggeration/deck3DConfigStore.verticalScaleRatio) *
353-
deck3DConfigStore.scale *
364+
.filter(d => {
365+
const h = d[heightField];
366+
return h >= elevMinData && h <= elevMaxData;
367+
})
368+
.map(d => {
369+
const [E, N] = proj4('EPSG:4326', dstCrs, [d[lonField], d[latField]]);
370+
// origin = SW corner → same framing; uniform metersToWorld keeps aspect ratio
371+
const x = metersToWorld * (E - Emin); // East
372+
const y = metersToWorld * (N - Nmin); // North
373+
374+
// Z uses your percentile window; scale by scaleZ so ticks match the Z axis length
375+
const z =
376+
(deck3DConfigStore.verticalExaggeration / deck3DConfigStore.verticalScaleRatio) *
377+
scaleZ *
354378
(d[heightField] - elevMinScale) / elevRangeScale;
355379

356-
// But color is computed using clamped-to-percentile range
357-
const colorZ = Math.max(colorMin, Math.min(colorMax, d[heightField]));
358-
const colorNorm = (colorZ - colorMin) / colorRange;
359-
const index = Math.floor(colorNorm * (rgbaArray.length - 1));
360-
const rawColor = rgbaArray[Math.max(0, Math.min(index, rgbaArray.length - 1))] ?? [255, 255, 255, 1];
361-
362-
const color = [
363-
Math.round(rawColor[0]),
364-
Math.round(rawColor[1]),
365-
Math.round(rawColor[2]),
366-
Math.round(rawColor[3] * 255),
367-
];
368-
369-
return {
370-
position: [x, y, z] as [number, number, number],
371-
color,
372-
lat: d[latField],
373-
lon: d[lonField],
374-
elevation: d[heightField],
375-
cycle: d['cycle'],
376-
time: d[timeField] ?? null, // Handle time field if available
377-
};
378-
});
380+
// color unchanged
381+
const colorZ = Math.max(colorMin, Math.min(colorMax, d[heightField]));
382+
const colorNorm = (colorZ - colorMin) / colorRange;
383+
const index = Math.floor(colorNorm * (rgbaArray.length - 1));
384+
const rawColor =
385+
rgbaArray[Math.max(0, Math.min(index, rgbaArray.length - 1))] ??
386+
[255, 255, 255, 1];
387+
388+
const color = [
389+
Math.round(rawColor[0]),
390+
Math.round(rawColor[1]),
391+
Math.round(rawColor[2]),
392+
Math.round(rawColor[3] * 255),
393+
];
394+
395+
return {
396+
position: [x, y, z] as [number, number, number],
397+
color,
398+
lat: d[latField],
399+
lon: d[lonField],
400+
elevation: d[heightField],
401+
cycle: d['cycle'],
402+
time: d[timeField] ?? null,
403+
};
404+
});
405+
379406
computeCentroid(pointCloudData.map(p => p.position));
380-
407+
381408
// --- Layer creation ---
382409
const layer = new PointCloudLayer({
383-
id: 'point-cloud-layer', // <-- STABLE ID IS CRITICAL FOR PERFORMANCE
410+
id: 'point-cloud-layer',
384411
data: pointCloudData,
385412
getPosition: d => d.position,
386413
getColor: d => d.color,
387414
pointSize: deck3DConfigStore.pointSize,
388415
opacity: 0.95,
389416
pickable: true,
390417
});
391-
418+
392419
const layers: Layer<any>[] = [layer];
393-
420+
394421
if (deck3DConfigStore.showAxes) {
395-
// ... your axes creation logic ...
396-
layers.push(...createAxesAndLabels(
397-
100,
398-
'East',
399-
'North',
400-
'Elev (m)',
401-
[255, 255, 255], // text color
402-
[200, 200, 200], // line color
403-
5, // font size
404-
1, // line width
405-
elevMinScale,
406-
elevMaxScale,
407-
latMin,
408-
latMax,
409-
lonMin,
410-
lonMax,
411-
));
422+
layers.push(
423+
...createAxesAndLabels(
424+
/* scaleX */ scaleX,
425+
/* scaleY */ scaleY,
426+
/* scaleZ */ scaleZ,
427+
'East',
428+
'North',
429+
'Elev (m)',
430+
[255, 255, 255], // text color
431+
[200, 200, 200], // line color
432+
5, // font size
433+
1, // line width
434+
elevMinScale,
435+
elevMaxScale,
436+
/* N meters */ Nmin, Nmax,
437+
/* E meters */ Emin, Emax // pass meter extents
438+
)
439+
);
412440
}
413-
414-
// --- Update Deck ---
441+
415442
requestAnimationFrame(() => {
416443
deckInstance.value?.setProps({ layers });
417444
});

0 commit comments

Comments
 (0)