@wandelbots/wandelbots-js-react-components 2.37.0 → 2.38.0-pr.bugfix-add-bg-to-cycle-timer.380.f7d61ae
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/CycleTimer/CycleTimer.d.ts +3 -0
- package/dist/components/CycleTimer/CycleTimer.d.ts.map +1 -0
- package/dist/components/CycleTimer/DefaultVariant.d.ts +10 -0
- package/dist/components/CycleTimer/DefaultVariant.d.ts.map +1 -0
- package/dist/components/CycleTimer/SmallVariant.d.ts +11 -0
- package/dist/components/CycleTimer/SmallVariant.d.ts.map +1 -0
- package/dist/components/CycleTimer/index.d.ts +28 -0
- package/dist/components/CycleTimer/index.d.ts.map +1 -0
- package/dist/components/CycleTimer/types.d.ts +51 -0
- package/dist/components/CycleTimer/types.d.ts.map +1 -0
- package/dist/components/CycleTimer/useAnimations.d.ts +15 -0
- package/dist/components/CycleTimer/useAnimations.d.ts.map +1 -0
- package/dist/components/CycleTimer/useTimerLogic.d.ts +26 -0
- package/dist/components/CycleTimer/useTimerLogic.d.ts.map +1 -0
- package/dist/components/CycleTimer/utils.d.ts +13 -0
- package/dist/components/CycleTimer/utils.d.ts.map +1 -0
- package/dist/components/CycleTimer.d.ts +2 -96
- package/dist/components/CycleTimer.d.ts.map +1 -1
- package/dist/components/jogging/PoseCartesianValues.d.ts +8 -4
- package/dist/components/jogging/PoseCartesianValues.d.ts.map +1 -1
- package/dist/components/jogging/PoseJointValues.d.ts +8 -4
- package/dist/components/jogging/PoseJointValues.d.ts.map +1 -1
- package/dist/index.cjs +50 -50
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +9302 -8800
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/components/CycleTimer/CycleTimer.ts +6 -0
- package/src/components/CycleTimer/DefaultVariant.tsx +325 -0
- package/src/components/CycleTimer/SmallVariant.tsx +230 -0
- package/src/components/CycleTimer/index.tsx +157 -0
- package/src/components/CycleTimer/types.ts +60 -0
- package/src/components/CycleTimer/useAnimations.ts +202 -0
- package/src/components/CycleTimer/useTimerLogic.ts +386 -0
- package/src/components/CycleTimer/utils.ts +53 -0
- package/src/components/CycleTimer.tsx +6 -715
- package/src/components/jogging/PoseCartesianValues.tsx +85 -7
- package/src/components/jogging/PoseJointValues.tsx +86 -8
- package/src/i18n/locales/de/translations.json +4 -0
- package/src/i18n/locales/en/translations.json +4 -0
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+
import { useCallback, useEffect, useRef, useState } from "react"
|
|
2
|
+
import { useInterpolation } from "../utils/interpolation"
|
|
3
|
+
import type { TimerState } from "./types"
|
|
4
|
+
import { calculateExactProgress } from "./utils"
|
|
5
|
+
|
|
6
|
+
interface UseTimerLogicProps {
|
|
7
|
+
autoStart: boolean
|
|
8
|
+
onCycleEnd?: () => void
|
|
9
|
+
onMeasuringComplete?: () => void
|
|
10
|
+
hasError: boolean
|
|
11
|
+
onPauseAnimation: () => void
|
|
12
|
+
onErrorAnimation: () => void
|
|
13
|
+
onClearErrorAnimation: () => void
|
|
14
|
+
onStartPulsating: (onComplete?: () => void) => void
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const useTimerLogic = ({
|
|
18
|
+
autoStart,
|
|
19
|
+
onCycleEnd,
|
|
20
|
+
onMeasuringComplete,
|
|
21
|
+
hasError,
|
|
22
|
+
onPauseAnimation,
|
|
23
|
+
onErrorAnimation,
|
|
24
|
+
onClearErrorAnimation,
|
|
25
|
+
onStartPulsating,
|
|
26
|
+
}: UseTimerLogicProps) => {
|
|
27
|
+
const [timerState, setTimerState] = useState<TimerState>({
|
|
28
|
+
currentState: "idle",
|
|
29
|
+
remainingTime: 0,
|
|
30
|
+
maxTime: null,
|
|
31
|
+
isRunning: false,
|
|
32
|
+
isPausedState: false,
|
|
33
|
+
currentProgress: 0,
|
|
34
|
+
wasRunningBeforeError: false,
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
// Timer-related refs
|
|
38
|
+
const animationRef = useRef<number | null>(null)
|
|
39
|
+
const startTimeRef = useRef<number | null>(null)
|
|
40
|
+
const pausedTimeRef = useRef<number>(0)
|
|
41
|
+
|
|
42
|
+
// Spring-based interpolator for smooth gauge progress animations
|
|
43
|
+
const [progressInterpolator] = useInterpolation([0], {
|
|
44
|
+
tension: 80,
|
|
45
|
+
friction: 18,
|
|
46
|
+
onChange: ([progress]) => {
|
|
47
|
+
setTimerState((prev) => ({ ...prev, currentProgress: progress }))
|
|
48
|
+
},
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
const setIdle = useCallback(() => {
|
|
52
|
+
setTimerState((prev) => ({
|
|
53
|
+
...prev,
|
|
54
|
+
currentState: "idle",
|
|
55
|
+
maxTime: null,
|
|
56
|
+
remainingTime: 0,
|
|
57
|
+
isRunning: false,
|
|
58
|
+
isPausedState: false,
|
|
59
|
+
currentProgress: 0, // Immediately reset progress to 0
|
|
60
|
+
}))
|
|
61
|
+
pausedTimeRef.current = 0
|
|
62
|
+
startTimeRef.current = null
|
|
63
|
+
progressInterpolator.setImmediate([0]) // Use setImmediate for instant reset
|
|
64
|
+
}, [progressInterpolator])
|
|
65
|
+
|
|
66
|
+
const startMeasuring = useCallback(
|
|
67
|
+
(elapsedSeconds: number = 0) => {
|
|
68
|
+
const initialProgress = ((elapsedSeconds / 60) % 1) * 100
|
|
69
|
+
setTimerState((prev) => ({
|
|
70
|
+
...prev,
|
|
71
|
+
currentState: "measuring",
|
|
72
|
+
maxTime: null,
|
|
73
|
+
remainingTime: elapsedSeconds,
|
|
74
|
+
isPausedState: false,
|
|
75
|
+
currentProgress: initialProgress, // Immediately set progress
|
|
76
|
+
}))
|
|
77
|
+
pausedTimeRef.current = 0
|
|
78
|
+
|
|
79
|
+
progressInterpolator.setImmediate([initialProgress]) // Use setImmediate for instant reset
|
|
80
|
+
|
|
81
|
+
if (autoStart) {
|
|
82
|
+
startTimeRef.current = Date.now() - elapsedSeconds * 1000
|
|
83
|
+
setTimerState((prev) => ({ ...prev, isRunning: true }))
|
|
84
|
+
} else {
|
|
85
|
+
startTimeRef.current = null
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
[autoStart, progressInterpolator],
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
const startCountUp = useCallback(
|
|
92
|
+
(elapsedSeconds: number = 0) => {
|
|
93
|
+
const initialProgress = ((elapsedSeconds / 60) % 1) * 100
|
|
94
|
+
setTimerState((prev) => ({
|
|
95
|
+
...prev,
|
|
96
|
+
currentState: "countup",
|
|
97
|
+
maxTime: null,
|
|
98
|
+
remainingTime: elapsedSeconds,
|
|
99
|
+
isPausedState: false,
|
|
100
|
+
currentProgress: initialProgress, // Immediately set progress
|
|
101
|
+
}))
|
|
102
|
+
pausedTimeRef.current = 0
|
|
103
|
+
|
|
104
|
+
progressInterpolator.setImmediate([initialProgress]) // Use setImmediate for instant reset
|
|
105
|
+
|
|
106
|
+
if (autoStart) {
|
|
107
|
+
startTimeRef.current = Date.now() - elapsedSeconds * 1000
|
|
108
|
+
setTimerState((prev) => ({ ...prev, isRunning: true }))
|
|
109
|
+
} else {
|
|
110
|
+
startTimeRef.current = null
|
|
111
|
+
}
|
|
112
|
+
},
|
|
113
|
+
[autoStart, progressInterpolator],
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
const startNewCycle = useCallback(
|
|
117
|
+
(maxTimeSeconds?: number, elapsedSeconds: number = 0) => {
|
|
118
|
+
// Stop any running timer first to prevent conflicts
|
|
119
|
+
setTimerState((prev) => ({ ...prev, isRunning: false }))
|
|
120
|
+
startTimeRef.current = null
|
|
121
|
+
|
|
122
|
+
const newState = maxTimeSeconds !== undefined ? "countdown" : "countup"
|
|
123
|
+
setTimerState((prev) => ({
|
|
124
|
+
...prev,
|
|
125
|
+
currentState: newState,
|
|
126
|
+
maxTime: maxTimeSeconds ?? null,
|
|
127
|
+
isPausedState: false,
|
|
128
|
+
}))
|
|
129
|
+
pausedTimeRef.current = 0
|
|
130
|
+
|
|
131
|
+
if (maxTimeSeconds !== undefined) {
|
|
132
|
+
// Count-down mode
|
|
133
|
+
const remainingSeconds = Math.max(0, maxTimeSeconds - elapsedSeconds)
|
|
134
|
+
const initialProgress =
|
|
135
|
+
elapsedSeconds > 0 ? (elapsedSeconds / maxTimeSeconds) * 100 : 0
|
|
136
|
+
|
|
137
|
+
setTimerState((prev) => ({
|
|
138
|
+
...prev,
|
|
139
|
+
remainingTime: remainingSeconds,
|
|
140
|
+
currentProgress: initialProgress, // Immediately set progress
|
|
141
|
+
}))
|
|
142
|
+
|
|
143
|
+
progressInterpolator.setImmediate([initialProgress]) // Use setImmediate for instant reset
|
|
144
|
+
|
|
145
|
+
if (remainingSeconds === 0) {
|
|
146
|
+
setTimerState((prev) => ({ ...prev, isRunning: false }))
|
|
147
|
+
startTimeRef.current = null
|
|
148
|
+
if (onCycleEnd) {
|
|
149
|
+
queueMicrotask(() => onCycleEnd())
|
|
150
|
+
}
|
|
151
|
+
} else if (autoStart) {
|
|
152
|
+
setTimeout(() => {
|
|
153
|
+
startTimeRef.current = Date.now() - elapsedSeconds * 1000
|
|
154
|
+
setTimerState((prev) => ({ ...prev, isRunning: true }))
|
|
155
|
+
}, 0)
|
|
156
|
+
} else {
|
|
157
|
+
startTimeRef.current = null
|
|
158
|
+
}
|
|
159
|
+
} else {
|
|
160
|
+
// Count-up mode
|
|
161
|
+
const initialProgress = ((elapsedSeconds / 60) % 1) * 100
|
|
162
|
+
setTimerState((prev) => ({
|
|
163
|
+
...prev,
|
|
164
|
+
remainingTime: elapsedSeconds,
|
|
165
|
+
currentProgress: initialProgress, // Immediately set progress
|
|
166
|
+
}))
|
|
167
|
+
|
|
168
|
+
progressInterpolator.setImmediate([initialProgress]) // Use setImmediate for instant reset
|
|
169
|
+
|
|
170
|
+
if (autoStart) {
|
|
171
|
+
setTimeout(() => {
|
|
172
|
+
startTimeRef.current = Date.now() - elapsedSeconds * 1000
|
|
173
|
+
setTimerState((prev) => ({ ...prev, isRunning: true }))
|
|
174
|
+
}, 0)
|
|
175
|
+
} else {
|
|
176
|
+
startTimeRef.current = null
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
},
|
|
180
|
+
[autoStart, onCycleEnd, progressInterpolator],
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
const completeMeasuring = useCallback(() => {
|
|
184
|
+
if (timerState.currentState === "measuring") {
|
|
185
|
+
setTimerState((prev) => ({
|
|
186
|
+
...prev,
|
|
187
|
+
isRunning: false,
|
|
188
|
+
currentState: "measured",
|
|
189
|
+
}))
|
|
190
|
+
startTimeRef.current = null
|
|
191
|
+
|
|
192
|
+
onStartPulsating(() => {
|
|
193
|
+
if (onMeasuringComplete) {
|
|
194
|
+
onMeasuringComplete()
|
|
195
|
+
}
|
|
196
|
+
})
|
|
197
|
+
}
|
|
198
|
+
}, [timerState.currentState, onStartPulsating, onMeasuringComplete])
|
|
199
|
+
|
|
200
|
+
const pause = useCallback(() => {
|
|
201
|
+
if (startTimeRef.current && timerState.isRunning) {
|
|
202
|
+
const now = Date.now()
|
|
203
|
+
const additionalElapsed = now - startTimeRef.current
|
|
204
|
+
pausedTimeRef.current += additionalElapsed
|
|
205
|
+
|
|
206
|
+
const totalElapsed = pausedTimeRef.current / 1000
|
|
207
|
+
const exactProgress = calculateExactProgress(
|
|
208
|
+
timerState.currentState,
|
|
209
|
+
totalElapsed,
|
|
210
|
+
timerState.maxTime,
|
|
211
|
+
)
|
|
212
|
+
progressInterpolator.setTarget([exactProgress])
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
setTimerState((prev) => ({
|
|
216
|
+
...prev,
|
|
217
|
+
isRunning: false,
|
|
218
|
+
isPausedState: true,
|
|
219
|
+
}))
|
|
220
|
+
onPauseAnimation()
|
|
221
|
+
}, [
|
|
222
|
+
timerState.isRunning,
|
|
223
|
+
timerState.currentState,
|
|
224
|
+
timerState.maxTime,
|
|
225
|
+
progressInterpolator,
|
|
226
|
+
onPauseAnimation,
|
|
227
|
+
])
|
|
228
|
+
|
|
229
|
+
const resume = useCallback(() => {
|
|
230
|
+
if (
|
|
231
|
+
timerState.isPausedState &&
|
|
232
|
+
(timerState.remainingTime > 0 || timerState.currentState !== "countdown")
|
|
233
|
+
) {
|
|
234
|
+
startTimeRef.current = Date.now()
|
|
235
|
+
setTimerState((prev) => ({
|
|
236
|
+
...prev,
|
|
237
|
+
isRunning: true,
|
|
238
|
+
isPausedState: false,
|
|
239
|
+
}))
|
|
240
|
+
}
|
|
241
|
+
}, [
|
|
242
|
+
timerState.isPausedState,
|
|
243
|
+
timerState.remainingTime,
|
|
244
|
+
timerState.currentState,
|
|
245
|
+
])
|
|
246
|
+
|
|
247
|
+
const isPaused = useCallback(() => {
|
|
248
|
+
return timerState.isPausedState
|
|
249
|
+
}, [timerState.isPausedState])
|
|
250
|
+
|
|
251
|
+
// Handle error state changes
|
|
252
|
+
useEffect(() => {
|
|
253
|
+
if (hasError) {
|
|
254
|
+
if (timerState.isRunning && !timerState.isPausedState) {
|
|
255
|
+
setTimerState((prev) => ({ ...prev, wasRunningBeforeError: true }))
|
|
256
|
+
pause()
|
|
257
|
+
}
|
|
258
|
+
onErrorAnimation()
|
|
259
|
+
} else {
|
|
260
|
+
if (timerState.wasRunningBeforeError && timerState.isPausedState) {
|
|
261
|
+
setTimerState((prev) => ({ ...prev, wasRunningBeforeError: false }))
|
|
262
|
+
resume()
|
|
263
|
+
}
|
|
264
|
+
onClearErrorAnimation()
|
|
265
|
+
}
|
|
266
|
+
}, [
|
|
267
|
+
hasError,
|
|
268
|
+
timerState.isRunning,
|
|
269
|
+
timerState.isPausedState,
|
|
270
|
+
timerState.wasRunningBeforeError,
|
|
271
|
+
pause,
|
|
272
|
+
resume,
|
|
273
|
+
onErrorAnimation,
|
|
274
|
+
onClearErrorAnimation,
|
|
275
|
+
])
|
|
276
|
+
|
|
277
|
+
// Main timer loop
|
|
278
|
+
useEffect(() => {
|
|
279
|
+
if (timerState.isRunning) {
|
|
280
|
+
const updateTimer = () => {
|
|
281
|
+
if (startTimeRef.current) {
|
|
282
|
+
const now = Date.now()
|
|
283
|
+
const elapsed =
|
|
284
|
+
(now - startTimeRef.current + pausedTimeRef.current) / 1000
|
|
285
|
+
|
|
286
|
+
if (
|
|
287
|
+
timerState.currentState === "countdown" &&
|
|
288
|
+
timerState.maxTime !== null
|
|
289
|
+
) {
|
|
290
|
+
const remaining = Math.max(0, timerState.maxTime - elapsed)
|
|
291
|
+
setTimerState((prev) => ({
|
|
292
|
+
...prev,
|
|
293
|
+
remainingTime: Math.ceil(remaining),
|
|
294
|
+
}))
|
|
295
|
+
|
|
296
|
+
const progress = Math.min(100, (elapsed / timerState.maxTime) * 100)
|
|
297
|
+
progressInterpolator.setTarget([progress])
|
|
298
|
+
|
|
299
|
+
if (remaining <= 0) {
|
|
300
|
+
setTimerState((prev) => ({
|
|
301
|
+
...prev,
|
|
302
|
+
isRunning: false,
|
|
303
|
+
remainingTime: 0,
|
|
304
|
+
}))
|
|
305
|
+
startTimeRef.current = null
|
|
306
|
+
progressInterpolator.setTarget([100])
|
|
307
|
+
if (onCycleEnd) {
|
|
308
|
+
queueMicrotask(() => onCycleEnd())
|
|
309
|
+
}
|
|
310
|
+
return
|
|
311
|
+
}
|
|
312
|
+
} else if (timerState.currentState === "measuring") {
|
|
313
|
+
setTimerState((prev) => ({
|
|
314
|
+
...prev,
|
|
315
|
+
remainingTime: Math.floor(elapsed),
|
|
316
|
+
}))
|
|
317
|
+
const progress = ((elapsed / 60) % 1) * 100
|
|
318
|
+
progressInterpolator.setTarget([progress])
|
|
319
|
+
} else if (timerState.currentState === "countup") {
|
|
320
|
+
setTimerState((prev) => ({
|
|
321
|
+
...prev,
|
|
322
|
+
remainingTime: Math.floor(elapsed),
|
|
323
|
+
}))
|
|
324
|
+
const progress = ((elapsed / 60) % 1) * 100
|
|
325
|
+
progressInterpolator.setTarget([progress])
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
if (timerState.isRunning) {
|
|
329
|
+
animationRef.current = requestAnimationFrame(updateTimer)
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
animationRef.current = requestAnimationFrame(updateTimer)
|
|
335
|
+
} else {
|
|
336
|
+
if (animationRef.current) {
|
|
337
|
+
cancelAnimationFrame(animationRef.current)
|
|
338
|
+
animationRef.current = null
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
return () => {
|
|
343
|
+
if (animationRef.current) {
|
|
344
|
+
cancelAnimationFrame(animationRef.current)
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
}, [
|
|
348
|
+
timerState.isRunning,
|
|
349
|
+
onCycleEnd,
|
|
350
|
+
timerState.currentState,
|
|
351
|
+
timerState.maxTime,
|
|
352
|
+
progressInterpolator,
|
|
353
|
+
])
|
|
354
|
+
|
|
355
|
+
// Interpolation animation loop
|
|
356
|
+
useEffect(() => {
|
|
357
|
+
let interpolationAnimationId: number | null = null
|
|
358
|
+
|
|
359
|
+
const animateInterpolation = () => {
|
|
360
|
+
progressInterpolator.update(1 / 60)
|
|
361
|
+
interpolationAnimationId = requestAnimationFrame(animateInterpolation)
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
interpolationAnimationId = requestAnimationFrame(animateInterpolation)
|
|
365
|
+
|
|
366
|
+
return () => {
|
|
367
|
+
if (interpolationAnimationId) {
|
|
368
|
+
cancelAnimationFrame(interpolationAnimationId)
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
}, [progressInterpolator])
|
|
372
|
+
|
|
373
|
+
return {
|
|
374
|
+
timerState,
|
|
375
|
+
controls: {
|
|
376
|
+
startNewCycle,
|
|
377
|
+
startMeasuring,
|
|
378
|
+
startCountUp,
|
|
379
|
+
setIdle,
|
|
380
|
+
completeMeasuring,
|
|
381
|
+
pause,
|
|
382
|
+
resume,
|
|
383
|
+
isPaused,
|
|
384
|
+
},
|
|
385
|
+
}
|
|
386
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Formats time in seconds to MM:SS format
|
|
3
|
+
*/
|
|
4
|
+
export const formatTime = (seconds: number): string => {
|
|
5
|
+
const minutes = Math.floor(seconds / 60)
|
|
6
|
+
const remainingSeconds = seconds % 60
|
|
7
|
+
return `${minutes}:${remainingSeconds.toString().padStart(2, "0")}`
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Calculates progress percentage for different timer states
|
|
12
|
+
*/
|
|
13
|
+
export const calculateProgress = (
|
|
14
|
+
currentState: string,
|
|
15
|
+
remainingTime: number,
|
|
16
|
+
maxTime: number | null,
|
|
17
|
+
): number => {
|
|
18
|
+
if (currentState === "idle") {
|
|
19
|
+
return 0
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (currentState === "countdown" && maxTime !== null) {
|
|
23
|
+
// Count-down mode: progress based on elapsed time
|
|
24
|
+
const elapsed = maxTime - remainingTime
|
|
25
|
+
return Math.min(100, (elapsed / maxTime) * 100)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (currentState === "measuring" || currentState === "countup") {
|
|
29
|
+
// Count-up modes: progress based on minute steps (0-100% per minute)
|
|
30
|
+
return ((remainingTime / 60) % 1) * 100
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return 0
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Calculates exact progress position based on elapsed time
|
|
38
|
+
*/
|
|
39
|
+
export const calculateExactProgress = (
|
|
40
|
+
currentState: string,
|
|
41
|
+
totalElapsed: number,
|
|
42
|
+
maxTime: number | null,
|
|
43
|
+
): number => {
|
|
44
|
+
if (currentState === "countdown" && maxTime !== null) {
|
|
45
|
+
return Math.min(100, (totalElapsed / maxTime) * 100)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (currentState === "measuring" || currentState === "countup") {
|
|
49
|
+
return ((totalElapsed / 60) % 1) * 100
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return 0
|
|
53
|
+
}
|