# -*- coding: utf-8 -*-
#
# quakepy/pmc/PMC_NZ.py
# $Id: PMC_NZ.py 252 2009-11-06 14:36:39Z fab $
#
# The QuakePy package
# http://www.quakepy.org
#

############################################################################
#    Copyright (C) 2007-2009 by Fabian Euchner and Danijel Schorlemmer     #
#    fabian@fabian-euchner.de                                              #
#                                                                          #
#    This program is free software; you can redistribute it and#or modify  #
#    it under the terms of the GNU General Public License as published by  #
#    the Free Software Foundation; either version 2 of the License, or     #
#    (at your option) any later version.                                   #
#                                                                          #
#    This program is distributed in the hope that it will be useful,       #
#    but WITHOUT ANY WARRANTY; without even the implied warranty of        #
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the         #
#    GNU General Public License for more details.                          #
#                                                                          #
#    You should have received a copy of the GNU General Public License     #
#    along with this program; if not, write to the                         #
#    Free Software Foundation, Inc.,                                       #
#    59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.             #
############################################################################

"""
The QuakePy package
http://www.quakepy.org
"""

__version__  = '$Id: PMC_NZ.py 252 2009-11-06 14:36:39Z fab $'
__revision__ = '$Revision: 252 $'
__author__   = "Danijel Schorlemmer <ds@usc.edu>, Fabian Euchner <fabian@fabian-euchner.de>"
__license__  = "GPL"

import sys
import copy
import re
import cPickle
import math
import numpy

import pyRXP
from   xml.sax import saxutils

from mx.DateTime import DateTime, TimeDelta
from mx.DateTime.ISO import ParseDateTimeUTC

sys.path.append('..')

from QPCore                   import *
from PMC                      import *
from PMCInventory             import *
from PMCData                  import *
from PMCMetadata              import *


class PMC_NZ( PMC ):
    """
    QuakePy: PMC_NZ
    PMC method for Geonet network (New Zealand)
    """

    # these attributes are redefined for Geonet network
    stationsRequiredForTriggering = 4            # 4
    probThreshold                 = 0.999

    # time shift in NZ time zone (hours)
    timeZoneShiftStandard = 12.0
    timeZoneShiftDaylight = 13.0
    timeZoneShift         = timeZoneShiftStandard
    
    networkCode = 'NZ'
    stationOnTimeStartLowerLimit = DateTime( 1800, 1, 1 )
    stationOnTimeEndUpperLimit   = DateTime( 2100, 1, 1 )

    
    def __init__( self, stationlist_filename = None, dist_style = None, **kwargs ):
        super( PMC_NZ, self ).__init__( stationlist_filename, dist_style, **kwargs )


    def _calcMagnitudeFromDistance( self, distance ):
        # this function is redefined for Geonet network

        ## from Jochen Woessner
        
        # Based on Robinson, PAGEOPH (1987) for stations at distance <= 100km

        # % Based on Robinson, PAGEOPH, 1987 and Haines, BSSA, 1981
        
        #fRadkm = 111.1*atand(tan(1.0)); % kilometers in 1 radian
        #% Based on Robinson, PAGEOPH, 1987 and Haines, BSSA, 1981
        #if fDistance <= 100
           #fMagnitude = log10(fSlantDistance) + 0.0029*fSlantDistance;
           #disp('Type 1')
        #else
          #% For shallow events
          #fAcoeff = 1.05;
          #fMagspr = 2;
         #% Distance including attenuation
          #fMagnitude = fMagspr * log10(fDistRad*63.4) + 24.883241*fAcoeff*fAdist;
          #disp('Type 2')
        #end

        # 63.4 ??? Danijel says: 63.6

        return ( 1.00 * math.log10( distance ) + 0.0029 * distance )


    def _stationToBeUsed( self, station ):
        # this function is redefined for Geonet network
        
        # exclude some networks
        unused_networks = ()
        if station.networkCode in unused_networks:
            return False

        return True


    def importStations( self, filename, encoding = 'ascii' ):
        """
        import station list of Geonet

        example data (note: ruler shown below is not part of data)
                      1         2         3         4         5         6         7
            01234567890123456789012345678901234567890123456789012345678901234567890123456789



            -------------------------------------------------------------------------------
            
            001A  -35.7269 174.3192  9999 NZGD49   1970-04-18T00:00:00 1980-06-13T00:00:00
            KETZ  -39.1005 175.6517  1208 NZGD49   1985-05-01T00:00:00 1992-01-22T00:00:00
            KFHS  -39.4351 176.4668   425 NZGD49   2002-04-25T00:00:00 9999-01-01T13:59:50
            KHEZ  -39.2942 174.0145   929 WGS84    2009-05-21T00:00:00 9999-01-01T15:29:27

            API   -13.8072 -171.7750     2 NZGD49   1902-01-01T00:00:00 1989-04-30T00:00:00

            OLD FORMAT
            
             KHZ/10-SHE/NZ    -42.4162  173.5408   59 20030225 1024 20030625 1149
             CRLZ/10-HHZ/NZ   -43.5763  172.6230   55 20030312 1839 20040208 0113
             DSZ/10-HHZ/NZ    -41.7467  171.8045  650 20021113 0122 20060329 1048

        Note: lat/lon can be negative as well as positive
              station file does not have fixed-width columns
              split input line in 7 tokens
              coordinate reference system is from: WGS84, NZGD49, NZGD2000

        Fragen an NZ:
        
        * Are times in station file in UTC?
        * Wieviele stationen zum Triggern?
        * attenuation relation
        * coordinate reference system
        """
        
        lines = getQPDataSource( filename )

        lines_processed = 0
        update_lines    = 0
        lastStaIdx      = -1

        IN_STATION, IN_LAT, IN_LON, IN_ELEV, IN_COORDREF, IN_ONTIME, IN_OFFTIME = range( 7 )
        
        for line in lines:

            # location code and channel code are not used
            # locationCode = ''
            # channelCode  = ''

            line_arr = line.split()
            
            # get network and station code
            networkCode = self.networkCode
            stationCode = line_arr[IN_STATION].strip()

            curr_lat_in   = float( line_arr[IN_LAT].strip() )
            curr_lon_in   = float( line_arr[IN_LON].strip() )
            coordinateRef = line_arr[IN_COORDREF].strip()

            if coordinateRef == 'NZGD49':

                # simple correction:
                # New Zealand Land Information, Geodetic System Technical Report, GS 1997/11
                # Authors: Merrin Pearse and Chris Crook
                # accuracy: ~15 metres
                # lat: + 6.1 arcsec
                # lon: - 0.5 arcsec
                curr_lat_deg = curr_lat_in + ( 6.1 / ( 360.0 * 60.0 * 60.0 ) )
                curr_lon_deg = curr_lon_in - ( 0.5 / ( 360.0 * 60.0 * 60.0 ) )

            elif coordinateRef in ( 'NZGD2000', 'WGS84' ):

                # NZGD2000 is very close to WGS84, no correction
                curr_lat_deg = curr_lat_in
                curr_lon_deg = curr_lon_in
                
            else:
                error_str = "PMC_NZ.importStations - unknown coordinate reference system: %s" % ( coordinateRef )
                raise ValueError, error_str
            
            curr_elev = float( line_arr[IN_ELEV].strip() )

            # undefined elevations have '9999', set to elevation 0.0
            if curr_elev == 9999.0:
                curr_elev = 0.0

            # start of on-time interval
            startTime = ParseDateTimeUTC( line_arr[IN_ONTIME].strip() )

            # check if proper end of on-time interval is given, if not, set to 2100.0
            if line_arr[IN_OFFTIME].strip().startswith( '9999' ):
                endTime = self.stationOnTimeEndUpperLimit
            else:
                endTime = ParseDateTimeUTC( line_arr[IN_OFFTIME].strip() )
            
            # ---------------------------------------------------------------------

            # look in station list if network/station combination is already there
            sta_idx = self._getStationIdx( networkCode, stationCode )

            self.stations[sta_idx].networkCode  = networkCode
            self.stations[sta_idx].stationCode  = stationCode
            # self.stations[sta_idx].locationCode = locationCode

            self.stations[sta_idx].latitude     = curr_lat_deg
            self.stations[sta_idx].longitude    = curr_lon_deg
            self.stations[sta_idx].elevation    = curr_elev

            # check if we add to the last created station (no new station created)
            if sta_idx == lastStaIdx:
                print " adding data to last created station %s w/ index %s in input line %s with %s stations already read" % (
                    stationCode, sta_idx, lines_processed+1, len( self.stations ) )
                curr_channel = self.stations[sta_idx].channels[0]
                update_lines += 1


            # check if we add to an already created station (no new station created)
            elif sta_idx < len( self.stations )-1:
                print " adding data to existing station w/ index %s in input line %s with %s stations already read" % (
                    sta_idx, lines_processed+1, len( self.stations ) )
                print "  current station: stationCode %s" % ( stationCode )
                print " modified station: stationCode %s" % ( self.stations[sta_idx].stationCode )
                curr_channel = self.stations[sta_idx].channels[0]
                update_lines += 1

            else:

                # new station, create new channel
                curr_channel = PMCChannel()
                self.stations[sta_idx].channels.append( curr_channel )

            # set channel data
            curr_channel.waveformID = WaveformStreamID( networkCode, stationCode )

            curr_channel.onTime.append( [ QPDateTime( startTime ),
                                          QPDateTime( endTime ) ] )

            # print " added data to station index %s with networkCode %s, stationCode %s" % ( sta_idx, self.stations[sta_idx].networkCode, self.stations[sta_idx].stationCode )

            lastStaIdx = sta_idx
            lines_processed += 1

        # set station-wide station.onTime
        for curr_sta in self.stations:

            # loop over channels, add onTime of all channels to station onTime
            # Note: could be improved by checking for overlapping time intervals
            for curr_ch in curr_sta.channels:
                curr_sta.onTime.extend( curr_ch.onTime )
        
        print " station import complete, read %s station lines, %s update lines, length of station array %s" % (
            lines_processed, update_lines, len( self.stations ) )

        # check if read numbers match
        if ( update_lines + len( self.stations ) ) != lines_processed:
            print " ATTENTION: numbers of processed stations do not match!"

    # ---------------------------------------------------------------------------------------------------------
            
    def preprocess( self, catalog = None, timePeriod = None ):
        # this function is redefined for Geonet network
        
        # - preprocess inventory
        # (1) exclude some networks
        # (2) exclude all channels which have no 'Z' component
        # (3) exclude all stations not operating in given time period
        #     timePeriod is [ startDate, endDate ]
        
        # - preprocess catalog
        # (4) use only Geonet locations with given parameters (time, lat, lon, magnitude)
        #     depth: if not given, use standard value
        # (5) exclude events that have no picks associated
        # (6) exclude all picks that are not 'P' picks: use only those starting with 'P'
        
        self.preprocessInventory( timePeriod )

        # preprocess catalog only if deck files are read into QPCatalog object
        if catalog is not None:
            self.preprocessCatalog( catalog, timePeriod )

            
    def preprocessInventory( self, timePeriod = None ):
        # this function is redefined for Geonet network
        
        # (1) exclude some networks
        # (2) exclude all channels which have no 'Z' component
        # (3) exclude all stations not operating in given time period
        #     timePeriod is [ startDate, endDate ]

        if ( not self.stations ):
            raise ValueError, "PMC_NZ.preprocessInventory - no station list loaded"
        
        unused_networks = ()

        # over stations
        for curr_sta_idx in reversed( xrange( len( self.stations ) ) ):
            
            # in excluded network?
            if self.stations[curr_sta_idx].networkCode in unused_networks:
                del self.stations[curr_sta_idx]
            else:
                # over channels
                for curr_cha_idx in reversed( xrange( len( self.stations[curr_sta_idx].channels ) ) ):
                    
                    # has channel 'Z' component? / does channel operate in given time period?
                    if (    not self.stations[curr_sta_idx].channels[curr_cha_idx].waveformID.channelCode.endswith('Z')
                         or not self._channelOperatingDuringPeriod( self.stations[curr_sta_idx].channels[curr_cha_idx],
                                                                    timePeriod ) ):
                        del self.stations[curr_sta_idx].channels[curr_cha_idx]
                        
                if len( self.stations[curr_sta_idx].channels ) == 0:
                    del self.stations[curr_sta_idx]
        
        # unselect station SMTC from Anza network
        # self.unselectStation( 'AZ', 'SMTC' )

        return True

            
    def preprocessCatalog( self, catalog, timePeriod = None ):
        # this function is redefined for Geonet network

        # (1) if timePeriod is set, exclude events outside time period
        # (2) use only Geonet locations with given parameters (time, lat, lon, magnitude)
        #     depth: if not given, use standard value
        # (3) exclude events that have no picks associated
        # (4) exclude all picks that are not 'P' picks: use only those starting with 'P'

        if catalog is None:
            raise ValueError, "PMC_NZ.preprocessCatalog - no valid catalog given"

        if timePeriod is not None:

            if not (      isinstance( timePeriod[0], DateTimeType )
                     and  isinstance( timePeriod[1], DateTimeType ) ):

                error_str = "PMC_NZ.preprocessCatalog - timePeriod has incorrect type"
                raise TypeError, error_str
            else:
                startTime = QPDateTime( timePeriod[0] )
                endTime   = QPDateTime( timePeriod[1] )
                
        event_cnt = len( catalog.eventParameters.event )
        for curr_ev_idx in reversed( xrange( event_cnt ) ):
            
            event = catalog.eventParameters.event[curr_ev_idx]

            # print " analyse event no. %s w/ ID %s" % ( curr_ev_idx, event.publicID )
            
            # get JMA origin and corresponding magnitude
            # check if preferred origin is JMA, if not, skip
            origin = event.getPreferredOrigin()

            # focal time is always present in catalog (ensured in reading method)
            # focal time in catalog and start/end times are UTC
            if timePeriod is not None:

                if (    ( origin.time.value < startTime )
                     or ( origin.time.value > endTime ) ):

                    del catalog.eventParameters.event[curr_ev_idx]
                    del event
                    continue
            
            if not ( origin.creationInfo.agencyID == 'JMA' ):
                
                del catalog.eventParameters.event[curr_ev_idx]
                
                # print " skipped non-JMA event %s, event count is now %s" % ( event.publicID, catalog.size() )
                del event
                continue

            # get corresponding magnitude for JMA origin
            ori_mag = event.getMagnitudes( origin )
            if len( ori_mag ) > 0:
                magnitude = ori_mag[0]
            else:
                # no magnitude, skip event
                del catalog.eventParameters.event[curr_ev_idx]
                
                # print " skipped event w/o magnitude %s, event count is now %s" % ( event.publicID, catalog.size() )
                del event
                continue
            
            # print " event %s, magnitude %s, origin %s no. of picks %s" % ( event.publicID, magnitude.publicID, origin.publicID, len(event.pick) )

            # check if origin is complete
            # - lat, lon, depth
            # focal time is always there and we have already checked for magnitude
            if (    ( ( not hasattr( origin, 'latitude' ) ) or ( origin.latitude is None ) )
                 or ( ( not hasattr( origin, 'longitude' ) ) or ( origin.longitude is None ) )
                 or ( ( not hasattr( origin, 'depth' ) ) or ( origin.depth is None ) ) ):

                del catalog.eventParameters.event[curr_ev_idx]

                # print " skipped event w/o coordinates %s, event count is now %s" % ( event.publicID, catalog.size() )
                del event
                continue

            # check if there are any picks
            if ( ( event.pick is None ) or ( len(event.pick) == 0 ) ):

                del catalog.eventParameters.event[curr_ev_idx]

                # print " skipped event w/o picks %s, event count is now %s" % ( event.publicID, catalog.size() )
                del event
                continue
            
            elif ( ( event.pick is not None ) and ( len(event.pick) > 0 ) ):

                # check for 'P', 'EP', 'IP' picks, delete picks which are not 'P', 'EP', 'IP'
                for curr_pick_idx in reversed( xrange( len(event.pick) ) ):

                    curr_pick         = event.pick[curr_pick_idx]
                    curr_arrivals_idx = origin.getArrivalsIdx( curr_pick )

                    # check if pick has an associated arrival
                    if len( curr_arrivals_idx ) == 0:
                        
                        del event.pick[curr_pick_idx]
                        
                        # print " deleted pick no. %s, id %s since no arrival is associated" %  ( curr_pick_idx, curr_pick.publicID )

                        del curr_pick

                    else:

                        curr_arr_idx   = curr_arrivals_idx[0]
                        curr_arr_phase = origin.arrival[curr_arr_idx].phase.code
                        
                        if curr_arr_phase not in ( 'P', 'EP', 'IP' ):
                            
                            del origin.arrival[curr_arr_idx]
                            del event.pick[curr_pick_idx]

                            # print " deleted %s phase for pick no. %s, id %s" %  ( curr_arr_phase, curr_pick_idx, curr_pick.publicID )

                            del curr_pick
                    
                # loop over picks has finished
                # if event has no more picks, delete event
                if len(event.pick) == 0:

                    del catalog.eventParameters.event[curr_ev_idx]
                    
                    # print " deleted event with no remaining picks %s, event count is now %s" % ( event.publicID, catalog.size() )
                    del event
                    continue

                # print " event was not deleted, proceed to next"
                
        return True


    def _preprocessStationScenario( self ):
        """
        has been redefined for class PMC_NZ derived from PMC
        deselect stations from 'all' list for scenario computation
        """

        ## this is an example of selecting only the HiNet network
        
        #selected_networks = ( 'HN', )
        #self.selectStationsByNetworks( selected_networks )

        return True


    def initAnnotation( self ):
        """
        has been redefined for class PMC_NZ derived from PMC
        set annotation for QPGrid output
        """

        self.annotation.setProperty(
            title = 'Completeness of the Geonet network',
            creator = ( 'Danijel Schorlemmer, Fabian Euchner, and Matthew C. Gerstenberger', ),
            publisher = ( 'Danijel Schorlemmer, Fabian Euchner, and Matthew C. Gerstenberger', ),
            rights = ( 'Copyright by Danijel Schorlemmer, Fabian Euchner, and Matthew C. Gerstenberger',
                       'Licensed under Creative Commons Attribution-Noncommercial-Share Alike 3.0',
                       'http://creativecommons.org/licenses/by-nc-sa/3.0/' ),
            subject = ( 'seismology', 'earthquake', 'detection probability', 'PMC', 'seismic network', 'completeness',
                        'magnitude of completeness' ),
            source = ( 'http://completenessweb.org', 'http://quakepy.org' ),
            bibliographicCitation = ( "D. Schorlemmer and J. Woessner: Probability of Detecting an Earthquake, Bull. Seismol. Soc. Am., 98(5), 2103-2117, 2008, doi:10.1785/0120070105.", ),
            comment = ( "Please cite the given reference when using the data.", )
        )

        # not used at the moment
        # identifier  version  acknowledgment

        # for these we need run-time information
        # date  coverageTemporal  coverageSpatial
        
        return True
    
## ---------------------------------------------------------------------------

