001/*
002 * Copyright (c) 2017 The Regents of the University of California.
003 * All rights reserved.
004 *
005 * '$Author: crawl $'
006 * '$Date: 2017-03-27 21:55:16 +0000 (Mon, 27 Mar 2017) $' 
007 * '$Revision: 34553 $'
008 * 
009 * Permission is hereby granted, without written agreement and without
010 * license or royalty fees, to use, copy, modify, and distribute this
011 * software and its documentation for any purpose, provided that the above
012 * copyright notice and the following two paragraphs appear in all copies
013 * of this software.
014 *
015 * IN NO EVENT SHALL THE UNIVERSITY OF CALIFORNIA BE LIABLE TO ANY PARTY
016 * FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES
017 * ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN IF
018 * THE UNIVERSITY OF CALIFORNIA HAS BEEN ADVISED OF THE POSSIBILITY OF
019 * SUCH DAMAGE.
020 *
021 * THE UNIVERSITY OF CALIFORNIA SPECIFICALLY DISCLAIMS ANY WARRANTIES,
022 * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
023 * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE
024 * PROVIDED HEREUNDER IS ON AN "AS IS" BASIS, AND THE UNIVERSITY OF
025 * CALIFORNIA HAS NO OBLIGATION TO PROVIDE MAINTENANCE, SUPPORT, UPDATES,
026 * ENHANCEMENTS, OR MODIFICATIONS.
027 *
028 */
029package org.kepler.gis.actor.pylaski;
030
031import java.io.IOException;
032import java.net.HttpURLConnection;
033import java.net.URI;
034import java.net.URISyntaxException;
035import java.util.Date;
036
037import org.apache.commons.io.IOUtils;
038import org.apache.http.HttpResponse;
039import org.apache.http.client.methods.HttpGet;
040import org.apache.http.impl.client.DefaultHttpClient;
041import org.apache.http.params.BasicHttpParams;
042import org.apache.http.params.HttpConnectionParams;
043import org.apache.http.params.HttpParams;
044import org.geotools.data.simple.SimpleFeatureCollection;
045import org.geotools.referencing.crs.DefaultGeographicCRS;
046import org.joda.time.DateTime;
047import org.joda.time.DateTimeZone;
048import org.json.JSONException;
049import org.kepler.gis.data.VectorToken;
050import org.kepler.gis.util.GISUtilities;
051import org.kepler.gis.util.VectorUtilities;
052import org.opengis.feature.simple.SimpleFeature;
053
054import com.vividsolutions.jts.geom.MultiPoint;
055import com.vividsolutions.jts.geom.Point;
056
057import ptolemy.actor.TypedAtomicActor;
058import ptolemy.actor.TypedIOPort;
059import ptolemy.actor.parameters.PortParameter;
060import ptolemy.data.BooleanToken;
061import ptolemy.data.DateToken;
062import ptolemy.data.DoubleToken;
063import ptolemy.data.IntToken;
064import ptolemy.data.StringToken;
065import ptolemy.data.Token;
066import ptolemy.data.expr.Parameter;
067import ptolemy.data.expr.StringParameter;
068import ptolemy.data.type.BaseType;
069import ptolemy.kernel.CompositeEntity;
070import ptolemy.kernel.util.Attribute;
071import ptolemy.kernel.util.IllegalActionException;
072import ptolemy.kernel.util.NameDuplicationException;
073import ptolemy.kernel.util.Settable;
074
075/** Query Pylaski REST service for weather data.
076 * 
077 * If neither startTime, stopTime specified: get latest measurement.
078 * If only startTime or stopTime specified: get measurement at that time.
079 * If both startTime, stopTime specified: get measurements between those times.
080 * 
081 * @author Ben Fleming, Daniel Crawl
082 * @version $Id: GetPylaskiMeasurements.java 34553 2017-03-27 21:55:16Z crawl $
083 * 
084 */
085public class GetPylaskiMeasurements extends TypedAtomicActor
086{
087        // Properties
088
089        public TypedIOPort location;
090        public TypedIOPort startTime;
091        public TypedIOPort stopTime;
092        public TypedIOPort output;
093
094        public StringParameter locationType;
095        public Parameter radius;
096        public Parameter timeBuffer;
097        public PortParameter observables;
098        public Parameter hrrrx;
099        public StringParameter pylaskiUrl;
100
101        private String _locationTypeStr;
102
103
104        // Public methods
105
106        /**
107         *
108         * @param container
109         * @param name
110         * @throws IllegalActionException
111         * @throws NameDuplicationException
112         */
113        public GetPylaskiMeasurements(CompositeEntity container, String name)
114        throws IllegalActionException, NameDuplicationException
115        {
116                super(container, name);
117                
118                location = new TypedIOPort(this, "location", true, false);
119                location.setTypeEquals(VectorToken.VECTOR);
120                new Attribute(location, "_showName");
121
122                locationType = new StringParameter(this, "locationType");
123                locationType.setExpression("nearest");
124                locationType.addChoice("nearest");
125                locationType.addChoice("boundingBox");
126                locationType.addChoice("withinRadius");
127
128                radius = new Parameter(this, "radius");
129                radius.setTypeEquals(BaseType.DOUBLE);
130
131                timeBuffer = new Parameter(this, "timeBuffer");
132                timeBuffer.setTypeEquals(BaseType.INT);
133
134                startTime = new TypedIOPort(this, "startTime", true, false);
135                startTime.setTypeEquals(BaseType.DATE);
136                new Attribute(startTime, "_showName");
137
138                stopTime = new TypedIOPort(this, "stopTime", true, false);
139                stopTime.setTypeEquals(BaseType.DATE);
140                new Attribute(stopTime, "_showName");
141
142                observables = new PortParameter(this, "observables");
143                observables.setStringMode(true);
144                observables.setTypeEquals(BaseType.STRING);
145                observables.getPort().setTypeEquals(BaseType.STRING);
146                new Attribute(observables.getPort(), "_showName");
147
148                hrrrx = new Parameter(this, "hrrrx");
149                hrrrx.setTypeEquals(BaseType.BOOLEAN);
150                hrrrx.setToken(BooleanToken.TRUE);
151                
152                pylaskiUrl = new StringParameter(this, "pylaskiUrl");
153                pylaskiUrl.setVisibility(Settable.EXPERT);
154                pylaskiUrl.setExpression("https://firemap.sdsc.edu:5443");
155                
156                output = new TypedIOPort(this, "output", false, true);
157                output.setTypeEquals(VectorToken.VECTOR);
158                output.setMultiport(true);
159        }
160
161        /**
162         *
163         * @param attribute The attribute that changed.
164         * @throws IllegalActionException
165         */
166        @Override
167        public void attributeChanged(Attribute attribute)
168        throws IllegalActionException
169        {
170                if(attribute == locationType)
171                {
172                        Token token = locationType.getToken();
173
174                        if(token != null)
175                        {
176                                String value = ((StringToken) token).stringValue();
177
178                                if(value != null && !value.trim().isEmpty())
179                                {
180
181                                        if(!value.matches("nearest|boundingBox|withinRadius"))
182                                        {
183                                                throw new IllegalActionException(this, "Unsupported locationType: " + value);
184                                        }
185
186                                        _locationTypeStr = value;
187                                }
188                        }
189                }
190                else
191                {
192                        super.attributeChanged(attribute);
193                }
194        }
195        
196        /*
197        @Override
198        public Object clone(Workspace workspace) throws CloneNotSupportedException {
199                GetPylaskiMeasurements newObject = (GetPylaskiMeasurements) super.clone(workspace);
200                newObject._locationTypeStr = null;
201                return newObject;
202        }
203        */
204
205        /**
206         *
207         * @throws IllegalActionException
208         */
209        @Override
210        public void fire()
211        throws IllegalActionException
212        {
213                super.fire();
214                
215                PylaskiUtilities.Settings settings = new PylaskiUtilities.Settings();
216
217
218                // Apply location setting
219                
220                Token locationToken = location.get(0);
221                SimpleFeatureCollection features = ((VectorToken) locationToken).getVectors();
222
223                if(features.isEmpty())
224                {
225                        throw new IllegalActionException(this, "Must specify location.");
226                }
227
228                if(features.size() > 1)
229                {
230                        System.err.println("WARNING: more than one feature in location; using first one.");
231                }
232
233                // Re-project if not wgs84
234                if(!GISUtilities.isCRSWGS84(features.getSchema().getCoordinateReferenceSystem()))
235                {
236                        features = VectorUtilities.reproject(features, DefaultGeographicCRS.WGS84);
237                }
238
239                SimpleFeature feature = features.features().next();
240
241                switch(_locationTypeStr)
242                {
243                        case "nearest":
244                        {
245                                double[] point = _getPointFromFeature(feature);
246
247                                settings.setLocation(point[0], point[1]);
248                        }
249                        break;
250                        case "boundingBox":
251                        {
252                                double[] box = _getBoxFromFeature(feature);
253
254                                settings.setLocation(box[0], box[1], box[2], box[3]);
255                        }
256                        break;
257                        case "withinRadius":
258                        {
259                                double[] point = _getPointFromFeature(feature);
260
261                                DoubleToken radiusToken = (DoubleToken) radius.getToken();
262
263                                if(radiusToken == null)
264                                {
265                                        throw new IllegalActionException(this, "Radius must be defined.");
266                                }
267
268                                double radiusValue = radiusToken.doubleValue();
269
270                                settings.setLocation(point[0], point[1], radiusValue);
271                        }
272                        break;
273                        default:
274                        {
275                                throw new IllegalActionException(this, "Unsupported type of location.");
276                        }
277                }
278
279
280                // Apply observables setting
281                
282                observables.update();
283                StringToken observablesToken = (StringToken) observables.getToken();
284                String observablesValue = (observablesToken == null ? "" : observablesToken.stringValue().trim());
285
286                if(observablesValue.isEmpty())
287                {
288                        throw new IllegalActionException(this, "Must specify observables.");
289                }
290
291                String[] observablesArray = observablesValue.split("\\s*,\\s*");
292                settings.setObservables(observablesArray);
293
294
295                // Apply date settings
296
297                Date fromDate = null;
298                Date toDate = null;
299
300                if(startTime.numberOfSources() > 0)
301                {
302                        DateToken dateToken = (DateToken) startTime.get(0);
303
304                        // Convert to local timezone
305                        DateTime dt = new DateTime(dateToken.getValue(), DateTimeZone.forTimeZone(dateToken.getTimeZone()));
306
307                        fromDate = dt.toLocalDateTime().toDate();
308                }
309
310                if(stopTime.numberOfSources() > 0)
311                {
312                        DateToken dateToken = (DateToken) stopTime.get(0);
313
314                        // Convert to local timezone
315                        DateTime dt = new DateTime(dateToken.getValue(), DateTimeZone.forTimeZone(dateToken.getTimeZone()));
316
317                        toDate = dt.toLocalDateTime().toDate();
318                }
319
320                try
321                {
322                        if(fromDate != null && toDate != null)
323                        {
324                                settings.setDate(fromDate, toDate);
325                        }
326                        else
327                        {
328                                Date atDate = (fromDate == null ? toDate : fromDate);
329
330                                if(atDate != null)
331                                {
332                                        IntToken timeBufferToken = (IntToken) timeBuffer.getToken();
333
334                                        if(timeBufferToken == null)
335                                        {
336                                                throw new IllegalActionException(this, "Time buffer must be defined.");
337                                        }
338
339                                        int timeBufferValue = timeBufferToken.intValue();
340
341                                        settings.setDate(atDate, timeBufferValue * 60 * 60 * 1000);
342                                }
343                        }
344                }
345                catch(Exception e)
346                {
347                        throw new IllegalActionException(this, e, "Invalid date range.");
348                }
349
350
351                // Apply URL setting
352
353                StringToken urlToken = (StringToken) pylaskiUrl.getToken();
354                String urlValue = (urlToken == null ? "" : urlToken.stringValue().trim());
355
356                if(!urlValue.isEmpty())
357                {
358                        settings.host = urlValue;
359                }
360
361
362                // Request data from Pylaski REST service
363
364                URI dataUri;
365                URI forecastUri;
366
367                BooleanToken hrrrxToken = (BooleanToken) hrrrx.getToken();
368                boolean hrrrxValue = hrrrxToken.booleanValue();
369
370                try
371                {
372                        dataUri = settings.getDataURI();
373                        forecastUri = settings.getForecastURI(hrrrxValue);
374
375                        if(dataUri != null)
376                        {
377                                _debug(dataUri.toString());
378                        }
379
380                        if(forecastUri != null)
381                        {
382                            _debug(forecastUri.toString());
383                        }
384                }
385                catch(URISyntaxException e)
386                {
387                        throw new IllegalActionException(this, e, "Bad Pylaski URI");
388                }
389
390                VectorToken dataToken = (dataUri == null ? null : _requestFromPylaski(dataUri));
391                VectorToken forecastToken = (forecastUri == null ? null : _requestFromPylaski(forecastUri));
392                VectorToken resultsToken;
393
394                // Merge the past/present data with the forecast data
395                try
396                {
397                        resultsToken = PylaskiUtilities.mergeVectors(dataToken, forecastToken);
398                        // System.out.println(VectorUtilities.toGeoJSONString(resultsToken.getVectors()));
399                }
400                catch(Exception e)
401                {
402                        throw new IllegalActionException(this, e, "Couldn't read data from Pylaski.");
403                }
404
405                // Send processed result to output
406                output.broadcast(resultsToken);
407        }
408
409        /**
410         *
411         * @throws IllegalActionException
412         */
413        @Override
414        public void preinitialize()
415        throws IllegalActionException
416        {
417                super.preinitialize();
418
419                if(_locationTypeStr == null || _locationTypeStr.trim().isEmpty())
420                {
421                        throw new IllegalActionException(this, "Must specify locationType.");
422                }
423        }
424
425        /**
426         *
427         * @param restUri
428         * @return
429         * @throws IllegalActionException
430         */
431        private VectorToken _requestFromPylaski(URI restUri)
432        throws IllegalActionException
433        {
434                String resultStr = _readUriData(restUri);
435                VectorToken resultsToken;
436
437                try
438                {
439                        resultsToken = VectorUtilities.readGeoJSONString(resultStr);
440                }
441                catch(IOException | JSONException e)
442                {
443                        throw new IllegalActionException(this, e, "Error converting results to vector token.");
444                }
445
446                return resultsToken;
447        }
448
449        /**
450         *
451         * @param uri
452         * @return
453         * @throws IllegalActionException
454         */
455        private String _readUriData(URI uri)
456        throws IllegalActionException
457        {
458                String resultStr;
459                DefaultHttpClient client = null;
460
461                try
462                {
463                        HttpParams params = new BasicHttpParams();
464                        HttpConnectionParams.setConnectionTimeout(params, 5000);
465                        HttpConnectionParams.setSoTimeout(params, 5000);
466                        client = new DefaultHttpClient(params);
467                        HttpGet get = new HttpGet(uri);
468                        HttpResponse response;
469
470                        try
471                        {
472                                response = client.execute(get);
473                        }
474                        catch(IOException e)
475                        {
476                                throw new IllegalActionException(this, e, "Error requesting measurements.");
477                        }
478
479                        if(response.getStatusLine().getStatusCode() != HttpURLConnection.HTTP_OK)
480                        {
481                                throw new IllegalActionException(this, "Request failed: " + response.getStatusLine().getReasonPhrase());
482                        }
483
484                        try
485                        {
486                                resultStr = IOUtils.toString(response.getEntity().getContent());
487                        }
488                        catch(IllegalStateException | IOException e)
489                        {
490                                throw new IllegalActionException(this, e, "Error reading results.");
491                        }
492                }
493                finally
494                {
495                        if(client != null)
496                        {
497                                client.getConnectionManager().shutdown();
498                        }
499                }
500
501                return resultStr;
502        }
503
504        /**
505         *
506         * @param feature
507         * @return
508         * @throws IllegalActionException
509         */
510        private double[] _getPointFromFeature(SimpleFeature feature)
511        throws IllegalActionException
512        {
513                Object geometry = feature.getDefaultGeometry();
514
515                if(!(geometry instanceof Point))
516                {
517                        throw new IllegalActionException("Feature is not a Point: " + geometry.getClass());
518                }
519
520                Point point = (Point) geometry;
521
522                return new double[] {
523                        point.getX(),
524                        point.getY(),
525                };
526        }
527
528        /**
529         *
530         * @param feature
531         * @return
532         * @throws IllegalActionException
533         */
534        private double[] _getBoxFromFeature(SimpleFeature feature)
535        throws IllegalActionException
536        {
537                Object geometry = feature.getDefaultGeometry();
538
539                if(!(geometry instanceof MultiPoint))
540                {
541                        throw new IllegalActionException("Feature is not a MultiPoint: " + geometry.getClass());
542                }
543
544                MultiPoint multiPoint = (MultiPoint) geometry;
545                int numPoints = multiPoint.getNumPoints();
546
547                if(numPoints < 2)
548                {
549                        throw new IllegalActionException("Feature does not have enough (two) points.");
550                }
551
552                if(numPoints > 2)
553                {
554                        System.err.println("WARNING: more than two points in geometry; using first two.");
555                }
556
557                Point minPoint = (Point) multiPoint.getGeometryN(0);
558                Point maxPoint = (Point) multiPoint.getGeometryN(1);
559
560                return new double[] {
561                        minPoint.getX(),
562                        minPoint.getY(),
563                        maxPoint.getX(),
564                        maxPoint.getY(),
565                };
566        }
567}