Repository: movingpandas/movingpandas

Issue:
Cannot write mf_json dict to file due to TypeError: Object of type Timestamp is not JSON serializable
```
import json

mf_json = tc.to_mf_json()
with open('mf.json', 'w') as json_file:
    json.dump(mf_json, json_file, indent=4)

```

fails with TypeError: Object of type Timestamp is not JSON serializable


Discussion:
codecov-commenter: :warning: Please install the !['codecov app svg image'](https://github.com/codecov/engineering-team/assets/152432831/e90313f4-9d3a-4b63-8b54-cfe14e7ec20d) to ensure uploads and comments are reliably processed by Codecov.

## [Codecov](https://app.codecov.io/gh/movingpandas/movingpandas/pull/407?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 88.74%. Comparing base [(`6f2c4b5`)](https://app.codecov.io/gh/movingpandas/movingpandas/commit/6f2c4b59b59faa8fe9b46e20b95ae58883c08af9?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) to head [(`109ee2c`)](https://app.codecov.io/gh/movingpandas/movingpandas/commit/109ee2c6f47bcca9bd7f9ebf984d0990c6b810f8?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None).

:exclamation: Your organization needs to install the [Codecov GitHub app](https://github.com/apps/codecov/installations/select_target) to enable full functionality.
> :exclamation:  There is a different number of reports uploaded between BASE (6f2c4b5) and HEAD (109ee2c). Click for more details.
> 
> <details><summary>HEAD has 2 uploads less than BASE</summary>
>
>| Flag | BASE (6f2c4b5) | HEAD (109ee2c) |
>|------|------|------|
>||5|3|
></details>

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##             main     #407      +/-   ##
==========================================
- Coverage   96.22%   88.74%   -7.49%     
==========================================
  Files          35       35              
  Lines        4185     4192       +7     
==========================================
- Hits         4027     3720     -307     
- Misses        158      472     +314     
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/movingpandas/movingpandas/pull/407?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None).


Patch:
diff --git a/movingpandas/io.py b/movingpandas/io.py
index a21d301..fe00cf3 100644
--- a/movingpandas/io.py
+++ b/movingpandas/io.py
@@ -5,6 +5,10 @@ from geopandas import GeoDataFrame
 from pandas import DataFrame
 
 
+def _datetime_to_string(dt, format="%Y-%m-%d %H:%M:%S"):
+    return dt.strftime(format)
+
+
 def gdf_to_mf_json(
     gdf: GeoDataFrame,
     traj_id_column: str,
@@ -15,6 +19,7 @@ def gdf_to_mf_json(
     crs=None,
     trs=None,
     datetime_encoder: Callable = None,
+    datetime_to_str: bool = False,
     # simplified typing hint due to https://github.com/movingpandas/movingpandas/issues/345  # noqa F401
 ) -> dict:
     """
@@ -50,6 +55,9 @@ def gdf_to_mf_json(
 
     rows = []
 
+    if datetime_to_str:
+        datetime_encoder = _datetime_to_string
+
     for identifier, row in gdf.groupby(traj_id_column):
         datetimes = _retrieve_datetimes_from_row(datetime_column, datetime_encoder, row)
 
diff --git a/movingpandas/trajectory.py b/movingpandas/trajectory.py
index 398ed15..e2eed65 100644
--- a/movingpandas/trajectory.py
+++ b/movingpandas/trajectory.py
@@ -643,7 +643,7 @@ class Trajectory:
         traj_gdf = GeoDataFrame(df, crs=self.crs)
         return traj_gdf
 
-    def to_mf_json(self):
+    def to_mf_json(self, datetime_to_str=True):
         """
         Converts a Trajectory to a dictionary compatible with the Moving
         Features JSON (MF-JSON) specification.
@@ -658,7 +658,12 @@ class Trajectory:
         """
         tmp = self.to_point_gdf()
         t = tmp.index.name
-        mf_json = gdf_to_mf_json(tmp.reset_index(), self.get_traj_id_col(), t)
+        mf_json = gdf_to_mf_json(
+            tmp.reset_index(),
+            self.get_traj_id_col(),
+            t,
+            datetime_to_str=datetime_to_str,
+        )
         return mf_json
 
     def get_start_location(self):
diff --git a/movingpandas/trajectory_collection.py b/movingpandas/trajectory_collection.py
index e8645c2..053d506 100644
--- a/movingpandas/trajectory_collection.py
+++ b/movingpandas/trajectory_collection.py
@@ -180,7 +180,7 @@ class TrajectoryCollection:
         gdf.reset_index(drop=True, inplace=True)
         return gdf
 
-    def to_mf_json(self):
+    def to_mf_json(self, datetime_to_str=True):
         """
         Converts a TrajectoryCollection to a dictionary compatible with the Moving
         Features JSON (MF-JSON) specification.
@@ -195,7 +195,12 @@ class TrajectoryCollection:
         """
         tmp = self.to_point_gdf()
         t = tmp.index.name
-        mf_json = gdf_to_mf_json(tmp.reset_index(), self.get_traj_id_col(), t)
+        mf_json = gdf_to_mf_json(
+            tmp.reset_index(),
+            self.get_traj_id_col(),
+            t,
+            datetime_to_str=datetime_to_str,
+        )
         return mf_json
 
     def _df_to_trajectories(self, df, traj_id_col, obj_id_col, t, x, y, crs):
diff --git a/tutorials/8-ogc-moving-features.ipynb b/tutorials/8-ogc-moving-features.ipynb
index 567ba5f..41a7547 100644
--- a/tutorials/8-ogc-moving-features.ipynb
+++ b/tutorials/8-ogc-moving-features.ipynb
@@ -14,8 +14,9 @@
     "\n",
     "Ressources:\n",
     "\n",
-    "- OGC Moving Features Standard Working Group https://github.com/opengeospatial/mf-swg\n",
-    "- [Moving Features JSON Encoding](https://docs.ogc.org/is/19-045r3/19-045r3.html) | [Samples in Github](https://github.com/opengeospatial/mf-json)\n",
+    "- [OGC Moving Features Standard Working Group on Github](https://github.com/opengeospatial/mf-swg)\n",
+    "- [Moving Features JSON Encoding Standard](https://docs.ogc.org/is/19-045r3/19-045r3.html)\n",
+    "   - [Sample files on Github](https://github.com/opengeospatial/mf-json)\n",
     "\n",
     "\n",
     "\n",
@@ -95,6 +96,65 @@
     "traj.explore()"
    ]
   },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "## Writing MF-JSON"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "from geopandas import read_file\n",
+    "gdf = read_file('data/demodata_geolife.gpkg')\n",
+    "tc = mpd.TrajectoryCollection(gdf, 'trajectory_id', t='t')\n",
+    "tc = mpd.MinDistanceGeneralizer(tc).generalize(tolerance=100)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "mf_json = tc.to_mf_json()\n",
+    "mf_json"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "import json\n",
+    "with open('mf.json', 'w') as json_file:\n",
+    "    json.dump(mf_json, json_file, indent=4)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "tc = mpd.read_mf_json('mf.json', traj_id_property='trajectory_id' )\n",
+    "tc"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "tc.explore()"
+   ]
+  },
   {
    "cell_type": "code",
    "execution_count": null,


Test patch:
diff --git a/movingpandas/tests/test_trajectory.py b/movingpandas/tests/test_trajectory.py
index 939b200..b19c91c 100644
--- a/movingpandas/tests/test_trajectory.py
+++ b/movingpandas/tests/test_trajectory.py
@@ -1009,8 +1009,15 @@ class TestTrajectory:
 
         assert_frame_equal(traj_gdf_wkt, expected_line_gdf_wkt)
 
-    def test_to_mf_json(self):
+    def test_to_mf_json_tostr(self):
         json = self.default_traj_metric.to_mf_json()
+        assert (
+            str(json)
+            == """{'type': 'FeatureCollection', 'features': [{'type': 'Feature', 'properties': {'traj_id': 1, 'value': 1}, 'temporalGeometry': {'type': 'MovingPoint', 'coordinates': [(0.0, 0.0), (6.0, 0.0), (10.0, 0.0)], 'datetimes': ['1970-01-01 00:00:00', '1970-01-01 00:00:10', '1970-01-01 00:00:20']}}]}"""  # noqa F401
+        )
+
+    def test_to_mf_json(self):
+        json = self.default_traj_metric.to_mf_json(datetime_to_str=False)
         assert (
             str(json)
             == """{'type': 'FeatureCollection', 'features': [{'type': 'Feature', 'properties': {'traj_id': 1, 'value': 1}, 'temporalGeometry': {'type': 'MovingPoint', 'coordinates': [(0.0, 0.0), (6.0, 0.0), (10.0, 0.0)], 'datetimes': [Timestamp('1970-01-01 00:00:00'), Timestamp('1970-01-01 00:00:10'), Timestamp('1970-01-01 00:00:20')]}}]}"""  # noqa F401
diff --git a/movingpandas/tests/test_trajectory_collection.py b/movingpandas/tests/test_trajectory_collection.py
index 8a5a664..446745c 100644
--- a/movingpandas/tests/test_trajectory_collection.py
+++ b/movingpandas/tests/test_trajectory_collection.py
@@ -571,7 +571,7 @@ class TestTrajectoryCollection:
         assert_frame_equal(traj_gdf, expected_line_gdf)
 
     def test_to_mf_json(self):
-        json = self.collection.to_mf_json()
+        json = self.collection.to_mf_json(datetime_to_str=False)
         assert (
             str(json)
             == """{'type': 'FeatureCollection', 'features': [{'type': 'Feature', 'properties': {'id': 1, 'obj': 'A', 'val': 9, 'val2': 'a'}, 'temporalGeometry': {'type': 'MovingPoint', 'coordinates': [(0.0, 0.0), (6.0, 0.0), (6.0, 6.0), (9.0, 9.0)], 'datetimes': [Timestamp('2018-01-01 12:00:00'), Timestamp('2018-01-01 12:06:00'), Timestamp('2018-01-01 14:10:00'), Timestamp('2018-01-01 14:15:00')]}}, {'type': 'Feature', 'properties': {'id': 2, 'obj': 'A', 'val': 10, 'val2': 'e'}, 'temporalGeometry': {'type': 'MovingPoint', 'coordinates': [(10.0, 10.0), (16.0, 10.0), (16.0, 16.0), (190.0, 10.0)], 'datetimes': [Timestamp('2018-01-01 12:00:00'), Timestamp('2018-01-01 12:06:00'), Timestamp('2018-01-02 13:10:00'), Timestamp('2018-01-02 13:15:00')]}}]}"""  # noqa F401