1# -*- coding: utf-8 -*-
2"""
3Keyhole Markup Language (KML) output support in ObsPy
4
5:copyright:
6    The ObsPy Development Team (devs@obspy.org)
7:license:
8    GNU Lesser General Public License, Version 3
9    (https://www.gnu.org/copyleft/lesser.html)
10"""
11from __future__ import (absolute_import, division, print_function,
12                        unicode_literals)
13from future.builtins import *  # NOQA
14
15from math import log
16
17from lxml.etree import Element, SubElement, tostring
18from matplotlib.cm import get_cmap
19
20from obspy import UTCDateTime
21from obspy.core.event import Catalog
22from obspy.core.inventory.inventory import Inventory
23
24
25def inventory_to_kml_string(
26        inventory,
27        icon_url="https://maps.google.com/mapfiles/kml/shapes/triangle.png",
28        icon_size=1.5, label_size=1.0, cmap="Paired", encoding="UTF-8",
29        timespans=True, strip_far_future_end_times=True):
30    """
31    Convert an :class:`~obspy.core.inventory.inventory.Inventory` to a KML
32    string representation.
33
34    :type inventory: :class:`~obspy.core.inventory.inventory.Inventory`
35    :param inventory: Input station metadata.
36    :type icon_url: str
37    :param icon_url: Internet URL of icon to use for station (e.g. PNG image).
38    :type icon_size: float
39    :param icon_size: Icon size.
40    :type label_size: float
41    :param label_size: Label size.
42    :type encoding: str
43    :param encoding: Encoding used for XML string.
44    :type timespans: bool
45    :param timespans: Whether to add timespan information to the single station
46        elements in the KML or not. If timespans are used, the displayed
47        information in e.g. Google Earth will represent a snapshot in time,
48        such that using the time slider different states of the inventory in
49        time can be visualized. If timespans are not used, any station active
50        at any point in time is always shown.
51    :type strip_far_future_end_times: bool
52    :param strip_far_future_end_times: Leave out likely fictitious end times of
53        stations (more than twenty years after current time). Far future end
54        times may produce time sliders with bad overall time span in third
55        party applications viewing the KML file.
56    :rtype: byte string
57    :return: Encoded byte string containing KML information of the station
58        metadata.
59    """
60    twenty_years_from_now = UTCDateTime() + 3600 * 24 * 365 * 20
61    # construct the KML file
62    kml = Element("kml")
63    kml.set("xmlns", "http://www.opengis.net/kml/2.2")
64
65    document = SubElement(kml, "Document")
66    SubElement(document, "name").text = "Inventory"
67
68    # style definition
69    cmap = get_cmap(name=cmap, lut=len(inventory.networks))
70    for i in range(len(inventory.networks)):
71        color = _rgba_tuple_to_kml_color_code(cmap(i))
72        style = SubElement(document, "Style")
73        style.set("id", "station_%i" % i)
74
75        iconstyle = SubElement(style, "IconStyle")
76        SubElement(iconstyle, "color").text = color
77        SubElement(iconstyle, "scale").text = str(icon_size)
78        icon = SubElement(iconstyle, "Icon")
79        SubElement(icon, "href").text = icon_url
80        hotspot = SubElement(iconstyle, "hotSpot")
81        hotspot.set("x", "0.5")
82        hotspot.set("y", "0.5")
83        hotspot.set("xunits", "fraction")
84        hotspot.set("yunits", "fraction")
85
86        labelstyle = SubElement(style, "LabelStyle")
87        SubElement(labelstyle, "color").text = color
88        SubElement(labelstyle, "scale").text = str(label_size)
89
90    for i, net in enumerate(inventory):
91        folder = SubElement(document, "Folder")
92        SubElement(folder, "name").text = str(net.code)
93        SubElement(folder, "open").text = "1"
94
95        SubElement(folder, "description").text = str(net)
96
97        style = SubElement(folder, "Style")
98        liststyle = SubElement(style, "ListStyle")
99        SubElement(liststyle, "listItemType").text = "check"
100        SubElement(liststyle, "bgColor").text = "00ffff"
101        SubElement(liststyle, "maxSnippetLines").text = "5"
102
103        # add one marker per station code
104        for sta in net:
105            placemark = SubElement(folder, "Placemark")
106            SubElement(placemark, "name").text = ".".join((net.code, sta.code))
107            SubElement(placemark, "styleUrl").text = "#station_%i" % i
108            SubElement(placemark, "color").text = color
109            if sta.longitude is not None and sta.latitude is not None:
110                point = SubElement(placemark, "Point")
111                SubElement(point, "coordinates").text = "%.6f,%.6f,0" % \
112                    (sta.longitude, sta.latitude)
113
114            SubElement(placemark, "description").text = str(sta)
115
116            if timespans:
117                start = sta.start_date
118                end = sta.end_date
119                if start is not None or end is not None:
120                    timespan = SubElement(placemark, "TimeSpan")
121                    if start is not None:
122                        SubElement(timespan, "begin").text = str(start)
123                    if end is not None:
124                        if not strip_far_future_end_times or \
125                                end < twenty_years_from_now:
126                            SubElement(timespan, "end").text = str(end)
127        if timespans:
128            start = net.start_date
129            end = net.end_date
130            if start is not None or end is not None:
131                timespan = SubElement(folder, "TimeSpan")
132                if start is not None:
133                    SubElement(timespan, "begin").text = str(start)
134                if end is not None:
135                    if not strip_far_future_end_times or \
136                            end < twenty_years_from_now:
137                        SubElement(timespan, "end").text = str(end)
138
139    # generate and return KML string
140    return tostring(kml, pretty_print=True, xml_declaration=True,
141                    encoding=encoding)
142
143
144def catalog_to_kml_string(
145        catalog,
146        icon_url="https://maps.google.com/mapfiles/kml/shapes/earthquake.png",
147        label_func=None, icon_size_func=None, encoding="UTF-8",
148        timestamps=True):
149    """
150    Convert a :class:`~obspy.core.event.Catalog` to a KML string
151    representation.
152
153    :type catalog: :class:`~obspy.core.event.Catalog`
154    :param catalog: Input catalog data.
155    :type icon_url: str
156    :param icon_url: Internet URL of icon to use for events (e.g. PNG image).
157    :type label_func: func
158    :type label_func: Custom function to use for determining each event's
159        label. User provided function is supposed to take an
160        :class:`~obspy.core.event.Event` object as single argument, e.g. for
161        empty labels use `label_func=lambda x: ""`.
162    :type icon_size_func: func
163    :type icon_size_func: Custom function to use for determining each
164        event's icon size. User provided function should take an
165        :class:`~obspy.core.event.Event` object as single argument and return a
166        float.
167    :type encoding: str
168    :param encoding: Encoding used for XML string.
169    :type timestamps: bool
170    :param timestamps: Whether to add timestamp information to the event
171        elements in the KML or not. If timestamps are used, the displayed
172        information in e.g. Google Earth will represent a snapshot in time,
173        such that using the time slider different states of the catalog in time
174        can be visualized. If timespans are not used, any event happening at
175        any point in time is always shown.
176    :rtype: byte string
177    :return: Encoded byte string containing KML information of the event
178        metadata.
179    """
180    # default label and size functions
181    if not label_func:
182        def label_func(event):
183            origin = (event.preferred_origin() or
184                      event.origins and event.origins[0] or
185                      None)
186            mag = (event.preferred_magnitude() or
187                   event.magnitudes and event.magnitudes[0] or
188                   None)
189            label = origin.time and str(origin.time.date) or ""
190            if mag:
191                label += " %.1f" % mag.mag
192            return label
193    if not icon_size_func:
194        def icon_size_func(event):
195            mag = (event.preferred_magnitude() or
196                   event.magnitudes and event.magnitudes[0] or
197                   None)
198            if mag:
199                try:
200                    icon_size = 1.2 * log(1.5 + mag.mag)
201                except ValueError:
202                    icon_size = 0.1
203            else:
204                icon_size = 0.5
205            return icon_size
206
207    # construct the KML file
208    kml = Element("kml")
209    kml.set("xmlns", "http://www.opengis.net/kml/2.2")
210
211    document = SubElement(kml, "Document")
212    SubElement(document, "name").text = "Catalog"
213
214    # style definitions for earthquakes
215    style = SubElement(document, "Style")
216    style.set("id", "earthquake")
217
218    iconstyle = SubElement(style, "IconStyle")
219    SubElement(iconstyle, "scale").text = "0.5"
220    icon = SubElement(iconstyle, "Icon")
221    SubElement(icon, "href").text = icon_url
222    hotspot = SubElement(iconstyle, "hotSpot")
223    hotspot.set("x", "0.5")
224    hotspot.set("y", "0.5")
225    hotspot.set("xunits", "fraction")
226    hotspot.set("yunits", "fraction")
227
228    labelstyle = SubElement(style, "LabelStyle")
229    SubElement(labelstyle, "color").text = "ff0000ff"
230    SubElement(labelstyle, "scale").text = "0.8"
231
232    folder = SubElement(document, "Folder")
233    SubElement(folder, "name").text = "Catalog"
234    SubElement(folder, "open").text = "1"
235
236    SubElement(folder, "description").text = str(catalog)
237
238    style = SubElement(folder, "Style")
239    liststyle = SubElement(style, "ListStyle")
240    SubElement(liststyle, "listItemType").text = "check"
241    SubElement(liststyle, "bgColor").text = "00ffffff"
242    SubElement(liststyle, "maxSnippetLines").text = "5"
243
244    # add one marker per event
245    for event in catalog:
246        origin = (event.preferred_origin() or
247                  event.origins and event.origins[0] or
248                  None)
249
250        placemark = SubElement(folder, "Placemark")
251        SubElement(placemark, "name").text = label_func(event)
252        SubElement(placemark, "styleUrl").text = "#earthquake"
253        style = SubElement(placemark, "Style")
254        icon_style = SubElement(style, "IconStyle")
255        liststyle = SubElement(style, "ListStyle")
256        SubElement(liststyle, "maxSnippetLines").text = "5"
257        SubElement(icon_style, "scale").text = "%.5f" % icon_size_func(event)
258        if origin:
259            if origin.longitude is not None and origin.latitude is not None:
260                point = SubElement(placemark, "Point")
261                SubElement(point, "coordinates").text = "%.6f,%.6f,0" % \
262                    (origin.longitude, origin.latitude)
263
264        SubElement(placemark, "description").text = str(event)
265
266        if timestamps:
267            time = _get_event_timestamp(event)
268            if time is not None:
269                SubElement(placemark, "TimeStamp").text = str(time)
270
271    # generate and return KML string
272    return tostring(kml, pretty_print=True, xml_declaration=True,
273                    encoding=encoding)
274
275
276def _write_kml(obj, filename, **kwargs):
277    """
278    Write :class:`~obspy.core.inventory.inventory.Inventory` or
279    :class:`~obspy.core.event.Catalog` object to a KML file.
280    For additional parameters see :meth:`inventory_to_kml_string` and
281    :meth:`catalog_to_kml_string`.
282
283    :type obj: :class:`~obspy.core.event.Catalog` or
284        :class:`~obspy.core.inventory.Inventory`
285    :param obj: ObsPy object for KML output
286    :type filename: str
287    :param filename: Filename to write to. Suffix ".kml" will be appended if
288        not already present.
289    """
290    if isinstance(obj, Catalog):
291        kml_string = catalog_to_kml_string(obj, **kwargs)
292    elif isinstance(obj, Inventory):
293        kml_string = inventory_to_kml_string(obj, **kwargs)
294    else:
295        msg = ("Object for KML output must be "
296               "a Catalog or Inventory.")
297        raise TypeError(msg)
298    if not filename.endswith(".kml"):
299        filename += ".kml"
300    with open(filename, "wb") as fh:
301        fh.write(kml_string)
302
303
304def _rgba_tuple_to_kml_color_code(rgba):
305    """
306    Convert tuple of (red, green, blue, alpha) float values (0.0-1.0) to KML
307    hex color code string "aabbggrr".
308    """
309    try:
310        r, g, b, a = rgba
311    except Exception:
312        r, g, b = rgba
313        a = 1.0
314    return "".join(["%02x" % int(x * 255) for x in (a, b, g, r)])
315
316
317def _get_event_timestamp(event):
318    """
319    Get timestamp information for the event. Search is perfomed in the
320    following order:
321
322     - origin time of preferred origin
323     - origin time of first origin found that has a origin time
324     - minimum of all found pick times
325     - `None` if no time is found in the above search
326    """
327    origin = event.preferred_origin()
328    if origin is not None and origin.time is not None:
329        return origin.time
330    for origin in event.origins:
331        if origin.time is not None:
332            return origin.time
333    pick_times = [pick.time for pick in event.picks
334                  if pick.time is not None]
335    if pick_times:
336        return min(pick_times)
337    return None
338
339
340if __name__ == '__main__':
341    import doctest
342    doctest.testmod(exclude_empty=True)
343