Skip to content

Commit bad690c

Browse files
committed
Fix scaling of 3-D view so that the North and East axes have the correct scale
Fixes #645
1 parent 6c10ff2 commit bad690c

File tree

2 files changed

+157
-178
lines changed

2 files changed

+157
-178
lines changed

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)