001/*
002 * Copyright (c) 2016 The Regents of the University of California.
003 * All rights reserved.
004 *
005 * '$Author: crawl $'
006 * '$Date: 2016-04-21 05:55:25 +0000 (Thu, 21 Apr 2016) $' 
007 * '$Revision: 34480 $'
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.landscape;
030
031import java.io.BufferedReader;
032import java.io.File;
033import java.io.IOException;
034import java.io.InputStream;
035import java.io.InputStreamReader;
036import java.util.ArrayList;
037import java.util.HashSet;
038import java.util.List;
039import java.util.Set;
040
041import org.apache.commons.io.FilenameUtils;
042import org.geotools.geometry.jts.ReferencedEnvelope;
043import org.geotools.referencing.crs.DefaultGeographicCRS;
044import org.kepler.gis.data.RasterToken;
045import org.kepler.gis.util.RasterUtilities;
046import org.kepler.gis.util.RasterUtilities.RasterInfo;
047import org.opengis.referencing.FactoryException;
048import org.opengis.referencing.operation.TransformException;
049
050import ptolemy.actor.TypedAtomicActor;
051import ptolemy.actor.TypedIOPort;
052import ptolemy.actor.parameters.PortParameter;
053import ptolemy.data.BooleanToken;
054import ptolemy.data.StringToken;
055import ptolemy.data.Token;
056import ptolemy.data.expr.Parameter;
057import ptolemy.data.expr.StringParameter;
058import ptolemy.data.type.BaseType;
059import ptolemy.kernel.CompositeEntity;
060import ptolemy.kernel.util.Attribute;
061import ptolemy.kernel.util.IllegalActionException;
062import ptolemy.kernel.util.NameDuplicationException;
063import ptolemy.kernel.util.Workspace;
064
065/** Create a Landscape file from component raster layers.
066 * 
067 *  @author Daniel Crawl
068 *  @version $Id: CreateLandscape.java 34480 2016-04-21 05:55:25Z crawl $
069 */
070public class CreateLandscape extends TypedAtomicActor {
071
072    public CreateLandscape(CompositeEntity container, String name)
073            throws IllegalActionException, NameDuplicationException {
074        super(container, name);
075
076        elevation = new TypedIOPort(this, "elevation", true, false);
077        _createRasterInputPort(elevation);
078
079        slope = new TypedIOPort(this, "slope", true, false);
080        _createRasterInputPort(slope);
081        
082        aspect = new TypedIOPort(this, "aspect", true, false);
083        _createRasterInputPort(aspect);
084        
085        fuel = new TypedIOPort(this, "fuel", true, false);
086        _createRasterInputPort(fuel);
087        
088        canopyCover = new TypedIOPort(this, "canopyCover", true, false);
089        _createRasterInputPort(canopyCover);
090        
091        canopyHeight = new TypedIOPort(this, "canopyHeight", true, false);
092        _createRasterInputPort(canopyHeight);
093        
094        canopyBaseHeight = new TypedIOPort(this, "canopyBaseHeight", true, false);
095        _createRasterInputPort(canopyBaseHeight);
096        
097        canopyBulkDensity = new TypedIOPort(this, "canopyBulkDensity", true, false);
098        _createRasterInputPort(canopyBulkDensity);
099        
100        duff = new TypedIOPort(this, "duff", true, false);
101        _createRasterInputPort(duff);
102        
103        woody = new TypedIOPort(this, "woody", true, false);
104        _createRasterInputPort(woody);
105        
106        
107        lcpmake = new StringParameter(this, "lcpmake");
108        lcpmake.setToken("lcpmake");
109        
110        outputName = new PortParameter(this, "outputName");
111        outputName.setTypeEquals(BaseType.STRING);
112        outputName.getPort().setTypeEquals(BaseType.STRING);
113        outputName.setStringMode(true);
114        new Attribute(outputName.getPort(), "_showName");
115        
116        deleteInputsOnWrapup = new Parameter(this, "deleteInputsOnWrapup");
117        deleteInputsOnWrapup.setTypeEquals(BaseType.BOOLEAN);
118        deleteInputsOnWrapup.setToken(BooleanToken.FALSE);
119
120        deleteOutputOnWrapup = new Parameter(this, "deleteOutputOnWrapup");
121        deleteOutputOnWrapup.setTypeEquals(BaseType.BOOLEAN);
122        deleteOutputOnWrapup.setToken(BooleanToken.FALSE);
123
124        out = new TypedIOPort(this, "out", false, true);
125        out.setTypeEquals(BaseType.STRING);
126    }
127    
128    /** Clone this actor into the specified workspace. */
129    @Override
130    public Object clone(Workspace workspace) throws CloneNotSupportedException {
131        CreateLandscape newObject = (CreateLandscape)super.clone(workspace);
132        newObject._deleteOnWrapup = new HashSet<File>();
133        return newObject;
134    }
135    
136    @Override
137    public void fire() throws IllegalActionException {
138     
139        super.fire();
140        
141        outputName.update();
142        Token token = outputName.getToken();
143        if(token == null) {
144            throw new IllegalActionException(this, "Must specify output name.");
145        }
146        String outputNameStr = ((StringToken)token).stringValue();
147        if(outputNameStr.trim().isEmpty()) {
148            throw new IllegalActionException(this, "Must specify output name.");
149        }
150        
151        // strip off trailing .lcp since lcpmake automatically adds it
152        int index = outputNameStr.lastIndexOf(".lcp");
153        if(index > -1) {
154            outputNameStr = outputNameStr.substring(0, index);
155        }
156        
157        String elevationStr = _readRasterInput(elevation);
158        String slopeStr = _readRasterInput(slope);
159        String aspectStr = _readRasterInput(aspect);
160        String fuelStr = _readRasterInput(fuel);
161        String canopyCoverStr = _readRasterInput(canopyCover);
162        String canopyHeightStr = _readRasterInput(canopyHeight);
163        String canopyBaseHeightStr = _readRasterInput(canopyBaseHeight);
164        String canopyBulkDensityStr = _readRasterInput(canopyBulkDensity);
165        String duffStr = _readRasterInput(duff);
166        String woodyStr = _readRasterInput(woody);
167        
168        List<String> args = new ArrayList<String>();
169        
170        args.add(_lcpMakeStr);
171        
172        args.add("-landscape");
173        args.add(outputNameStr);
174        
175        // add .lcp back to output name
176        outputNameStr += ".lcp";
177        
178        args.add("-latitude");
179        args.add(String.valueOf(_latitude));        
180        
181        args.add("-elevation");
182        args.add(elevationStr);
183        
184        args.add("-slope");
185        args.add(slopeStr);
186        
187        args.add("-aspect");
188        args.add(aspectStr);
189        
190        args.add("-fuel");
191        args.add(fuelStr);
192        
193        args.add("-cover");
194        args.add(canopyCoverStr);
195        
196        if(canopyHeightStr != null) {
197            args.add("-height");
198            args.add(canopyHeightStr);
199        }
200
201        if(canopyBaseHeightStr != null) {
202            args.add("-base");
203            args.add(canopyBaseHeightStr);
204        }
205        
206        if(canopyBulkDensityStr != null) {
207            args.add("-density");
208            args.add(canopyBulkDensityStr);
209        }
210        
211        if(duffStr != null) {
212            args.add("-duff");
213            args.add(duffStr);
214        }
215        
216        if(woodyStr != null) {
217            args.add("-woody");
218            args.add(woodyStr);
219        }
220
221        ProcessBuilder builder = new ProcessBuilder(args);
222        try {
223            Process process = builder.start();
224            if(_debugging) {
225                try(InputStream stdout = process.getInputStream();
226                    BufferedReader reader = new BufferedReader(new InputStreamReader(stdout));) {
227                    String line;
228                    while((line = reader.readLine()) != null) {
229                        _debug(line);
230                    }
231                }
232            }
233            if(process.waitFor() != 0) {
234                throw new IllegalActionException(this, "lcpmake return non-zero.");
235            }
236            
237            boolean deleteOutput = ((BooleanToken)deleteOutputOnWrapup.getToken()).booleanValue();
238            if(deleteOutput) {
239                _deleteOnWrapup.add(new File(outputNameStr));
240            }
241        } catch (IOException | InterruptedException e) {
242            throw new IllegalActionException(this, e, "Error running lcpmake.");
243        }
244        
245        out.broadcast(new StringToken(outputNameStr));
246        
247    }
248    
249    @Override
250    public void preinitialize() throws IllegalActionException {
251
252        super.preinitialize();
253
254        _latitude = null;
255        
256        _checkRasterInputAvailable(elevation);
257        _checkRasterInputAvailable(slope);
258        _checkRasterInputAvailable(aspect);
259        _checkRasterInputAvailable(fuel);
260        _checkRasterInputAvailable(canopyCover);
261
262        
263        _lcpMakeStr = ((StringToken)lcpmake.getToken()).stringValue();
264        if(_lcpMakeStr.trim().isEmpty()) {
265            throw new IllegalActionException(this, "Must specify path to lcpmake binary.");
266        }
267        
268        _deleteInputs = ((BooleanToken)deleteInputsOnWrapup.getToken()).booleanValue();
269        
270    }
271    
272    @Override
273    public void wrapup() throws IllegalActionException {
274        
275        super.wrapup();
276        
277        for(File file: _deleteOnWrapup) {
278            if(!file.delete()) {
279                System.err.println("WARNING: could not delete " + file);
280            }
281            
282            // delete .aux.xml if exists
283            String name = file.getAbsolutePath();
284            File auxFile = new File(name + ".aux.xml");
285            if(auxFile.exists()) {
286                if(!auxFile.delete()) {
287                    System.err.println("WARNING: could not delete " + auxFile);  
288                }
289            }
290            
291            // delete .prj if exists
292            String nameNoExtension = FilenameUtils.removeExtension(name);
293            File prjFile = new File(nameNoExtension + ".prj");
294            if(prjFile.exists()) {
295                if(!prjFile.delete()) {
296                    System.err.println("WARNING: could not delete " + prjFile);
297                }
298            }
299        }
300        
301        _deleteOnWrapup.clear();
302        
303    }
304
305    ///////////////////////////////////////////////////////////////////
306    ////                       public variables                    ////
307
308    /** Elevation raster (required). */
309    public TypedIOPort elevation;
310
311    /** Slope raster (required). */
312    public TypedIOPort slope;
313    
314    /** Aspect raster (required). */
315    public TypedIOPort aspect;
316    
317    /** Fuel models raster (required). */
318    public TypedIOPort fuel;
319    
320    /** Canoopy cover raster (required). */
321    public TypedIOPort canopyCover;
322    
323    /** Canopy height raster (optional). */
324    public TypedIOPort canopyHeight;
325    
326    /** Canopy base height raster (optional). */
327    public TypedIOPort canopyBaseHeight;
328    
329    /** Canopy bulk density raster (optional). */
330    public TypedIOPort canopyBulkDensity;
331    
332    /** Duff raster (optional). */
333    public TypedIOPort duff;
334    
335    /** Woody raster (optional). */
336    public TypedIOPort woody;
337    
338    /** Path and name to lcpmake program. */
339    public StringParameter lcpmake;
340    
341    /** Path and name of landscape file to write. */
342    public PortParameter outputName;
343    
344    /** If true, delete input raster files when workflow stops execution. */
345    public Parameter deleteInputsOnWrapup;
346
347    /** If true, delete output raster files when workflow stops execution. */
348    public Parameter deleteOutputOnWrapup;
349    
350    /** Path and name of landscape file that's created. */
351    public TypedIOPort out;
352
353
354    ///////////////////////////////////////////////////////////////////
355    ////                       private methods                     ////
356
357    /** Create an input port for a raster band. */
358    private void _createRasterInputPort(TypedIOPort port) throws IllegalActionException, NameDuplicationException {
359        port.setTypeEquals(RasterToken.RASTER);
360        new Attribute(port, "_showName");
361    }
362    
363    /** Check that an input port is connected. */
364    private void _checkRasterInputAvailable(TypedIOPort port) throws IllegalActionException {
365        if(port.numberOfSources() < 1) {
366            throw new IllegalActionException(this, "Must specify " + port.getName() + " raster.");
367        }
368    }
369    
370    /** Read a raster input port and return the file path. Returns null if port not connected. */
371    private String _readRasterInput(TypedIOPort port) throws IllegalActionException {
372        if(port.numberOfSources() > 0) {
373            RasterToken token = (RasterToken) port.get(0);
374            File file = token.rasterFile();
375            if(file == null) {
376                throw new IllegalActionException(this,
377                        "No file in raster " + port.getName());
378            }
379            
380            if(!file.exists()) {
381                throw new IllegalActionException(this,
382                        "Raster does not exist: " + file);
383            }
384            
385            if(_deleteInputs) {
386                _deleteOnWrapup.add(file);
387            }
388            
389            if(_latitude == null) {
390                RasterInfo info = RasterUtilities.getInfo(file);
391                ReferencedEnvelope envelope = info.getEnvelope();
392                ReferencedEnvelope wgs84Envelope;
393                try {
394                    wgs84Envelope = envelope.transform(DefaultGeographicCRS.WGS84, true);
395                } catch (TransformException | FactoryException e) {
396                    throw new IllegalActionException(this, "Error converting to WGS84");
397                }                
398                _latitude = wgs84Envelope.getMedian(1);
399                //System.out.println("lat = " + _latitude);
400            }
401            
402            return file.getAbsolutePath();
403        }
404        return null;
405    }
406    
407    ///////////////////////////////////////////////////////////////////
408    ////                       private variables                   ////
409
410    /** Path and name to lcpmake. */
411    private String _lcpMakeStr;
412    
413    /** Latitude of center of input rasters. */
414    private Double _latitude;
415    
416    /** Set of files to delete on wrapup. */
417    Set<File> _deleteOnWrapup = new HashSet<File>();
418    
419    /** If true, delete input raster in wrapup(). */
420    private boolean _deleteInputs = false;
421}