/*******************************************************************************
* Copyright (c) 2014 Open Door Logistics (www.opendoorlogistics.com)
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the GNU Lesser Public License v3
* which accompanies this distribution, and is available at http://www.gnu.org/licenses/lgpl.txt
******************************************************************************/
package com.opendoorlogistics.core.gis.map;
import gnu.trove.impl.Constants;
import gnu.trove.map.hash.TByteLongHashMap;
import gnu.trove.map.hash.TObjectByteHashMap;
import gnu.trove.map.hash.TObjectLongHashMap;
import gnu.trove.procedure.TByteLongProcedure;
import gnu.trove.procedure.TObjectLongProcedure;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.font.FontRenderContext;
import java.awt.font.TextLayout;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;
import java.util.AbstractMap;
import java.util.AbstractMap.SimpleEntry;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.TreeMap;
import javax.swing.SwingUtilities;
import com.opendoorlogistics.api.tables.ODLTableReadOnly;
import com.opendoorlogistics.core.gis.map.Symbols.SymbolType;
import com.opendoorlogistics.core.gis.map.data.DrawableObject;
import com.opendoorlogistics.core.gis.map.data.DrawableObjectImpl;
import com.opendoorlogistics.core.tables.utils.ExampleData;
import com.opendoorlogistics.core.utils.Colours;
import com.opendoorlogistics.core.utils.Colours.CalculateAverageColour;
import com.opendoorlogistics.core.utils.images.ImageUtils;
import com.opendoorlogistics.core.utils.strings.Strings;
final public class Legend {
private static final Color LEGEND_BACKGROUND_COLOUR = new Color(240, 240, 240);
public static final int DEFAULT_FONT_SIZE = 16;
public static final LegendAlignment DEFAULT_ALIGNMENT = LegendAlignment.VERTICAL;
public static void main(String[] args) {
List<Map.Entry<String, Color>> list = createDummyEntries();
for (int font : new int[] { 100,50, 30, 20, 10 }) {
final BufferedImage img = createLegendImage(list, font, LegendAlignment.VERTICAL);
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
ImageUtils.createImageFrame(img).setVisible(true);
}
});
}
}
public static BufferedImage createLegendImageFromDrawables(Iterable<? extends DrawableObject> pnts,int fontSize, LegendAlignment align) {
List<Map.Entry<String, Color>> text = getLegendText(pnts);
if (text.size() > 0) {
BufferedImage image = createLegendImage(text,fontSize,align);
return image;
}
return null;
}
public static List<Map.Entry<String, BufferedImage>> getLegendItemImages(Iterable<? extends DrawableObject> drawables, Dimension size){
List<Map.Entry<String, Color>> cols = getLegendText(drawables);
Point2D centre = new Point2D.Double(size.width/2.0, size.height/2.0);
int circum = Math.min(size.width, size.height);
circum = 2 * circum/3;
ArrayList<Map.Entry<String, BufferedImage>> ret = new ArrayList<>();
for(Map.Entry<String, Color> entry : cols){
BufferedImage img = ImageUtils.createBlankImage(size.width, size.height,Color.WHITE);
Graphics2D g = (Graphics2D)img.getGraphics();
DatastoreRenderer.drawOutlinedSymbol(g, SymbolType.CIRCLE,centre, circum, entry.getValue(), true);
g.dispose();
ret.add(new AbstractMap.SimpleEntry<String, BufferedImage>(entry.getKey(),img));
}
return ret;
}
/**
* Gets the text for the legend by averaging the colour for each distinct value of the legend key
*
* @param pnts
* @return
*/
private static List<Map.Entry<String, Color>> getLegendText(Iterable<? extends DrawableObject> pnts) {
// get average column for each legend item
TreeMap<String, CalculateAverageColour> map = new TreeMap<>();
for (DrawableObject pnt : pnts) {
if (Strings.isEmpty(pnt.getLegendKey()) == false) {
CalculateAverageColour av = map.get(pnt.getLegendKey());
if (av == null) {
av = new CalculateAverageColour();
map.put(pnt.getLegendKey(), av);
}
Color col = pnt.getLegendColour();
if(col==null){
col = DatastoreRenderer.getRenderColour(pnt,false);
}
av.add(col);
}
}
// return
ArrayList<Map.Entry<String, Color>> ret = new ArrayList<>();
for (Map.Entry<String, CalculateAverageColour> entry : map.entrySet()) {
AbstractMap.SimpleEntry<String, Color> pair = new SimpleEntry<>(entry.getKey(), entry.getValue().getAverage());
ret.add(pair);
}
// check if all entries are numbers and sort numerically if so
boolean numeric=true;
for(Map.Entry<String, Color> entry:ret){
if(Strings.isNumber(entry.getKey())==false){
numeric = false;
break;
}
}
if(numeric){
Collections.sort(ret, new Comparator<Map.Entry<String, Color> >() {
@Override
public int compare(Entry<String, Color> o1, Entry<String, Color> o2) {
try {
// parse as doubles
double d1 = Double.parseDouble(o1.getKey());
double d2 = Double.parseDouble(o2.getKey());
return Double.compare(d1, d2);
} catch (Throwable e) {
// parse as string if double parsing fails
return o1.getKey().compareTo(o2.getKey());
}
}
});
}
return ret;
}
public static BufferedImage createLegendImage(Iterable<Map.Entry<String, Color>> list) {
return createLegendImage(list, DEFAULT_FONT_SIZE, DEFAULT_ALIGNMENT);
}
public enum LegendAlignment {
HORIZONTAL, VERTICAL
}
public static BufferedImage createLegendImage(Iterable<Map.Entry<String, Color>> list, final int fontSize, final LegendAlignment align) {
class Item {
Color col;
TextLayout textLayout;
Rectangle2D textBounds;
}
// get layouts and bounds for each string
int maxTextHeight = 0;
int maxTextWidth = 0;
Font font = new Font(Font.SANS_SERIF, Font.BOLD, fontSize);
FontRenderContext frc = new FontRenderContext(font.getTransform(), true, true);
ArrayList<Item> items = new ArrayList<>();
for (Map.Entry<String, Color> pair : list) {
Item item = new Item();
item.col = pair.getValue();
item.textLayout = new TextLayout(pair.getKey(), font, frc);
item.textBounds = item.textLayout.getBounds();
maxTextHeight = Math.max((int) Math.ceil(item.textBounds.getHeight()), maxTextHeight);
maxTextWidth = Math.max((int) Math.ceil(item.textBounds.getWidth()), maxTextWidth);
items.add(item);
}
// calculate sizes based on font size
final int gapBetweenRows = 2 + fontSize / 3;
final int pointCircumference = 3 * fontSize / 4;
final int gapAfterPoint = fontSize / 3;
final int gapBeforePoint = align == LegendAlignment.VERTICAL?0: 4 * gapAfterPoint;
final int border = 2 + fontSize / 3;
final double pointVOffsetFraction = 0.475;
// get the fixed row height
int fixedRowHeight = Math.max(maxTextHeight, pointCircumference) + gapBetweenRows;
// get boxes for the point, text, whole row and finally image
Dimension pointsBox = new Dimension(gapBeforePoint + pointCircumference + gapAfterPoint, fixedRowHeight);
Dimension textBox = new Dimension(maxTextWidth, fixedRowHeight);
Dimension rowBox = new Dimension(pointsBox.width + textBox.width, fixedRowHeight);
Dimension imageSize = align == LegendAlignment.VERTICAL ? new Dimension(rowBox.width + 2 * border, rowBox.height * items.size() + 2 * border)
: new Dimension(rowBox.width * items.size() + 2 * border, rowBox.height + 2 * border);
final BufferedImage img = ImageUtils.createBlankImage(imageSize.width, imageSize.height, LEGEND_BACKGROUND_COLOUR);
Graphics2D g = img.createGraphics();
g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC);
// draw all
int y = fixedRowHeight;
int x = border;
Color fontColor = LowLevelTextRenderer.LABEL_FONT_COLOUR;
for (Item item : items) {
DatastoreRenderer.drawOutlinedSymbol(g,SymbolType.CIRCLE, new Point2D.Float(gapBeforePoint + x + pointCircumference / 2,
y - (int) (pointVOffsetFraction * pointCircumference)), pointCircumference, item.col, true);
g.setColor(fontColor);
item.textLayout.draw(g, x + pointsBox.width, y);
if(align == LegendAlignment.VERTICAL){
y += rowBox.height;
}
else{
x += pointsBox.width + (int)Math.round(item.textBounds.getWidth());
}
}
// draw border
g.setColor(Color.DARK_GRAY);
g.drawRect(0, 0, imageSize.width - 1, imageSize.height - 1);
g.dispose();
return img;
}
private static List<Map.Entry<String, Color>> createDummyEntries() {
ArrayList<Map.Entry<String, Color>> ret = new ArrayList<>();
for (String s : ExampleData.getExampleNouns()) {
AbstractMap.SimpleEntry<String, Color> entry = new SimpleEntry<>(s, Colours.getRandomColour(s));
ret.add(entry);
if (ret.size() >= 10) {
break;
}
}
return ret;
}
public static String getStandardisedLegendKey(ODLTableReadOnly drawables, int row){
String key =(String) drawables.getValueAt(row, DrawableObjectImpl.COL_LEGEND_KEY);
return getStandardisedLegendKey(key);
}
/**
* Get the standardised legend key or return null if key would not be used as a legend
* @param rawKeyFieldValue
* @return
*/
public static String getStandardisedLegendKey(String rawKeyFieldValue) {
if(rawKeyFieldValue!=null && rawKeyFieldValue.length()>0){
String stdKey = Strings.std(rawKeyFieldValue);
if(stdKey!=null && stdKey.length()>0){
return stdKey;
}
}
return null;
}
/**
* A class which processes the legend logic directly from a drawable table
* @author Phil
*
*/
public static class LegendDrawableTableBuilder{
private static class CalcLegendEntry{
String unstandard;
String std;
CalculateAverageColour col = new CalculateAverageColour();
//StandardisedStringTreeMap<Long> symbolCount = new StandardisedStringTreeMap<Long>();
TObjectLongHashMap<String> symbolCount = new TObjectLongHashMap<String>(Constants.DEFAULT_CAPACITY, Constants.DEFAULT_LOAD_FACTOR, 0);
TByteLongHashMap outlinedCount = new TByteLongHashMap(Constants.DEFAULT_CAPACITY, Constants.DEFAULT_LOAD_FACTOR,(byte) 0, (byte)0);
}
private final HashMap<String,CalcLegendEntry> workingMap = new HashMap<String,Legend.LegendDrawableTableBuilder.CalcLegendEntry>();
// public static interface LegendRowData{
// public String key();
// public Color color();
// public Color legendColour();
// public String colourKey();
// public String symbol();
// public Long outline();
// }
public void processRow(ODLTableReadOnly drawables, int row){
String key =(String) drawables.getValueAt(row, DrawableObjectImpl.COL_LEGEND_KEY);
String stdKey = getStandardisedLegendKey(key);
if(stdKey!=null){
// get entry or create if needed
CalcLegendEntry entry = workingMap.get(stdKey);
if(entry==null){
entry = new CalcLegendEntry();
entry.unstandard = key;
entry.std = stdKey;
workingMap.put(stdKey, entry);
}
// process the colour
Color color = (Color )drawables.getValueAt(row, DrawableObjectImpl.COL_LEGEND_COLOUR);
if(color==null){
color = DatastoreRenderer.getNoAlphaColour((Color)drawables.getValueAt(row, DrawableObjectImpl.COL_COLOUR), (String)drawables.getValueAt(row, DrawableObjectImpl.COL_COLOUR_KEY));
}
entry.col.add(color);
// process the symbol
String symbol = (String)drawables.getValueAt(row, DrawableObjectImpl.COL_SYMBOL);
if(symbol == null){
symbol = "";
}
else{
symbol = Strings.std(symbol);
}
long count = entry.symbolCount.get(symbol);
count++;
entry.symbolCount.put(symbol, count);
// process outlined
Long outlined = (Long)drawables.getValueAt(row, DrawableObjectImpl.COL_OUTLINE);
if(outlined==null){
outlined = DrawableObjectImpl.DEFAULT_DRAW_OUTLINE;
}
byte byteOutlined = (byte)(outlined==1 ? 1 : 0);
count = entry.outlinedCount.get(byteOutlined);
count++;
entry.outlinedCount.put(byteOutlined, count);
}
}
public List<Map.Entry<String, BufferedImage>> build(Dimension imageSize){
// sort all entries in a manner sensitive to numbers etc...
ArrayList<CalcLegendEntry> entries = new ArrayList<Legend.LegendDrawableTableBuilder.CalcLegendEntry>(workingMap.values());
Collections.sort(entries, new Comparator<CalcLegendEntry>() {
@Override
public int compare(CalcLegendEntry o1, CalcLegendEntry o2) {
return Strings.compareStd(o1.std, o2.std, true);
}
});
// work out symbol size
Point2D centre = new Point2D.Double(imageSize.width/2.0, imageSize.height/2.0);
int circum = Math.min(imageSize.width, imageSize.height);
circum = 2 * circum/3;
ArrayList<Map.Entry<String, BufferedImage>> ret = new ArrayList<>();
// process each one
for(CalcLegendEntry entry: entries){
// get average colour
Color col = entry.col.getAverage();
// get modal average symbol
class CountMax{
long max;
String symbol;
long outlined;
}
CountMax countMax = new CountMax();
entry.symbolCount.forEachEntry(new TObjectLongProcedure<String>() {
@Override
public boolean execute(String a, long b) {
if(countMax.symbol==null || countMax.max < b){
countMax.symbol = a;
countMax.max = b;
}
return true;
}
});
// get modal average outlined boolean
countMax.max=0;
countMax.outlined = DrawableObjectImpl.DEFAULT_DRAW_OUTLINE;
entry.outlinedCount.forEachEntry(new TByteLongProcedure() {
@Override
public boolean execute(byte a, long b) {
if(countMax.max < b){
countMax.outlined = a;
countMax.max = b;
}
return true;
}
});
// draw the picture using average colour, symbol and outlining across all entries with the same std legend key
BufferedImage img = ImageUtils.createBlankImage(imageSize.width, imageSize.height,Color.WHITE);
Graphics2D g = (Graphics2D)img.getGraphics();
SymbolType type = DatastoreRenderer.getSymbolType(countMax.symbol);
DatastoreRenderer.drawOutlinedSymbol(g, type,centre, circum, col, countMax.outlined==1);
g.dispose();
ret.add(new AbstractMap.SimpleEntry<String, BufferedImage>(entry.unstandard,img));
}
return ret;
}
}
}