Implement custom Renderer like ClassBreaksRenderer

1356
4
05-18-2017 01:57 AM
BatbayarBazarragchaa
New Contributor III

I'm trying to implement custom renderer to display device aging in different colors.

For example:

0-10 years > green

10-15 years > yellow

15+ years > red

My feature has installed date attribute which we use to calculate its age.

I've tried using ClassBreaksRenderer but its returning wrong color for age when we show the feature on map, when we test getSymbol(feature) method it returns correct color. I don't know if its a bug or I'm doing something wrong.

So I've implemented my own renderer by extending BaseRenderer but its getSymbol(feature) method is not called. Only getType() is called.

Here's the source code of my custom renderer:

import com.esri.core.geometry.Geometry;
import com.esri.core.map.Feature;
import com.esri.core.renderer.BaseRenderer;
import com.esri.core.symbol.SimpleFillSymbol;
import com.esri.core.symbol.SimpleLineSymbol;
import com.esri.core.symbol.SimpleMarkerSymbol;
import com.esri.core.symbol.Symbol;
import java.awt.Color;
import java.util.Date;
import java.util.TreeMap;

/**
 *
 * @author MethoD
 */
public class DeviceAgingRenderer extends BaseRenderer {

    private final String attribute;
    private final Geometry.Type geometryType;
    private final TreeMap<Integer, Color> ageToColorMap;

    public DeviceAgingRenderer(String attribute, Geometry.Type geometryType) {
        this.attribute = attribute;
        this.geometryType = geometryType;
        //this.ageToColorMap = ageToColorMap;
        ageToColorMap = new TreeMap<>();
        ageToColorMap.put(-1, new Color(30, 109, 237)); // no data > blue
        ageToColorMap.put(0, new Color(22, 183, 22)); // 0-10 > green
        ageToColorMap.put(10, new Color(244, 156, 4)); // 10-15 > yellow
        ageToColorMap.put(15, new Color(224, 30, 13)); // 15+ red
    }

    @Override
    protected String getType() {
        return "deviceAging";
    }

    @Override
    public Symbol getSymbol(Feature feature) {
        System.out.println("feature: " + feature); // not prninted and not called
        int age = -1;
        if (feature.getAttributeValue(attribute) != null) {
            Long timestamp = (Long) feature.getAttributeValue(attribute);
            age = getDiffYears(new Date(), new Date(timestamp));
        }
        Color color = ageToColorMap.floorEntry(age).getValue();
        switch (geometryType) {
            case POLYLINE:
            case LINE:
                return new SimpleLineSymbol(color, 2, SimpleLineSymbol.Style.SOLID);
            case POLYGON:
            case ENVELOPE:
                return new SimpleFillSymbol(color);
            case MULTIPOINT:
            case POINT:
            default:
                return new SimpleMarkerSymbol(color, 5, SimpleMarkerSymbol.Style.CIRCLE);
        }
    }

    public static int getDiffYears(Date end, Date start) {
        Calendar c_start = getCalendar(start);
        Calendar c_end = getCalendar(end);
        int diff = c_end.get(Calendar.YEAR) - c_start.get(Calendar.YEAR);
        if (c_start.get(Calendar.MONTH) > c_end.get(Calendar.MONTH)
                || (c_start.get(Calendar.MONTH) == c_end.get(Calendar.MONTH) && c_start.get(Calendar.DATE) > c_end.get(Calendar.DATE))) {
            diff--;
        }
        return diff;
    }
}
‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍
‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍


I'm adding my renderer as following.

final GeodatabaseFeatureServiceTable fsTable = new GeodatabaseFeatureServiceTable(layerData.getFullUrl(), arcgisSdkConfigurator.getUserCredentials(), layerData.getLayerIndex());
fsTable.initialize(new CallbackListener<GeodatabaseFeatureServiceTable.Status>() {
    @Override
    public void onCallback(GeodatabaseFeatureServiceTable.Status status) {
        if (status == GeodatabaseFeatureServiceTable.Status.INITIALIZED) {
            // prepare out fields
            List<String> attributeList = AstFeatureNameUtil.extractAttributeList(layerData.getIdentifierPattern());
            attributeList.add("InstallationDate");
            String[] attributes = new String[attributeList.size()];
            attributeList.toArray(attributes);
            fsTable.setOutFields(attributes);

            final FeatureLayer featureLayer = new FeatureLayer(fsTable);
            featureLayer.initializeAsync(); // need?
            featureLayer.addLayerInitializeCompleteListener(new LayerInitializeCompleteListener() {
                @Override
                public void layerInitializeComplete(LayerInitializeCompleteEvent lice) {
                    //LOGGER.log(Level.INFO, "Found features: {0} - {1}", new Object[]{layerData.getLayerId(), fsTable.getNumberOfFeatures()});
                    //LOGGER.log(Level.INFO, "Layer initialized: {0}", lice.getID());
                    DeviceAgingRenderer renderer = new DeviceAgingRenderer("InstallationDate", featureLayer.getGeometryType());
                    //LOGGER.log(Level.INFO, "Renderer: {0}", renderer);
                    featureLayer.setRenderer(renderer);

                    jMap.getLayers().add(featureLayer);
                    jMap.addMapOverlay(new DeviceAgingFeatureMapOverlay(jMap, featureLayer));
                    jLayerTree.refresh();
                }
            });
        }
    }

    @Override
    public void onError(Throwable e) {
        LOGGER.log(Level.SEVERE, null, e);
        makeBusy(false);
    }
});‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍
0 Kudos
4 Replies
EricBader
Occasional Contributor III

Your custom renderer will not work because extending BaseRenderer is not supported.

BaseRenderer (ArcGIS_Runtime_Java 10.2.4 API) 

Can you share your ClassBreaksRenderer code that isn't working for you?

Have you reviewed this sample? Class breaks renderer | ArcGIS for Developers 

0 Kudos
BatbayarBazarragchaa
New Contributor III

I'm using ClassBreaksRenderer on Date field. I prepare ageToColorMap where I store ages and its display color and iterate it to create breaks for ClassBreakRenderer.

The problem is it chose the wrong color, 2015-11-25 00:00:00 (1448380800000) should be green but its displayed as red as on following screenshot:

Here's the age to color map:

ageToColorMap.put(0, new Color(22, 183, 22)); // 0-10 green
ageToColorMap.put(10, new Color(144, 244, 4)); // 10-15
ageToColorMap.put(15, new Color(192, 244, 4)); // 15-20
ageToColorMap.put(20, new Color(244, 240, 4)); // 20-25
ageToColorMap.put(25, new Color(244, 208, 4)); // 25-30
ageToColorMap.put(30, new Color(244, 156, 4)); // 30-35, yellow
ageToColorMap.put(35, new Color(224, 86, 13)); // 35-40
ageToColorMap.put(40, new Color(224, 30, 13)); // 40+ red‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍

So the actual Breaks are:

AgingTimestampsColor
0-101179806346212 - 1495421946211java.awt.Color[r=22,g=183,b=22]
10-151022036346212 - 1179806346211java.awt.Color[r=144,g=244,b=4]
15-20864269946212 - 1022036346211java.awt.Color[r=192,g=244,b=4]
20-25706503546212 - 864269946211java.awt.Color[r=244,g=240,b=4]
25-30548650746212 - 706503546211java.awt.Color[r=244,g=208,b=4]
30-35390887946212 - 548650746211java.awt.Color[r=244,g=156,b=4]
35-40233125146212 - 390887946211java.awt.Color[r=224,g=86,b=13]
40+0 - 233125146211java.awt.Color[r=224,g=30,b=13]

Here's my code to create ClassBreaksRenderer:

    private ClassBreaksRenderer getClassBreaksRenderer(String selectedField, Geometry.Type geometryType) {
        ClassBreaksRenderer classBreaksRenderer = new ClassBreaksRenderer(getMarkerSymbol(geometryType, new Color(30, 109, 237)), selectedField);

        Date _now = new Date();
        for (Integer age : ageToColorMap.keySet()) {
            Integer nextAge = ageToColorMap.ceilingKey(age + 1);

            double endTimestamp = DateUtils.add(_now, -age, 0, 0).getTime() - 1;
            double startTimestamp;
            if (nextAge != null) {
                startTimestamp = DateUtils.add(_now, -nextAge, 0, 0).getTime();
            } else {
                startTimestamp = new Date(0).getTime();
            }

            classBreaksRenderer.addBreak(startTimestamp, endTimestamp, getMarkerSymbol(geometryType, ageToColorMap.get(age)));
        }
        return classBreaksRenderer;
    }‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍

I call this after feature service table is initialized:

    final GeodatabaseFeatureServiceTable fsTable = new GeodatabaseFeatureServiceTable(layerData.getFullUrl(), arcgisSdkConfigurator.getUserCredentials(), layerData.getLayerIndex());
    fsTable.initialize(new CallbackListener<GeodatabaseFeatureServiceTable.Status>() {
        @Override
        public void onCallback(GeodatabaseFeatureServiceTable.Status status) {
            if (status == GeodatabaseFeatureServiceTable.Status.INITIALIZED) {
                // prepare out fields
                List<String> attributeList = AstFeatureNameUtil.extractAttributeList(layerData.getIdentifierPattern());
                attributeList.add("InstallationDate");
                String[] attributes = new String[attributeList.size()];
                attributeList.toArray(attributes);
                fsTable.setOutFields(attributes);

                final FeatureLayer featureLayer = new FeatureLayer(fsTable);
                featureLayer.initializeAsync(); // need?
                featureLayer.addLayerInitializeCompleteListener(new LayerInitializeCompleteListener() {
                    @Override
                    public void layerInitializeComplete(LayerInitializeCompleteEvent lice) {
                        Renderer renderer = getClassBreaksRenderer("InstallationDate", featureLayer.getGeometryType());
                        featureLayer.setRenderer(renderer);

                        jMap.getLayers().add(featureLayer);
                        jMap.addMapOverlay(new DeviceAgingFeatureMapOverlay(jMap, featureLayer));
                        jLayerTree.refresh();
                    }
                });
            }
        }

        @Override
        public void onError(Throwable e) {
            LOGGER.log(Level.SEVERE, null, e);
            makeBusy(false);
        }
    });‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍

getMarkerSymbol is simple method to get symbol for each geometry type with given color:

private Symbol getMarkerSymbol(Geometry.Type geometryType, Color color) {
    switch (geometryType) {
        case POLYLINE:
        case LINE:
            return new SimpleLineSymbol(color, 2, SimpleLineSymbol.Style.SOLID);
        case POLYGON:
        case ENVELOPE:
            return new SimpleFillSymbol(color);
        case MULTIPOINT:
        case POINT:
        default:
            return new SimpleMarkerSymbol(color, 10, SimpleMarkerSymbol.Style.CIRCLE);
    }
}
0 Kudos
BatbayarBazarragchaa
New Contributor III

I found out the problem. When I add breaks with hard-coded values it works correctly.

ClassBreaksRenderer renderer = new ClassBreaksRenderer(getMarkerSymbol(featureLayer.getGeometryType(), new Color(30, 109, 237)), "InstallationDate");

renderer.addBreak(0, 233238209948d, getMarkerSymbol(featureLayer.getGeometryType(), new Color(22, 183, 22)));
renderer.addBreak(233238209949d, 391001009948d, getMarkerSymbol(featureLayer.getGeometryType(), new Color(144, 244, 4)));
renderer.addBreak(391001009949d, 548763809948d, getMarkerSymbol(featureLayer.getGeometryType(), new Color(192, 244, 4)));
renderer.addBreak(548763809949d, 706616609948d, getMarkerSymbol(featureLayer.getGeometryType(), new Color(244, 240, 4)));
renderer.addBreak(706616609949d, 864383009948d, getMarkerSymbol(featureLayer.getGeometryType(), new Color(244, 208, 4)));
renderer.addBreak(864383009949d, 1022149409948d, getMarkerSymbol(featureLayer.getGeometryType(), new Color(244, 156, 4)));
renderer.addBreak(1022149409949d, 1179919409948d, getMarkerSymbol(featureLayer.getGeometryType(), new Color(224, 86, 13)));
renderer.addBreak(1179919409949d, 1495535009948d, getMarkerSymbol(featureLayer.getGeometryType(), new Color(224, 30, 13)));
‍‍‍‍‍‍‍‍‍‍

When I generate values and add using for loop it doesn't work correctly. I double checked the values in array list.

ClassBreaksRenderer renderer = new ClassBreaksRenderer(getMarkerSymbol(featureLayer.getGeometryType(), new Color(30, 109, 237)), "InstallationDate");
for (AgeRangeData ageRangeData : ageRangeList) {
    System.out.println(ageRangeData.getAge() + " - " + ageRangeData.getMin() + " - " + ageRangeData.getMax() + " - " + ageToColorMap.get(ageRangeData.getAge()));
    renderer.addBreak(ageRangeData.getMin(), ageRangeData.getMax(), getMarkerSymbol(featureLayer.getGeometryType(), ageToColorMap.get(ageRangeData.getAge())));
}‍‍‍‍‍

It's so weird. I prepare ageRangeList using following method:

private void generateRenderers() {
    ageRangeList = new ArrayList<>();

    Date _now = new Date();
    for (Integer age : ageToColorMap.keySet()) {
        Integer nextAge = ageToColorMap.ceilingKey(age + 1);

        double endTimestamp = new Long(DateUtils.add(_now, -age, 0, 0).getTime() - 1).doubleValue();
        double startTimestamp;
        if (nextAge != null) {
            startTimestamp = new Long(DateUtils.add(_now, -nextAge, 0, 0).getTime()).doubleValue();
        } else {
            startTimestamp = new Long(new Date(0).getTime()).doubleValue();
        }

        //System.out.println(df.format(startTimestamp) + " - " + df.format(endTimestamp));
        ageRangeList.add(new AgeRangeData(age, startTimestamp, endTimestamp));
    }
    Collections.sort(ageRangeList, new Comparator<AgeRangeData>() {
        @Override
        public int compare(AgeRangeData o1, AgeRangeData o2) {
            if (o1.getMin() > o2.getMin()) {
                return 1;
            } else if (o1.getMin() < o2.getMin()) {
                return -1;
            } else {
                return 0;
            }
        }
    });
}‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍

AgeRangeData is just a simple POJO class.

public class AgeRangeData {

    private final int age;
    private final double min;
    private final double max;

    public AgeRangeData(int age, double min, double max) {
        this.age = age;
        this.min = min;
        this.max = max;
    }

    public int getAge() {
        return age;
    }

    public double getMin() {
        return min;
    }

    public double getMax() {
        return max;
    }
}
0 Kudos
BatbayarBazarragchaa
New Contributor III

After doing further testing my hard-coded values not working correctly either.

I'm seeing that it compares the double values differently or there's different problem or a bug.

Even though I add breaks it founds incorrect renderer.

I've tested with only 2 break and the date "2016-12-20 00:00:00" (1482163200000) finds color green from following break:

ClassBreaksRenderer renderer = new ClassBreaksRenderer(getMarkerSymbol(geometryType, new Color(30, 109, 237)), field); // default color = blue

renderer.addBreak(0, 117999234L, getMarkerSymbol(geometryType, new Color(22, 183, 22))); // green

renderer.addBreak(1179992346809L, 1493568000000L, getMarkerSymbol(geometryType, new Color(244, 240, 4))); // yellow

1482163200000 should rendered in yellow but it rendered green where break min and max are 0 and 117999234.

But "2016-12-20 00:00:00" (1482163200000) is greater than range 117999234 and is between 1179992346809 and 1493568000000,

0 Kudos