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.net.URI;
032import java.net.URISyntaxException;
033import java.text.DateFormat;
034import java.text.SimpleDateFormat;
035import java.util.Date;
036import java.util.HashSet;
037import java.util.Set;
038
039import org.geotools.feature.DefaultFeatureCollection;
040import org.kepler.gis.data.VectorToken;
041
042/**
043 * Utility functions and classes for interfacing with the Pylaski REST API
044 *
045 * @see https://github.com/words-sdsc/wifire/blob/master/pylaski.ipynb
046 * 
047 * @author Ben Fleming
048 * @version $Id: PylaskiUtilities.java 34553 2017-03-27 21:55:16Z crawl $
049 */
050public class PylaskiUtilities {
051    /** This class cannot be instantiated. */
052    private PylaskiUtilities() {
053    }
054
055    /**
056     * Maps the selection enums (see above) to their string counterparts. Used
057     * when building URI's.
058     *
059     * @param selection
060     *            The selection enum
061     * @return The string counterpart
062     */
063    public static String getSelectionString(Selection selection) {
064        switch (selection) {
065        case CLOSEST_TO:
066            return "closestTo";
067        case BOUNDING_BOX:
068            return "boundingBox";
069        case WITHIN_RADIUS:
070            return "withinRadius";
071        }
072
073        return null;
074    }
075
076    /**
077     * Merges two vector tokens together into one. Feature collections inside
078     * vectors are weaved together. If one vector token is null, the other will
079     * simply be returned.
080     *
081     * TODO: This probably belongs in the VectorUtilities class
082     *
083     * @param tokenA
084     *            The first vector token
085     * @param tokenB
086     *            The second vector token
087     * @return VectorToken as a merge between tokenA and tokenB
088     * @throws Exception
089     *             Raised if both tokens are null
090     */
091    public static VectorToken mergeVectors(VectorToken tokenA, VectorToken tokenB) throws Exception {
092        if (tokenA == null && tokenB == null) {
093            throw new Exception("Both vector tokens cannot be null.");
094        }
095
096        if (tokenA == null) {
097            return tokenB;
098        }
099
100        if (tokenB == null) {
101            return tokenA;
102        }
103
104        DefaultFeatureCollection features = new DefaultFeatureCollection();
105
106        features.addAll(tokenA.getVectors());
107        features.addAll(tokenB.getVectors());
108
109        return new VectorToken(features);
110    }
111
112    /** The host URL of the REST API - all requests are made to this URL */
113    public static final String DEFAULT_HOST = "https://firemap.sdsc.edu:5443";
114
115    /** The format of the date/time values provided by the Pylaski API */
116    public static final String DATE_FORMAT = "yyyy-MM-dd'T'HH:mm:ssZ";
117
118    /**
119     * Station selection types
120     */
121    public enum Selection {
122        CLOSEST_TO, BOUNDING_BOX, WITHIN_RADIUS,
123    }
124
125    /**
126     * Configure settings for sending REST API requests.
127     */
128    public static class Settings {
129        // Properties
130
131        // The host URL of the REST API - all requests are made to this URL
132        public String host = DEFAULT_HOST;
133
134        // The station selection type
135        private Selection _selection = Selection.CLOSEST_TO;
136
137        // Search coordinates
138        private double _minLat = 0.0;
139        private double _minLon = 0.0;
140        private double _maxLat = 0.0;
141        private double _maxLon = 0.0;
142        private double _radius = 0.0;
143
144        // Search timestamps
145        private Date _from;
146        private Date _to;
147
148        // Observables to request
149        private Set<String> _observables;
150
151        // Public methods
152
153        /**
154         * Constructor
155         */
156        public Settings() {
157            _observables = new HashSet<>();
158        }
159
160        /**
161         * The selection type (inferred from the location settings).
162         *
163         * @return Selection enum
164         */
165        public Selection getSelectionType() {
166            return _selection;
167        }
168
169        /**
170         * Sets the location to a single coordinate. This creates a "closest to"
171         * selection search.
172         *
173         * @param lat
174         *            Latitude
175         * @param lon
176         *            Longitude
177         */
178        public void setLocation(double lat, double lon) {
179            _selection = Selection.CLOSEST_TO;
180
181            _minLat = lat;
182            _minLon = lon;
183        }
184
185        /**
186         * Sets the location to within a radius around a single coordinate. This
187         * creates a "within radius" selection search.
188         *
189         * @param lat
190         *            Latitude
191         * @param lon
192         *            Longitude
193         * @param radius
194         *            Radius (in meters)
195         */
196        public void setLocation(double lat, double lon, double radius) {
197            _selection = Selection.WITHIN_RADIUS;
198
199            _minLat = lat;
200            _minLon = lon;
201            _radius = radius;
202        }
203
204        /**
205         * Sets the location to within a rectangle. This creates a "bounding
206         * box" selection search.
207         *
208         * @param minLat
209         *            Minimum latitude
210         * @param minLon
211         *            Minimum longitude
212         * @param maxLat
213         *            Maximum latitude
214         * @param maxLon
215         *            Maximum longitude
216         */
217        public void setLocation(double minLat, double minLon, double maxLat, double maxLon) {
218            _selection = Selection.BOUNDING_BOX;
219
220            _minLat = minLat;
221            _minLon = minLon;
222            _maxLat = maxLat;
223            _maxLon = maxLon;
224        }
225
226        /**
227         * Sets the date range for data search. If the "to" date exceeds the
228         * current date, a forecast URI will be possible to generate (see
229         * below). If both "from" and "to" dates excee the current date, a data
230         * URI will not be possible to generate.
231         *
232         * @param from
233         *            A date object to search from
234         * @param to
235         *            A date object to search to - must be after the "from" date
236         * @throws Exception
237         *             Raised if "to" date is at or before the "from" date
238         */
239        public void setDate(Date from, Date to) throws Exception {
240            if (!from.before(to)) {
241                throw new Exception("From date must be before the to date.");
242            }
243
244            _from = from;
245            _to = to;
246        }
247
248        /**
249         * Sets the date around a central time.
250         *
251         * @param at
252         *            A date object to search around
253         * @param buffer
254         *            A buffer to pad the date with (in milliseconds)
255         * @throws Exception
256         *             An exception that should never be raised when using this
257         *             particular method
258         */
259        public void setDate(Date at, int buffer) throws Exception {
260            Date from = new Date(at.getTime() - buffer);
261            Date to = new Date(at.getTime() + buffer);
262
263            setDate(from, to);
264        }
265
266        /**
267         * Sets the date around a central time. Uses a default buffer of +/- 2
268         * hours.
269         *
270         * @param at
271         *            A date object to search around
272         * @throws Exception
273         *             An exception that should never be raised when using this
274         *             particular method
275         */
276        public void setDate(Date at) throws Exception {
277            // 2 hour buffer
278            setDate(at, 2 * 60 * 60 * 1000);
279        }
280
281        /**
282         * Removes the date range filter. This will result in the REST API
283         * returning the latest readings.
284         */
285        public void removeDate() {
286            _from = null;
287            _to = null;
288        }
289
290        /**
291         * Adds an observable to be requested from the API.
292         *
293         * @param observable
294         *            An observable as a string - refer to the Pylaski
295         *            documentation (link above)
296         */
297        public void addObservable(String observable) {
298            _observables.add(observable);
299        }
300
301        /**
302         * Removes a previously added observable.
303         *
304         * @param observable
305         *            An observable as a string
306         */
307        public void removeObservable(String observable) {
308            _observables.remove(observable);
309        }
310
311        /**
312         * Sets the list of observables.
313         *
314         * @param observables
315         *            A list of observables - refer to the Pylaski documentation
316         *            (link above)
317         */
318        public void setObservables(String[] observables) {
319            _observables.clear();
320
321            for (String observable : observables) {
322                _observables.add(observable);
323            }
324        }
325
326        /**
327         * Generates the REST URI for retrieving past and present observable
328         * data.
329         *
330         * @return A request URI
331         * @throws URISyntaxException
332         *             If the URI is invalid
333         */
334        public URI getDataURI() throws URISyntaxException {
335            Date now = new Date();
336            StringBuilder p = new StringBuilder(_getBaseParams());
337
338            if (_from == null && _to == null) {
339                return new URI(host + "/stations/data/latest?" + p.toString());
340            }
341
342            if (_from == null || _from.before(now)) {
343                DateFormat df = new SimpleDateFormat(DATE_FORMAT);
344
345                if (_from != null) {
346                    p.append("from=").append(df.format(_from)).append("&");
347                }
348
349                if (_to != null) {
350                    // Cap the "to" date at the latest date
351                    Date to = _to.after(now) ? now : _to;
352                    p.append("to=").append(df.format(to)).append("&");
353                }
354
355                return new URI(host + "/stations/data?" + p.toString());
356            }
357
358            return null;
359        }
360
361        /**
362         * Generates the REST URI for retrieving future/forecast observable
363         * data.
364         *
365         * @param hrrrx
366         *            Whether to use the HRRRx forecast data
367         * @return A request URI
368         * @throws URISyntaxException
369         *             If the URI is invalid
370         */
371        public URI getForecastURI(boolean hrrrx) throws URISyntaxException {
372            Date now = new Date();
373            StringBuilder p = new StringBuilder(_getBaseParams());
374
375            p.append("hrrrx=").append(hrrrx ? "true" : "false").append("&");
376
377            // Dates don't seem to do anything using the REST service, but put
378            // them in for good measure
379            if (_to != null && _to.after(now)) {
380                DateFormat df = new SimpleDateFormat(DATE_FORMAT);
381
382                if (_from != null) {
383                    // Cap the "from" date at the latest date
384                    Date from = _from.before(now) ? now : _from;
385                    p.append("from=").append(df.format(from)).append("&");
386                }
387
388                p.append("to=").append(df.format(_to)).append("&");
389
390                return new URI(host + "/forecast?" + p.toString());
391            }
392
393            return null;
394        }
395
396        /**
397         * Generates the REST URI for retrieving future/forecast observable
398         * data. Defaults the HRRRx setting to false.
399         *
400         * @return A request URI
401         * @throws URISyntaxException
402         *             If the URI is invalid
403         */
404        public URI getForecastURI() throws URISyntaxException {
405            return getForecastURI(false);
406        }
407
408        // Private methods
409
410        /**
411         * Generates shared URI parameters for past/present and forecast URI's
412         *
413         * @return A string of URI parameters
414         */
415        private String _getBaseParams() {
416            StringBuilder p = new StringBuilder();
417
418            p.append("selection=").append(getSelectionString(_selection)).append("&");
419
420            if (_selection == Selection.BOUNDING_BOX) {
421                p.append("minLat=").append(_minLat).append("&");
422                p.append("minLon=").append(_minLon).append("&");
423                p.append("maxLat=").append(_maxLat).append("&");
424                p.append("maxLon=").append(_maxLon).append("&");
425            } else {
426                p.append("lat=").append(_minLat).append("&");
427                p.append("lon=").append(_minLon).append("&");
428            }
429
430            if (_selection == Selection.WITHIN_RADIUS) {
431                p.append("radius=").append(_radius).append("&");
432            }
433
434            for (String observable : _observables) {
435                p.append("observable=").append(observable).append("&");
436            }
437
438            return p.toString();
439        }
440    }
441}