@@ -8,18 +8,17 @@ import { useFieldNameStore } from '@/stores/fieldNameStore';
88import { useSrToastStore } from '@/stores/srToastStore' ;
99import { useDeck3DConfigStore } from '@/stores/deck3DConfigStore' ;
1010import { useElevationColorMapStore } from '@/stores/elevationColorMapStore' ;
11- import { getVerticalScaleRatio } from '@/utils/deckAxes' ;
1211import { OrbitView , OrbitController } from '@deck.gl/core' ;
1312import { ref , type Ref } from 'vue' ;
1413import { 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
1919import { toRaw , isProxy } from 'vue' ;
2020import { formatTime } from '@/utils/formatUtils' ;
2121
22- const toast = useSrToastStore ( ) ;
2322const deck3DConfigStore = useDeck3DConfigStore ( ) ;
2423const elevationStore = useElevationColorMapStore ( ) ;
2524const fieldStore = useFieldNameStore ( ) ;
@@ -36,6 +35,18 @@ let cachedRawData: any[] = [];
3635let 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-
8291function 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 */
294286export 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