001/*
002 * Copyright (c) 2014-2015 The Regents of the University of California.
003 * All rights reserved.
004 *
005 * '$Author: crawl $'
006 * '$Date: 2018-09-06 23:20:04 +0000 (Thu, 06 Sep 2018) $' 
007 * '$Revision: 34710 $'
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.kml;
030
031import java.io.FileWriter;
032import java.io.IOException;
033import java.io.StringWriter;
034import java.io.Writer;
035import java.util.Date;
036import java.util.HashMap;
037import java.util.LinkedList;
038import java.util.List;
039import java.util.Map;
040
041import org.geotools.data.simple.SimpleFeatureCollection;
042import org.geotools.data.simple.SimpleFeatureIterator;
043import org.geotools.geometry.jts.JTS;
044import org.geotools.referencing.CRS;
045import org.geotools.referencing.crs.DefaultGeographicCRS;
046import org.kepler.gis.data.VectorToken;
047import org.kepler.gis.util.GISUtilities;
048import org.opengis.feature.Property;
049import org.opengis.feature.simple.SimpleFeature;
050import org.opengis.feature.simple.SimpleFeatureType;
051import org.opengis.geometry.MismatchedDimensionException;
052import org.opengis.referencing.FactoryException;
053import org.opengis.referencing.crs.CoordinateReferenceSystem;
054import org.opengis.referencing.operation.MathTransform;
055import org.opengis.referencing.operation.TransformException;
056
057import com.vividsolutions.jts.geom.Coordinate;
058import com.vividsolutions.jts.geom.Geometry;
059import com.vividsolutions.jts.geom.LineString;
060import com.vividsolutions.jts.geom.MultiPolygon;
061import com.vividsolutions.jts.geom.Point;
062import com.vividsolutions.jts.geom.Polygon;
063
064import de.micromata.opengis.kml.v_2_2_0.ColorMode;
065import de.micromata.opengis.kml.v_2_2_0.Data;
066import de.micromata.opengis.kml.v_2_2_0.Document;
067import de.micromata.opengis.kml.v_2_2_0.Folder;
068import de.micromata.opengis.kml.v_2_2_0.Kml;
069import de.micromata.opengis.kml.v_2_2_0.KmlFactory;
070import de.micromata.opengis.kml.v_2_2_0.LinearRing;
071import de.micromata.opengis.kml.v_2_2_0.MultiGeometry;
072import de.micromata.opengis.kml.v_2_2_0.Placemark;
073import de.micromata.opengis.kml.v_2_2_0.Style;
074import ptolemy.actor.TypedAtomicActor;
075import ptolemy.actor.TypedIOPort;
076import ptolemy.actor.parameters.ParameterPort;
077import ptolemy.actor.parameters.PortParameter;
078import ptolemy.data.ArrayToken;
079import ptolemy.data.BooleanToken;
080import ptolemy.data.DateToken;
081import ptolemy.data.StringToken;
082import ptolemy.data.Token;
083import ptolemy.data.UnsignedByteToken;
084import ptolemy.data.expr.Parameter;
085import ptolemy.data.expr.StringParameter;
086import ptolemy.data.type.ArrayType;
087import ptolemy.data.type.BaseType;
088import ptolemy.kernel.CompositeEntity;
089import ptolemy.kernel.util.Attribute;
090import ptolemy.kernel.util.IllegalActionException;
091import ptolemy.kernel.util.NameDuplicationException;
092import ptolemy.kernel.util.SingletonAttribute;
093
094/** An actor that writes a vector/feature data set to a KML file.
095 *  An input port must be added for each data set to read. The features
096 *  will be placed in the KML inside a folder with the same name as
097 *  the input port.
098 *  <p>
099 *  If a feature contains a property called "name", then the value
100 *  will be used to specify the name of the KML placemark for that feature.
101 *  </p>
102 *  <p>
103 *  If a feature contains a property called "color", then the value
104 *  will be used to specify the color of the KML placemark for that feature.
105 *  </p>
106 *  <p>
107 *  If a feature contains a property called "timestamp" and a timestamp is not
108 *  supplied to the <i>date</i> port, then the value in the property will be
109 *  used to specify the timestamp of the KML placemark for that feature. The
110 *  format for KML timestamps is: yyyy-MM-dd'T'HH:mmXXX.
111 *  </p>
112 *  
113 *  @author Daniel Crawl
114 *  @version $Id: KMLWriter.java 34710 2018-09-06 23:20:04Z crawl $
115 */
116public class KMLWriter extends TypedAtomicActor {
117
118    public KMLWriter(CompositeEntity container, String name)
119            throws IllegalActionException, NameDuplicationException {
120        
121        super(container, name);
122        
123        outputType = new StringParameter(this, "outputType");
124        outputType.addChoice("file");
125        outputType.addChoice("text");
126        outputType.addChoice("binary");
127        outputType.setToken("file");
128        
129        filename = new PortParameter(this, "filename");
130        filename.setTypeEquals(BaseType.STRING);
131        filename.getPort().setTypeEquals(BaseType.STRING);
132        filename.setStringMode(true);
133        new SingletonAttribute(filename.getPort(), "_showName");
134        filename.setToken("output.kml");
135        
136        date = new TypedIOPort(this, "date", true, false);
137        date.setTypeEquals(BaseType.DATE);
138        new SingletonAttribute(date, "_showName");
139
140        folderName = new PortParameter(this, "folderName");
141        folderName.setTypeEquals(BaseType.STRING);
142        folderName.getPort().setTypeEquals(BaseType.STRING);
143        folderName.setStringMode(true);
144        new SingletonAttribute(folderName.getPort(), "_showName");
145
146        fillPolygons = new Parameter(this, "fillPolygons");
147        fillPolygons.setTypeEquals(BaseType.BOOLEAN);
148        fillPolygons.setToken(BooleanToken.TRUE);
149        
150        placemarkNameProperty = new PortParameter(this, "placemarkNameProperty");
151        placemarkNameProperty.setStringMode(true);
152        placemarkNameProperty.setTypeEquals(BaseType.STRING);
153        placemarkNameProperty.getPort().setTypeEquals(BaseType.STRING);
154        new SingletonAttribute(placemarkNameProperty.getPort(), "_showName");
155                
156        groupNameProperty = new PortParameter(this, "groupNameProperty");
157        groupNameProperty.setStringMode(true);
158        groupNameProperty.setTypeEquals(BaseType.STRING);
159        groupNameProperty.getPort().setTypeEquals(BaseType.STRING);
160        new SingletonAttribute(groupNameProperty.getPort(), "_showName");
161        
162        output = new TypedIOPort(this, "output", false, true);
163    }
164    
165    @Override
166    public void attributeChanged(Attribute attribute) throws IllegalActionException {
167        if(attribute == outputType) {
168            StringToken token = (StringToken) outputType.getToken();
169            if(token != null) {
170                String val = ((StringToken)token).stringValue();
171                if(val.trim().isEmpty()) {
172                    _outputTypeStr = "file";
173                } else if(_outputTypeStr != null && 
174                    !_outputTypeStr.equals("file") &&
175                    !_outputTypeStr.equals("text") &&
176                    !_outputTypeStr.equals("binary")) {
177                    throw new IllegalActionException(this, "Unsupported type of output: " + val);
178                } else {
179                    _outputTypeStr = val;
180                }
181            }            
182        } else {
183            super.attributeChanged(attribute);
184        }
185    }
186    
187    @Override
188    public void fire() throws IllegalActionException {
189        
190        super.fire();
191        
192        filename.update();
193                
194        String filenameStr = null;
195        if(_outputTypeStr.equals("file")) {        
196            filenameStr = ((StringToken)filename.getToken()).stringValue();
197            if(filenameStr.trim().isEmpty()) {
198                throw new IllegalActionException(this, "Must specify output file.");
199            }
200        }
201
202        placemarkNameProperty.update();
203        
204        _placemarkNameProperty = null;
205        Token token = placemarkNameProperty.getToken();
206        if(token != null) {
207            _placemarkNameProperty = ((StringToken)token).stringValue().trim();    
208        }
209
210        groupNameProperty.update();
211        
212        _groupNameProperty = null;
213        token = groupNameProperty.getToken();
214        if(token != null) {
215            _groupNameProperty = ((StringToken)token).stringValue().trim();
216            if(_groupNameProperty.isEmpty()) {
217                _groupNameProperty = null;
218            }
219        }
220
221        
222        Writer writer = null;
223        try {
224            if(_outputTypeStr.equals("file")) {
225                writer = new FileWriter(filenameStr);
226            } else if(_outputTypeStr.equals("text") || _outputTypeStr.equals("binary")) {
227                writer = new StringWriter();
228            } else {
229                throw new IllegalActionException(this, "Unsupported type of output: " + _outputTypeStr);
230            }
231    
232            
233            folderName.update();
234            String folderNameStr = null;
235            StringToken stringToken = (StringToken) folderName.getToken();
236            if(stringToken != null) {
237                folderNameStr = stringToken.stringValue();
238            }
239            
240            String formattedDateStr = null;
241            DateToken dateToken = null;
242            if(date.numberOfSources() > 0) {
243                dateToken = (DateToken)date.get(0);
244    
245                if(dateToken == null) {
246                    //System.out.println("WARNING: no timestamp.");
247                } else {
248                    //String whenStr = String.format("%02d-%02d-%02d", year, mon, day);
249                    
250                    Date dateVal = new Date(dateToken.getValue());
251                    synchronized(WriteTimestampToKML.KML_TIMESTAMP_FORMAT) {
252                        formattedDateStr = WriteTimestampToKML.KML_TIMESTAMP_FORMAT.format(dateVal);   
253                    }
254                }
255            }
256    
257            final Kml kml = new Kml();
258            final Document doc = kml.createAndSetDocument();
259            
260            // only create a top level folder if a name is specified.
261            Folder topFolder = null;
262            if(folderNameStr != null && !folderNameStr.trim().isEmpty()) {
263                topFolder = doc.createAndAddFolder();
264                topFolder.setName(folderNameStr);
265            }
266            
267            // add the features read by each input port into a separate
268            // folder
269            for(TypedIOPort port : inputPortList()) {
270                // do not use the class-specific input ports
271                if(port != date && !(port instanceof ParameterPort)) {
272    
273                    SimpleFeatureCollection features = ((VectorToken)port.get(0)).getVectors();
274                    
275                    // create the folder with the port name.
276                    Folder portFolder;
277                    if(topFolder != null) {
278                        portFolder = topFolder.createAndAddFolder();
279                    } else {
280                        portFolder = doc.createAndAddFolder();
281                    }
282                    portFolder.setName(port.getName());
283                    _addToFolder(portFolder, features, formattedDateStr);                
284                }
285            }
286            
287            //kml.marshal(System.out);
288            kml.marshal(writer);
289                    
290            if(_outputTypeStr.equals("file")) {
291                output.broadcast(new StringToken(filenameStr));
292            } else if(_outputTypeStr.equals("text")) {
293                output.broadcast(new StringToken(writer.toString()));                
294            } else if(_outputTypeStr.equals("binary")) {
295                byte[] bytes = writer.toString().getBytes("UTF-8");
296                Token[] tokens = new Token[bytes.length];
297                for(int i = 0; i < bytes.length; i++) {
298                    tokens[i] = new UnsignedByteToken(bytes[i]);
299                }
300                output.broadcast(new ArrayToken(tokens));
301            }
302            
303        } catch(IOException e) {
304            throw new IllegalActionException(this, e, "Error writing KML.");
305        } finally {
306            if(writer != null) {
307                try {
308                    writer.close();
309                } catch(IOException e) {
310                    throw new IllegalActionException(this, e, "Error writing KML.");
311                }
312            }
313        }
314    }
315    
316    /** Set the input ports to vector. */
317    @Override
318    public void preinitialize() throws IllegalActionException {
319        super.preinitialize();
320        
321        for(TypedIOPort port: inputPortList()) {
322            if(port != date && !(port instanceof ParameterPort)) {
323                port.setTypeEquals(VectorToken.VECTOR);
324            }
325        }
326        
327        if(_outputTypeStr.equals("file") || _outputTypeStr.equals("text")) {
328            output.setTypeEquals(BaseType.STRING);
329        } else if(_outputTypeStr.equals("binary")) {
330            output.setTypeEquals(new ArrayType(BaseType.UNSIGNED_BYTE));
331        }
332        
333        Token token = fillPolygons.getToken();
334        if(token == null) {
335            _fillPolygons = true;
336        } else {
337            _fillPolygons = ((BooleanToken)token).booleanValue();
338        }        
339        
340    }
341    
342    ////////////////////////////////////////////////////////////////////////
343    //// public fields                                                  ////
344
345    /** The output type: file, text, or binary. If type if file,
346     *  KML is written to a file with the name specified in <i>filename</i>,
347     *  and <i>output</i> is a true boolean token. If type is text,
348     *  KML is written as a string to <i>output</i>. Otherwise if type is
349     *  binary, KML is written as an unsigned byte array to <i>output</i>.
350     */
351    public StringParameter outputType;
352    
353    /** The name of the output file. Only required when <i>outputType</i> is file. */
354    public PortParameter filename;
355        
356    /** The date to use for the timestamp of each feature. */
357    public TypedIOPort date;
358    
359    /** The name of the top level folder containing the features in 
360     *  the output KML file.
361     */
362    public PortParameter folderName;
363    
364    /** The name of the output file is sent to this port when the
365     *  data has been written.
366     */
367    public TypedIOPort output;
368
369    /** If true, polygons are filled. */
370    public Parameter fillPolygons;
371    
372    /** The name of the property specifying the placemark name. */    
373    public PortParameter placemarkNameProperty;
374    
375    /** The name of the property specifying the group name. */
376    public PortParameter groupNameProperty;
377    
378    
379    ////////////////////////////////////////////////////////////////////////
380    //// private methods                                                ////
381
382    /** Add a set of features to a folder. */
383    private void _addToFolder(Folder folder, SimpleFeatureCollection features,
384        String formattedDateStr) throws IllegalActionException {
385        
386        SimpleFeatureType currentSchema = features.getSchema();
387        CoordinateReferenceSystem srcCRS = currentSchema.getCoordinateReferenceSystem();       
388        
389        if(srcCRS == null) {
390            //throw new IllegalActionException(this, "Unknown input CRS.");
391            System.out.println("WARNING: Unknown input CRS; assuming WGS84.");
392            srcCRS = DefaultGeographicCRS.WGS84;
393        }
394                
395        MathTransform transform = null;
396        if(!GISUtilities.isCRSWGS84(srcCRS)) {
397            System.out.println(getName() + ": source projection is not WGS84");
398            try {
399                transform = CRS.findMathTransform(srcCRS, DefaultGeographicCRS.WGS84, true);
400            } catch (FactoryException e) {
401                throw new IllegalActionException(this, e,
402                        "Unable to find math transform.");
403            }
404        }
405        
406        // if group name is set, find the groups and create folders for each.
407        Map<String,Folder> groupFolders = new HashMap<String,Folder>();
408        if(_groupNameProperty != null) {
409            try(SimpleFeatureIterator iterator = features.features();) {
410                
411                while(iterator.hasNext()) {
412                   
413                    SimpleFeature feature = iterator.next();
414                    Property property = feature.getProperty(_groupNameProperty);
415                    if(property == null) {
416                        System.err.println("WARNING: missing group " +
417                            _groupNameProperty + " in property");
418                    } else if(!groupFolders.containsKey(property.getValue().toString())) {
419                        Folder groupFolder = folder.createAndAddFolder();
420                        groupFolder.setName(property.getValue().toString());
421                        groupFolders.put(property.getValue().toString(), groupFolder);
422                    }
423                }                
424            }
425            
426        }
427
428        try(SimpleFeatureIterator iterator = features.features();) {
429        
430            while(iterator.hasNext()) {
431               
432                SimpleFeature feature = iterator.next();
433                
434                Geometry geometry = (Geometry) feature.getDefaultGeometry();
435                Geometry newGeometry = geometry;
436                if(transform != null) {
437                    try {
438                        newGeometry = JTS.transform(geometry, transform);
439                    } catch (MismatchedDimensionException | TransformException e) {
440                        throw new IllegalActionException(this, e,
441                                "Error transforming geometry.");
442                    }
443                }
444
445                Folder containerFolder = null;
446                if(_groupNameProperty != null) {
447                    Property property = feature.getProperty(_groupNameProperty);
448                    if(property != null) {
449                        containerFolder = groupFolders.get(property.getValue().toString());
450                    }
451                }
452                
453                if(containerFolder == null) {
454                    containerFolder = folder;
455                }
456                
457                Placemark placemark = containerFolder.createAndAddPlacemark();
458                
459                if(newGeometry instanceof MultiPolygon) {
460                
461                    MultiGeometry multigeometry = placemark.createAndSetMultiGeometry();
462                   
463                    for(int i = 0; i < newGeometry.getNumGeometries(); i++) {
464                        Geometry subGeometry = newGeometry.getGeometryN(i);
465                        
466                        if(subGeometry instanceof Polygon) {
467                            _addPolygon((Polygon)subGeometry,
468                                    multigeometry.createAndAddPolygon());
469                        } else if(subGeometry instanceof Point) {
470                            _addPoint((Point)subGeometry,
471                                    multigeometry.createAndAddPoint());
472                        } else if(subGeometry instanceof LineString) {
473                            _addLineString((LineString)subGeometry,
474                                    multigeometry.createAndAddLineString());
475                        } else {
476                            throw new IllegalActionException(this,
477                                    "Unsupported type of geometry: " +
478                                            subGeometry.getClass());
479                        }                       
480                    }
481                } else if(newGeometry instanceof Polygon) {
482                    _addPolygon((Polygon)newGeometry, placemark.createAndSetPolygon());
483                } else if(newGeometry instanceof Point) {
484                    _addPoint((Point)newGeometry, placemark.createAndSetPoint());
485                } else if(newGeometry instanceof LineString) {
486                    _addLineString((LineString)newGeometry,
487                            placemark.createAndSetLineString());
488                } else {
489                    throw new IllegalActionException(this,
490                            "Unsupported type of geometry: " + newGeometry.getClass());
491                }
492
493                String colorStr = "ff0000ff";
494                String curTimeStampStr = null;
495
496                // TODO sort names
497                List<Data> dataList = new LinkedList<Data>();
498                
499                for(Property property : feature.getProperties()) {
500                    String propertyNameStr = property.getName().toString();
501                    Object value = property.getValue();
502                    String valueStr = value.toString();
503                    if(!(value instanceof Geometry) && value != null) {
504                        if(propertyNameStr.toLowerCase().equals("name")) {
505                            placemark.setName(valueStr);
506                        } else {
507                            dataList.add(KmlFactory.createData(valueStr)
508                                    .withName(propertyNameStr));                            
509                        }
510                    }
511                   
512                    if(propertyNameStr.toLowerCase().equals("name") ||
513                        (_placemarkNameProperty != null &&
514                        _placemarkNameProperty.equals(propertyNameStr))) {
515                        placemark.setName(valueStr);
516                    }
517
518                    if(propertyNameStr.equals("color")) {
519                        colorStr = _getColorHex(valueStr);
520                    }
521                    
522                    if(formattedDateStr == null && propertyNameStr.equals("timestamp")) {
523                        curTimeStampStr = valueStr;
524                    }
525
526                }
527                placemark.createAndSetExtendedData().setData(dataList);
528                
529                // TODO choose time
530                /*
531                int year = _parseDateObject(feature.getAttribute("YEAR_"));
532                int mon = _parseDateObject(feature.getAttribute("MONTH_"));
533                if(mon == 0) {
534                    mon = 1;
535                }
536                int day = _parseDateObject(feature.getAttribute("DAY_"));
537                if(day == 0) {
538                    day = 1;
539                }
540                */
541
542                if(formattedDateStr != null) {
543                    placemark.createAndSetTimeStamp().setWhen(formattedDateStr);                    
544                } else if(curTimeStampStr != null) {
545                    placemark.createAndSetTimeStamp().setWhen(curTimeStampStr);
546                }
547                                
548                Style style = placemark.createAndAddStyle();
549                if(_fillPolygons) {
550                    style.createAndSetPolyStyle()
551                        .withColorMode(ColorMode.NORMAL).setColor(colorStr);
552                } else {
553                    style.createAndSetPolyStyle().setFill(false);
554                    style.createAndSetLineStyle().withWidth(5)
555                        .withColorMode(ColorMode.NORMAL).setColor(colorStr);
556                }
557
558            }
559        }
560    }
561    
562    private void _addLineString(LineString lineString, de.micromata.opengis.kml.v_2_2_0.LineString kmlLineString) {
563        for(Coordinate coordinate : lineString.getCoordinates()) {
564            kmlLineString.addToCoordinates(coordinate.x, coordinate.y, 0);
565        }
566    }
567    
568    private void _addPolygon(Polygon polygon, de.micromata.opengis.kml.v_2_2_0.Polygon kmlPolygon) {
569                
570        //System.out.println("add polygon");
571        
572        // convert exterior boundary
573        final LineString exterior = polygon.getExteriorRing();
574        
575        final LinearRing kmlExterior = KmlFactory.createLinearRing();
576        
577        for(Coordinate coordinate : exterior.getCoordinates()) {
578            kmlExterior.addToCoordinates(coordinate.x, coordinate.y, 0);
579        }
580                                    
581        kmlPolygon.setOuterBoundaryIs(KmlFactory.createBoundary().withLinearRing(kmlExterior));
582        
583        // convert any interior boundaries
584        for(int j = 0; j < polygon.getNumInteriorRing(); j++) {
585            
586            final LineString interior = polygon.getInteriorRingN(j);
587            
588            final LinearRing kmlInterior = KmlFactory.createLinearRing();
589            
590            for(Coordinate coordinate : interior.getCoordinates()) {
591                kmlInterior.addToCoordinates(coordinate.x, coordinate.y, 0);
592            }
593                                        
594            kmlPolygon.addToInnerBoundaryIs(KmlFactory.createBoundary().withLinearRing(kmlInterior));
595            
596        }
597    }
598    
599    private void _addPoint(Point point, de.micromata.opengis.kml.v_2_2_0.Point kmlPoint) {
600
601        //System.out.println("add point");
602
603        for(Coordinate coordinate : point.getCoordinates()) {
604            // TODO this is reversed for geojson
605            //kmlPoint.addToCoordinates(coordinate.y, coordinate.x, 0);//coordinate.z);
606            kmlPoint.addToCoordinates(coordinate.x, coordinate.y, 0);//coordinate.z);
607        }
608               
609    }
610    
611    /*
612    private static int _parseDateObject(Object value) {
613        if(value instanceof Double) {
614            return ((Double)value).intValue(); 
615        }
616        return Integer.valueOf(value.toString());
617    }
618     */
619    
620    /** Get the KML color string from a color name. */
621    private static String _getColorHex(String name) {        
622        int[] rgb = _colorMap.get(name);
623        if(rgb == null) {
624            return "ffffffff";
625        }
626        return String.format("ff%02x%02x%02x",
627            rgb[ColorIndex.Blue.ordinal()],
628            rgb[ColorIndex.Green.ordinal()],
629            rgb[ColorIndex.Red.ordinal()]).toLowerCase();
630    }
631    
632    /** Type of output to write. */
633    private String _outputTypeStr;
634    
635    /** If true, polygons are filled. */
636    private boolean _fillPolygons;
637    
638    /** The name of the property specifying the placemark name. */    
639    private String _placemarkNameProperty;
640
641    /** The name of the property specifying the group name. */    
642    private String _groupNameProperty;
643    
644    /** Order of colors in colorMap. */
645    private enum ColorIndex {Red, Green, Blue};
646    
647    /** Mapping of HTML/CSS color names to RGB values. */
648    private final static Map<String,int[]> _colorMap = new HashMap<String,int[]>();
649    
650    static {
651        // from https://gist.github.com/XiaoxiaoLi/8031146
652        _colorMap.put("AliceBlue".toLowerCase(), new int[] {0xF0, 0xF8, 0xFF});
653        _colorMap.put("AntiqueWhite".toLowerCase(), new int[] {0xFA, 0xEB, 0xD7});
654        _colorMap.put("Aqua".toLowerCase(), new int[] {0x00, 0xFF, 0xFF});
655        _colorMap.put("Aquamarine".toLowerCase(), new int[] {0x7F, 0xFF, 0xD4});
656        _colorMap.put("Azure".toLowerCase(), new int[] {0xF0, 0xFF, 0xFF});
657        _colorMap.put("Beige".toLowerCase(), new int[] {0xF5, 0xF5, 0xDC});
658        _colorMap.put("Bisque".toLowerCase(), new int[] {0xFF, 0xE4, 0xC4});
659        _colorMap.put("Black".toLowerCase(), new int[] {0x00, 0x00, 0x00});
660        _colorMap.put("BlanchedAlmond".toLowerCase(), new int[] {0xFF, 0xEB, 0xCD});
661        _colorMap.put("Blue".toLowerCase(), new int[] {0x00, 0x00, 0xFF});
662        _colorMap.put("BlueViolet".toLowerCase(), new int[] {0x8A, 0x2B, 0xE2});
663        _colorMap.put("Brown".toLowerCase(), new int[] {0xA5, 0x2A, 0x2A});
664        _colorMap.put("BurlyWood".toLowerCase(), new int[] {0xDE, 0xB8, 0x87});
665        _colorMap.put("CadetBlue".toLowerCase(), new int[] {0x5F, 0x9E, 0xA0});
666        _colorMap.put("Chartreuse".toLowerCase(), new int[] {0x7F, 0xFF, 0x00});
667        _colorMap.put("Chocolate".toLowerCase(), new int[] {0xD2, 0x69, 0x1E});
668        _colorMap.put("Coral".toLowerCase(), new int[] {0xFF, 0x7F, 0x50});
669        _colorMap.put("CornflowerBlue".toLowerCase(), new int[] {0x64, 0x95, 0xED});
670        _colorMap.put("Cornsilk".toLowerCase(), new int[] {0xFF, 0xF8, 0xDC});
671        _colorMap.put("Crimson".toLowerCase(), new int[] {0xDC, 0x14, 0x3C});
672        _colorMap.put("Cyan".toLowerCase(), new int[] {0x00, 0xFF, 0xFF});
673        _colorMap.put("DarkBlue".toLowerCase(), new int[] {0x00, 0x00, 0x8B});
674        _colorMap.put("DarkCyan".toLowerCase(), new int[] {0x00, 0x8B, 0x8B});
675        _colorMap.put("DarkGoldenRod".toLowerCase(), new int[] {0xB8, 0x86, 0x0B});
676        _colorMap.put("DarkGray".toLowerCase(), new int[] {0xA9, 0xA9, 0xA9});
677        _colorMap.put("DarkGreen".toLowerCase(), new int[] {0x00, 0x64, 0x00});
678        _colorMap.put("DarkKhaki".toLowerCase(), new int[] {0xBD, 0xB7, 0x6B});
679        _colorMap.put("DarkMagenta".toLowerCase(), new int[] {0x8B, 0x00, 0x8B});
680        _colorMap.put("DarkOliveGreen".toLowerCase(), new int[] {0x55, 0x6B, 0x2F});
681        _colorMap.put("DarkOrange".toLowerCase(), new int[] {0xFF, 0x8C, 0x00});
682        _colorMap.put("DarkOrchid".toLowerCase(), new int[] {0x99, 0x32, 0xCC});
683        _colorMap.put("DarkRed".toLowerCase(), new int[] {0x8B, 0x00, 0x00});
684        _colorMap.put("DarkSalmon".toLowerCase(), new int[] {0xE9, 0x96, 0x7A});
685        _colorMap.put("DarkSeaGreen".toLowerCase(), new int[] {0x8F, 0xBC, 0x8F});
686        _colorMap.put("DarkSlateBlue".toLowerCase(), new int[] {0x48, 0x3D, 0x8B});
687        _colorMap.put("DarkSlateGray".toLowerCase(), new int[] {0x2F, 0x4F, 0x4F});
688        _colorMap.put("DarkTurquoise".toLowerCase(), new int[] {0x00, 0xCE, 0xD1});
689        _colorMap.put("DarkViolet".toLowerCase(), new int[] {0x94, 0x00, 0xD3});
690        _colorMap.put("DeepPink".toLowerCase(), new int[] {0xFF, 0x14, 0x93});
691        _colorMap.put("DeepSkyBlue".toLowerCase(), new int[] {0x00, 0xBF, 0xFF});
692        _colorMap.put("DimGray".toLowerCase(), new int[] {0x69, 0x69, 0x69});
693        _colorMap.put("DodgerBlue".toLowerCase(), new int[] {0x1E, 0x90, 0xFF});
694        _colorMap.put("FireBrick".toLowerCase(), new int[] {0xB2, 0x22, 0x22});
695        _colorMap.put("FloralWhite".toLowerCase(), new int[] {0xFF, 0xFA, 0xF0});
696        _colorMap.put("ForestGreen".toLowerCase(), new int[] {0x22, 0x8B, 0x22});
697        _colorMap.put("Fuchsia".toLowerCase(), new int[] {0xFF, 0x00, 0xFF});
698        _colorMap.put("Gainsboro".toLowerCase(), new int[] {0xDC, 0xDC, 0xDC});
699        _colorMap.put("GhostWhite".toLowerCase(), new int[] {0xF8, 0xF8, 0xFF});
700        _colorMap.put("Gold".toLowerCase(), new int[] {0xFF, 0xD7, 0x00});
701        _colorMap.put("GoldenRod".toLowerCase(), new int[] {0xDA, 0xA5, 0x20});
702        _colorMap.put("Gray".toLowerCase(), new int[] {0x80, 0x80, 0x80});
703        _colorMap.put("Green".toLowerCase(), new int[] {0x00, 0x80, 0x00});
704        _colorMap.put("GreenYellow".toLowerCase(), new int[] {0xAD, 0xFF, 0x2F});
705        _colorMap.put("HoneyDew".toLowerCase(), new int[] {0xF0, 0xFF, 0xF0});
706        _colorMap.put("HotPink".toLowerCase(), new int[] {0xFF, 0x69, 0xB4});
707        _colorMap.put("IndianRed".toLowerCase(), new int[] {0xCD, 0x5C, 0x5C});
708        _colorMap.put("Indigo".toLowerCase(), new int[] {0x4B, 0x00, 0x82});
709        _colorMap.put("Ivory".toLowerCase(), new int[] {0xFF, 0xFF, 0xF0});
710        _colorMap.put("Khaki".toLowerCase(), new int[] {0xF0, 0xE6, 0x8C});
711        _colorMap.put("Lavender".toLowerCase(), new int[] {0xE6, 0xE6, 0xFA});
712        _colorMap.put("LavenderBlush".toLowerCase(), new int[] {0xFF, 0xF0, 0xF5});
713        _colorMap.put("LawnGreen".toLowerCase(), new int[] {0x7C, 0xFC, 0x00});
714        _colorMap.put("LemonChiffon".toLowerCase(), new int[] {0xFF, 0xFA, 0xCD});
715        _colorMap.put("LightBlue".toLowerCase(), new int[] {0xAD, 0xD8, 0xE6});
716        _colorMap.put("LightCoral".toLowerCase(), new int[] {0xF0, 0x80, 0x80});
717        _colorMap.put("LightCyan".toLowerCase(), new int[] {0xE0, 0xFF, 0xFF});
718        _colorMap.put("LightGoldenRodYellow".toLowerCase(), new int[] {0xFA, 0xFA, 0xD2});
719        _colorMap.put("LightGray".toLowerCase(), new int[] {0xD3, 0xD3, 0xD3});
720        _colorMap.put("LightGreen".toLowerCase(), new int[] {0x90, 0xEE, 0x90});
721        _colorMap.put("LightPink".toLowerCase(), new int[] {0xFF, 0xB6, 0xC1});
722        _colorMap.put("LightSalmon".toLowerCase(), new int[] {0xFF, 0xA0, 0x7A});
723        _colorMap.put("LightSeaGreen".toLowerCase(), new int[] {0x20, 0xB2, 0xAA});
724        _colorMap.put("LightSkyBlue".toLowerCase(), new int[] {0x87, 0xCE, 0xFA});
725        _colorMap.put("LightSlateGray".toLowerCase(), new int[] {0x77, 0x88, 0x99});
726        _colorMap.put("LightSteelBlue".toLowerCase(), new int[] {0xB0, 0xC4, 0xDE});
727        _colorMap.put("LightYellow".toLowerCase(), new int[] {0xFF, 0xFF, 0xE0});
728        _colorMap.put("Lime".toLowerCase(), new int[] {0x00, 0xFF, 0x00});
729        _colorMap.put("LimeGreen".toLowerCase(), new int[] {0x32, 0xCD, 0x32});
730        _colorMap.put("Linen".toLowerCase(), new int[] {0xFA, 0xF0, 0xE6});
731        _colorMap.put("Magenta".toLowerCase(), new int[] {0xFF, 0x00, 0xFF});
732        _colorMap.put("Maroon".toLowerCase(), new int[] {0x80, 0x00, 0x00});
733        _colorMap.put("MediumAquaMarine".toLowerCase(), new int[] {0x66, 0xCD, 0xAA});
734        _colorMap.put("MediumBlue".toLowerCase(), new int[] {0x00, 0x00, 0xCD});
735        _colorMap.put("MediumOrchid".toLowerCase(), new int[] {0xBA, 0x55, 0xD3});
736        _colorMap.put("MediumPurple".toLowerCase(), new int[] {0x93, 0x70, 0xDB});
737        _colorMap.put("MediumSeaGreen".toLowerCase(), new int[] {0x3C, 0xB3, 0x71});
738        _colorMap.put("MediumSlateBlue".toLowerCase(), new int[] {0x7B, 0x68, 0xEE});
739        _colorMap.put("MediumSpringGreen".toLowerCase(), new int[] {0x00, 0xFA, 0x9A});
740        _colorMap.put("MediumTurquoise".toLowerCase(), new int[] {0x48, 0xD1, 0xCC});
741        _colorMap.put("MediumVioletRed".toLowerCase(), new int[] {0xC7, 0x15, 0x85});
742        _colorMap.put("MidnightBlue".toLowerCase(), new int[] {0x19, 0x19, 0x70});
743        _colorMap.put("MintCream".toLowerCase(), new int[] {0xF5, 0xFF, 0xFA});
744        _colorMap.put("MistyRose".toLowerCase(), new int[] {0xFF, 0xE4, 0xE1});
745        _colorMap.put("Moccasin".toLowerCase(), new int[] {0xFF, 0xE4, 0xB5});
746        _colorMap.put("NavajoWhite".toLowerCase(), new int[] {0xFF, 0xDE, 0xAD});
747        _colorMap.put("Navy".toLowerCase(), new int[] {0x00, 0x00, 0x80});
748        _colorMap.put("OldLace".toLowerCase(), new int[] {0xFD, 0xF5, 0xE6});
749        _colorMap.put("Olive".toLowerCase(), new int[] {0x80, 0x80, 0x00});
750        _colorMap.put("OliveDrab".toLowerCase(), new int[] {0x6B, 0x8E, 0x23});
751        _colorMap.put("Orange".toLowerCase(), new int[] {0xFF, 0xA5, 0x00});
752        _colorMap.put("OrangeRed".toLowerCase(), new int[] {0xFF, 0x45, 0x00});
753        _colorMap.put("Orchid".toLowerCase(), new int[] {0xDA, 0x70, 0xD6});
754        _colorMap.put("PaleGoldenRod".toLowerCase(), new int[] {0xEE, 0xE8, 0xAA});
755        _colorMap.put("PaleGreen".toLowerCase(), new int[] {0x98, 0xFB, 0x98});
756        _colorMap.put("PaleTurquoise".toLowerCase(), new int[] {0xAF, 0xEE, 0xEE});
757        _colorMap.put("PaleVioletRed".toLowerCase(), new int[] {0xDB, 0x70, 0x93});
758        _colorMap.put("PapayaWhip".toLowerCase(), new int[] {0xFF, 0xEF, 0xD5});
759        _colorMap.put("PeachPuff".toLowerCase(), new int[] {0xFF, 0xDA, 0xB9});
760        _colorMap.put("Peru".toLowerCase(), new int[] {0xCD, 0x85, 0x3F});
761        _colorMap.put("Pink".toLowerCase(), new int[] {0xFF, 0xC0, 0xCB});
762        _colorMap.put("Plum".toLowerCase(), new int[] {0xDD, 0xA0, 0xDD});
763        _colorMap.put("PowderBlue".toLowerCase(), new int[] {0xB0, 0xE0, 0xE6});
764        _colorMap.put("Purple".toLowerCase(), new int[] {0x80, 0x00, 0x80});
765        _colorMap.put("Red".toLowerCase(), new int[] {0xFF, 0x00, 0x00});
766        _colorMap.put("RosyBrown".toLowerCase(), new int[] {0xBC, 0x8F, 0x8F});
767        _colorMap.put("RoyalBlue".toLowerCase(), new int[] {0x41, 0x69, 0xE1});
768        _colorMap.put("SaddleBrown".toLowerCase(), new int[] {0x8B, 0x45, 0x13});
769        _colorMap.put("Salmon".toLowerCase(), new int[] {0xFA, 0x80, 0x72});
770        _colorMap.put("SandyBrown".toLowerCase(), new int[] {0xF4, 0xA4, 0x60});
771        _colorMap.put("SeaGreen".toLowerCase(), new int[] {0x2E, 0x8B, 0x57});
772        _colorMap.put("SeaShell".toLowerCase(), new int[] {0xFF, 0xF5, 0xEE});
773        _colorMap.put("Sienna".toLowerCase(), new int[] {0xA0, 0x52, 0x2D});
774        _colorMap.put("Silver".toLowerCase(), new int[] {0xC0, 0xC0, 0xC0});
775        _colorMap.put("SkyBlue".toLowerCase(), new int[] {0x87, 0xCE, 0xEB});
776        _colorMap.put("SlateBlue".toLowerCase(), new int[] {0x6A, 0x5A, 0xCD});
777        _colorMap.put("SlateGray".toLowerCase(), new int[] {0x70, 0x80, 0x90});
778        _colorMap.put("Snow".toLowerCase(), new int[] {0xFF, 0xFA, 0xFA});
779        _colorMap.put("SpringGreen".toLowerCase(), new int[] {0x00, 0xFF, 0x7F});
780        _colorMap.put("SteelBlue".toLowerCase(), new int[] {0x46, 0x82, 0xB4});
781        _colorMap.put("Tan".toLowerCase(), new int[] {0xD2, 0xB4, 0x8C});
782        _colorMap.put("Teal".toLowerCase(), new int[] {0x00, 0x80, 0x80});
783        _colorMap.put("Thistle".toLowerCase(), new int[] {0xD8, 0xBF, 0xD8});
784        _colorMap.put("Tomato".toLowerCase(), new int[] {0xFF, 0x63, 0x47});
785        _colorMap.put("Turquoise".toLowerCase(), new int[] {0x40, 0xE0, 0xD0});
786        _colorMap.put("Violet".toLowerCase(), new int[] {0xEE, 0x82, 0xEE});
787        _colorMap.put("Wheat".toLowerCase(), new int[] {0xF5, 0xDE, 0xB3});
788        _colorMap.put("White".toLowerCase(), new int[] {0xFF, 0xFF, 0xFF});
789        _colorMap.put("WhiteSmoke".toLowerCase(), new int[] {0xF5, 0xF5, 0xF5});
790        _colorMap.put("Yellow".toLowerCase(), new int[] {0xFF, 0xFF, 0x00});
791        _colorMap.put("YellowGreen".toLowerCase(), new int[] {0x9A, 0xCD, 0x32});
792    }
793    
794}