@@ -32,6 +32,249 @@ class TrajectoryUnit(Enum): # pylint: disable=too-few-public-methods
3232 """centimeter (cm)"""
3333
3434
35+ def load_trajectory_from_pathfinder_json (
36+ * ,
37+ trajectory_file : pathlib .Path ,
38+ ) -> TrajectoryData :
39+ """Loads Pathfinder-JSON as :class:`~trajectory_data.TrajectoryData`.
40+
41+ This function reads a JSON file containing trajectory data
42+ from Pathfinder simulations and converts it
43+ into a :class:`~trajectory_data.TrajectoryData` object.
44+
45+ .. note::
46+
47+ Pathfinder JSON data have a time-based structure that is going to be
48+ converted to a frame column for use with *PedPy*.
49+
50+ Args:
51+ trajectory_file: The full path of the JSON containing the Pathfinder
52+ trajectory data. The expected format is a JSON file with agent IDs
53+ as top-level keys, and time-stamped position data as nested objects.
54+
55+ Returns:
56+ TrajectoryData: :class:`~trajectory_data.TrajectoryData` representation
57+ of the file data
58+
59+ Raises:
60+ LoadTrajectoryError: If the provided path does not exist or is not a
61+ file, or if the JSON structure is invalid.
62+ """
63+ _validate_is_file (trajectory_file )
64+
65+ traj_dataframe = _load_trajectory_data_from_pathfinder_json (
66+ trajectory_file = trajectory_file
67+ )
68+ traj_dataframe ["frame" ], traj_frame_rate = _calculate_frames_and_fps (
69+ traj_dataframe
70+ )
71+
72+ return TrajectoryData (
73+ data = traj_dataframe [[ID_COL , FRAME_COL , X_COL , Y_COL ]],
74+ frame_rate = traj_frame_rate ,
75+ )
76+
77+
78+ def _load_trajectory_data_from_pathfinder_json (
79+ * , trajectory_file : pathlib .Path
80+ ) -> pd .DataFrame :
81+ """Parse the trajectory JSON file for trajectory data.
82+
83+ Args:
84+ trajectory_file (pathlib.Path): The file containing the trajectory data.
85+ The expected format is a JSON file with agent IDs as top-level keys,
86+ and time-stamped position data as nested objects.
87+
88+ Returns:
89+ The trajectory data as :class:`DataFrame`, the coordinates are
90+ in meter (m).
91+ """
92+ common_error_message = (
93+ "The given trajectory file seems to be incorrect or empty. "
94+ "It should be a valid JSON file with agent IDs as top-level keys "
95+ "and time-stamped position data containing 'position' objects with "
96+ "'x' and 'y' coordinates. "
97+ f"Please check your trajectory file: { trajectory_file } ."
98+ )
99+
100+ try :
101+ with open (trajectory_file , "r" , encoding = "utf-8" ) as file :
102+ data = json .load (file )
103+ except (json .JSONDecodeError , FileNotFoundError , PermissionError ) as e :
104+ raise LoadTrajectoryError (
105+ f"{ common_error_message } \n Original error: { e } "
106+ ) from e
107+
108+ if not isinstance (data , dict ) or not data :
109+ raise LoadTrajectoryError (
110+ f"{ common_error_message } \n Empty or invalid JSON structure."
111+ )
112+
113+ trajectory_records = []
114+
115+ try :
116+ for agent_id_str , time_data in data .items ():
117+ agent_id = int (agent_id_str )
118+
119+ if not isinstance (time_data , dict ):
120+ continue
121+
122+ for time_str , agent_data in time_data .items ():
123+ time_value = float (time_str )
124+
125+ if not isinstance (agent_data , dict ):
126+ continue
127+
128+ position = agent_data .get ("position" )
129+ if not isinstance (position , dict ):
130+ continue
131+
132+ x_pos = position .get ("x" )
133+ y_pos = position .get ("y" )
134+
135+ if x_pos is not None and y_pos is not None :
136+ trajectory_records .append (
137+ {
138+ ID_COL : agent_id ,
139+ TIME_COL : time_value ,
140+ X_COL : float (x_pos ),
141+ Y_COL : float (y_pos ),
142+ }
143+ )
144+
145+ except (ValueError , KeyError , TypeError ) as e :
146+ raise LoadTrajectoryError (
147+ f"{ common_error_message } \n Error parsing JSON structure: { e } "
148+ ) from e
149+
150+ if not trajectory_records :
151+ raise LoadTrajectoryError (
152+ f"{ common_error_message } \n No valid trajectory data found."
153+ )
154+
155+ traj_dataframe = pd .DataFrame (trajectory_records )
156+
157+ # Sort by agent ID and time for consistency
158+ traj_dataframe = traj_dataframe .sort_values ([ID_COL , TIME_COL ]).reset_index (
159+ drop = True
160+ )
161+
162+ return traj_dataframe
163+
164+
165+ def load_trajectory_from_pathfinder_csv (
166+ * ,
167+ trajectory_file : pathlib .Path ,
168+ ) -> TrajectoryData :
169+ """Loads data from Pathfinder-CSV file as :class:`~trajectory_data.TrajectoryData`.
170+
171+ This function reads a CSV file containing trajectory data from Pathfinder
172+ simulations and converts it into a :class:`~trajectory_data.TrajectoryData`
173+ object.
174+
175+ .. note::
176+
177+ Pathfinder data have a time column, that is going to be converted to a
178+ frame column for use with *PedPy*.
179+
180+ .. warning::
181+
182+ Currently only Pathfinder files with a time column can be loaded.
183+
184+ Args:
185+ trajectory_file: The full path of the CSV file containing the Pathfinder
186+ trajectory data. The expected format is a CSV file with comma
187+ as delimiter, and it should contain at least the following
188+ columns: id, t, x, y.
189+
190+ Returns:
191+ TrajectoryData: :class:`~trajectory_data.TrajectoryData` representation
192+ of the file data
193+
194+ Raises:
195+ LoadTrajectoryError: If the provided path does not exist or is not a
196+ file.
197+ """ # noqa: E501
198+ _validate_is_file (trajectory_file )
199+
200+ traj_dataframe = _load_trajectory_data_from_pathfinder_csv (
201+ trajectory_file = trajectory_file
202+ )
203+ traj_dataframe ["frame" ], traj_frame_rate = _calculate_frames_and_fps (
204+ traj_dataframe
205+ )
206+
207+ return TrajectoryData (
208+ data = traj_dataframe [[ID_COL , FRAME_COL , X_COL , Y_COL ]],
209+ frame_rate = traj_frame_rate ,
210+ )
211+
212+
213+ def _load_trajectory_data_from_pathfinder_csv (
214+ * , trajectory_file : pathlib .Path
215+ ) -> pd .DataFrame :
216+ """Parse the trajectory file for trajectory data.
217+
218+ Args:
219+ trajectory_file (pathlib.Path): The file containing the trajectory data.
220+ The expected format is a CSV file with comma as delimiter, and it
221+ should contain at least the following columns: name, t, x, y.
222+
223+ Returns:
224+ The trajectory data as :class:`DataFrame`, the coordinates are
225+ in meter (m).
226+ """
227+ columns_to_keep = ["id" , "t" , "x" , "y" ]
228+ rename_mapping = {
229+ "id" : ID_COL ,
230+ "t" : TIME_COL ,
231+ "x" : X_COL ,
232+ "y" : Y_COL ,
233+ }
234+ column_types = {"id" : int , "time" : float , "x" : float , "y" : float }
235+
236+ common_error_message = (
237+ "The given trajectory file seems to be incorrect or empty. "
238+ "It should contain at least the following columns: "
239+ "id, t, x, y, separated by comma. "
240+ f"Please check your trajectory file: { trajectory_file } ."
241+ )
242+ # csv has a unit line. Usually the second line,
243+ # but not 100% sure if this is always the case.
244+ try :
245+ raw = pd .read_csv (
246+ trajectory_file ,
247+ encoding = "utf-8-sig" ,
248+ )
249+ except Exception as e :
250+ raise LoadTrajectoryError (
251+ f"{ common_error_message } \n Original error: { e } "
252+ ) from e
253+ # filter out the unit line
254+ data = raw [
255+ raw ["t" ].apply (lambda v : str (v ).replace ("." , "" , 1 ).isdigit ())
256+ ].copy ()
257+ missing_columns = set (columns_to_keep ) - set (data .columns )
258+ if missing_columns :
259+ raise LoadTrajectoryError (
260+ f"{ common_error_message } "
261+ f"Missing columns: { ', ' .join (missing_columns )} ."
262+ )
263+ try :
264+ data = data [columns_to_keep ]
265+ data = data .rename (columns = rename_mapping )
266+ data = data .astype (column_types )
267+ except Exception as e :
268+ raise LoadTrajectoryError (
269+ f"{ common_error_message } \n Original error: { e } "
270+ ) from e
271+
272+ if data .empty :
273+ raise LoadTrajectoryError (f"{ common_error_message } .\n Empty dataframe." )
274+
275+ return data
276+
277+
35278def _validate_is_file (file : pathlib .Path ) -> None :
36279 """Validates if the given file is a valid file, if valid raises Exception.
37280
0 commit comments