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}