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}