@@ -112,28 +112,30 @@ def scarpAnalysisMain(cfg, baseDir):
112112 ellipsoidsMaxDepth = list (map (float , SHPdata ['maxdepth' ]))
113113 ellipsoidsSemiMajor = list (map (float , SHPdata ['semimajor' ]))
114114 ellipsoidsSemiMinor = list (map (float , SHPdata ['semiminor' ]))
115+ ellipsoidsTilt = list (map (float , SHPdata ['tilt' ]))
116+ ellipsoidsDir = list (map (float , SHPdata ['direc' ]))
117+ ellipsoidsOffset = list (map (float , SHPdata ['offset' ]))
118+ ellipsoidDip = list (map (float , SHPdata ['dip' ]))
115119 except KeyError as e :
116- raise ValueError (f"Required attribute '{ e .args [0 ]} ' not found in shapefile. Ensure the fields 'maxdepth', 'semimajor', and 'semiminor' exist." )
117-
118- if not (
119- len (ellipsoidsMaxDepth )
120- == len (ellipsoidsSemiMajor )
121- == len (ellipsoidsSemiMinor )
122- == SHPdata ["nFeatures" ]
123- ):
124- raise ValueError (
125- "Mismatch between number of shapefile features and ellipsoid parameters in the .ini file."
126- )
127-
120+ raise ValueError (f"Required attribute '{ e .args [0 ]} ' not found in shapefile. Ensure the fields 'maxdepth', 'semimajor', 'semiminor', 'tilt', 'dir', 'dip', and 'offset' exist." )
121+
122+ if not all (len (lst ) == SHPdata ["nFeatures" ] for lst in [ellipsoidsMaxDepth , ellipsoidsSemiMajor , ellipsoidsSemiMinor , ellipsoidsTilt , ellipsoidsDir , ellipsoidsOffset , ellipsoidDip ]):
123+ raise ValueError ("Mismatch between number of shapefile features and ellipsoid parameters." )
124+
128125 for i in range (SHPdata ["nFeatures" ]):
129126 xCenter = SHPdata ["x" ][int (SHPdata ["Start" ][i ])]
130127 yCenter = SHPdata ["y" ][int (SHPdata ["Start" ][i ])]
131128 maxDepth = ellipsoidsMaxDepth [i ]
132129 semiMajor = ellipsoidsSemiMajor [i ]
133130 semiMinor = ellipsoidsSemiMinor [i ]
134- ellipsoidFeatures .extend ([xCenter , yCenter , maxDepth , semiMajor , semiMinor ])
135-
131+ tilt = ellipsoidsTilt [i ]
132+ direction = ellipsoidsDir [i ]
133+ offset = ellipsoidsOffset [i ]
134+ dip = ellipsoidDip [i ]
135+ ellipsoidFeatures .extend ([xCenter , yCenter , maxDepth , semiMajor , semiMinor , tilt , direction , offset , dip ])
136+
136137 features = "," .join (map (str , ellipsoidFeatures ))
138+
137139 log .debug ("Ellipsoid features extracted and combined: %s" , features )
138140
139141 if method == 'plane' :
@@ -262,55 +264,104 @@ def calculateScarpWithPlanes(elevData, periData, elevTransform, planes):
262264 return scarpData
263265
264266def calculateScarpWithEllipsoids (elevData , periData , elevTransform , ellipsoids ):
265- """Calculate the scarp using sliding circles (rotational ellipsoids) .
267+ """Calculate the scarp using tilted and offset ellipsoids.
266268
267269 Parameters
268270 ----------
269271 elevData : np.ndarray
270272 The elevation data as a 2D array.
271273 periData : np.ndarray
272- The perimeter data as a 2D array, which defines the extent of the scarp .
274+ The perimeter data as a 2D array.
273275 elevTransform : Affine
274- The affine transformation matrix of the raster (used to convert pixel to geographic coordinates) .
276+ The affine transformation matrix of the raster.
275277 ellipsoids : str
276- Comma-separated string defining ellipsoids (x_center, y_center, max_depth, semi_major_axis, semi_minor_axis).
278+ Comma-separated string defining ellipsoids with parameters:
279+ (x_center, y_center, max_depth, semi_major, semi_minor, tilt, dir, offset)
277280
278281 Returns
279282 -------
280283 np.ndarray
281284 A 2D array with the calculated scarp values.
282285 """
286+
283287 n , m = elevData .shape
284288 scarpData = np .zeros_like (elevData , dtype = np .float32 )
285289
290+ # Parse ellipsoid definitions
286291 ellipsoids = list (map (float , ellipsoids .split (',' )))
287- nEllipsoids = int (len (ellipsoids ) / 5 )
288-
289- xCenter = [ellipsoids [0 ]]
290- yCenter = [ellipsoids [1 ]]
291- maxDepth = [ellipsoids [2 ]]
292- semiMajor = [ellipsoids [3 ]]
293- semiMinor = [ellipsoids [4 ]]
294-
295- for i in range (1 , nEllipsoids ):
296- xCenter .append (ellipsoids [5 * i ])
297- yCenter .append (ellipsoids [5 * i + 1 ])
298- maxDepth .append (ellipsoids [5 * i + 2 ])
299- semiMajor .append (ellipsoids [5 * i + 3 ])
300- semiMinor .append (ellipsoids [5 * i + 4 ])
292+ nEllipsoids = int (len (ellipsoids ) / 9 )
293+
294+ xCenter , yCenter , maxDepth = [], [], []
295+ semiMajor , semiMinor = [], []
296+ tilt , tiltDir , offset , dip = [], [], [], []
297+
298+ for i in range (nEllipsoids ):
299+ xCenter .append (ellipsoids [9 * i ])
300+ yCenter .append (ellipsoids [9 * i + 1 ])
301+ maxDepth .append (ellipsoids [9 * i + 2 ])
302+ semiMajor .append (ellipsoids [9 * i + 3 ])
303+ semiMinor .append (ellipsoids [9 * i + 4 ])
304+ tilt .append (ellipsoids [9 * i + 5 ])
305+ tiltDir .append (np .radians (ellipsoids [9 * i + 6 ])) # tilt direction in radians
306+ offset .append (ellipsoids [9 * i + 7 ])
307+ dip .append (np .radians (ellipsoids [9 * i + 8 ])) # rotation of base ellipse in radians
308+
309+ # Compute slope direction and magnitude from DEM gradients
310+ grad_y , grad_x = np .gradient (elevData , abs (elevTransform [4 ]), elevTransform [0 ])
311+ slopeDir = np .arctan2 (- grad_y , grad_x ) # Azimuth of steepest descent
312+ slopeMagnitude = np .sqrt (grad_x ** 2 + grad_y ** 2 )
301313
302314 for row in range (n ):
303315 for col in range (m ):
304316 west , north = rasterio .transform .xy (elevTransform , row , col , offset = 'center' )
305317 scarpVal = elevData [row , col ]
318+
306319 for k in range (nEllipsoids ):
307- x2 = ((west - xCenter [k ]) / semiMajor [k ]) ** 2
308- y2 = ((north - yCenter [k ]) / semiMinor [k ]) ** 2
309- if x2 + y2 < 1 :
310- scarpVal = min (scarpVal , elevData [row , col ] - maxDepth [k ] * (1 - (x2 + y2 )))
311- if periData [row , col ] > 0 :
312- scarpData [row , col ] = scarpVal
313- else :
314- scarpData [row , col ] = elevData [row , col ]
320+ center_col , center_row = ~ elevTransform * (xCenter [k ], yCenter [k ])
321+ center_row = int (round (center_row ))
322+ center_col = int (round (center_col ))
323+
324+ if 0 <= center_row < n and 0 <= center_col < m :
325+ slopeAzimuth = slopeDir [center_row , center_col ]
326+ slopeAzimuth = (slopeAzimuth + 2 * np .pi ) % (2 * np .pi )
327+ slopeMag = slopeMagnitude [center_row , center_col ]
328+
329+ if not np .isnan (slopeAzimuth ) and slopeMag > 0 :
330+ normal_dx = np .cos (slopeAzimuth )
331+ normal_dy = np .sin (slopeAzimuth )
332+ slopeAngle = math .atan (slopeMag )
333+ dxOffset = - offset [k ] * normal_dx * math .sin (slopeAngle )
334+ dyOffset = - offset [k ] * normal_dy * math .sin (slopeAngle )
335+ dzOffset = - offset [k ] * math .cos (slopeAngle )
336+ else :
337+ dxOffset = dyOffset = dzOffset = 0
338+ else :
339+ dxOffset = dyOffset = dzOffset = 0
340+
341+ x0 = xCenter [k ] + dxOffset
342+ y0 = yCenter [k ] + dyOffset
343+ z0 = dzOffset
344+
345+ dxPos = west - x0
346+ dyPos = north - y0
347+
348+ # Rotate the position by dip angle
349+ dxRot = dxPos * np .cos (dip [k ]) + dyPos * np .sin (dip [k ])
350+ dyRot = - dxPos * np .sin (dip [k ]) + dyPos * np .cos (dip [k ])
351+
352+ # Normalize to ellipsoid axes
353+ xNorm = dxRot / semiMajor [k ]
354+ yNorm = dyRot / semiMinor [k ]
355+ distance = xNorm ** 2 + yNorm ** 2
356+
357+ if distance <= 1 :
358+ baseDepth = maxDepth [k ] * (1 - distance )
359+ tiltEffect = math .tan (math .radians (tilt [k ])) * (
360+ dxRot * np .cos (tiltDir [k ]) + dyRot * np .sin (tiltDir [k ])
361+ )
362+ totalDepth = baseDepth + tiltEffect + z0
363+ scarpVal = min (scarpVal , elevData [row , col ] - totalDepth )
364+
365+ scarpData [row , col ] = scarpVal if periData [row , col ] > 0 else elevData [row , col ]
315366
316367 return scarpData
0 commit comments