2

The shapefiles that I am processing display information on an operation of a piece of equipment over an area, where the information is stored in steps as polygon features with various attributes and values. My goal is to present the information using a colour spectrum so the colors represent the value of a chosen attribute.

So for example, lets say my shapefile describes the operation of a vacuum cleaner over an imaginery 10km^2 house, where the vacuum cleaner sends data every 50cm regarding how much dust it sucked in, how wet the dust was, how many human hairs were found there etc and stores each step as a feature with a sequence number and all these attributes and values.

Now as far as I've seen, the only way to achieve different colors is to use different layers, as a layer can only have one style, so in order to do this I need to have a layer per feature, so in the given example that means I will have (1000/50)^2=4000 layers which is pretty heavy (the actual data that I am processing is of this magnitude, so real scenario). This becomes a problem in two specific places:

  1. Where I am calling MapContent.getMaxBounds(), this requires iteration over the features

  2. Where I am rendering the graphics, as that also requires such an iteration. So for a shapefile with about 4000 features (which I am using in my test case) it takes about 10 minutes.

My question is: am I approaching this right? Is there a better way to colour polygon features differently? Is there a way where I dont have to create a layer per feature?

Here's my code:

    /**
 * Create the geotiff image based on featureCollection and the resolution params for an numerical value attribute.
 * 
 * @param featureCollection
 * @param resolution
 * @return byte[]
 * @throws IOException
 */
public static byte[] createGeotiffImageForNumericalValues(SimpleFeatureCollection featureCollection,
        Rectangle resolution, String attributeName, String feaureUniqueIdentifier) throws IOException {

    MapContent map = new MapContent();
    map.setTitle("GeoTiff");

    featureCollection.features();
    SimpleFeatureIterator iterator = featureCollection.features();
    SimpleFeature feature;
    Collection<Property> properties;
    float valueDivided;
    double highestValue = extractHighestValue(iterator, attributeName); //this is for creating the colour spectrum, used later to create the style
    Object val;
    iterator = featureCollection.features();
    Color fillerColor;
    FeatureLayer featureLayer;
    FilterFactory ff = CommonFactoryFinder.getFilterFactory(null);
    try {
        while (iterator.hasNext()) {
            feature = iterator.next();
            SimpleFeatureType type = feature.getType();
            properties = feature.getProperties();
            for (Property property : properties) {
                String propName = property.getName().getLocalPart();
                if (propName.equals(attributeName)) {
                    val = property.getValue();
                    if (val == null) {
                        continue;
                    }
                    valueDivided = new Float((double) val / highestValue);
                    fillerColor = new Color(0, 1, 0, valueDivided);
                    featureLayer = new FeatureLayer(featureCollection, SLD.createPolygonStyle(new Color(1, 1, 1),
                            fillerColor, (float) 0.5));
                    Filter fil = ff.equals(ff.property(feaureUniqueIdentifier),
                            ff.literal(feature.getProperty(feaureUniqueIdentifier).getValue()));
                    featureLayer.setQuery(new Query(type.getName().getLocalPart(), fil));
                    map.addLayer(featureLayer);
                }
            }
        }
    } finally {
        iterator.close();
    }

    Rectangle screen = new Rectangle(1500, 1500);
    StreamingRenderer renderer = new StreamingRenderer();
    ReferencedEnvelope maxBounds = map.getMaxBounds(); // <-- this the 1st point that slows everything down
    renderer.setMapContent(map);

    BufferedImage image = new BufferedImage(screen.width, screen.height, BufferedImage.TYPE_INT_ARGB);
    map.getViewport().setBounds(maxBounds);

    Graphics2D destGraphics = image.createGraphics();
    renderer.paint(destGraphics, screen, maxBounds); // <-- this the 2nd

    Hints hints = GeoTools.getDefaultHints();
    GridCoverage2D coverage = new GridCoverageFactory(hints)
            .create("GeoTiff", image, featureCollection.getBounds());

    ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
    GeoTiffWriter writer = new GeoTiffWriter(byteArrayOutputStream);
    GeoTiffWriteParams params = new GeoTiffWriteParams();
    ParameterValue<GeoToolsWriteParams> value = GeoTiffFormat.GEOTOOLS_WRITE_PARAMS.createValue();
    value.setValue(params);
    writer.write(coverage, new GeneralParameterValue[] { value });
    writer.dispose();

    return byteArrayOutputStream.toByteArray();
}

And here's an example of what i get using this code: (which is also what i'm trying to achieve, but in less time)

enter image description here

  • Why do you believe "Now as far as i've seen, the only way to achieve different colors is to use different layers, as a layer can only have one style, so in order to do this i need to have a layer per feature" - all you need is one featureStyle in your style per class (or less if it is simple classification) – Ian Turton Dec 1 '15 at 11:44
2

You can create a style (SLD) that colors each polygon based on a classification pretty easily. Then you just add your polygon layer once with that style and map it.

See this code for a full example. Basically you get a classification function (add the gt-brewer module to your project) and then pass it a feature Collection, an attribute and the number of classes you want.

Function classify = ff.function(classifier.getName(),
    ff.property(attribute), ff.literal(numberOfClasses));
Classifier groups = (Classifier) classify.evaluate(collection);

Then for each group returned you create a Rule with a filter (based on the group min/max value) and a symboliser with a color. See the SLD Docs for more information.

Then add the layer with the created style as usual.

  • Thanks, i think that was exactly what i was looking for, i'm integrating it now. the one problem i have is that i need a very wide palate of colors, do you know which one is best? – ido flax Dec 1 '15 at 16:41
  • I usually use colorbrewer2.org (as you can see in the code) Cynthia beats you with a big stick if you use more than 12 classes! or you could use a ramp function for how ever many you need. Try createColorMap in StyleBuilder – Ian Turton Dec 1 '15 at 16:45
  • is there some document on the web that gave you the impression you can only do one color per layer? – Ian Turton Dec 1 '15 at 16:46
  • no, it was just my poor understanding. – ido flax Dec 2 '15 at 9:21
  • I have a lot more than 12 classes though... if you look at the raster that i attached, i want to achieve something like that, i tried using the pallate with the highest number of colors but that wasn't good... – ido flax Dec 2 '15 at 9:24
1

Thanks to iant i was able to solve this, i augmented the example that you provided in your answer and added a method for creating a style using a custom pallate instead of the color brewer. you can define the color spectrum and the number of colors (resolution) you want to create in the constructor that i added.

This allowed me to create rasters in various colors and resoultions, see images below the code

for anyone joining the conversation, you can check out iant's answer and example code and then see the changes in my solution.

package com.rdthree.plenty.gis;

import java.awt.Color;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Set;

import org.geotools.brewer.color.BrewerPalette;
import org.geotools.brewer.color.ColorBrewer;
import org.geotools.data.simple.SimpleFeatureCollection;
import org.geotools.data.simple.SimpleFeatureIterator;
import org.geotools.factory.CommonFactoryFinder;
import org.geotools.factory.GeoTools;
import org.geotools.filter.function.ClassificationFunction;
import org.geotools.filter.function.Classifier;
import org.geotools.filter.function.JenksNaturalBreaksFunction;
import org.geotools.filter.function.RangedClassifier;
import org.geotools.geometry.jts.Geometries;
import org.geotools.styling.FeatureTypeStyle;
import org.geotools.styling.Rule;
import org.geotools.styling.Style;
import org.geotools.styling.StyleBuilder;
import org.geotools.styling.Symbolizer;
import org.geotools.styling.TextSymbolizer;
import org.opengis.feature.Property;
import org.opengis.feature.simple.SimpleFeature;
import org.opengis.feature.simple.SimpleFeatureType;
import org.opengis.filter.Filter;
import org.opengis.filter.FilterFactory2;
import org.opengis.filter.expression.Function;
import org.opengis.filter.expression.Literal;
import org.opengis.filter.expression.PropertyName;

import com.vividsolutions.jts.geom.Geometry;

public class ClassifiedStyleCreator {

ColorBrewer brewer = ColorBrewer.instance();
public static final int MAX_NUMBER_OF_CLASSES = 100;
public static final String DEFAULT_PALETTE = "Pastel1";
private static final Function DEFAULT_CLASSIFIER = new JenksNaturalBreaksFunction();
private int numberOfClasses;
private String paletteName = DEFAULT_PALETTE;
private static StyleBuilder sb = new StyleBuilder();
private static FilterFactory2 ff = CommonFactoryFinder.getFilterFactory2();
private Function classifier = DEFAULT_CLASSIFIER;
private ArrayList<Function> classifiers = new ArrayList<Function>();
private List<Color> colors = new ArrayList<Color>();
private boolean labeled = false;
private boolean coloredStrokes = true;
private String attribute;
private BrewerPalette palette;
private String label = "";
private Color labelColor = Color.black;
private java.awt.Font font = new java.awt.Font(java.awt.Font.SANS_SERIF, java.awt.Font.PLAIN, 10);
private ArrayList<String> titles = new ArrayList<String>();
private SimpleFeatureCollection collection;

/**
 * Initialize and use Color Brewer
 * 
 * @param collection the FeatureCollection
 */
public ClassifiedStyleCreator(SimpleFeatureCollection collection, String attribute) {
    Set<Function> funcs = CommonFactoryFinder.getFunctions(GeoTools.getDefaultHints());
    ArrayList<String> fNames = new ArrayList<String>();
    for (Function func : funcs) {
        if (func instanceof ClassificationFunction) {
            classifiers.add(func);
            fNames.add(func.getName());
        }
    }
    this.collection = collection;
    this.attribute = attribute;
    setNumberOfClasses(collection.size(), true);
    setPaletteWithBrewer();
    setClassifier(DEFAULT_CLASSIFIER);
}

/**
 * Initialize and use custom color pallate. RGB values supplied will be used to set the color of the spectrum.
 * 
 * @param collection the FeatureCollection
 * @param attribute the attrubute name
 * @param changingColor the color that should be chnaging to create the pallate
 * @param rOffset the red offset 0-1
 * @param gOffset the blue offset 0-1
 * @param bOffset the green offset 0-1
 * @param resolution the number of colors the resulting pallate will have
 */
public ClassifiedStyleCreator(SimpleFeatureCollection collection, String attribute, ChangingColor changingColor,
        float rOffset, float gOffset, float bOffset, int resolution) {
    if (rOffset > 1 || gOffset > 1 || bOffset > 1) {
        throw new IllegalArgumentException("RGB vlaues must be between 0 and 1");
    }
    Set<Function> funcs = CommonFactoryFinder.getFunctions(GeoTools.getDefaultHints());
    ArrayList<String> fNames = new ArrayList<String>();
    for (Function func : funcs) {
        if (func instanceof ClassificationFunction) {
            classifiers.add(func);
            fNames.add(func.getName());
        }
    }
    this.collection = collection;
    this.attribute = attribute;
    setNumberOfClasses(collection.size(), false);
    setPaletteWithCustomSpectrum(resolution, changingColor, rOffset, gOffset, bOffset);
    setClassifier(DEFAULT_CLASSIFIER);
}

public Style getStyle() {
    Style ret = sb.createStyle();
    if (attribute == null || attribute.isEmpty()) {
        return ret;
    }

    if (colors.isEmpty()) {
        Color[] c = palette.getColors(numberOfClasses);
        colors.addAll(Arrays.asList(c));
    }
    SimpleFeatureType schema = collection.getSchema();
    Classifier groups = classify(collection, attribute, schema);
    if (groups instanceof RangedClassifier) {
        RangedClassifier rcGroups = (RangedClassifier) groups;
        if (rcGroups.getSize() != numberOfClasses) {
            throw new RuntimeException("incorrect number of classes " + rcGroups.getSize() + " expected "
                    + numberOfClasses);
        }

        Geometries geomType = Geometries.getForBinding((Class<? extends Geometry>) schema.getGeometryDescriptor()
                .getType().getBinding());
        titles = new ArrayList<String>(numberOfClasses);
        Rule[] rules = new Rule[numberOfClasses];
        for (int i = 0; i < numberOfClasses; i++) {
            Symbolizer symbolizer = null;
            Filter filter = Filter.INCLUDE;
            Object min2 = rcGroups.getMin(i);
            String min;
            if (min2 != null) {
                min = min2.toString();
            } else {
                min = "";
            }
            Object max2 = rcGroups.getMax(i);
            String max;
            if (max2 != null) {
                max = max2.toString();
            } else {
                max = "";
            }
            if (min.contains("[")) {// unique classifier set
                // TODO handle unique sets
            } else {
                PropertyName property = ff.property(attribute);

                if (!min.isEmpty() && !max.isEmpty()) { // between
                    titles.add(min + " - " + max);
                    Literal lMin = ff.literal(Double.parseDouble(min));
                    Literal lMax = ff.literal(Double.parseDouble(max));
                    filter = ff.between(property, lMin, lMax);
                } else if (min.isEmpty()) { // less than max
                    titles.add("< " + max);
                    Literal lMax = ff.literal(Double.parseDouble(max));
                    filter = ff.less(property, lMax);
                } else if (max.isEmpty()) { // greater than min
                    titles.add(">= " + min);
                    Literal lMin = ff.literal(Double.parseDouble(min));
                    filter = ff.greaterOrEqual(property, lMin);
                }
            }
            Color border;
            if (isColoredStrokes()) {
                border = colors.get(i);
            } else {
                border = Color.black;
            }
            switch (geomType) {
            case POINT:
            case MULTIPOINT:
                symbolizer = sb.createPolygonSymbolizer(colors.get(i), border, 1);
                break;
            case LINESTRING:
            case MULTILINESTRING:
                symbolizer = sb.createLineSymbolizer(colors.get(i));
                break;
            case POLYGON:
            case MULTIPOLYGON:
            case GEOMETRY:
                symbolizer = sb.createPolygonSymbolizer(colors.get(i), border, 1);
                break;
            case GEOMETRYCOLLECTION:
                throw new IllegalArgumentException("Classifier doesn't support GeometryCollections");

            }
            rules[i] = sb.createRule(symbolizer);

            rules[i].setFilter(filter);
        }
        FeatureTypeStyle fts = sb.createFeatureTypeStyle("Feature", rules);
        if (isLabeled()) {
            TextSymbolizer text = sb.createTextSymbolizer(labelColor, sb.createFont(font), label);
            fts.rules().add(sb.createRule(text));
        }
        ret.featureTypeStyles().add(fts);
    }
    return ret;
}

private Classifier classify(SimpleFeatureCollection collection, String attribute, SimpleFeatureType schema) {
    if (attribute == null || attribute.isEmpty()) {
        return null;

    }

    this.attribute = attribute;

    Function classify = ff.function(classifier.getName(), ff.property(attribute), ff.literal(numberOfClasses));
    Classifier groups = (Classifier) classify.evaluate(collection);
    return groups;

}

public void setClassifier(Function defaultClassifier) {

    this.classifier = defaultClassifier;
}

public void setClassifier(String name) {
    for (Function f : classifiers) {
        if (f.getName().equalsIgnoreCase(name)) {
            setClassifier(f);
            return;
        }
    }
}

public Function getClassifier() {
    return classifier;
}

public String getClassifierName() {
    return classifier.getName();
}

/**
 * @return the numberOfClasses
 */
public int getNumberOfClasses() {
    return numberOfClasses;
}

/**
 * @param numberOfClasses the numberOfClasses to set
 */
public void setNumberOfClasses(int numberOfClasses, boolean doBrewer) {
    if (numberOfClasses < 2)
        numberOfClasses = 2;
    this.numberOfClasses = numberOfClasses;
    // check if there are enough colors with the current pallete
    if (doBrewer) {
        setPaletteWithBrewer();
    }
}

/**
 * @return the paletteName
 */
public String getPaletteName() {
    return paletteName;
}

public BrewerPalette getPalette() {
    return palette;
}

/**
 * @param palette the palette to set
 */
public void setPaletteWithBrewer() {
    String bestPalleteName = brewer.getPaletteNames()[0];
    int differenceBetweenMaxColorsInPalateAndClassNumber;
    int lowestDifference = Integer.MAX_VALUE;
    String[] pallateNames = brewer.getPaletteNames();
    for (String pallateName : pallateNames) {
        differenceBetweenMaxColorsInPalateAndClassNumber = (Math.abs(brewer.getPalette(pallateName).getMaxColors()
                - numberOfClasses));
        if (differenceBetweenMaxColorsInPalateAndClassNumber < lowestDifference) {
            lowestDifference = differenceBetweenMaxColorsInPalateAndClassNumber;
            bestPalleteName = pallateName;
        }
    }
    this.paletteName = bestPalleteName;
    palette = brewer.getPalette(bestPalleteName);
    if (numberOfClasses > palette.getMaxColors()) {
        System.out.println("reset " + numberOfClasses + " to " + palette.getMaxColors());
        numberOfClasses = palette.getMaxColors();
    } else if (numberOfClasses < palette.getMinColors()) {
        System.out.println("reset " + numberOfClasses + " to " + palette.getMinColors());
        numberOfClasses = palette.getMinColors();
    }
    colors = Arrays.asList(palette.getColors(numberOfClasses));
}

/**
 * @param palette the palette to set
 */
public void setPaletteWithCustomSpectrum(int numberOfColors, ChangingColor changingColor, float rOffset, float gOffset, float bOffset) {
    List<Color> theColors = new ArrayList<>();
    float alpha;
    if (numberOfColors < 2) {
        System.out.println("Requested number of colors must be greater than 1, seeting number of colors to 2");
        numberOfColors = 2;
    }
    if (numberOfColors > MAX_NUMBER_OF_CLASSES) {
        System.out
                .println("Requested number of colors exceeds maximum number of classes, seeting number of colors to: "
                        + MAX_NUMBER_OF_CLASSES);
        numberOfColors = MAX_NUMBER_OF_CLASSES;
    }
    float portion = new Float((double) 1 / numberOfColors);
    for (int i = 0; i < numberOfColors; i++) {
        alpha = portion * (i + 1);
        theColors.add(new Color((changingColor.equals(ChangingColor.RED) ? 1 : 1 - alpha) * (1 - rOffset),
                (changingColor.equals(ChangingColor.GREEN) ? 1 : 1 - alpha) * (1 - gOffset), (changingColor
                        .equals(ChangingColor.BLUE) ? 1 : 1 - alpha) * (1 - bOffset)));
    }
    colors = theColors;
    numberOfClasses = numberOfColors;
}

/**
 * @return the colors
 */
public List<Color> getColors() {
    return colors;
}

/**
 * @return the labeled
 */
public boolean isLabeled() {
    return labeled;
}

/**
 * @param label the label to set (or a null or empty string to turn of labels)
 */
public void setLabelAttribute(String label) {
    if (label == null || label.isEmpty()) {
        this.label = "";
        this.labeled = false;
    }

    this.label = label;
    this.labeled = true;
}

/**
 * @return the coloredStrokes
 */
public boolean isColoredStrokes() {
    return coloredStrokes;
}

/**
 * @param coloredStrokes the coloredStrokes to set
 */
public void setColoredStrokes(boolean coloredStrokes) {
    this.coloredStrokes = coloredStrokes;
}

/**
 * @return the labelColor
 */
public Color getLabelColor() {
    return labelColor;
}

/**
 * @param labelColor the labelColor to set
 */
public void setLabelColor(Color labelColor) {
    this.labelColor = labelColor;
}

/**
 * @return the font
 */
public java.awt.Font getFont() {
    return font;
}

/**
 * @param font2 the font to set
 */
public void setFont(java.awt.Font font2) {
    this.font = font2;
}

public List<String> getPaletteNames() {
    // TODO Auto-generated method stub
    return Arrays.asList(brewer.getPaletteNames());
}

public List<String> getFunctionNames() {
    ArrayList<String> ret = new ArrayList<String>();

    for (Function f : classifiers) {
        ret.add(f.getName());

    }
    return ret;
}

public List<String> getTitles() {
    // TODO Auto-generated method stub
    return titles;
}

public double extractHighestValue(SimpleFeatureIterator iterator, String attributeName) {
    SimpleFeatureType schema = collection.getSchema();
    SimpleFeature feature;
    Collection<Property> properties;
    double highestValue = 0;
    try {
        while (iterator.hasNext()) {
            feature = iterator.next();
            properties = feature.getProperties();
            for (Property property : properties) {
                String propName = property.getName().getLocalPart();
                if (propName.equals(attributeName)) {
                    Object val = property.getValue();
                    if (val == null) {
                        continue;
                    }
                    if ((double) val > highestValue) {
                        highestValue = (double) val;
                    }
                }
            }
        }
    } finally {
        iterator.close();
    }
    return highestValue;
}

public enum ChangingColor {
    RED, GREEN, BLUE;
}

}

white to green

violets

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.