Example #1
0
    def derive(self,
               toff_fdr_apt=A('FDR Takeoff Airport'),
               toff_afr_rwy=A('AFR Takeoff Runway'),
               toff_hdg=KPV('Heading During Takeoff'),
               toff_lat=KPV('Latitude At Liftoff'),
               toff_lon=KPV('Longitude At Liftoff'),
               precision=A('Precise Positioning')):
        '''
        '''
        fallback = False
        precise = bool(getattr(precision, 'value', False))

        try:
            airport = toff_fdr_apt.value  # FIXME
        except AttributeError:
            self.warning('Invalid airport... Fallback to AFR Takeoff Runway.')
            fallback = True
        else:
            if airport is None:
                fallback = True

        try:
            heading = toff_hdg.get_first().value
            if heading is None:
                raise ValueError
        except (AttributeError, ValueError):
            self.warning('Invalid heading... Fallback to AFR Takeoff Runway.')
            fallback = True

        # 1. If we have airport and heading, look for the nearest runway:
        if not fallback:
            kwargs = {}

            # Even if we do not have precise latitude and longitude
            # information, we still use this for the takeoff runway detection
            # as it is often accurate at the start of a flight, and in the
            # absence of an ILS tuned frequency we have no better option. (We
            # did consider using the last direction of turn onto the runway,
            # but this would require an airport database with terminal and
            # taxiway details that was not felt justified).
            if toff_lat and toff_lon:
                lat = toff_lat.get_first()
                lon = toff_lon.get_first()
                if lat and lon:
                    kwargs.update(
                        latitude=lat.value,
                        longitude=lon.value,
                    )
                else:
                    self.warning('No coordinates for takeoff runway lookup.')
            if not precise:
                kwargs.update(hint='takeoff')

            runway = nearest_runway(airport, heading, **kwargs)
            if not runway:
                msg = 'No runway found for airport #%d @ %03.1f deg with %s.'
                self.warning(msg, airport['id'], heading, kwargs)
                # No runway was found, so fall through and try AFR.
            else:
                self.info(
                    'Detected takeoff runway: %s for airport #%d @ %03.1f deg with %s',
                    runway['identifier'], airport['id'], heading, kwargs)
                self.set_flight_attr(runway)
                return  # We found a runway, so finish here.

        # 2. If we have a runway provided in achieved flight record, use it:
        if toff_afr_rwy:
            runway = toff_afr_rwy.value
            self.debug('Using takeoff runway from AFR: %s', runway)
            self.set_flight_attr(runway)
            return  # We found a runway in the AFR, so finish here.

        # 3. After all that, we still couldn't determine a runway...
        self.error('Unable to determine runway at takeoff!')
        self.set_flight_attr(None)
Example #2
0
    def _lookup_airport_and_runway(self,
                                   _slice,
                                   precise,
                                   lowest_lat,
                                   lowest_lon,
                                   lowest_hdg,
                                   appr_ils_freq,
                                   land_afr_apt=None,
                                   land_afr_rwy=None,
                                   hint='approach',
                                   ac_type=aeroplane):
        handler = api.get_handler(settings.API_HANDLER)
        kwargs = {}
        airport, runway, match = None, None, None

        # A1. If we have latitude and longitude, look for the nearest airport:
        if lowest_lat not in (None, np.ma.masked) and lowest_lon not in (
                None, np.ma.masked):
            kwargs.update(latitude=lowest_lat, longitude=lowest_lon)
            try:
                airports = handler.get_nearest_airport(**kwargs)
            except (ValueError, TypeError):
                self.warning('No coordinates for looking up approach airport.')
            except api.NotFoundError:
                msg = 'No approach airport found near coordinates (%f, %f).'
                self.warning(msg, lowest_lat, lowest_lon)
                # No airport was found, so fall through and try AFR.
            else:
                airport_info = self._evaluate_airports(airports, lowest_lat,
                                                       lowest_lon, lowest_hdg,
                                                       appr_ils_freq)
                if land_afr_apt and land_afr_apt.value['id'] in airport_info:
                    # use afr airprot
                    match = airport_info[land_afr_apt.value['id']]
                elif land_afr_apt and precise and ac_type != helicopter:
                    # raise error as afr and flight data do not match
                    msg = "'%s' provided by AFR is not in list of aiports within range of %s, %s. Aircraft has precise positioning"\
                        % (land_afr_apt.value['code'], lowest_lat, lowest_lon)
                    raise AFRMissmatchError(self.name, msg)
                elif not land_afr_apt and precise:
                    # filter by runway coordinates
                    filtered = [
                        x for x in airport_info.values()
                        if x['min_rwy_start_dist'] is not None
                    ]
                    if filtered:
                        match = min(filtered,
                                    key=itemgetter('min_rwy_start_dist'))
                else:
                    # filter by runway heading
                    airport = None
                    potential_airports = [
                        x for x in airport_info.values() if x['heading_match']
                    ]
                    if appr_ils_freq:
                        # filter by ils frequency
                        ils_airports = [
                            x for x in airport_info.values() if x['ils_match']
                        ]
                        if len(ils_airports) == 1:
                            potential_airports = [ils_airports[0]]
                        elif len(ils_airports) > 1:
                            potential_airports = ils_airports
                    if len(potential_airports) == 1:
                        match = potential_airports[0]
                    elif len(potential_airports) > 1:
                        # filter by runway distances
                        filtered = [
                            x for x in potential_airports
                            if x['min_rwy_start_dist'] is not None
                        ]
                        if filtered:
                            match = min(filtered,
                                        key=itemgetter('min_rwy_start_dist'))
                    else:
                        # filter by airport distances
                        if airports:
                            airport = min(airports, key=itemgetter('distance'))
                if match:
                    airport = match['airport']
                if airport:
                    self.debug('Detected approach airport: %s', airport)
                else:
                    self.warning(
                        'Unable to locate Airport from provided coordinates')
        else:
            # No suitable coordinates, so fall through and try AFR.
            self.warning('No coordinates for looking up approach airport.')
            # return None, None

        # A2. If and we have an airport in achieved flight record, use it:
        # NOTE: AFR data is only provided if this approach is a landing.
        if not airport and land_afr_apt:
            airport = handler.get_airport(land_afr_apt.value['id'])
            self.debug('Using approach airport from AFR: %s', airport['name'])

        # A3. After all that, we still couldn't determine an airport...
        if not airport:
            self.error('Unable to determine airport on approach!')
            return None, None

        if lowest_hdg is not None:

            # R1. If we have airport and heading, look for the nearest runway:
            if appr_ils_freq:
                kwargs['ilsfreq'] = appr_ils_freq

                # We already have latitude and longitude in kwargs from looking up
                # the airport. If the measurments are not precise, remove them.
                if not precise:
                    kwargs['hint'] = hint

            runway = nearest_runway(airport, lowest_hdg, **kwargs)
            if not runway:
                msg = 'No runway found for airport #%d @ %03.1f deg with %s.'
                self.warning(msg, airport['id'], lowest_hdg, kwargs)
                # No runway was found, so fall through and try AFR.
                if 'ilsfreq' in kwargs:
                    # This is a trap for airports where the ILS data is not
                    # available, but the aircraft approached with the ILS
                    # tuned. A good prompt for an omission in the database.
                    self.warning('Fix database? No runway but ILS was tuned.')
            else:
                self.debug('Detected approach runway: %s', runway)

        # R2. If we have a runway provided in achieved flight record, use it:
        if not runway and land_afr_rwy:
            runway = land_afr_rwy.value
            self.debug('Using approach runway from AFR: %s', runway)

        # R3. After all that, we still couldn't determine a runway...
        if not runway:
            self.error('Unable to determine runway on approach!')

        return airport, runway
Example #3
0
    def derive(
        self,
        alt_aal=P('Altitude AAL'),
        alt_agl=P('Altitude AGL'),
        ac_type=A('Aircraft Type'),
        app=S('Approach And Landing'),
        hdg=P('Heading Continuous'),
        lat=P('Latitude Prepared'),
        lon=P('Longitude Prepared'),
        ils_loc=P('ILS Localizer'),
        ils_gs=S('ILS Glideslope'),
        ils_freq=P('ILS Frequency'),
        land_afr_apt=A('AFR Landing Airport'),
        land_afr_rwy=A('AFR Landing Runway'),
        lat_land=KPV('Latitude At Touchdown'),
        lon_land=KPV('Longitude At Touchdown'),
        precision=A('Precise Positioning'),
        fast=S('Fast'),

        #lat_smoothed=P('Latitude Smoothed'),
        #lon_smoothed=P('Longitude Smoothed'),
        u=P('Airspeed'),
        gspd=P('Groundspeed'),
        height_from_rig=P('Altitude ADH'),
        hdot=P('Vertical Speed'),
        roll=P('Roll'),
        heading=P('Heading'),
        distance_land=P('Distance To Landing'),
        tdwns=KTI('Touchdown'),
        offshore=M('Offshore'),
        takeoff=S('Takeoff')):

        precise = bool(getattr(precision, 'value', False))
        alt = alt_agl if ac_type == helicopter else alt_aal
        app_slices = sorted(app.get_slices())

        for index, _slice in enumerate(app_slices):
            # a) The last approach is assumed to be landing:
            if index == len(app_slices) - 1:
                approach_type = 'LANDING'
                landing = True
            # b) We have a touch and go if Altitude AAL reached zero:
            #elif np.ma.any(alt.array[_slice] <= 0):
            elif np.ma.any(alt.array[_slice.start:_slice.stop +
                                     (5 * alt.frequency)] <= 0):
                if ac_type == aeroplane:
                    approach_type = 'TOUCH_AND_GO'
                    landing = False
                elif ac_type == helicopter:
                    approach_type = 'LANDING'
                    landing = True
                else:
                    raise ValueError('Not doing hovercraft!')
            # c) In any other case we have a go-around:
            else:
                approach_type = 'GO_AROUND'
                landing = False

            # Rough reference index to allow for go-arounds
            ref_idx = index_at_value(alt.array,
                                     0.0,
                                     _slice=_slice,
                                     endpoint='nearest')

            turnoff = None
            if landing:
                search_end = fast.get_surrounding(_slice.start)
                if search_end and search_end[0].slice.stop >= ref_idx:
                    search_end = min(search_end[0].slice.stop, _slice.stop)
                else:
                    search_end = _slice.stop

                tdn_hdg = np.ma.median(hdg.array[ref_idx:search_end + 1])
                # Complex trap for the all landing heading data is masked case...
                if (tdn_hdg % 360.0) is np.ma.masked:
                    lowest_hdg = bearing_and_distance(lat.array[ref_idx],
                                                      lon.array[ref_idx],
                                                      lat.array[search_end],
                                                      lon.array[search_end])[0]
                else:
                    lowest_hdg = (tdn_hdg % 360.0).item()

                # While we're here, let's compute the turnoff index for this landing.
                head_landing = hdg.array[(ref_idx + _slice.stop) /
                                         2:_slice.stop]
                if len(head_landing) > 2:
                    peak_bend = peak_curvature(head_landing,
                                               curve_sense='Bipolar')
                    fifteen_deg = index_at_value(
                        np.ma.abs(head_landing - head_landing[0]), 15.0)
                    if peak_bend:
                        turnoff = (ref_idx + _slice.stop) / 2 + peak_bend
                    else:
                        if fifteen_deg and fifteen_deg < peak_bend:
                            turnoff = start_search + landing_turn
                        else:
                            # No turn, so just use end of landing run.
                            turnoff = _slice.stop
                else:
                    # No turn, so just use end of landing run.
                    turnoff = _slice.stop
            else:
                # We didn't land, but this is indicative of the runway heading
                lowest_hdg = (hdg.array[ref_idx] % 360.0).item()

            # Pass latitude, longitude and heading
            lowest_lat = None
            lowest_lon = None
            if lat and lon and ref_idx:
                lowest_lat = lat.array[ref_idx] or None
                lowest_lon = lon.array[ref_idx] or None
                if lowest_lat and lowest_lon and approach_type == 'GO_AROUND':
                    # Doing a go-around, we extrapolate to the threshold
                    # in case we abort the approach abeam a different airport,
                    # using the rule of three miles per thousand feet.
                    distance = np.ma.array([
                        ut.convert(alt_aal.array[ref_idx] * (3 / 1000.0),
                                   ut.NM, ut.METER)
                    ])
                    bearing = np.ma.array([lowest_hdg])
                    reference = {
                        'latitude': lowest_lat,
                        'longitude': lowest_lon
                    }
                    lat_ga, lon_ga = latitudes_and_longitudes(
                        bearing, distance, reference)
                    lowest_lat = lat_ga[0]
                    lowest_lon = lon_ga[0]

            if lat_land and lon_land and not (lowest_lat and lowest_lon):
                # use lat/lon at landing if values at ref_idx are masked
                # only interested in landing within approach slice.
                lat_land = lat_land.get(within_slice=_slice)
                lon_land = lon_land.get(within_slice=_slice)
                if lat_land and lon_land:
                    lowest_lat = lat_land[0].value or None
                    lowest_lon = lon_land[0].value or None

            kwargs = dict(
                precise=precise,
                _slice=_slice,
                lowest_lat=lowest_lat,
                lowest_lon=lowest_lon,
                lowest_hdg=lowest_hdg,
                appr_ils_freq=None,
                ac_type=ac_type,
            )

            # If the approach is a landing, pass through information from the
            # achieved flight record in case we cannot determine airport and
            # runway:
            if landing:
                kwargs.update(
                    land_afr_apt=land_afr_apt,
                    land_afr_rwy=land_afr_rwy,
                    hint='landing',
                )
            if landing or approach_type == 'GO_AROUND':
                # if we have a frequency and valid localiser signal at lowest point in approach
                appr_ils_freq = None
                if ils_freq:
                    appr_ils_freq = np.ma.round(ils_freq.array[ref_idx] or 0,
                                                2)
                if not precise and appr_ils_freq and ils_loc and np.ma.abs(
                        ils_loc.array[ref_idx]) < 2.5:
                    kwargs['appr_ils_freq'] = appr_ils_freq

            airport, landing_runway = self._lookup_airport_and_runway(**kwargs)
            if not airport and ac_type == aeroplane:
                continue

            if ac_type == aeroplane and not airport.get('runways'):
                self.error("Airport %s: contains no runways", airport['code'])

            # Simple determination of heliport.
            heliport = is_heliport(ac_type, airport, landing_runway)

            sorted_tdwns = sorted(tdwns, key=lambda touchdown: touchdown.index)
            sorted_takeoffs = sorted(takeoff.get_slices(),
                                     key=lambda tkoff: tkoff.start)

            for touchdown, tkoff in zip(sorted_tdwns, sorted_takeoffs):
                # If both the takeoff and touchdown point are offshore then we consider
                # the approach to be a 'SHUTTLING APPROACH'. Else we continue to look for
                # an 'AIRBORNE RADAR DIRECT/OVERHEAD APPROACH' or a 'STANDARD APPROACH'
                #
                # A couple of seconds are added to the end of the slice as some flights used
                # to test this had the touchdown a couple of seconds outside the approach slice
                if is_index_within_slice(
                        touchdown.index,
                        slice(_slice.start, _slice.stop + 5 * alt.frequency)):
                    if offshore and offshore.array[
                            touchdown.
                            index] == 'Offshore' and tkoff.start < touchdown.index:
                        if not distance_land:
                            if offshore.array[tkoff.start] == 'Offshore':
                                approach_type = 'SHUTTLING'
                        elif offshore.array[
                                tkoff.
                                start] == 'Offshore' and tkoff.start < len(
                                    distance_land.array
                                ) and distance_land.array[tkoff.start] <= 40:
                            approach_type = 'SHUTTLING'
                        elif height_from_rig:
                            Vy = 80.0  # Type dependent?

                            # conditions_defs is a dict of condition name : expression to evaluate pairs, listed this way for clarity
                            condition_defs = {
                                'Below 120 kts':
                                lambda p: p['Airspeed'] < 120,
                                'Below Vy+5':
                                lambda p: p['Airspeed'] < Vy + 5.0,
                                'Over Vy':
                                lambda p: p['Airspeed'] > Vy,
                                'Over Vy-5':
                                lambda p: p['Airspeed'] > Vy - 5.0,
                                'Below 70 gspd':
                                lambda p: p['Groundspeed'] < 72,
                                'Below 60 gspd':
                                lambda p: p['Groundspeed'] < 60,
                                #'Below Vy-10' : lambda p : p['Airspeed'] < Vy-10.0,
                                #'Over Vy-10' : lambda p : p['Airspeed'] > Vy-10.0,
                                #'Above 30 gspd' : lambda p : p['Groundspeed'] > 30,
                                'Over 900 ft':
                                lambda p: p['Altitude ADH'] > 900,
                                'Over 200 ft':
                                lambda p: p['Altitude ADH'] > 200,
                                'Below 1750 ft':
                                lambda p: p['Altitude ADH'] < 1750,
                                'Below 1100 ft':
                                lambda p: p['Altitude ADH'] < 1100,
                                'Over 350 ft':
                                lambda p: p['Altitude ADH'] > 350,
                                'Below 700 ft':
                                lambda p: p['Altitude ADH'] < 700,
                                'ROD < 700 fpm':
                                lambda p: p['Vertical Speed'] > -700,
                                'ROD > 200 fpm':
                                lambda p: p['Vertical Speed'] < -200,
                                'Not climbing':
                                lambda p: p['Vertical Speed'] < 200,
                                #'Over 400 ft' : lambda p : p['Altitude ADH'] > 400,
                                #'Below 1500 ft': lambda p : p['Altitude ADH'] < 1500,
                                #'Below 1300 ft': lambda p : p['Altitude ADH'] < 1300,
                                'Roll below 25 deg':
                                lambda p: valid_between(
                                    p['Roll'], -25.0, 25.0),
                                'Wings Level':
                                lambda p: valid_between(
                                    p['Roll'], -10.0, 10.0),
                                'Within 20 deg of final heading':
                                lambda p: np.ma.abs(p['head_off_final']) <
                                20.0,
                                #'Within 45 deg of downwind leg' : 'valid_between(np.ma.abs(head_off_final), 135.0, 225.0)',
                                #'15 deg off final heading' : lambda p : np.ma.abs(np.ma.abs(p['head_off_two_miles'])-15.0) < 5.0,
                                #'Heading towards oil rig' : lambda p : np.ma.abs(p['head_off_two_miles']) < 6.0,
                                'Beyond 0.7 NM':
                                lambda p: p['Distance To Landing'] > 0.7,
                                'Within 0.8 NM':
                                lambda p: p['Distance To Landing'] < 0.8,
                                'Beyond 1.5 NM':
                                lambda p: p['Distance To Landing'] > 1.5,
                                'Within 2.0 NM':
                                lambda p: p['Distance To Landing'] < 2.0,
                                'Within 3.0 NM':
                                lambda p: p['Distance To Landing'] < 3.0,
                                'Beyond 3.0 NM':
                                lambda p: p['Distance To Landing'] > 3.0,
                                'Within 10.0 NM':
                                lambda p: p['Distance To Landing'] < 10.0,
                                #'Within 1.5 NM' : lambda p : p['Distance To Landing'] < 1.5,
                            }

                            # Phase map is a dict of the flight phases with the list of conditions which must be
                            # satisfied for the phase to be active.
                            phase_map = {
                                'Circuit': [
                                    'Below 120 kts',
                                    'Over Vy',
                                    'Below 1100 ft',
                                    'Over 900 ft',
                                    'Roll below 25 deg',  # includes downwind turn
                                ],
                                'Level within 2NM': [
                                    'Below Vy+5',
                                    'Over Vy-5',
                                    'Below 1100 ft',
                                    'Over 900 ft',
                                    'Wings Level',
                                    'Within 20 deg of final heading',
                                    'Within 2.0 NM',
                                    'Beyond 1.5 NM',
                                ],
                                'Initial Descent': [
                                    'Wings Level',
                                    'Within 20 deg of final heading',
                                    'ROD < 700 fpm',
                                    'ROD > 200 fpm',
                                    'Beyond 0.7 NM',
                                    'Over 350 ft',
                                ],
                                'Final Approach': [
                                    'Wings Level',
                                    'Within 20 deg of final heading',
                                    'ROD < 700 fpm',
                                    'Within 0.8 NM',
                                    'Below 60 gspd',
                                    'Below 700 ft',
                                ],

                                # Phases for ARDA/AROA
                                #
                                # All heading conditions are commented out as the pilots usually
                                # go outside the boundaries; the other conditions seem to be
                                # enough to detect them
                                'ARDA/AROA 10 to 3': [
                                    'Within 10.0 NM',
                                    'Beyond 3.0 NM',
                                    'Below 1750 ft',
                                    'Not climbing',
                                    #'Heading towards oil rig',
                                ],
                                'ARDA/AROA Level within 3NM': [
                                    'Below 70 gspd',
                                    'Over 200 ft',
                                    'Wings Level',
                                    'Within 3.0 NM',
                                    'Beyond 1.5 NM',
                                    #'Within 20 deg of final heading',
                                ],
                                'ARDA/AROA Final': [
                                    'Not climbing',
                                    'Within 2.0 NM',
                                    #'15 deg off final heading'
                                ],
                            }
                            """
                            #Phases that can be used to tighten the conditions for ARDA/AROA
                        
                            'Radar Heading Change':['15 deg off final heading', 'Within 1.5 NM', 'Beyond 0.7 NM'],
                            'Low Approach':['Below Vy+5', 'Below 700 ft', 'Over 350 ft', 'Within 20 deg of final heading', 'Wings Level'],
                            'Low Circuit':['Below 120 kts', 'Over Vy-5', 'Below 700 ft', 'Over 350 ft', 'Roll below 25 deg']
                            """

                            approach_map = {
                                'RIG': [
                                    'Circuit', 'Level within 2NM',
                                    'Initial Descent', 'Final Approach'
                                ],
                                'AIRBORNE_RADAR': [
                                    'ARDA/AROA 10 to 3',
                                    'ARDA/AROA Level within 3NM',
                                    'ARDA/AROA Final'
                                ]
                            }

                            # Making sure the approach slice contains enough information to be able
                            # to properly identify ARDA/AROA approaches (the procedure starts from 10NM
                            # before touchdown)

                            app_slice = slice(
                                index_at_value(distance_land.array,
                                               11,
                                               _slice=slice(
                                                   0, touchdown.index)),
                                touchdown.index)

                            heading_repaired = repair_mask(
                                heading.array[app_slice],
                                frequency=heading.frequency,
                                repair_duration=np.ma.count_masked(
                                    heading.array[app_slice]),
                                copy=True,
                                extrapolate=True)

                            param_arrays = {
                                'Airspeed':
                                u.array[app_slice],
                                'Groundspeed':
                                gspd.array[app_slice],
                                'Altitude ADH':
                                height_from_rig.array[app_slice],
                                'Vertical Speed':
                                hdot.array[app_slice],
                                'Roll':
                                roll.array[app_slice],
                                'Distance To Landing':
                                distance_land.array[app_slice],
                                'Heading':
                                heading_repaired,
                                'Latitude':
                                lat.array[app_slice],
                                'Longitude':
                                lon.array[app_slice],
                            }

                            longest_approach_type, longest_approach_durn, longest_approach_slice = find_rig_approach(
                                condition_defs,
                                phase_map,
                                approach_map,
                                Vy,
                                None,
                                param_arrays,
                                debug=False)

                            if longest_approach_type is not None:
                                approach_type = longest_approach_type.upper()
                                _slice = slice(
                                    app_slice.start +
                                    longest_approach_slice.start,
                                    app_slice.stop)

            if heliport:
                self.create_approach(
                    approach_type,
                    _slice,
                    runway_change=False,
                    offset_ils=False,
                    airport=airport,
                    landing_runway=None,
                    approach_runway=None,
                    gs_est=None,
                    loc_est=None,
                    ils_freq=None,
                    turnoff=None,
                    lowest_lat=lowest_lat,
                    lowest_lon=lowest_lon,
                    lowest_hdg=lowest_hdg,
                )
                continue

            #########################################################################
            ## Analysis of fixed wing approach to a runway
            ##
            ## First step is to check the ILS frequency for the runway in use
            ## and cater for a change from the approach runway to the landing runway.
            #########################################################################

            appr_ils_freq = None
            runway_change = False
            offset_ils = False

            # Do we have a recorded ILS frequency? If so, what was it tuned to at the start of the approach??
            if ils_freq:
                appr_ils_freq = ils_freq.array[_slice.start]
            # Was this valid, and if so did the start of the approach match the landing runway?
            if appr_ils_freq and not (np.isnan(appr_ils_freq)
                                      or np.ma.is_masked(appr_ils_freq)):
                appr_ils_freq = round(appr_ils_freq, 2)
                runway_kwargs = {
                    'ilsfreq': appr_ils_freq,
                    'latitude': lowest_lat,
                    'longitude': lowest_lon,
                }
                if not precise:
                    runway_kwargs['hint'] = kwargs.get('hint', 'approach')
                approach_runway = nearest_runway(airport, lowest_hdg,
                                                 **runway_kwargs)
                # Have we have identified runways for both conditions that are both different and parallel?
                if all((approach_runway, landing_runway)) \
                   and approach_runway['id'] != landing_runway['id'] \
                   and approach_runway['identifier'][:2] == landing_runway['identifier'][:2]:
                    runway_change = True
            else:
                # Without a frequency source, we just have to hope any localizer signal is for this runway!
                approach_runway = landing_runway

            if approach_runway and 'frequency' in approach_runway['localizer']:
                if np.ma.count(ils_loc.array[_slice]) > 10:
                    if runway_change:
                        # We only use the first frequency tuned. This stops scanning across both runways if the pilot retunes.
                        loc_slice = shift_slices(
                            runs_of_ones(
                                np.ma.abs(ils_freq.array[_slice] -
                                          appr_ils_freq) < 0.001),
                            _slice.start)[0]
                    else:
                        loc_slice = _slice
                else:
                    # No localizer or inadequate data for this approach.
                    loc_slice = None
            else:
                # The approach was to a runway without an ILS, so even if it was tuned, we ignore this.
                appr_ils_freq = None
                loc_slice = None

            if np.ma.is_masked(appr_ils_freq):
                loc_slice = None
                appr_ils_freq = None
            else:
                if appr_ils_freq and loc_slice:
                    if appr_ils_freq != round(
                            ut.convert(
                                approach_runway['localizer']['frequency'],
                                ut.KHZ, ut.MHZ), 2):
                        loc_slice = None

            #######################################################################
            ## Identification of the period established on the localizer
            #######################################################################

            loc_est = None
            if loc_slice:
                valid_range = np.ma.flatnotmasked_edges(ils_loc.array[_slice])
                # I have some data to scan. Shorthand names;
                loc_start = valid_range[0] + _slice.start
                loc_end = valid_range[1] + _slice.start
                scan_back = slice(ref_idx, loc_start, -1)

                # If we are turning in, we are not interested in signals that are not related to this approach.
                # The value of 45 deg was selected to encompass Washington National airport with a 40 deg offset.
                hdg_diff = np.ma.abs(
                    np.ma.mod((hdg.array - lowest_hdg) + 180.0, 360.0) - 180.0)
                ils_hdg_45 = index_at_value(hdg_diff, 45.0, _slice=scan_back)

                # We are not interested above 1,500 ft, so may trim back the start point to that point:
                ils_alt_1500 = index_at_value(alt_aal.array,
                                              1500.0,
                                              _slice=scan_back)

                # The criteria for start of established phase is the latter of the approach phase start, the turn-in or 1500ft.
                # The "or 0" allow for flights that do not turn through 45 deg or keep below 1500ft.
                loc_start = max(loc_start, ils_hdg_45 or 0, ils_alt_1500 or 0)

                if loc_start < ref_idx:
                    # Did I get established on the localizer, and if so,
                    # when? We only look AFTER the aircraft is already within
                    # 45deg of the runway heading, below 1500ft and the data
                    # is valid for this runway. Testing that the aircraft is
                    # not just passing across the localizer is built into the
                    # ils_established function.
                    loc_estab = ils_established(ils_loc.array,
                                                slice(loc_start, ref_idx),
                                                ils_loc.hz)
                else:
                    # If localiser start is after we touchdown bail.
                    loc_estab = None

                if loc_estab:

                    # Refine the end of the localizer established phase...
                    if (approach_runway
                            and approach_runway['localizer']['is_offset']):
                        offset_ils = True
                        # The ILS established phase ends when the deviation becomes large.
                        loc_end = ils_established(ils_loc.array,
                                                  slice(
                                                      ref_idx, loc_estab, -1),
                                                  ils_loc.hz,
                                                  point='immediate')

                    elif approach_type in ['TOUCH_AND_GO', 'GO_AROUND']:
                        # We finish at the lowest point
                        loc_end = ref_idx

                    elif runway_change:
                        # Use the end of localizer phase as this already reflects the tuned frequency.
                        est_end = ils_established(ils_loc.array,
                                                  slice(loc_estab, ref_idx),
                                                  ils_loc.hz,
                                                  point='end')
                        # Make sure we dont end up with a negative slice i.e. end before we are established.
                        loc_end = min([
                            x for x in (loc_slice.stop, loc_end,
                                        est_end or np.inf) if x > loc_estab
                        ])

                    elif approach_type == 'LANDING':
                        # Just end at 2 dots where we turn off the runway
                        loc_end_2_dots = index_at_value(
                            np.ma.abs(ils_loc.array),
                            2.0,
                            _slice=slice(
                                turnoff + 5 *
                                (_slice.stop - _slice.start) / 100, loc_estab,
                                -1))
                        if loc_end_2_dots and is_index_within_slice(
                                loc_end_2_dots,
                                _slice) and not np.ma.is_masked(
                                    ils_loc.array[loc_end_2_dots]
                                ) and loc_end_2_dots > loc_estab:
                            loc_end = loc_end_2_dots
                    loc_est = slice(loc_estab, loc_end + 1)

            #######################################################################
            ## Identification of the period established on the glideslope
            #######################################################################

            gs_est = None
            if loc_est and 'glideslope' in approach_runway and ils_gs:
                # We only look for glideslope established periods if the localizer is already established.

                # The range to scan for the glideslope starts with localizer capture and ends at
                # 200ft or the minimum height point for a go-around or the end of
                # localizer established, if either is earlier.
                ils_gs_start = loc_estab
                ils_gs_200 = index_at_value(alt.array,
                                            200.0,
                                            _slice=slice(
                                                loc_end, ils_gs_start, -1))
                # The expression "ils_gs_200 or np.inf" caters for the case where the aircraft did not pass
                # through 200ft, so the result is None, in which case any other value is left to be the minimum.
                ils_gs_end = min(ils_gs_200 or np.inf, ref_idx, loc_end)

                # Look for ten seconds within half a dot
                ils_gs_estab = ils_established(ils_gs.array,
                                               slice(ils_gs_start, ils_gs_end),
                                               ils_gs.hz)

                if ils_gs_estab:
                    gs_est = slice(ils_gs_estab, ils_gs_end + 1)
            '''
            # These statements help set up test cases.
            print()
            print(airport['name'])
            print(approach_runway['identifier'])
            print(landing_runway['identifier'])
            print(_slice)
            if loc_est:
                print('Localizer established ', loc_est.start, loc_est.stop)
            if gs_est:
                print('Glideslope established ', gs_est.start, gs_est.stop)
            print()
            '''

            self.create_approach(
                approach_type,
                _slice,
                runway_change=runway_change,
                offset_ils=offset_ils,
                airport=airport,
                landing_runway=landing_runway,
                approach_runway=approach_runway,
                gs_est=gs_est,
                loc_est=loc_est,
                ils_freq=appr_ils_freq,
                turnoff=turnoff,
                lowest_lat=lowest_lat,
                lowest_lon=lowest_lon,
                lowest_hdg=lowest_hdg,
            )
    def derive(self,
               alt_aal=P('Altitude AAL'),
               alt_agl=P('Altitude AGL'),
               ac_type=A('Aircraft Type'),
               app=S('Approach And Landing'),
               hdg=P('Heading Continuous'),
               lat=P('Latitude Prepared'),
               lon=P('Longitude Prepared'),
               ils_loc=P('ILS Localizer'),
               ils_gs=S('ILS Glideslope'),
               ils_freq=P('ILS Frequency'),
               land_afr_apt=A('AFR Landing Airport'),
               land_afr_rwy=A('AFR Landing Runway'),
               lat_land=KPV('Latitude At Touchdown'),
               lon_land=KPV('Longitude At Touchdown'),
               precision=A('Precise Positioning'),
               fast=S('Fast'),

               #lat_smoothed=P('Latitude Smoothed'),
               #lon_smoothed=P('Longitude Smoothed'),
               u=P('Airspeed'),
               gspd=P('Groundspeed'),
               height_from_rig=P('Altitude ADH'),
               hdot=P('Vertical Speed'),
               roll=P('Roll'),
               heading=P('Heading'),
               distance_land=P('Distance To Landing'),
               tdwns=KTI('Touchdown'),
               offshore=M('Offshore'),
               takeoff=S('Takeoff')
               ):

        precise = bool(getattr(precision, 'value', False))
        alt = alt_agl if ac_type == helicopter else alt_aal
        app_slices = sorted(app.get_slices())

        for index, _slice in enumerate(app_slices):
            # a) The last approach is assumed to be landing:
            if index == len(app_slices) - 1:
                approach_type = 'LANDING'
                landing = True
            # b) We have a touch and go if Altitude AAL reached zero:
            #elif np.ma.any(alt.array[_slice] <= 0):
            elif np.ma.any(alt.array[slices_int(_slice.start, _slice.stop+(5*alt.frequency))] <= 0):
                if ac_type == aeroplane:
                    approach_type = 'TOUCH_AND_GO'
                    landing = False
                elif ac_type == helicopter:
                    approach_type = 'LANDING'
                    landing = True
                else:
                    raise ValueError('Not doing hovercraft!')
            # c) In any other case we have a go-around:
            else:
                approach_type = 'GO_AROUND'
                landing = False

            # Rough reference index to allow for go-arounds
            ref_idx = int(index_at_value(alt.array, 0.0, _slice=_slice, endpoint='nearest'))

            turnoff = None
            if landing:
                search_end = fast.get_surrounding(_slice.start)
                if search_end and search_end[0].slice.stop >= ref_idx:
                    search_end = min(search_end[0].slice.stop, _slice.stop)
                else:
                    search_end = _slice.stop

                tdn_hdg = np.ma.median(hdg.array[slices_int(ref_idx, search_end+1)])
                # Complex trap for the all landing heading data is masked case...
                if (tdn_hdg % 360.0) is np.ma.masked:
                    lowest_hdg = bearing_and_distance(lat.array[ref_idx], lon.array[ref_idx],
                                                      lat.array[search_end], lon.array[search_end])[0]
                else:
                    lowest_hdg = (tdn_hdg % 360.0).item()

                # While we're here, let's compute the turnoff index for this landing.
                head_landing = hdg.array[slices_int((ref_idx+_slice.stop)/2, _slice.stop)]
                if len(head_landing) > 2:
                    peak_bend = peak_curvature(head_landing, curve_sense='Bipolar')
                    fifteen_deg = index_at_value(
                        np.ma.abs(head_landing - head_landing[0]), 15.0)
                    if peak_bend:
                        turnoff = (ref_idx+_slice.stop)/2 + peak_bend
                    else:
                        if fifteen_deg and fifteen_deg < peak_bend:
                            turnoff = start_search + landing_turn
                        else:
                            # No turn, so just use end of landing run.
                            turnoff = _slice.stop
                else:
                    # No turn, so just use end of landing run.
                    turnoff = _slice.stop
            else:
                # We didn't land, but this is indicative of the runway heading
                lowest_hdg = (hdg.array[ref_idx] % 360.0).item()

            # Pass latitude, longitude and heading
            lowest_lat = None
            lowest_lon = None
            if lat and lon and ref_idx:
                lowest_lat = lat.array[ref_idx] or None
                lowest_lon = lon.array[ref_idx] or None
                if lowest_lat and lowest_lon and approach_type == 'GO_AROUND':
                    # Doing a go-around, we extrapolate to the threshold
                    # in case we abort the approach abeam a different airport,
                    # using the rule of three miles per thousand feet.
                    distance = np.ma.array([ut.convert(alt_aal.array[ref_idx] * (3 / 1000.0), ut.NM, ut.METER)])
                    bearing = np.ma.array([lowest_hdg])
                    reference = {'latitude': lowest_lat, 'longitude': lowest_lon}
                    lat_ga, lon_ga = latitudes_and_longitudes(bearing, distance, reference)
                    lowest_lat = lat_ga[0]
                    lowest_lon = lon_ga[0]

            if lat_land and lon_land and not (lowest_lat and lowest_lon):
                # use lat/lon at landing if values at ref_idx are masked
                # only interested in landing within approach slice.
                lat_land = lat_land.get(within_slice=_slice)
                lon_land = lon_land.get(within_slice=_slice)
                if lat_land and lon_land:
                    lowest_lat = lat_land[0].value or None
                    lowest_lon = lon_land[0].value or None

            kwargs = dict(
                precise=precise,
                _slice=_slice,
                lowest_lat=lowest_lat,
                lowest_lon=lowest_lon,
                lowest_hdg=lowest_hdg,
                appr_ils_freq=None,
                ac_type=ac_type,
            )

            # If the approach is a landing, pass through information from the
            # achieved flight record in case we cannot determine airport and
            # runway:
            if landing:
                kwargs.update(
                    land_afr_apt=land_afr_apt,
                    land_afr_rwy=land_afr_rwy,
                    hint='landing',
                )
            if landing or approach_type == 'GO_AROUND':
                # if we have a frequency and valid localiser signal at lowest point in approach
                appr_ils_freq = None
                if ils_freq:
                    appr_ils_freq = np.ma.round(ils_freq.array[ref_idx] or 0, 2)
                if not precise and appr_ils_freq  and ils_loc and np.ma.abs(ils_loc.array[ref_idx]) < 2.5:
                    kwargs['appr_ils_freq'] = appr_ils_freq

            airport, landing_runway = self._lookup_airport_and_runway(**kwargs)
            if not airport and ac_type == aeroplane:
                continue

            if ac_type == aeroplane and not airport.get('runways'):
                self.error("Airport %s: contains no runways", airport['code'])

            # Simple determination of heliport.
            heliport = is_heliport(ac_type, airport, landing_runway)

            for touchdown, tkoff in zip(tdwns.get_ordered_by_index(), takeoff.get_ordered_by_index()):
                # If both the takeoff and touchdown point are offshore then we consider
                # the approach to be a 'SHUTTLING APPROACH'. Else we continue to look for
                # an 'AIRBORNE RADAR DIRECT/OVERHEAD APPROACH' or a 'STANDARD APPROACH'
                #
                # A couple of seconds are added to the end of the slice as some flights used
                # to test this had the touchdown a couple of seconds outside the approach slice
                if is_index_within_slice(touchdown.index, slice(_slice.start, _slice.stop+5*alt.frequency)):
                    if offshore and \
                       offshore.array[int(touchdown.index)] == 'Offshore' and \
                       tkoff.start < touchdown.index:
                        if not distance_land:
                            if offshore.array[tkoff.start] == 'Offshore':
                                approach_type = 'SHUTTLING'
                        elif offshore.array[tkoff.start] == 'Offshore' and \
                             tkoff.start < len(distance_land.array) and \
                             distance_land.array[int(tkoff.start)] <= 40:
                            approach_type = 'SHUTTLING'
                        elif height_from_rig:
                            Vy = 80.0 # Type dependent?

                            # conditions_defs is a dict of condition name : expression to evaluate pairs, listed this way for clarity
                            condition_defs={'Below 120 kts' : lambda p : p['Airspeed'] < 120,
                                                'Below Vy+5' : lambda p : p['Airspeed'] < Vy+5.0,
                                                'Over Vy' : lambda p : p['Airspeed'] > Vy,
                                                'Over Vy-5' : lambda p : p['Airspeed'] > Vy-5.0,
                                                'Below 70 gspd' : lambda p : p['Groundspeed'] < 72,
                                                'Below 60 gspd' : lambda p : p['Groundspeed'] < 60,
                                                #'Below Vy-10' : lambda p : p['Airspeed'] < Vy-10.0,
                                                #'Over Vy-10' : lambda p : p['Airspeed'] > Vy-10.0,
                                                #'Above 30 gspd' : lambda p : p['Groundspeed'] > 30,

                                                'Over 900 ft' : lambda p : p['Altitude ADH'] > 900,
                                                'Over 200 ft' : lambda p : p['Altitude ADH'] > 200,
                                                'Below 1750 ft': lambda p : p['Altitude ADH'] < 1750,
                                                'Below 1100 ft' : lambda p : p['Altitude ADH'] < 1100,
                                                'Over 350 ft' : lambda p : p['Altitude ADH'] > 350,
                                                'Below 700 ft' : lambda p : p['Altitude ADH'] < 700,
                                                'ROD < 700 fpm' : lambda p : p['Vertical Speed'] > -700,
                                                'ROD > 200 fpm' : lambda p : p['Vertical Speed'] < -200,
                                                'Not climbing' : lambda p : p['Vertical Speed'] < 200,
                                                #'Over 400 ft' : lambda p : p['Altitude ADH'] > 400,
                                                #'Below 1500 ft': lambda p : p['Altitude ADH'] < 1500,
                                                #'Below 1300 ft': lambda p : p['Altitude ADH'] < 1300,

                                                'Roll below 25 deg' : lambda p : valid_between(p['Roll'], -25.0, 25.0),
                                                'Wings Level' : lambda p : valid_between(p['Roll'], -10.0, 10.0),
                                                'Within 20 deg of final heading' : lambda p : np.ma.abs(p['head_off_final']) < 20.0,
                                                #'Within 45 deg of downwind leg' : 'valid_between(np.ma.abs(head_off_final), 135.0, 225.0)',
                                                #'15 deg off final heading' : lambda p : np.ma.abs(np.ma.abs(p['head_off_two_miles'])-15.0) < 5.0,
                                                #'Heading towards oil rig' : lambda p : np.ma.abs(p['head_off_two_miles']) < 6.0,

                                                'Beyond 0.7 NM' : lambda p : p['Distance To Landing'] > 0.7,
                                                'Within 0.8 NM' : lambda p : p['Distance To Landing'] < 0.8,
                                                'Beyond 1.5 NM' : lambda p : p['Distance To Landing'] > 1.5,
                                                'Within 2.0 NM' : lambda p : p['Distance To Landing'] < 2.0,
                                                'Within 3.0 NM' : lambda p : p['Distance To Landing'] < 3.0,
                                                'Beyond 3.0 NM' : lambda p : p['Distance To Landing'] > 3.0,
                                                'Within 10.0 NM' : lambda p : p['Distance To Landing'] < 10.0,
                                                #'Within 1.5 NM' : lambda p : p['Distance To Landing'] < 1.5,
                                                }

                            # Phase map is a dict of the flight phases with the list of conditions which must be
                            # satisfied for the phase to be active.
                            phase_map={'Circuit':['Below 120 kts',
                                                  'Over Vy',
                                                  'Below 1100 ft',
                                                  'Over 900 ft',
                                                  'Roll below 25 deg', # includes downwind turn
                                                  ],
                                       'Level within 2NM':['Below Vy+5',
                                                           'Over Vy-5',
                                                           'Below 1100 ft',
                                                           'Over 900 ft',
                                                           'Wings Level',
                                                           'Within 20 deg of final heading',
                                                           'Within 2.0 NM',
                                                           'Beyond 1.5 NM',
                                                           ],
                                       'Initial Descent':['Wings Level',
                                                          'Within 20 deg of final heading',
                                                          'ROD < 700 fpm',
                                                          'ROD > 200 fpm',
                                                          'Beyond 0.7 NM',
                                                          'Over 350 ft',
                                                          ],
                                       'Final Approach':['Wings Level',
                                                         'Within 20 deg of final heading',
                                                         'ROD < 700 fpm',
                                                         'Within 0.8 NM',
                                                         'Below 60 gspd',
                                                         'Below 700 ft',
                                                         ],

                                       # Phases for ARDA/AROA
                                       #
                                       # All heading conditions are commented out as the pilots usually
                                       # go outside the boundaries; the other conditions seem to be
                                       # enough to detect them
                                       'ARDA/AROA 10 to 3':['Within 10.0 NM',
                                                       'Beyond 3.0 NM',
                                                       'Below 1750 ft',
                                                       'Not climbing',
                                                       #'Heading towards oil rig',
                                                       ],
                                       'ARDA/AROA Level within 3NM':['Below 70 gspd',
                                                           'Over 200 ft',
                                                           'Wings Level',
                                                           'Within 3.0 NM',
                                                           'Beyond 1.5 NM',
                                                           #'Within 20 deg of final heading',
                                                           ],
                                       'ARDA/AROA Final':['Not climbing',
                                                          'Within 2.0 NM',
                                                          #'15 deg off final heading'
                                                          ],
                                       }

                            """
                            #Phases that can be used to tighten the conditions for ARDA/AROA

                            'Radar Heading Change':['15 deg off final heading', 'Within 1.5 NM', 'Beyond 0.7 NM'],
                            'Low Approach':['Below Vy+5', 'Below 700 ft', 'Over 350 ft', 'Within 20 deg of final heading', 'Wings Level'],
                            'Low Circuit':['Below 120 kts', 'Over Vy-5', 'Below 700 ft', 'Over 350 ft', 'Roll below 25 deg']
                            """

                            approach_map = {'RIG': ['Circuit',
                                                    'Level within 2NM',
                                                    'Initial Descent',
                                                    'Final Approach'],
                                            'AIRBORNE_RADAR': ['ARDA/AROA 10 to 3',
                                                               'ARDA/AROA Level within 3NM',
                                                               'ARDA/AROA Final']}

                            # Making sure the approach slice contains enough information to be able
                            # to properly identify ARDA/AROA approaches (the procedure starts from 10NM
                            # before touchdown)

                            app_slice = slice(index_at_value(distance_land.array, 11, _slice=slice(0,touchdown.index)), touchdown.index)

                            heading_repaired = repair_mask(heading.array[app_slice],
                                                           frequency=heading.frequency,
                                                           repair_duration=np.ma.count_masked(heading.array[app_slice]),
                                                           copy=True,
                                                           extrapolate=True)

                            param_arrays = {
                                    'Airspeed': u.array[app_slice],
                                    'Groundspeed': gspd.array[app_slice],
                                    'Altitude ADH': height_from_rig.array[app_slice],
                                    'Vertical Speed': hdot.array[app_slice],
                                    'Roll': roll.array[app_slice],
                                    'Distance To Landing': distance_land.array[app_slice],
                                    'Heading': heading_repaired,
                                    'Latitude': lat.array[app_slice],
                                    'Longitude': lon.array[app_slice],
                            }

                            longest_approach_type, longest_approach_durn, longest_approach_slice = find_rig_approach(condition_defs,
                                                                                                                     phase_map, approach_map,
                                                                                                                     Vy, None, param_arrays,
                                                                                                                     debug=False)

                            if longest_approach_type is not None:
                                approach_type = longest_approach_type.upper()
                                _slice = slice(app_slice.start + longest_approach_slice.start, app_slice.stop)

            if heliport:
                self.create_approach(
                    approach_type,
                    _slice,
                    runway_change=False,
                    offset_ils=False,
                    airport=airport,
                    landing_runway=None,
                    approach_runway=None,
                    gs_est=None,
                    loc_est=None,
                    ils_freq=None,
                    turnoff=None,
                    lowest_lat=lowest_lat,
                    lowest_lon=lowest_lon,
                    lowest_hdg=lowest_hdg,
                )
                continue

            #########################################################################
            ## Analysis of fixed wing approach to a runway
            ##
            ## First step is to check the ILS frequency for the runway in use
            ## and cater for a change from the approach runway to the landing runway.
            #########################################################################

            appr_ils_freq = None
            runway_change = False
            offset_ils = False

            # Do we have a recorded ILS frequency? If so, what was it tuned to at the start of the approach??
            if ils_freq:
                appr_ils_freq = ils_freq.array[int(_slice.start)]
            # Was this valid, and if so did the start of the approach match the landing runway?
            if appr_ils_freq and not (np.isnan(appr_ils_freq) or np.ma.is_masked(appr_ils_freq)):
                appr_ils_freq = round(appr_ils_freq, 2)
                runway_kwargs = {
                    'ilsfreq': appr_ils_freq,
                    'latitude': lowest_lat,
                    'longitude': lowest_lon,
                }
                if not precise:
                    runway_kwargs['hint'] = kwargs.get('hint', 'approach')
                approach_runway = nearest_runway(airport, lowest_hdg, **runway_kwargs)
                # Have we have identified runways for both conditions that are both different and parallel?
                if all((approach_runway, landing_runway)) \
                   and approach_runway['id'] != landing_runway['id'] \
                   and approach_runway['identifier'][:2] == landing_runway['identifier'][:2]:
                    runway_change = True
            else:
                # Without a frequency source, we just have to hope any localizer signal is for this runway!
                approach_runway = landing_runway

            if approach_runway and 'frequency' in approach_runway['localizer']:
                if np.ma.count(ils_loc.array[slices_int(_slice)]) > 10:
                    if runway_change:
                        # We only use the first frequency tuned. This stops scanning across both runways if the pilot retunes.
                        loc_slice = shift_slices(
                            runs_of_ones(np.ma.abs(ils_freq.array[slices_int(_slice)] - appr_ils_freq) < 0.001),
                            _slice.start
                        )[0]
                    else:
                        loc_slice = _slice
                else:
                    # No localizer or inadequate data for this approach.
                    loc_slice = None
            else:
                # The approach was to a runway without an ILS, so even if it was tuned, we ignore this.
                appr_ils_freq = None
                loc_slice = None

            if np.ma.is_masked(appr_ils_freq):
                loc_slice = None
                appr_ils_freq = None
            else:
                if appr_ils_freq and loc_slice:
                    if appr_ils_freq != round(ut.convert(approach_runway['localizer']['frequency'], ut.KHZ, ut.MHZ), 2):
                        loc_slice = None

            #######################################################################
            ## Identification of the period established on the localizer
            #######################################################################

            loc_est = None
            if loc_slice:
                valid_range = np.ma.flatnotmasked_edges(ils_loc.array[slices_int(_slice)])
                # I have some data to scan. Shorthand names;
                loc_start = valid_range[0] + _slice.start
                loc_end = valid_range[1] + _slice.start
                scan_back = slice(ref_idx, loc_start, -1)

                # If we are turning in, we are not interested in signals that are not related to this approach.
                # The value of 45 deg was selected to encompass Washington National airport with a 40 deg offset.
                hdg_diff = np.ma.abs(np.ma.mod((hdg.array-lowest_hdg)+180.0, 360.0)-180.0)
                ils_hdg_45 = index_at_value(hdg_diff, 45.0, _slice=scan_back)

                # We are not interested above 1,500 ft, so may trim back the start point to that point:
                ils_alt_1500 = index_at_value(alt_aal.array, 1500.0, _slice=scan_back)

                # The criteria for start of established phase is the latter of the approach phase start, the turn-in or 1500ft.
                # The "or 0" allow for flights that do not turn through 45 deg or keep below 1500ft.
                loc_start = max(loc_start, ils_hdg_45 or 0, ils_alt_1500 or 0)

                if loc_start < ref_idx:
                    # Did I get established on the localizer, and if so,
                    # when? We only look AFTER the aircraft is already within
                    # 45deg of the runway heading, below 1500ft and the data
                    # is valid for this runway. Testing that the aircraft is
                    # not just passing across the localizer is built into the
                    # ils_established function.
                    loc_estab = ils_established(ils_loc.array, slice(loc_start, ref_idx), ils_loc.hz)
                else:
                    # If localiser start is after we touchdown bail.
                    loc_estab = None

                if loc_estab:

                    # Refine the end of the localizer established phase...
                    if (approach_runway and approach_runway['localizer']['is_offset']):
                        offset_ils = True
                        # The ILS established phase ends when the deviation becomes large.
                        loc_end = ils_established(ils_loc.array, slice(ref_idx, loc_estab, -1), ils_loc.hz, point='immediate')

                    elif approach_type in ['TOUCH_AND_GO', 'GO_AROUND']:
                        # We finish at the lowest point
                        loc_end = ref_idx

                    elif runway_change:
                        # Use the end of localizer phase as this already reflects the tuned frequency.
                        est_end = ils_established(ils_loc.array, slice(loc_estab, ref_idx), ils_loc.hz, point='end')
                        # Make sure we dont end up with a negative slice i.e. end before we are established.
                        loc_end = min([x for x in (loc_slice.stop, loc_end, est_end or np.inf) if x > loc_estab])

                    elif approach_type == 'LANDING':
                        # Just end at 2 dots where we turn off the runway
                        loc_end_2_dots = index_at_value(np.ma.abs(ils_loc.array), 2.0, _slice=slice(turnoff+5*(_slice.stop-_slice.start)/100, loc_estab, -1))
                        if loc_end_2_dots and \
                           is_index_within_slice(loc_end_2_dots, _slice) and \
                           not np.ma.is_masked(ils_loc.array[int(loc_end_2_dots)]) and \
                           loc_end_2_dots > loc_estab:
                            loc_end = loc_end_2_dots
                    loc_est = slice(loc_estab, loc_end+1)

            #######################################################################
            ## Identification of the period established on the glideslope
            #######################################################################

            gs_est = None
            if loc_est and 'glideslope' in approach_runway and ils_gs:
                # We only look for glideslope established periods if the localizer is already established.

                # The range to scan for the glideslope starts with localizer capture and ends at
                # 200ft or the minimum height point for a go-around or the end of
                # localizer established, if either is earlier.
                ils_gs_start = loc_estab
                ils_gs_200 = index_at_value(alt.array, 200.0, _slice=slice(loc_end, ils_gs_start, -1))
                # The expression "ils_gs_200 or np.inf" caters for the case where the aircraft did not pass
                # through 200ft, so the result is None, in which case any other value is left to be the minimum.
                ils_gs_end = min(ils_gs_200 or np.inf, ref_idx, loc_end)

                # Look for ten seconds within half a dot
                ils_gs_estab = ils_established(ils_gs.array, slice(ils_gs_start, ils_gs_end), ils_gs.hz)

                if ils_gs_estab:
                    gs_est = slice(ils_gs_estab, ils_gs_end+1)


            '''
            # These statements help set up test cases.
            print()
            print(airport['name'])
            print(approach_runway['identifier'])
            print(landing_runway['identifier'])
            print(_slice)
            if loc_est:
                print('Localizer established ', loc_est.start, loc_est.stop)
            if gs_est:
                print('Glideslope established ', gs_est.start, gs_est.stop)
            print()
            '''

            self.create_approach(
                approach_type,
                _slice,
                runway_change=runway_change,
                offset_ils=offset_ils,
                airport=airport,
                landing_runway=landing_runway,
                approach_runway=approach_runway,
                gs_est=gs_est,
                loc_est=loc_est,
                ils_freq=appr_ils_freq,
                turnoff=turnoff,
                lowest_lat=lowest_lat,
                lowest_lon=lowest_lon,
                lowest_hdg=lowest_hdg,
            )
    def derive(self,
               toff_fdr_apt=A('FDR Takeoff Airport'),
               toff_afr_rwy=A('AFR Takeoff Runway'),
               toff_hdg=KPV('Heading During Takeoff'),
               toff_lat=KPV('Latitude At Liftoff'),
               toff_lon=KPV('Longitude At Liftoff'),
               accel_start_lat=KPV('Latitude At Takeoff Acceleration Start'),
               accel_start_lon=KPV('Longitude At Takeoff Acceleration Start'),
               precision=A('Precise Positioning'),
               lat=P('Latitude'),
               lon=P('Longitude'),
               lat_c=P('Latitude (Coarse)'),
               lon_c=P('Longitude (Coarse)')):
        '''
        '''
        fallback = False
        precise = bool(getattr(precision, 'value', False))

        if not precise and toff_afr_rwy:
            self.use_afr(toff_afr_rwy)
            return

        try:
            airport = toff_fdr_apt.value
        except AttributeError:
            self.warning('Invalid airport... Fallback to AFR Takeoff Runway.')
            fallback = True
        else:
            if airport is None:
                fallback = True

        try:
            heading = toff_hdg.get_first().value
            if heading is None:
                raise ValueError
        except (AttributeError, ValueError):
            self.warning('Invalid heading... Fallback to AFR Takeoff Runway.')
            fallback = True

        # If we have airport and heading, look for the nearest runway:
        if not fallback:
            kwargs = {}

            # Even if we do not have precise latitude and longitude
            # information, we still use this for the takeoff runway detection
            # as it is often accurate at the start of a flight, and in the
            # absence of an ILS tuned frequency we have no better option. (We
            # did consider using the last direction of turn onto the runway,
            # but this would require an airport database with terminal and
            # taxiway details that was not felt justified).
            lat = lat or lat_c
            lon = lon or lon_c
            lats, lons = self._takeoff_roll(
                lat, lon, accel_start_lat, accel_start_lon, toff_lat, toff_lon
            )
            if lats is not None and lons is not None:
                kwargs.update(
                    latitude=np.array(lats),
                    longitude=np.array(lons),
                )
            else:
                self.warning('No coordinates for takeoff runway lookup.')
            if not precise:
                kwargs.update(hint='takeoff')

            runway = nearest_runway(airport, heading, **kwargs)
            if not runway:
                msg = 'No runway found for airport #%d @ %03.1f deg with %s.'
                self.warning(msg, airport['id'], heading, kwargs)
                # No runway was found, so fall through and try AFR.
            else:
                self.info('Detected takeoff runway: %s for airport #%d @ %03.1f deg with %s', runway['identifier'], airport['id'], heading, kwargs)
                self.set_flight_attr(runway)
                return

        if toff_afr_rwy:
            self.use_afr(toff_afr_rwy)
            return

        self.error('Unable to determine runway at takeoff!')
        self.set_flight_attr(None)
    def _lookup_airport_and_runway(self,
                                   _slice,
                                   precise,
                                   lowest_lat,
                                   lowest_lon,
                                   lowest_hdg,
                                   appr_ils_freq,
                                   land_afr_apt=None,
                                   land_afr_rwy=None,
                                   hint='approach'):
        handler = api.get_handler(settings.API_HANDLER)
        kwargs = {}
        airport, runway = None, None

        # A1. If we have latitude and longitude, look for the nearest airport:
        if lowest_lat is not None and lowest_lon is not None:
            kwargs.update(latitude=lowest_lat, longitude=lowest_lon)
            try:
                airport = handler.get_nearest_airport(**kwargs)
            except api.NotFoundError:
                msg = 'No approach airport found near coordinates (%f, %f).'
                self.warning(msg, lowest_lat, lowest_lon)
                # No airport was found, so fall through and try AFR.
            else:
                self.debug('Detected approach airport: %s', airport)
        else:
            # No suitable coordinates, so fall through and try AFR.
            self.warning('No coordinates for looking up approach airport.')
            return None, None

        # A2. If and we have an airport in achieved flight record, use it:
        # NOTE: AFR data is only provided if this approach is a landing.
        if not airport and land_afr_apt:
            airport = land_afr_apt.value
            self.debug('Using approach airport from AFR: %s', airport)

        # A3. After all that, we still couldn't determine an airport...
        if not airport:
            self.error('Unable to determine airport on approach!')
            return None, None

        if lowest_hdg is not None:

            # R1. If we have airport and heading, look for the nearest runway:
            if appr_ils_freq:
                kwargs['ilsfreq'] = appr_ils_freq

                # We already have latitude and longitude in kwargs from looking up
                # the airport. If the measurments are not precise, remove them.
                if not precise:
                    kwargs['hint'] = hint
                    del kwargs['latitude']
                    del kwargs['longitude']

            runway = nearest_runway(airport, lowest_hdg, **kwargs)
            if not runway:
                msg = 'No runway found for airport #%d @ %03.1f deg with %s.'
                self.warning(msg, airport['id'], lowest_hdg, kwargs)
                # No runway was found, so fall through and try AFR.
                if 'ilsfreq' in kwargs:
                    # This is a trap for airports where the ILS data is not
                    # available, but the aircraft approached with the ILS
                    # tuned. A good prompt for an omission in the database.
                    self.warning('Fix database? No runway but ILS was tuned.')
            else:
                self.debug('Detected approach runway: %s', runway)

        # R2. If we have a runway provided in achieved flight record, use it:
        if not runway and land_afr_rwy:
            runway = land_afr_rwy.value
            self.debug('Using approach runway from AFR: %s', runway)

        # R3. After all that, we still couldn't determine a runway...
        if not runway:
            self.error('Unable to determine runway on approach!')

        return airport, runway
    def _lookup_airport_and_runway(self, _slice, precise, lowest_lat,
                                   lowest_lon, lowest_hdg, appr_ils_freq,
                                   land_afr_apt=None, land_afr_rwy=None,
                                   hint='approach', ac_type=aeroplane):
        handler = api.get_handler(settings.API_HANDLER)
        kwargs = {}
        airport, runway, match = None, None, None

        # A1. If we have latitude and longitude, look for the nearest airport:
        if lowest_lat not in (None, np.ma.masked) and lowest_lon not in (None, np.ma.masked):
            kwargs.update(latitude=lowest_lat, longitude=lowest_lon)
            try:
                airports = handler.get_nearest_airport(**kwargs)
            except (ValueError, TypeError):
                self.warning('No coordinates for looking up approach airport.')
            except api.NotFoundError:
                msg = 'No approach airport found near coordinates (%f, %f).'
                self.warning(msg, lowest_lat, lowest_lon)
                # No airport was found, so fall through and try AFR.
            else:
                airport_info = self._evaluate_airports(airports, lowest_lat, lowest_lon, lowest_hdg, appr_ils_freq)
                if land_afr_apt and land_afr_apt.value['id'] in airport_info:
                    # use afr airprot
                    match = airport_info[land_afr_apt.value['id']]
                elif land_afr_apt and precise and ac_type != helicopter:
                    # raise error as afr and flight data do not match
                    msg = "'%s' provided by AFR is not in list of aiports within range of %s, %s. Aircraft has precise positioning"\
                        % (land_afr_apt.value['code'], lowest_lat, lowest_lon)
                    raise AFRMissmatchError(self.name, msg)
                elif not land_afr_apt and precise:
                    # filter by runway coordinates
                    filtered = [x for x in airport_info.values() if x['min_rwy_start_dist'] is not None]
                    if filtered:
                        match = min(filtered, key=itemgetter('min_rwy_start_dist'))
                else:
                    # filter by runway heading
                    airport = None
                    potential_airports = [x for x in airport_info.values() if x['heading_match']]
                    if appr_ils_freq:
                        # filter by ils frequency
                        ils_airports = [x for x in airport_info.values() if x['ils_match']]
                        if len(ils_airports) == 1:
                            potential_airports = [ils_airports[0]]
                        elif len(ils_airports) > 1:
                            potential_airports = ils_airports
                    if len(potential_airports) == 1:
                        match = potential_airports[0]
                    elif len(potential_airports) > 1:
                        # filter by runway distances
                        filtered = [x for x in potential_airports if x['min_rwy_start_dist'] is not None]
                        if filtered:
                            match = min(filtered, key=itemgetter('min_rwy_start_dist'))
                    else:
                        # filter by airport distances
                        if airports:
                            airport = min(airports, key=itemgetter('distance'))
                if match:
                    airport = match['airport']
                if airport:
                    self.debug('Detected approach airport: %s', airport)
                else:
                    self.warning('Unable to locate Airport from provided coordinates')
        else:
            # No suitable coordinates, so fall through and try AFR.
            self.warning('No coordinates for looking up approach airport.')
            # return None, None

        # A2. If and we have an airport in achieved flight record, use it:
        # NOTE: AFR data is only provided if this approach is a landing.
        if not airport and land_afr_apt:
            airport = handler.get_airport(land_afr_apt.value['id'])
            self.debug('Using approach airport from AFR: %s', airport['name'])

        # A3. After all that, we still couldn't determine an airport...
        if not airport:
            self.error('Unable to determine airport on approach!')
            return None, None

        if lowest_hdg is not None:

            # R1. If we have airport and heading, look for the nearest runway:
            if appr_ils_freq:
                kwargs['ilsfreq'] = appr_ils_freq

                # We already have latitude and longitude in kwargs from looking up
                # the airport. If the measurments are not precise, remove them.
                if not precise:
                    kwargs['hint'] = hint

            runway = nearest_runway(airport, lowest_hdg, **kwargs)
            if not runway:
                msg = 'No runway found for airport #%d @ %03.1f deg with %s.'
                self.warning(msg, airport['id'], lowest_hdg, kwargs)
                # No runway was found, so fall through and try AFR.
                if 'ilsfreq' in kwargs:
                    # This is a trap for airports where the ILS data is not
                    # available, but the aircraft approached with the ILS
                    # tuned. A good prompt for an omission in the database.
                    self.warning('Fix database? No runway but ILS was tuned.')
            else:
                self.debug('Detected approach runway: %s', runway)

        # R2. If we have a runway provided in achieved flight record, use it:
        if not runway and land_afr_rwy:
            runway = land_afr_rwy.value
            self.debug('Using approach runway from AFR: %s', runway)

        # R3. After all that, we still couldn't determine a runway...
        if not runway:
            self.error('Unable to determine runway on approach!')

        return airport, runway
Example #8
0
    def derive(self,
               alt_aal=P('Altitude AAL'),
               alt_agl=P('Altitude AGL'),
               ac_type=A('Aircraft Type'),
               app=S('Approach And Landing'),
               hdg=P('Heading Continuous'),
               lat=P('Latitude Prepared'),
               lon=P('Longitude Prepared'),
               ils_loc=P('ILS Localizer'),
               ils_gs=S('ILS Glideslope'),
               ils_freq=P('ILS Frequency'),
               land_afr_apt=A('AFR Landing Airport'),
               land_afr_rwy=A('AFR Landing Runway'),
               lat_land=KPV('Latitude At Touchdown'),
               lon_land=KPV('Longitude At Touchdown'),
               precision=A('Precise Positioning'),
               fast=S('Fast'),
               ):

        precise = bool(getattr(precision, 'value', False))

        alt = alt_agl if ac_type == helicopter else alt_aal

        app_slices = app.get_slices()

        for index, _slice in enumerate(app_slices):
            # a) The last approach is assumed to be landing:
            if index == len(app_slices) - 1:
                approach_type = 'LANDING'
                landing = True
            # b) We have a touch and go if Altitude AAL reached zero:
            elif np.ma.any(alt.array[_slice] <= 0):
                if ac_type == aeroplane:
                    approach_type = 'TOUCH_AND_GO'
                    landing = False
                elif ac_type == helicopter:
                    approach_type = 'LANDING'
                    landing = True
                else:
                    raise ValueError('Not doing hovercraft!')
            # c) In any other case we have a go-around:
            else:
                approach_type = 'GO_AROUND'
                landing = False

            # Rough reference index to allow for go-arounds
            ref_idx = index_at_value(alt.array, 0.0, _slice=_slice, endpoint='nearest')

            turnoff = None
            if landing:
                search_end = fast.get_surrounding(_slice.start)
                if search_end:
                    search_end = min(search_end[0].slice.stop, _slice.stop)
                else:
                    search_end = _slice.stop

                tdn_hdg = np.ma.median(hdg.array[ref_idx:search_end+1])
                lowest_hdg = (tdn_hdg % 360.0).item()
                
                # While we're here, let's compute the turnoff index for this landing.
                head_landing = hdg.array[(ref_idx+_slice.stop)/2:_slice.stop]
                peak_bend = peak_curvature(head_landing, curve_sense='Bipolar')
                fifteen_deg = index_at_value(
                    np.ma.abs(head_landing - head_landing[0]), 15.0)
                if peak_bend:
                    turnoff = (ref_idx+_slice.stop)/2 + peak_bend
                else:
                    if fifteen_deg and fifteen_deg < peak_bend:
                        turnoff = start_search + landing_turn
                    else:
                        # No turn, so just use end of landing run.
                        turnoff = _slice.stop
            else:
                # We didn't land, but this is indicative of the runway heading
                lowest_hdg = (hdg.array[ref_idx] % 360.0).item()

            # Pass latitude, longitude and heading
            lowest_lat = None
            lowest_lon = None
            if lat and lon and ref_idx:
                lowest_lat = lat.array[ref_idx] or None
                lowest_lon = lon.array[ref_idx] or None
            if lat_land and lon_land and not (lowest_lat and lowest_lon):
                # use lat/lon at landing if values at ref_idx are masked
                # only interested in landing within approach slice.
                lat_land = lat_land.get(within_slice=_slice)
                lon_land = lon_land.get(within_slice=_slice)
                if lat_land and lon_land:
                    lowest_lat = lat_land[0].value or None
                    lowest_lon = lon_land[0].value or None

            kwargs = dict(
                precise=precise,
                _slice=_slice,
                lowest_lat=lowest_lat,
                lowest_lon=lowest_lon,
                lowest_hdg=lowest_hdg,
                appr_ils_freq=None,
            )

            # If the approach is a landing, pass through information from the
            # achieved flight record in case we cannot determine airport and
            # runway:
            if landing:
                kwargs.update(
                    land_afr_apt=land_afr_apt,
                    land_afr_rwy=land_afr_rwy,
                    hint='landing',
                )
            if landing or approach_type == 'GO_AROUND':
                # if we have a frequency and valid localiser signal at lowest point in approach
                appr_ils_freq = None
                if ils_freq:
                    appr_ils_freq = np.ma.round(ils_freq.array[ref_idx] or 0, 2)
                if not precise and appr_ils_freq  and ils_loc and np.ma.abs(ils_loc.array[ref_idx]) < 2.5:
                    kwargs['appr_ils_freq'] = appr_ils_freq

            airport, landing_runway = self._lookup_airport_and_runway(**kwargs)
            if not airport and ac_type == aeroplane:
                continue

            if ac_type == aeroplane and not airport.get('runways'):
                self.error("Airport %s: contains no runways", airport['code'])

            # Simple determination of heliport.
            # This function may be expanded to cater for rig approaches in future.
            heliport = is_heliport(ac_type, airport, landing_runway)

            if heliport:
                self.create_approach(
                    approach_type,
                    _slice,
                    runway_change=False,
                    offset_ils=False,
                    airport=airport,
                    landing_runway=None,
                    approach_runway=None,
                    gs_est=None,
                    loc_est=None,
                    ils_freq=None,
                    turnoff=None,
                    lowest_lat=lowest_lat,
                    lowest_lon=lowest_lon,
                    lowest_hdg=lowest_hdg,
                )
                continue

            #########################################################################
            ## Analysis of fixed wing approach to a runway
            ## 
            ## First step is to check the ILS frequency for the runway in use
            ## and cater for a change from the approach runway to the landing runway.
            #########################################################################
            
            appr_ils_freq = None
            runway_change = False
            offset_ils = False
            
            # Do we have a recorded ILS frequency? If so, what was it tuned to at the start of the approach??
            if ils_freq:
                appr_ils_freq = round(ils_freq.array[_slice.start], 2)
            # Was this valid, and if so did the start of the approach match the landing runway?
            if appr_ils_freq and not np.isnan(appr_ils_freq):
                runway_kwargs = {
                    'ilsfreq': appr_ils_freq,
                    'latitude': lowest_lat,
                    'longitude': lowest_lon,
                }
                if not precise:
                    runway_kwargs['hint'] = kwargs.get('hint', 'approach')
                approach_runway = nearest_runway(airport, lowest_hdg, **runway_kwargs)
                # Have we have identified runways for both conditions that are both different and parallel?
                if all((approach_runway, landing_runway)) \
                   and approach_runway['id'] != landing_runway['id'] \
                   and approach_runway['identifier'][:2] == landing_runway['identifier'][:2]:
                    runway_change = True
            else:
                # Without a frequency source, we just have to hope any localizer signal is for this runway!
                approach_runway = landing_runway

            if approach_runway and approach_runway['localizer'].has_key('frequency'):
                if np.ma.count(ils_loc.array[_slice]) > 10:
                    if runway_change:
                        # We only use the first frequency tuned. This stops scanning across both runways if the pilot retunes.
                        loc_slice = shift_slices(runs_of_ones(np.ma.abs(ils_freq.array[_slice]-appr_ils_freq)<0.001),
                                                 _slice.start)[0]
                    else:
                        loc_slice = _slice
                else:
                    # No localizer or inadequate data for this approach.
                    loc_slice = None
            else:
                # The approach was to a runway without an ILS, so even if it was tuned, we ignore this.
                appr_ils_freq = None
                loc_slice = None

            if appr_ils_freq and loc_slice:
                if appr_ils_freq != approach_runway['localizer']['frequency']/1000.0:
                    loc_slice = None

            #######################################################################
            ## Identification of the period established on the localizer
            #######################################################################
                    
            loc_est = None
            if loc_slice:
                valid_range = np.ma.flatnotmasked_edges(ils_loc.array[_slice])
                # I have some data to scan. Shorthand names;
                loc_start = valid_range[0] + _slice.start
                loc_end = valid_range[1] + _slice.start
                scan_back = slice(ref_idx, loc_start, -1)

                # If we are turning in, we are not interested in signals that are not related to this approach.
                # The value of 45 deg was selected to encompass Washington National airport with a 40 deg offset.
                hdg_diff = np.ma.abs(np.ma.mod((hdg.array-lowest_hdg)+180.0, 360.0)-180.0)
                ils_hdg_45 = index_at_value(hdg_diff, 45.0, _slice=scan_back)
                
                # We are not interested above 1,500 ft, so may trim back the start point to that point:
                ils_alt_1500 = index_at_value(alt_aal.array, 1500.0, _slice=scan_back)
                
                # The criteria for start of established phase is the latter of the approach phase start, the turn-in or 1500ft.
                # The "or 0" allow for flights that do not turn through 45 deg or keep below 1500ft.
                loc_start = max(loc_start, ils_hdg_45 or 0, ils_alt_1500 or 0)

                if loc_start < ref_idx:
                    # Did I get established on the localizer, and if so,
                    # when? We only look AFTER the aircraft is already within
                    # 45deg of the runway heading, below 1500ft and the data
                    # is valid for this runway. Testing that the aircraft is
                    # not just passing across the localizer is built into the
                    # ils_established function.
                    loc_estab = ils_established(ils_loc.array, slice(loc_start, ref_idx), ils_loc.hz)
                else:
                    # If localiser start is after we touchdown bail.
                    loc_estab = None

                if loc_estab:
                    
                    # Refine the end of the localizer established phase...
                    if (approach_runway and approach_runway['localizer']['is_offset']):
                        offset_ils = True
                        # The ILS established phase ends when the deviation becomes large.
                        loc_end = ils_established(ils_loc.array, slice(ref_idx, loc_estab, -1), ils_loc.hz, point='immediate')

                    elif approach_type in ['TOUCH_AND_GO', 'GO_AROUND']:
                        # We finish at the lowest point
                        loc_end = ref_idx
                        
                    elif runway_change:
                        # Use the end of localizer phase as this already reflects the tuned frequency.
                        est_end = ils_established(ils_loc.array, slice(loc_estab, ref_idx), ils_loc.hz, point='end')
                        loc_end = min(loc_slice.stop, loc_end, est_end or np.inf)
    
                    elif approach_type == 'LANDING':
                        # Just end at 2 dots where we turn off the runway
                        loc_end_2_dots = index_at_value(np.ma.abs(ils_loc.array), 2.0, _slice=slice(loc_end, loc_estab, -1))
                        if loc_end_2_dots:
                            loc_end = loc_end_2_dots
                        
                    loc_est = slice(loc_estab, loc_end+1)

            #######################################################################
            ## Identification of the period established on the glideslope
            #######################################################################

            gs_est = None
            if loc_est and approach_runway.has_key('glideslope') and ils_gs:
                # We only look for glideslope established periods if the localizer is already established.

                # The range to scan for the glideslope starts with localizer capture and ends at
                # 200ft or the minimum height point for a go-around or the end of 
                # localizer established, if either is earlier.
                ils_gs_start = loc_estab
                ils_gs_200 = index_at_value(alt.array, 200.0, _slice=slice(loc_end, ils_gs_start, -1))
                # The expression "ils_gs_200 or np.inf" caters for the case where the aircraft did not pass
                # through 200ft, so the result is None, in which case any other value is left to be the minimum.
                ils_gs_end = min(ils_gs_200 or np.inf, ref_idx, loc_end)

                # Look for ten seconds within half a dot
                ils_gs_estab = ils_established(ils_gs.array, slice(ils_gs_start, ils_gs_end), ils_gs.hz)

                if ils_gs_estab:
                    gs_est = slice(ils_gs_estab, ils_gs_end+1)


            '''
            # These statements help set up test cases.
            print
            print airport['name']
            print approach_runway['identifier']
            print landing_runway['identifier']
            print _slice
            if loc_est:
                print 'Localizer established ', loc_est.start, loc_est.stop
            if gs_est:
                print 'Glideslope established ', gs_est.start, gs_est.stop
            print
            '''

            self.create_approach(
                approach_type,
                _slice,
                runway_change=runway_change,
                offset_ils=offset_ils,
                airport=airport,
                landing_runway=landing_runway,
                approach_runway=approach_runway,
                gs_est=gs_est,
                loc_est=loc_est,
                ils_freq=appr_ils_freq,
                turnoff=turnoff,
                lowest_lat=lowest_lat,
                lowest_lon=lowest_lon,
                lowest_hdg=lowest_hdg,
            )
Example #9
0
    def _lookup_airport_and_runway(self, _slice, precise, lowest_lat,
                                   lowest_lon, lowest_hdg, appr_ils_freq,
                                   land_afr_apt=None, land_afr_rwy=None,
                                   hint='approach'):
        handler = api.get_handler(settings.API_HANDLER)
        kwargs = {}
        airport, runway = None, None

        # A1. If we have latitude and longitude, look for the nearest airport:
        if lowest_lat is not None and lowest_lon is not None:
            kwargs.update(latitude=lowest_lat, longitude=lowest_lon)
            try:
                airport = handler.get_nearest_airport(**kwargs)
            except api.NotFoundError:
                msg = 'No approach airport found near coordinates (%f, %f).'
                self.warning(msg, lowest_lat, lowest_lon)
                # No airport was found, so fall through and try AFR.
            else:
                self.debug('Detected approach airport: %s', airport)
        else:
            # No suitable coordinates, so fall through and try AFR.
            self.warning('No coordinates for looking up approach airport.')
            # return None, None

        # A2. If and we have an airport in achieved flight record, use it:
        # NOTE: AFR data is only provided if this approach is a landing.
        if not airport and land_afr_apt:
            airport = handler.get_airport(land_afr_apt.value['id'])
            self.debug('Using approach airport from AFR: %s', airport['name'])

        # A3. After all that, we still couldn't determine an airport...
        if not airport:
            self.error('Unable to determine airport on approach!')
            return None, None

        if lowest_hdg is not None:

            # R1. If we have airport and heading, look for the nearest runway:
            if appr_ils_freq:
                kwargs['ilsfreq'] = appr_ils_freq

                # We already have latitude and longitude in kwargs from looking up
                # the airport. If the measurments are not precise, remove them.
                if not precise:
                    kwargs['hint'] = hint

            runway = nearest_runway(airport, lowest_hdg, **kwargs)
            if not runway:
                msg = 'No runway found for airport #%d @ %03.1f deg with %s.'
                self.warning(msg, airport['id'], lowest_hdg, kwargs)
                # No runway was found, so fall through and try AFR.
                if 'ilsfreq' in kwargs:
                    # This is a trap for airports where the ILS data is not
                    # available, but the aircraft approached with the ILS
                    # tuned. A good prompt for an omission in the database.
                    self.warning('Fix database? No runway but ILS was tuned.')
            else:
                self.debug('Detected approach runway: %s', runway)

        # R2. If we have a runway provided in achieved flight record, use it:
        if not runway and land_afr_rwy:
            runway = land_afr_rwy.value
            self.debug('Using approach runway from AFR: %s', runway)

        # R3. After all that, we still couldn't determine a runway...
        if not runway:
            self.error('Unable to determine runway on approach!')

        return airport, runway
Example #10
0
    def _lookup_airport_and_runway(self,
                                   _slice,
                                   precise,
                                   lowest_lat,
                                   lowest_lon,
                                   final_lat,
                                   final_lon,
                                   lowest_hdg,
                                   appr_ils_freq,
                                   land_afr_apt=None,
                                   land_afr_rwy=None,
                                   hint='approach',
                                   ac_type=aeroplane):
        '''
        Determine the Airport and Runway.

        For the airport:

        1. Find the nearest airport to the coordinates at landing with precise positioning.
        2. Use the airport data provided in the achieved flight record if the aircraft
           does not have precise positioning.
        3. Find the nearest airport to the coordinates at landing without precise
           positioning.
        4. Use the airport data provided in the achieved flight record as a fallback.

        For the runway:

        1. Use the runway data provided in the achieved flight record if available and
           the aircraft does not have precise positioning.
        2. Using airport, heading and coordinates at landing.
        3. Use the runway data provided in the achieved flight record if heading is
           missing or the runway failed to derive.

        '''
        airport = self._find_nearest_airport(precise, lowest_lat, lowest_lon,
                                             lowest_hdg, appr_ils_freq,
                                             ac_type, land_afr_apt)

        handler = api.get_handler(settings.API_HANDLER)
        if airport:
            if not precise and land_afr_apt:
                # Favour AFR for non-precise aircrafts
                airport = handler.get_airport(land_afr_apt.value['id'])
                self.debug('Using approach airport from AFR: %s',
                           airport['name'])

        else:
            if land_afr_apt:
                airport = handler.get_airport(land_afr_apt.value['id'])
                self.debug('Using approach airport from AFR: %s',
                           airport['name'])
            else:
                self.error('Unable to determine airport on approach!')
                return None, None

        # Determine RWY
        runway = None
        if lowest_hdg is not None:
            runway = nearest_runway(
                airport,
                lowest_hdg,
                latitude=final_lat if final_lat is not None else lowest_lat,
                longitude=final_lon if final_lon is not None else lowest_lon,
                ilsfreq=appr_ils_freq,
                hint=hint if not precise else None)

        if not runway:
            msg = (
                'No runway found for airport #%d @ %03.1f deg with latitude %s, '
                'longitude %s, ilsfreq %s and hint %s.')
            self.warning(msg, airport['id'], lowest_hdg, lowest_lat,
                         lowest_lon, appr_ils_freq,
                         hint if not precise else None)
            if appr_ils_freq:
                # This is a trap for airports where the ILS data is not
                # available, but the aircraft approached with the ILS
                # tuned. A good prompt for an omission in the database.
                self.warning('Fix database? No runway but ILS was tuned.')
            # No runway was found, so fall through and try AFR.
            if land_afr_rwy:
                runway = land_afr_rwy.value
                self.debug('Using approach runway from AFR: %s', runway)
        else:
            self.debug('Detected approach runway: %s', runway)
            if not precise:
                # Favour AFR RWY info if available
                if land_afr_rwy:
                    runway = land_afr_rwy.value
                    self.debug('Using approach runway from AFR: %s', runway)

        if not runway:
            self.error('Unable to determine runway on approach!')

        return airport, runway
Example #11
0
    def derive(self,
               land_fdr_apt=A('FDR Landing Airport'),
               land_afr_rwy=A('AFR Landing Runway'),
               land_hdg=KPV('Heading During Landing'),
               land_lat=KPV('Latitude At Touchdown'),
               land_lon=KPV('Longitude At Touchdown'),
               precision=A('Precise Positioning'),
               approaches=S('Approach And Landing'),
               ils_freq_on_app=KPV('ILS Frequency During Approach')):
        '''
        '''
        fallback = False
        precise = bool(getattr(precision, 'value', False))

        try:
            airport = land_fdr_apt.value  # FIXME
        except AttributeError:
            self.warning('Invalid airport... Fallback to AFR Landing Runway.')
            fallback = True
        else:
            if airport is None:
                fallback = True

        try:
            heading = land_hdg.get_last().value
            if heading is None:
                raise ValueError
        except (AttributeError, ValueError):
            self.warning('Invalid heading... Fallback to AFR Landing Runway.')
            fallback = True

        try:
            landing = approaches.get_last()
            if landing is None:
                raise ValueError
        except (AttributeError, ValueError):
            self.warning('No approaches... Fallback to AFR Landing Runway.')
            # Don't set fallback - can still attempt to use heading only...

        # 1. If we have airport and heading, look for the nearest runway:
        if not fallback:
            kwargs = {}

            # The last approach is assumed to be the landing.
            # XXX: Last approach may not be landing for partial data?!
            if ils_freq_on_app:
                if landing.start_edge:
                    ils_app_slice = slice(landing.start_edge, landing.slice.stop)
                else:
                    ils_app_slice = landing.slice
                ils_freq = ils_freq_on_app.get_last(within_slice=ils_app_slice)
                if ils_freq:
                    kwargs.update(ilsfreq=ils_freq.value)

            '''
            Original comment:
            # We only provide coordinates when looking up a landing runway if
            # the recording of latitude and longitude on the aircraft is
            # precise. Inertial recordings are too inaccurate to pinpoint the
            # correct runway and we use ILS frequencies if possible to get a
            # more exact match.

            Revised comment:
            In the absence of an ILS frequency, the recorded latitude and longitude
            is better than nothing and reduces the chance of identifying the wrong runway.
            '''
            if (precise or not ils_freq_on_app) and landing and land_lat and land_lon:
                lat = land_lat.get_last(within_slice=landing.slice)
                lon = land_lon.get_last(within_slice=landing.slice)
                if lat and lon:
                    kwargs.update(
                        latitude=lat.value,
                        longitude=lon.value,
                    )
                else:
                    self.warning('No coordinates for landing runway lookup.')
            else:
                kwargs.update(hint='landing')

            runway = nearest_runway(airport, heading, **kwargs)
            if not runway:
                msg = 'No runway found for airport #%d @ %03.1f deg with %s.'
                self.warning(msg, airport['id'], heading, kwargs)
                # No runway was found, so fall through and try AFR.
                if 'lis_freq' in kwargs:
                    # This is a trap for airports where the ILS data is not
                    # available, but the aircraft approached with the ILS
                    # tuned. A good prompt for an omission in the database.
                    self.warning('Fix database? No runway but ILS was tuned.')
            else:
                self.info('Detected landing runway: %s for airport #%d @ %03.1f deg with %s', runway['identifier'], airport['id'], heading, kwargs)
                self.set_flight_attr(runway)
                return  # We found a runway, so finish here.

        # 2. If we have a runway provided in achieved flight record, use it:
        if land_afr_rwy:
            runway = land_afr_rwy.value
            self.debug('Using landing runway from AFR: %s', runway)
            self.set_flight_attr(runway)
            return  # We found a runway in the AFR, so finish here.

        # 3. After all that, we still couldn't determine a runway...
        self.error('Unable to determine runway at landing!')
        self.set_flight_attr(None)
Example #12
0
    def derive(self,
               toff_fdr_apt=A('FDR Takeoff Airport'),
               toff_afr_rwy=A('AFR Takeoff Runway'),
               toff_hdg=KPV('Heading During Takeoff'),
               toff_lat=KPV('Latitude At Liftoff'),
               toff_lon=KPV('Longitude At Liftoff'),
               precision=A('Precise Positioning')):
        '''
        '''
        fallback = False
        precise = bool(getattr(precision, 'value', False))

        try:
            airport = toff_fdr_apt.value  # FIXME
        except AttributeError:
            self.warning('Invalid airport... Fallback to AFR Takeoff Runway.')
            fallback = True
        else:
            if airport is None:
                fallback = True

        try:
            heading = toff_hdg.get_first().value
            if heading is None:
                raise ValueError
        except (AttributeError, ValueError):
            self.warning('Invalid heading... Fallback to AFR Takeoff Runway.')
            fallback = True

        # 1. If we have airport and heading, look for the nearest runway:
        if not fallback:
            kwargs = {}

            # Even if we do not have precise latitude and longitude
            # information, we still use this for the takeoff runway detection
            # as it is often accurate at the start of a flight, and in the
            # absence of an ILS tuned frequency we have no better option. (We
            # did consider using the last direction of turn onto the runway,
            # but this would require an airport database with terminal and
            # taxiway details that was not felt justified).
            if toff_lat and toff_lon:
                lat = toff_lat.get_first()
                lon = toff_lon.get_first()
                if lat and lon:
                    kwargs.update(
                        latitude=lat.value,
                        longitude=lon.value,
                    )
                else:
                    self.warning('No coordinates for takeoff runway lookup.')
            if not precise:
                kwargs.update(hint='takeoff')

            runway = nearest_runway(airport, heading, **kwargs)
            if not runway:
                msg = 'No runway found for airport #%d @ %03.1f deg with %s.'
                self.warning(msg, airport['id'], heading, kwargs)
                # No runway was found, so fall through and try AFR.
            else:
                self.info('Detected takeoff runway: %s for airport #%d @ %03.1f deg with %s', runway['identifier'], airport['id'], heading, kwargs)
                self.set_flight_attr(runway)
                return  # We found a runway, so finish here.

        # 2. If we have a runway provided in achieved flight record, use it:
        if toff_afr_rwy:
            runway = toff_afr_rwy.value
            self.debug('Using takeoff runway from AFR: %s', runway)
            self.set_flight_attr(runway)
            return  # We found a runway in the AFR, so finish here.

        # 3. After all that, we still couldn't determine a runway...
        self.error('Unable to determine runway at takeoff!')
        self.set_flight_attr(None)