# -*- coding: utf-8 -*-
import datetime
import re
import warnings
from obspy import Catalog, UTCDateTime
from obspy.core.event import Origin, Magnitude
from obspy.core.inventory import Inventory
from obspy.core.util.misc import to_int_or_zero
try:
import shapefile
except ImportError as e:
HAS_PYSHP = False
PYSHP_VERSION = None
PYSHP_VERSION_AT_LEAST_1_2_11 = False
IMPORTERROR_MSG = str(e) + (
". ObsPy's write support for shapefiles requires the 'pyshp' module "
"to be installed in addition to the general ObsPy dependencies.")
else:
HAS_PYSHP = True
try:
PYSHP_VERSION = list(map(to_int_or_zero,
shapefile.__version__.split('.')))
except AttributeError:
PYSHP_VERSION = None
PYSHP_VERSION_AT_LEAST_1_2_11 = False
else:
PYSHP_VERSION_AT_LEAST_1_2_11 = PYSHP_VERSION >= [1, 2, 11]
PYSHP_VERSION_WARNING = (
'pyshp versions < 1.2.11 are buggy, e.g. in writing numerical values to '
'the dbf table, so e.g. timestamp float values might lack proper '
'precision. You should update to a newer pyshp version.')
[docs]
def _write_shapefile(obj, filename, extra_fields=None, **kwargs):
"""
Write :class:`~obspy.core.inventory.inventory.Inventory` or
:class:`~obspy.core.event.Catalog` object to a ESRI shapefile.
:type obj: :class:`~obspy.core.event.Catalog` or
:class:`~obspy.core.inventory.inventory.Inventory`
:param obj: ObsPy object for shapefile output
:type filename: str
:param filename: Filename to write to. According to ESRI shapefile
definition, multiple files with the following suffixes will be written:
".shp", ".shx", ".dbj", ".prj". If filename does not end with ".shp",
it will be appended. Other files will be created with respective
suffixes accordingly.
:type extra_fields: list
:param extra_fields: List of extra fields to write to
the shapefile table. Each item in the list has to be specified as a
tuple of: field name (i.e. name of database column, ``str``), field
type (single character as used by ``pyshp``: ``'C'`` for string
fields, ``'N'`` for integer/float fields - use precision ``None`` for
integer fields, ``'L'`` for boolean fields), field width (``int``),
field precision (``int``) and field values (``list`` of individual
values, must have same length as given catalog object or as the sum of
all station objects across all networks of a given inventory).
"""
if not HAS_PYSHP:
raise ImportError(IMPORTERROR_MSG)
if not PYSHP_VERSION_AT_LEAST_1_2_11:
warnings.warn(PYSHP_VERSION_WARNING)
if not filename.endswith(".shp"):
filename += ".shp"
if PYSHP_VERSION >= [2., 0, 0]:
writer = shapefile.Writer(target=filename, shapeType=shapefile.POINT)
else:
writer = shapefile.Writer(shapeType=shapefile.POINT)
writer.autoBalance = 1
# create the layer
if isinstance(obj, Catalog):
_add_catalog_layer(writer, obj, extra_fields=extra_fields)
elif isinstance(obj, Inventory):
_add_inventory_layer(writer, obj, extra_fields=extra_fields)
else:
msg = ("Object for shapefile output must be "
"a Catalog or Inventory.")
raise TypeError(msg)
if PYSHP_VERSION >= [2.0, 0, 0]:
writer.close()
else:
writer.save(filename)
_save_projection_file(filename.rsplit('.', 1)[0] + '.prj')
[docs]
def _add_catalog_layer(writer, catalog, extra_fields=None):
"""
:type writer: :class:`shapefile.Writer`.
:param writer: pyshp Writer object
:type catalog: :class:`~obspy.core.event.Catalog`
:param catalog: Event data to add as a new layer.
:type extra_fields: list
:param extra_fields: List of extra fields to write to the shapefile table.
For details see :func:`_write_shapefile()`.
"""
# [name, type, width, precision]
# field name is 10 chars max
# ESRI shapefile attributes are stored in dbf files, which can not
# store datetimes, only dates, see:
# http://www.gdal.org/drv_shapefile.html
# use POSIX timestamp for exact origin time, set time of first pick
# for events with no origin
field_definitions = [
["EventID", 'C', 100, None],
["OriginID", 'C', 100, None],
["MagID", 'C', 100, None],
["Date", 'D', None, None],
["OriginTime", 'N', 20, 6],
["FirstPick", 'N', 20, 6],
["Longitude", 'N', 16, 10],
["Latitude", 'N', 16, 10],
["Depth", 'N', 8, 3],
["MinHorUncM", 'N', 12, 3],
["MaxHorUncM", 'N', 12, 3],
["MaxHorAzi", 'N', 7, 3],
["OriUncDesc", 'C', 40, None],
["Magnitude", 'N', 8, 3],
]
_create_layer(writer, field_definitions, extra_fields)
if extra_fields:
for name, type_, width, precision, values in extra_fields:
if len(values) != len(catalog):
msg = ("list of values for each item in 'extra_fields' must "
"have same length as Catalog object")
raise ValueError(msg)
for i, event in enumerate(catalog):
# try to use preferred origin/magnitude, fall back to first or use
# empty one with `None` values in it
origin = (event.preferred_origin() or
event.origins and event.origins[0] or
Origin(force_resource_id=False))
magnitude = (event.preferred_magnitude() or
event.magnitudes and event.magnitudes[0] or
Magnitude(force_resource_id=False))
t_origin = origin.time
pick_times = [pick.time for pick in event.picks
if pick.time is not None]
t_pick = pick_times and min(pick_times) or None
date = t_origin or t_pick
feature = {}
# setting fields with `None` results in values of `0.000`
# need to really omit setting values if they are `None`
if event.resource_id is not None:
feature["EventID"] = str(event.resource_id)
if origin.resource_id is not None:
feature["OriginID"] = str(origin.resource_id)
if t_origin is not None:
# Use timestamp for exact timing
feature["OriginTime"] = t_origin.timestamp
if t_pick is not None:
# Use timestamp for exact timing
feature["FirstPick"] = t_pick.timestamp
if date is not None:
# ESRI shapefile attributes are stored in dbf files, which can
# not store datetimes, only dates. We still need to use the
# GDAL API with precision up to seconds (aiming at other output
# drivers of GDAL; `100` stands for GMT)
feature["Date"] = date.datetime
if origin.latitude is not None:
feature["Latitude"] = origin.latitude
if origin.longitude is not None:
feature["Longitude"] = origin.longitude
if origin.depth is not None:
feature["Depth"] = origin.depth / 1e3
if magnitude.mag is not None:
feature["Magnitude"] = magnitude.mag
if magnitude.resource_id is not None:
feature["MagID"] = str(magnitude.resource_id)
if origin.origin_uncertainty is not None:
ou = origin.origin_uncertainty
ou_description = ou.preferred_description
if ou_description == 'uncertainty ellipse':
feature["MinHorUncM"] = ou.min_horizontal_uncertainty
feature["MaxHorUncM"] = ou.max_horizontal_uncertainty
feature["MaxHorAzi"] = \
ou.azimuth_max_horizontal_uncertainty
feature["OriUncDesc"] = ou_description
elif ou_description == 'horizontal uncertainty':
feature["MinHorUncM"] = ou.horizontal_uncertainty
feature["MaxHorUncM"] = ou.horizontal_uncertainty
feature["MaxHorAzi"] = 0.0
feature["OriUncDesc"] = ou_description
else:
msg = ('Encountered an event with origin uncertainty '
'description of type "{}". This is not yet '
'implemented for output as shapefile. No origin '
'uncertainty will be added to shapefile for such '
'events.').format(ou_description)
warnings.warn(msg)
if origin.latitude is not None and origin.longitude is not None:
writer.point(origin.longitude, origin.latitude)
if extra_fields:
for name, _, _, _, values in extra_fields:
feature[name] = values[i]
_add_record(writer, feature)
[docs]
def _add_inventory_layer(writer, inventory, extra_fields=None):
"""
:type writer: :class:`shapefile.Writer`.
:param writer: pyshp Writer object
:type inventory: :class:`~obspy.core.inventory.inventory.Inventory`
:param inventory: Inventory data to add as a new layer.
:type extra_fields: list
:param extra_fields: List of extra fields to write to the shapefile table.
For details see :func:`_write_shapefile()`.
"""
# [name, type, width, precision]
# field name is 10 chars max
# ESRI shapefile attributes are stored in dbf files, which can not
# store datetimes, only dates, see:
# http://www.gdal.org/drv_shapefile.html
# use POSIX timestamp for exact origin time, set time of first pick
# for events with no origin
field_definitions = [
["Network", 'C', 20, None],
["Station", 'C', 20, None],
["Longitude", 'N', 16, 10],
["Latitude", 'N', 16, 10],
["Elevation", 'N', 9, 3],
["StartDate", 'D', None, None],
["EndDate", 'D', None, None],
["Channels", 'C', 254, None],
]
_create_layer(writer, field_definitions, extra_fields)
station_count = sum(len(net) for net in inventory)
if extra_fields:
for name, type_, width, precision, values in extra_fields:
if len(values) != station_count:
msg = ("list of values for each item in 'extra_fields' must "
"have same length as the count of all Stations "
"combined across all Networks.")
raise ValueError(msg)
i = 0
for net in inventory:
for sta in net:
channel_list = ",".join(["%s.%s" % (cha.location_code, cha.code)
for cha in sta])
feature = {}
# setting fields with `None` results in values of `0.000`
# need to really omit setting values if they are `None`
if net.code is not None:
feature["Network"] = net.code
if sta.code is not None:
feature["Station"] = sta.code
if sta.latitude is not None:
feature["Latitude"] = sta.latitude
if sta.longitude is not None:
feature["Longitude"] = sta.longitude
if sta.elevation is not None:
feature["Elevation"] = sta.elevation
if sta.start_date is not None:
# ESRI shapefile attributes are stored in dbf files, which
# can not store datetimes, only dates. We still need to use
# the GDAL API with precision up to seconds (aiming at
# other output drivers of GDAL; `100` stands for GMT)
feature["StartDate"] = sta.start_date.datetime
if sta.end_date is not None:
# ESRI shapefile attributes are stored in dbf files, which
# can not store datetimes, only dates. We still need to use
# the GDAL API with precision up to seconds (aiming at
# other output drivers of GDAL; `100` stands for GMT)
feature["EndDate"] = sta.end_date.datetime
if channel_list:
feature["Channels"] = channel_list
if extra_fields:
for name, _, _, _, values in extra_fields:
feature[name] = values[i]
if sta.latitude is not None and sta.longitude is not None:
writer.point(sta.longitude, sta.latitude)
_add_record(writer, feature)
i += 1
wgs84_wkt = \
"""
GEOGCS["WGS 84",
DATUM["WGS_1984",
SPHEROID["WGS 84",6378137,298.257223563,
AUTHORITY["EPSG","7030"]],
AUTHORITY["EPSG","6326"]],
PRIMEM["Greenwich",0,
AUTHORITY["EPSG","8901"]],
UNIT["degree",0.0174532925199433,
AUTHORITY["EPSG","9122"]],
AUTHORITY["EPSG","4326"]]
"""
wgs84_wkt = re.sub(r'\s+', '', wgs84_wkt)
[docs]
def _save_projection_file(filename):
with open(filename, 'wt') as fh:
fh.write(wgs84_wkt)
[docs]
def _add_field(writer, name, type_, width, precision):
# default field width is not set correctly for dates and booleans in
# shapefile <=1.2.10, see
# GeospatialPython/pyshp@ba61854aa7161fd7d4cff12b0fd08b6ec7581bb7 and
# GeospatialPython/pyshp#71 so work around this
if type_ == 'D':
width = 8
precision = 0
elif type_ == 'L':
width = 1
precision = 0
kwargs = dict(fieldType=type_, size=width, decimal=precision)
# remove None's because shapefile.Writer.field() doesn't use None as
# placeholder but the default values directly
for key in list(kwargs.keys()):
if kwargs[key] is None:
kwargs.pop(key)
writer.field(name, **kwargs)
[docs]
def _create_layer(writer, field_definitions, extra_fields=None):
# Add the fields we're interested in
for name, type_, width, precision in field_definitions:
_add_field(writer, name, type_, width, precision)
field_names = [name for name, _, _, _ in field_definitions]
# add custom fields
if extra_fields is not None:
for name, type_, width, precision, _ in extra_fields:
if name in field_names:
msg = "Conflict with existing field named '{}'.".format(name)
raise ValueError(msg)
_add_field(writer, name, type_, width, precision)
[docs]
def _add_record(writer, feature):
values = []
for key, type_, width, precision in writer.fields:
value = feature.get(key)
# various hacks for old pyshp < 1.2.11
if not PYSHP_VERSION_AT_LEAST_1_2_11:
if type_ == 'C':
# mimic pyshp 1.2.12 behavior of putting 'None' in string
# fields for value of `None`
if value is None:
value = 'None'
# older pyshp is not correctly writing dates as used nowadays
# '%Y%m%d' (8 chars), work around this
elif type_ == 'D':
if isinstance(value, (UTCDateTime, datetime.date)):
value = value.strftime('%Y%m%d')
# work around issues with older pyshp, backport 1.2.12 behavior
elif type_ == 'L':
# logical: 1 byte - initialized to 0x20 (space)
# otherwise T or F
if value in [True, 1]:
value = "T"
elif value in [False, 0]:
value = "F"
else:
value = ' '
# work around issues with older pyshp, backport 1.2.12 behavior
elif type_ in ('N', 'F'):
# numeric or float: number stored as a string, right justified,
# and padded with blanks to the width of the field.
if value in (None, ''):
value = ' ' * width # QGIS NULL
elif not precision:
# caps the size if exceeds the field size
value = format(value, "d")[:width].rjust(width)
else:
# caps the size if exceeds the field size
value = format(value, ".%sf" % precision)[:width].rjust(
width)
# work around older pyshp not converting `None`s properly (e.g. for
# float fields)
elif value is None:
value = ''
values.append(value)
writer.record(*values)
if __name__ == '__main__':
import doctest
doctest.testmod(exclude_empty=True)