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()