class PredictionEventPromise(PredictionPromise):
    """ Promise to obtain event-style predictions for a given station and local date.
    """

    # initialize this promise for a given manager, station and date.
    def __init__(self, manager, stationFeature, date):
        super(PredictionEventPromise, self).__init__()
        self.manager = manager
        self.stationFeature = stationFeature
        self.predictions = None

        # convert local station timezone QDate to a full UTC QDateTime.
        self.localDate = date
        self.datetime = QDateTime(date, QTime(0, 0),
                                  stationTimeZone(stationFeature)).toUTC()

    """ Get all the data needed to resolve this promise
    """

    def doStart(self):
        self.eventRequest = CurrentPredictionRequest(
            self.manager, self.stationFeature, self.datetime,
            self.datetime.addDays(1), CurrentPredictionRequest.EventType)
        self.addDependency(self.eventRequest)

    def doProcessing(self):
        self.predictions = self.eventRequest.predictions
 def test_mocked_request(self):
     self.assertEqual(len(PredictionManagerTest.request_urls), 0)
     resolver = Mock()
     datetime = QDateTime(2020, 1, 1, 5, 0)
     cpr = CurrentPredictionRequest(self.pm, self.subStation, datetime,
                                    datetime.addDays(1),
                                    CurrentPredictionRequest.EventType)
     cpr.resolved(resolver)
     resolver.assert_not_called()
     cpr.start()
     resolver.assert_called_once()
     self.assertEqual(len(cpr.predictions), 9)
     self.assertEqual(len(PredictionManagerTest.request_urls), 1)
     self.assertEqual(
         PredictionManagerTest.request_urls[0],
         'https://api.tidesandcurrents.noaa.gov/api/prod/datagetter?application=qgis-noaa-tidal-predictions&begin_date=20200101 05:00&end_date=20200102 04:59&units=english&time_zone=gmt&product=currents_predictions&format=xml&station=ACT0926&bin=1&interval=MAX_SLACK'
     )
class PredictionDataPromise(PredictionPromise):
    """ Promise to obtain a full set of predictions (events and timeline) for a given station and local date.
    """

    # initialize this promise for a given manager, station and date.
    def __init__(self, manager, stationFeature, date):
        super(PredictionDataPromise, self).__init__()
        self.manager = manager
        self.stationFeature = stationFeature
        self.predictions = None

        # convert local station timezone QDate to a full UTC QDateTime.
        self.localDate = date
        self.datetime = QDateTime(date, QTime(0, 0),
                                  stationTimeZone(stationFeature)).toUTC()

    """ Get all the data needed to resolve this promise
    """

    def doStart(self):
        # first see if we can pull data from the predictions layer
        startTime = self.datetime
        endTime = self.datetime.addDays(1)

        featureRequest = QgsFeatureRequest()
        stationPt = QgsPointXY(self.stationFeature.geometry().vertexAt(0))
        searchRect = QgsRectangle(stationPt, stationPt)
        searchRect.grow(
            0.01 / 60
        )  # in the neighborhood of .01 nm as 1/60 = 1 arc minute in this proj.
        featureRequest.setFilterRect(searchRect)

        # Create a time based query
        ctx = featureRequest.expressionContext()
        scope = QgsExpressionContextScope()
        scope.setVariable('startTime', startTime)
        scope.setVariable('endTime', endTime)
        scope.setVariable('station', self.stationFeature['station'])
        ctx.appendScope(scope)
        featureRequest.setFilterExpression(
            "station = @station and time >= @startTime and time < @endTime")
        featureRequest.addOrderBy('time')

        savedFeatureIterator = self.manager.predictionsLayer.getFeatures(
            featureRequest)
        savedFeatures = list(savedFeatureIterator)
        if len(savedFeatures) > 0:
            # We have some features, so go ahead and stash them in the layer and resolve this promise
            print('{}: retrieved {} features from layer'.format(
                self.stationFeature['station'], len(savedFeatures)))
            self.predictions = savedFeatures
            self.resolve()
        else:
            # The layer didn't have what we wanted, so we must request the data we need.
            # At this point, the situation falls into several possible cases.

            # Case 1: A Harmonic station with known flood/ebb directions. Here
            # we need two requests which can simply be combined and sorted:
            #   1a: EventType, i.e. slack, flood and ebb
            #   1b: SpeedDirType, as velocity can be calculated by projecting along flood/ebb
            #
            # Case 2: A Harmonic station with unknown flood and/or ebb.
            # We actually need to combine 3 requests:
            #   2a: EventType
            #   2b: SpeedDirType, which only provides vector magnitude/angle
            #   2c: VelocityMajorType, which only provides current velocity (but for same times as 2b)

            # Here we set up requests for cases 1 and 2
            if self.stationFeature['type'] == 'H':
                self.speedDirRequest = CurrentPredictionRequest(
                    self.manager, self.stationFeature, startTime, endTime,
                    CurrentPredictionRequest.SpeedDirectionType)
                self.addDependency(self.speedDirRequest)

                self.eventRequest = CurrentPredictionRequest(
                    self.manager, self.stationFeature, startTime, endTime,
                    CurrentPredictionRequest.EventType)
                self.addDependency(self.eventRequest)

                floodDir = self.stationFeature['meanFloodDir']
                ebbDir = self.stationFeature['meanEbbDir']
                if floodDir == NULL or ebbDir == NULL:
                    self.velocityRequest = CurrentPredictionRequest(
                        self.manager, self.stationFeature, startTime, endTime,
                        CurrentPredictionRequest.VelocityMajorType)
                    self.addDependency(self.velocityRequest)
                else:
                    self.velocityRequest = None

            # Case 3: A Subordinate station which only knows its events. Here we need the following:
            #   3a: PredictionEventPromises for this station in a 3-day window surrounding the date of interest
            #   3b: PredictionDataPromises for the reference station in the same 3-day window.
            else:
                self.eventPromises = []
                self.refPromises = []
                refStation = self.manager.getStation(
                    self.stationFeature['refStation'])
                if refStation is None:
                    print("Could not find ref station {} for {}".format(
                        self.stationFeature['refStation'],
                        self.stationFeature['station']))
                else:
                    for dayOffset in [-1, 0, 1]:
                        windowDate = self.localDate.addDays(dayOffset)
                        dataPromise = self.manager.getDataPromise(
                            refStation, windowDate)
                        self.refPromises.append(dataPromise)
                        self.addDependency(dataPromise)
                        eventPromise = self.manager.getEventPromise(
                            self.stationFeature, windowDate)
                        self.eventPromises.append(eventPromise)
                        self.addDependency(eventPromise)

    def doProcessing(self):
        if self.stationFeature['type'] == 'H':
            # We will always have a speed/direction request
            self.predictions = self.speedDirRequest.predictions

            # If we also had a velocity request with the same number of results
            # try to combine it with this one.
            if (self.velocityRequest is not None
                    and (len(self.velocityRequest.predictions) == len(
                        self.predictions))):
                for i, p in enumerate(self.predictions):
                    p['value'] = self.velocityRequest.predictions[i]['value']

            # Now fold in the events and sort everything by time
            self.predictions.extend(self.eventRequest.predictions)
            self.predictions.sort(key=(lambda p: p['time']))
        else:
            # subordinate-station case: we need to cook up two different interpolations based on
            # the 3-day windows of a) subordinate events and b) reference currents.

            print('Interpolating ref station {} for {}'.format(
                self.stationFeature['refStation'],
                self.stationFeature['station']))

            interpolator = PredictionInterpolator(self.stationFeature,
                                                  self.datetime,
                                                  self.eventPromises,
                                                  self.refPromises)
            subTimes = np.linspace(0, 24 * 60 * 60,
                                   24 * 60 // PredictionManager.STEP_MINUTES,
                                   False)
            refValues = interpolator.valuesFor(subTimes)
            ebbDir = self.stationFeature['meanEbbDir']
            floodDir = self.stationFeature['meanFloodDir']

            fields = self.manager.predictionsLayer.fields()
            self.predictions = []
            for i in range(0, len(subTimes)):
                f = QgsFeature(fields)
                f.setGeometry(QgsGeometry(self.stationFeature.geometry()))
                f['station'] = self.stationFeature['station']
                f['depth'] = self.stationFeature['depth']
                f['time'] = self.datetime.addSecs(int(subTimes[i]))
                f['value'] = float(refValues[i])
                f['dir'] = ebbDir if refValues[i] < 0 else floodDir
                f['magnitude'] = abs(f['value'])
                f['type'] = 'current'
                self.predictions.append(f)

            # Now mix in the event data from the central day in the 3-day window and sort everything
            self.predictions.extend(self.eventPromises[1].predictions)
            self.predictions.sort(key=(lambda p: p['time']))

        # add everything into the predictions layer
        self.manager.predictionsLayer.startEditing()
        self.manager.predictionsLayer.addFeatures(self.predictions,
                                                  QgsFeatureSink.FastInsert)
        self.manager.predictionsLayer.commitChanges()
        self.manager.predictionsLayer.triggerRepaint()