44 * This file is licensed under the MIT License.
55 * License text available at https://opensource.org/licenses/MIT
66 */
7- import React , { useState , useRef , useEffect } from 'react' ;
7+ import React , { useState , useEffect , useCallback , useRef } from 'react' ;
88import Collapsible from 'react-collapsible' ;
99import { useTranslation } from 'react-i18next' ;
1010import { faCheckCircle } from '@fortawesome/free-solid-svg-icons/faCheckCircle' ;
@@ -36,67 +36,83 @@ import { EventManager } from 'chaire-lib-common/lib/services/events/EventManager
3636import { MapUpdateLayerEventType } from 'chaire-lib-frontend/lib/services/map/events/MapEventsCallbacks' ;
3737import { calculateRouting } from '../../../services/routing/RoutingUtils' ;
3838import { RoutingResultsByMode } from 'chaire-lib-common/lib/services/routing/types' ;
39+ import { useHistoryTracker } from 'chaire-lib-frontend/lib/components/forms/useHistoryTracker' ;
40+ import UndoRedoButtons from 'chaire-lib-frontend/lib/components/pageParts/UndoRedoButtons' ;
3941
4042export interface TransitRoutingFormProps {
4143 availableRoutingModes ?: string [ ] ;
4244}
4345
44- const TransitRoutingForm : React . FC < TransitRoutingFormProps > = ( props ) => {
46+ const TransitRoutingForm : React . FC < TransitRoutingFormProps > = ( props : TransitRoutingFormProps ) => {
4547 // State hooks to replace class state
46- const transitRouting = useRef < TransitRouting > (
48+ const transitRoutingRef = useRef < TransitRouting > (
4749 new TransitRouting ( _cloneDeep ( Preferences . get ( 'transit.routing.transit' ) ) )
48- ) . current ;
50+ ) ;
51+ const transitRouting = transitRoutingRef . current ;
4952 // State value is not used
5053 const [ , setRoutingAttributes ] = useState < TransitRoutingAttributes > ( transitRouting . attributes ) ;
51- // FIXME using any to avoid typing the formValues, which would be tedious, will be rewritten soon anyway
52- const [ formValues , setFormValues ] = useState < any > ( ( ) => ( {
53- routingName : transitRouting . attributes . routingName || '' ,
54- routingModes : transitRouting . attributes . routingModes || [ 'transit' ] ,
55- minWaitingTimeSeconds : transitRouting . attributes . minWaitingTimeSeconds ,
56- maxAccessEgressTravelTimeSeconds : transitRouting . attributes . maxAccessEgressTravelTimeSeconds ,
57- maxTransferTravelTimeSeconds : transitRouting . attributes . maxTransferTravelTimeSeconds ,
58- maxFirstWaitingTimeSeconds : transitRouting . attributes . maxFirstWaitingTimeSeconds ,
59- maxTotalTravelTimeSeconds : transitRouting . attributes . maxTotalTravelTimeSeconds ,
60- scenarioId : transitRouting . attributes . scenarioId ,
61- withAlternatives : transitRouting . attributes . withAlternatives
62- } ) ) ;
63-
6454 const [ currentResult , setCurrentResult ] = useState < RoutingResultsByMode | undefined > ( undefined ) ;
6555 const [ scenarioCollection , setScenarioCollection ] = useState ( serviceLocator . collectionManager . get ( 'scenarios' ) ) ;
6656 const [ loading , setLoading ] = useState ( false ) ;
6757 const [ routingErrors , setRoutingErrors ] = useState < ErrorMessage [ ] | undefined > ( undefined ) ;
6858 const [ selectedMode , setSelectedMode ] = useState < RoutingOrTransitMode | undefined > ( undefined ) ;
59+ const [ changeCount , setChangeCount ] = useState ( 0 ) ; // Used to force a rerender when the object changes
6960
7061 const { t } = useTranslation ( [ 'transit' , 'main' , 'form' ] ) ;
7162
7263 // Using refs for stateful values that don't trigger renders
73- const invalidFieldsRef = useRef < { [ key : string ] : boolean } > ( { } ) ;
7464 const calculateRoutingNonceRef = useRef < object > ( new Object ( ) ) ;
7565
76- // Functionality from ChangeEventsForm
77- const hasInvalidFields = ( ) : boolean => {
78- return Object . keys ( invalidFieldsRef . current ) . filter ( ( key ) => invalidFieldsRef . current [ key ] ) . length > 0 ;
79- } ;
66+ const {
67+ onValueChange : onFieldValueChange ,
68+ hasInvalidFields,
69+ formValues,
70+ updateHistory,
71+ canRedo,
72+ canUndo,
73+ undo,
74+ redo
75+ } = useHistoryTracker ( { object : transitRouting } ) ;
76+
77+ // Update scenario collection when it changes
78+ const onScenarioCollectionUpdate = useCallback ( ( ) => {
79+ setScenarioCollection ( serviceLocator . collectionManager . get ( 'scenarios' ) ) ;
80+ } , [ ] ) ;
8081
81- const onFormFieldChange = (
82- path : string ,
83- newValue : { value : any ; valid ?: boolean } = { value : null , valid : true }
84- ) => {
85- setFormValues ( ( prevValues ) => ( { ...prevValues , [ path ] : newValue . value } ) ) ;
86- if ( newValue . valid !== undefined && ! newValue . valid ) {
87- invalidFieldsRef . current [ path ] = true ;
88- } else {
89- invalidFieldsRef . current [ path ] = false ;
82+ // Setup event listeners on mount and cleanup on unmount
83+ useEffect ( ( ) => {
84+ serviceLocator . eventManager . on ( 'collection.update.scenarios' , onScenarioCollectionUpdate ) ;
85+
86+ return ( ) => {
87+ serviceLocator . eventManager . off ( 'collection.update.scenarios' , onScenarioCollectionUpdate ) ;
88+ } ;
89+ } , [ onScenarioCollectionUpdate ] ) ;
90+
91+ // Setup event listeners on mount and cleanup on unmount
92+ useEffect ( ( ) => {
93+ if ( transitRouting . hasOrigin ( ) ) {
94+ ( serviceLocator . eventManager as EventManager ) . emitEvent < MapUpdateLayerEventType > ( 'map.updateLayer' , {
95+ layerName : 'routingPoints' ,
96+ data : transitRouting . originDestinationToGeojson ( )
97+ } ) ;
9098 }
91- } ;
99+ } , [ changeCount ] ) ;
100+
101+ const resetResultsData = useCallback ( ( ) => {
102+ setCurrentResult ( undefined ) ;
103+ serviceLocator . eventManager . emit ( 'map.updateLayers' , {
104+ routingPaths : undefined ,
105+ routingPathsStrokes : undefined
106+ } ) ;
107+ } , [ ] ) ;
92108
93- const onValueChange = (
109+ const onValueChange = useCallback ( (
94110 path : keyof TransitRoutingAttributes ,
95111 newValue : { value : any ; valid ?: boolean } = { value : null , valid : true } ,
96112 resetResults = true
97113 ) => {
98114 setRoutingErrors ( [ ] ) ; //When a value is changed, remove the current routingErrors to stop displaying them.
99- onFormFieldChange ( path , newValue ) ;
115+ onFieldValueChange ( path , newValue ) ;
100116 if ( newValue . valid || newValue . valid === undefined ) {
101117 const updatedObject = transitRouting ;
102118 updatedObject . set ( path , newValue . value ) ;
@@ -106,15 +122,8 @@ const TransitRoutingForm: React.FC<TransitRoutingFormProps> = (props) => {
106122 if ( resetResults ) {
107123 resetResultsData ( ) ;
108124 }
109- } ;
110-
111- const resetResultsData = ( ) => {
112- setCurrentResult ( undefined ) ;
113- serviceLocator . eventManager . emit ( 'map.updateLayers' , {
114- routingPaths : undefined ,
115- routingPathsStrokes : undefined
116- } ) ;
117- } ;
125+ updateHistory ( ) ;
126+ } , [ onFieldValueChange , resetResultsData , transitRouting , updateHistory ] ) ;
118127
119128 const isValid = ( ) : boolean => {
120129 // Are all form fields valid and the routing object too
@@ -129,11 +138,11 @@ const TransitRoutingForm: React.FC<TransitRoutingFormProps> = (props) => {
129138 originGeojson,
130139 destinationGeojson
131140 } = routing . attributes ;
141+
132142 if ( ! originGeojson || ! destinationGeojson ) {
133143 return ;
134144 }
135- // Save the origin et destinations lat/lon, and time, along with whether it is arrival or departure
136- // TODO Support specifying departure/arrival as variable in batch routing
145+
137146 routing . addElementForBatch ( {
138147 routingName,
139148 departureTimeSecondsSinceMidnight,
@@ -144,10 +153,6 @@ const TransitRoutingForm: React.FC<TransitRoutingFormProps> = (props) => {
144153
145154 routing . set ( 'routingName' , '' ) ; // empty routing name for the next route
146155 setRoutingAttributes ( { ...routing . attributes } ) ;
147- setFormValues ( ( prevValues ) => ( {
148- ...prevValues ,
149- routingName : routing . attributes . routingName
150- } ) ) ;
151156 } ;
152157
153158 const calculate = async ( refresh = false ) => {
@@ -213,20 +218,19 @@ const TransitRoutingForm: React.FC<TransitRoutingFormProps> = (props) => {
213218 }
214219 setRoutingAttributes ( { ...routing . attributes } ) ;
215220 setCurrentResult ( undefined ) ;
216- } ;
217-
218- const onScenarioCollectionUpdate = ( ) => {
219- setScenarioCollection ( serviceLocator . collectionManager . get ( 'scenarios' ) ) ;
221+ updateHistory ( ) ;
220222 } ;
221223
222224 const downloadCsv = ( ) => {
223225 const elements = transitRouting . attributes . savedForBatch ;
224226 const lines : string [ ] = [ ] ;
225227 lines . push ( 'id,routingName,originLon,originLat,destinationLon,destinationLat,time' ) ;
228+
226229 elements . forEach ( ( element , index ) => {
227230 const time = ! _isBlank ( element . arrivalTimeSecondsSinceMidnight )
228231 ? element . arrivalTimeSecondsSinceMidnight
229232 : element . departureTimeSecondsSinceMidnight ;
233+
230234 lines . push (
231235 index +
232236 ',' +
@@ -243,6 +247,7 @@ const TransitRoutingForm: React.FC<TransitRoutingFormProps> = (props) => {
243247 ( ! _isBlank ( time ) ? secondsSinceMidnightToTimeStr ( time as number ) : '' )
244248 ) ;
245249 } ) ;
250+
246251 const csvFileContent = lines . join ( '\n' ) ;
247252
248253 const element = document . createElement ( 'a' ) ;
@@ -257,32 +262,18 @@ const TransitRoutingForm: React.FC<TransitRoutingFormProps> = (props) => {
257262 const updatedObject = transitRouting ;
258263 updatedObject . resetBatchSelection ( ) ;
259264 setRoutingAttributes ( { ...updatedObject . attributes } ) ;
265+ updateHistory ( ) ;
260266 } ;
261267
262- const onTripTimeChange = ( time : { value : any ; valid ?: boolean } , timeType : 'departure' | 'arrival' ) => {
263- onValueChange (
264- timeType === 'departure' ? 'departureTimeSecondsSinceMidnight' : 'arrivalTimeSecondsSinceMidnight' ,
265- time
266- ) ;
267- } ;
268-
269- // Handle componentDidMount and componentWillUnmount
270- useEffect ( ( ) => {
271- // ComponentDidMount
272- if ( transitRouting . hasOrigin ( ) ) {
273- ( serviceLocator . eventManager as EventManager ) . emitEvent < MapUpdateLayerEventType > ( 'map.updateLayer' , {
274- layerName : 'routingPoints' ,
275- data : transitRouting . originDestinationToGeojson ( )
276- } ) ;
277- }
278-
279- serviceLocator . eventManager . on ( 'collection.update.scenarios' , onScenarioCollectionUpdate ) ;
280-
281- // ComponentWillUnmount
282- return ( ) => {
283- serviceLocator . eventManager . off ( 'collection.update.scenarios' , onScenarioCollectionUpdate ) ;
284- } ;
285- } , [ ] ) ;
268+ const onTripTimeChange = useCallback (
269+ ( time : { value : any ; valid ?: boolean } , timeType : 'departure' | 'arrival' ) => {
270+ onValueChange (
271+ timeType === 'departure' ? 'departureTimeSecondsSinceMidnight' : 'arrivalTimeSecondsSinceMidnight' ,
272+ time
273+ ) ;
274+ } ,
275+ [ onValueChange ]
276+ ) ;
286277
287278 // If the previously selected scenario was deleted, the current scenario ID will remain but the scenario itself will no longer exist, leading to an error.
288279 // In that case, change it to undefined.
@@ -319,6 +310,31 @@ const TransitRoutingForm: React.FC<TransitRoutingFormProps> = (props) => {
319310 } ;
320311 } ) ;
321312
313+ const updateCurrentObject = ( newObject : TransitRouting ) => {
314+ transitRoutingRef . current = newObject ;
315+ resetResultsData ( ) ;
316+ setChangeCount ( changeCount + 1 ) ;
317+ // Update routing preferences if the object is valid.
318+ // FIXME Should we calculate too?
319+ if ( isValid ( ) ) {
320+ newObject . updateRoutingPrefs ( ) ;
321+ }
322+ } ;
323+
324+ const onUndo = ( ) => {
325+ const newObject = undo ( ) ;
326+ if ( newObject ) {
327+ updateCurrentObject ( newObject ) ;
328+ }
329+ } ;
330+
331+ const onRedo = ( ) => {
332+ const newObject = redo ( ) ;
333+ if ( newObject ) {
334+ updateCurrentObject ( newObject ) ;
335+ }
336+ } ;
337+
322338 return (
323339 < React . Fragment >
324340 < form id = "tr__form-transit-routing" className = "tr__form-transit-routing apptr__form" >
@@ -335,6 +351,7 @@ const TransitRoutingForm: React.FC<TransitRoutingFormProps> = (props) => {
335351 value = { selectedRoutingModes }
336352 localePrefix = "transit:transitPath:routingModes"
337353 onValueChange = { ( e ) => onValueChange ( 'routingModes' , { value : e . target . value } ) }
354+ key = { `formFieldTransitRoutingRoutingModes${ changeCount } ` }
338355 />
339356 </ InputWrapper >
340357 ) }
@@ -347,18 +364,21 @@ const TransitRoutingForm: React.FC<TransitRoutingFormProps> = (props) => {
347364 transitRouting . attributes . arrivalTimeSecondsSinceMidnight
348365 }
349366 onValueChange = { onTripTimeChange }
367+ key = { `formFieldTransitRoutingTimeOfTrip${ changeCount } ` }
350368 />
351369 ) }
352370 { hasTransitModeSelected && (
353371 < TransitRoutingBaseComponent
354372 onValueChange = { onValueChange }
355373 attributes = { transitRouting . attributes }
374+ key = { `formFieldTransitRoutingBaseComponents${ changeCount } ` }
356375 />
357376 ) }
358377 { hasTransitModeSelected && (
359378 < InputWrapper label = { t ( 'transit:transitRouting:Scenario' ) } >
360379 < InputSelect
361380 id = { 'formFieldTransitRoutingScenario' }
381+ key = { `formFieldTransitRoutingScenario${ changeCount } ` }
362382 value = { formValues . scenarioId }
363383 choices = { scenarios }
364384 t = { t }
@@ -370,6 +390,7 @@ const TransitRoutingForm: React.FC<TransitRoutingFormProps> = (props) => {
370390 < InputWrapper label = { t ( 'transit:transitRouting:WithAlternatives' ) } >
371391 < InputRadio
372392 id = { 'formFieldTransitRoutingWithAlternatives' }
393+ key = { `formFieldTransitRoutingWithAlternatives${ changeCount } ` }
373394 value = { formValues . withAlternatives }
374395 sameLine = { true }
375396 disabled = { false }
@@ -396,10 +417,12 @@ const TransitRoutingForm: React.FC<TransitRoutingFormProps> = (props) => {
396417 originGeojson = { transitRouting . attributes . originGeojson }
397418 destinationGeojson = { transitRouting . attributes . destinationGeojson }
398419 onUpdateOD = { onUpdateOD }
420+ key = { `formFieldTransitRoutingCoordinates${ changeCount } ` }
399421 />
400422 < InputWrapper label = { t ( 'transit:transitRouting:RoutingName' ) } >
401423 < InputString
402424 id = { 'formFieldTransitRoutingRoutingName' }
425+ key = { `formFieldTransitRoutingRoutingName${ changeCount } ` }
403426 value = { formValues . routingName }
404427 onValueUpdated = { ( value ) => onValueChange ( 'routingName' , value , false ) }
405428 pattern = { '[^,"\':;\r\n\t\\\\]*' }
@@ -440,6 +463,7 @@ const TransitRoutingForm: React.FC<TransitRoutingFormProps> = (props) => {
440463
441464 < div >
442465 < div className = "tr__form-buttons-container" >
466+ < UndoRedoButtons canUndo = { canUndo } canRedo = { canRedo } onUndo = { onUndo } onRedo = { onRedo } />
443467 { loading && < Loader size = { 8 } color = { '#aaaaaa' } loading = { true } > </ Loader > }
444468 < span title = { t ( 'main:Calculate' ) } >
445469 < Button
0 commit comments