Repository: obspy/obspy

Issue:
obspy.io.zmap.core Error: None type not handled
### Avoid duplicates

- [X] I searched existing issues

### Bug Summary

Error when you want to convert a FDSN catalog in zmap format. If you request a large catalog, there may be NoneTypes in the Depth attribute. This case is not handled correctly

### Code to Reproduce

```python
from obspy.clients.fdsn.client import Client

client = Client("IRIS")

catalog = client.get_events(minlatitude=-40.0, maxlatitude=-15.0,
                            minlongitude=-78.0, maxlongitude=-66.0,
                            minmagnitude=6.0)

print(catalog)

catalog.write(filename="catalog_zmap2.txt", format="ZMAP", with_uncertainties=True)
```


### Error Traceback

```Python traceback
350 Event(s) in Catalog:
2023-03-30T17:33:06.366000Z | -35.663,  -73.540 | 6.3  mww
2023-03-22T16:00:31.218000Z | -23.480,  -66.511 | 6.5  mww
...
1960-01-13T15:40:24.000000Z | -16.020,  -73.020 | 7.1  None
1960-01-02T03:21:48.000000Z | -17.680,  -69.210 | 6.2  None
To see all events call 'print(CatalogObject.__str__(print_all=True))'
Traceback (most recent call last):
  File "/home/andrea/Documents/test_zmap/save_catalog_zmap_format.py", line 11, in <module>
    catalog.write(filename="catalog_zmap2.txt", format="ZMAP", with_uncertainties=True)
  File "/home/andrea/miniconda3/envs/obspy_env/lib/python3.11/site-packages/obspy/core/event/catalog.py", line 506, in write
    return write_format(self, filename, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/andrea/miniconda3/envs/obspy_env/lib/python3.11/site-packages/obspy/io/zmap/core.py", line 306, in _write_zmap
    Pickler(with_uncertainties).dump(catalog, filename)
  File "/home/andrea/miniconda3/envs/obspy_env/lib/python3.11/site-packages/obspy/io/zmap/core.py", line 53, in dump
    cat_string = self._serialize(catalog)
                 ^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/andrea/miniconda3/envs/obspy_env/lib/python3.11/site-packages/obspy/io/zmap/core.py", line 141, in _serialize
    'depth': self._num2str(origin.depth / 1000.0),  # m to km
                           ~~~~~~~~~~~~~^~~~~~~~
TypeError: unsupported operand type(s) for /: 'NoneType' and 'float'
```


### ObsPy Version?

1.4.0

### Operating System?

Ubuntu 22.04

### Python Version?

3.11.1

### Installation Method?

conda


Discussion:
I don't really know zmap format, is there a way to express `None` or rather skip such events with a warning?
I will try to provide more helpful information about this bug. The catalog return from iris might cause it.

I print the Origin infomation which caused bug here

```
Origin
           resource_id: ResourceIdentifier(id="smi:service.iris.edu/fdsnws/event/1/query?originid=3156893")
                  time: UTCDateTime(2002, 3, 28, 4, 56, 22, 570000)
             longitude: -68.305
              latitude: -21.663
         creation_info: CreationInfo(author='ISC')
```

The code is equivalent to acquiring by curl, and there is no depth label in the XML result.

```
curl http://service.iris.edu/fdsnws/event/1/query?eventid=1306873  
```

Here are the output of curl

```
<?xml version="1.0" encoding="UTF-8"?>
<q:quakeml xmlns:q="http://quakeml.org/xmlns/quakeml/1.2" xmlns:iris="http://service.iris.edu/fdsnws/event/1/" xmlns="http://quakeml.org/xmlns/bed/1.2" xmlns:xsi="http://www.w3.org/2000/10/XMLSchema-instance" xsi:schemaLocation="http://quakeml.org/schema/xsd http://quakeml.org/schema/xsd/QuakeML-1.2.xsd">
<eventParameters publicID="smi:service.iris.edu/fdsnws/event/1/query">
<event publicID="smi:service.iris.edu/fdsnws/event/1/query?eventid=1306873">
<type>earthquake</type>
<description xmlns:iris="http://service.iris.edu/fdsnws/event/1/" iris:FEcode="124">
<type>Flinn-Engdahl region</type>
<text>CHILE-BOLIVIA BORDER REGION</text>
</description><preferredMagnitudeID>smi:service.iris.edu/fdsnws/event/1/query?magnitudeid=16778594</preferredMagnitudeID>
<preferredOriginID>smi:service.iris.edu/fdsnws/event/1/query?originid=3156893</preferredOriginID>
<origin xmlns:iris="http://service.iris.edu/fdsnws/event/1/" publicID="smi:service.iris.edu/fdsnws/event/1/query?originid=3156893" iris:contributorOriginId="5628669" iris:contributor="ISC" iris:contributorEventId="3951419" iris:catalog="ISC">
<time>
<value>2002-03-28T04:56:22.570</value>
</time>
<creationInfo>
<author>ISC</author>
</creationInfo>
<latitude>
<value>-21.663</value>
</latitude>
<longitude>
<value>-68.305</value>
</longitude>
</origin>
<magnitude publicID="smi:service.iris.edu/fdsnws/event/1/query?magnitudeid=16778594">
<mag>
<value>6.5</value>
</mag>
<type>MW</type><creationInfo>
<author>HRVD</author>
</creationInfo>
</magnitude>
</event>
</eventParameters>
</q:quakeml>
```

So I think it is fine to skip it if the depth is not in the origin. The quick fix might be adding the following code:

```
if origin.depth is None:
    print("some waning messages here")
    continue
```

before strings.update around the line 141 of obspy/io/zmap/core.py


Resolving patch:
diff --git a/obspy/io/zmap/core.py b/obspy/io/zmap/core.py
--- a/obspy/io/zmap/core.py
+++ b/obspy/io/zmap/core.py
@@ -98,6 +98,8 @@ def _depth_error(origin):
         """
         # TODO: add option to extract depth error from origin uncertainty
         # when ellipsoid is used
+        if origin.depth_errors is None:
+            return None
         if origin.depth_errors.uncertainty is None:
             return None
         return origin.depth_errors.uncertainty / 1000.0
@@ -138,8 +140,6 @@ def _serialize(self, catalog):
                 dec_second = origin.time.second + \
                     origin.time.microsecond / 1e6
                 strings.update({
-                    'depth': self._num2str(origin.depth / 1000.0),  # m to km
-                    'z_err': self._num2str(self._depth_error(origin)),
                     'lat': self._num2str(origin.latitude),
                     'lon': self._num2str(origin.longitude),
                     'h_err': self._num2str(self._hz_error(origin)),
@@ -150,6 +150,13 @@ def _serialize(self, catalog):
                     'minute': self._num2str(origin.time.minute, 0),
                     'second': str(dec_second)
                 })
+                # origin depth is optional in QuakeML so it can be missing
+                if origin.depth is not None:
+                    # m to km
+                    strings['depth'] = self._num2str(origin.depth / 1000.0)
+                    z_err = self._depth_error(origin)
+                    if z_err is not None:
+                        strings['z_err'] = self._num2str(z_err)
             # magnitude
             magnitude = ev.preferred_magnitude()
             if magnitude is None and ev.magnitudes:


Test patch:
diff --git a/obspy/io/zmap/tests/test_zmap.py b/obspy/io/zmap/tests/test_zmap.py
--- a/obspy/io/zmap/tests/test_zmap.py
+++ b/obspy/io/zmap/tests/test_zmap.py
@@ -1,7 +1,8 @@
 # -*- coding: utf-8 -*-
+import io
 import pytest
 
-from obspy.core.event import read_events
+from obspy.core.event import read_events, Catalog, Event, Origin
 from obspy.core.utcdatetime import UTCDateTime
 from obspy.core.util import NamedTemporaryFile, get_example_file
 from obspy.io.zmap import core as zmap
@@ -302,3 +303,31 @@ def _expected_string(self, zmap_dict):
         full_zmap.update(zmap_dict)
         string = '\t'.join(full_zmap[f] for f in self.zmap_fields)
         return string
+
+    def test_no_origin_depth(self):
+        """
+        Test writing event without origin depth
+        """
+        t = UTCDateTime("2021-08-12T10:11:12")
+        origin = Origin(time=t, latitude=12.34, longitude=23.45)
+        event = Event(origins=[origin])
+        cat = Catalog(events=[event])
+        # test writing an event with origin that has no depth or depth
+        # uncertainties
+        bio = io.BytesIO()
+        cat.write(bio, 'ZMAP')
+        bio.seek(0)
+        got = bio.read()
+        expected = (b'23.450000\t12.340000\t2021.612121765601\t8\t12\t'
+                    b'NaN\tNaN\t10\t11\t12.0\n')
+        assert got == expected
+        # test writing an event with origin that has a depth but no depth
+        # uncertainties
+        origin.depth = 34567
+        bio = io.BytesIO()
+        cat.write(bio, 'ZMAP')
+        bio.seek(0)
+        got = bio.read()
+        expected = (b'23.450000\t12.340000\t2021.612121765601\t8\t12\t'
+                    b'NaN\t34.567000\t10\t11\t12.0\n')
+        assert got == expected