/*******************************************************************************
* 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 static com.opendoorlogistics.core.gis.map.RenderProperties.LEGEND_BOTTOM;
import static com.opendoorlogistics.core.gis.map.RenderProperties.LEGEND_BOTTOM_LEFT;
import static com.opendoorlogistics.core.gis.map.RenderProperties.LEGEND_BOTTOM_RIGHT;
import static com.opendoorlogistics.core.gis.map.RenderProperties.LEGEND_TOP;
import static com.opendoorlogistics.core.gis.map.RenderProperties.LEGEND_TOP_LEFT;
import static com.opendoorlogistics.core.gis.map.RenderProperties.LEGEND_TOP_RIGHT;
import static com.opendoorlogistics.core.gis.map.RenderProperties.SHOW_TEXT;
import gnu.trove.list.array.TIntArrayList;
import gnu.trove.list.array.TLongArrayList;
import gnu.trove.set.hash.TLongHashSet;
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.Rectangle;
import java.awt.Shape;
import java.awt.Stroke;
import java.awt.geom.AffineTransform;
import java.awt.geom.Area;
import java.awt.geom.Path2D;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import org.geotools.geometry.jts.LiteShape;
import com.opendoorlogistics.api.geometry.LatLongToScreen;
import com.opendoorlogistics.core.AppConstants;
import com.opendoorlogistics.core.cache.ApplicationCache;
import com.opendoorlogistics.core.cache.RecentlyUsedCache;
import com.opendoorlogistics.core.geometry.JTSUtils;
import com.opendoorlogistics.core.geometry.ODLGeomImpl;
import com.opendoorlogistics.core.geometry.ODLGeomImpl.AtomicGeomType;
import com.opendoorlogistics.core.gis.map.Legend.LegendAlignment;
import com.opendoorlogistics.core.gis.map.OnscreenGeometry.CachedGeomKey;
import com.opendoorlogistics.core.gis.map.Symbols.SymbolType;
import com.opendoorlogistics.core.gis.map.data.DrawableObject;
import com.opendoorlogistics.core.gis.map.data.LatLongImpl;
import com.opendoorlogistics.core.gis.map.data.UserRenderFlags;
import com.opendoorlogistics.core.utils.Colours;
import com.opendoorlogistics.core.utils.IntUtils;
import com.opendoorlogistics.core.utils.SimpleSoftReferenceMap;
import com.opendoorlogistics.core.utils.images.ImageUtils;
import com.opendoorlogistics.core.utils.iterators.IteratorUtils;
import com.opendoorlogistics.core.utils.strings.StandardisedCache;
import com.opendoorlogistics.core.utils.strings.Strings;
import com.vividsolutions.jts.awt.PolygonShape;
import com.vividsolutions.jts.geom.Coordinate;
import com.vividsolutions.jts.geom.CoordinateSequence;
import com.vividsolutions.jts.geom.Envelope;
import com.vividsolutions.jts.geom.Geometry;
import com.vividsolutions.jts.geom.GeometryCollection;
import com.vividsolutions.jts.geom.LineString;
import com.vividsolutions.jts.geom.Point;
import com.vividsolutions.jts.geom.Polygon;
import com.vividsolutions.jts.index.quadtree.Quadtree;
import com.vividsolutions.jts.math.MathUtil;
public class DatastoreRenderer implements ObjectRenderer{
public static final int CACHE_MIN_IMAGE_SIZE_LIMIT = 4 * 4;
public static final int MAX_IMAGE_SIZE_LIMIT = 3000 * 3000;
static final Color SELECTION_COLOUR = new Color(0, 0, 255);
private static final float SELECTION_DARKEN_OUTLINE_FACTOR = 0.6f;
private final SimpleSoftReferenceMap<DrawnSymbol, DrawnSymbol> circleImageCache = new SimpleSoftReferenceMap<>();
private static final Symbols symbols = new Symbols();
private static class LineDashConfig{
final long flag;
final float [] dash;
final float phase;
LineDashConfig(long flag, float[] dash, float phase) {
super();
this.flag = flag;
this.dash = dash;
this.phase = phase;
}
}
private static final LineDashConfig [] DASH_CONFIGS = new LineDashConfig[]{
new LineDashConfig(UserRenderFlags.DOT_DASH_LINE, new float[]{3,9,21,9}, 0),
new LineDashConfig(UserRenderFlags.DOTTED_LINE, new float[]{3,9}, 0),
};
/**
* Remove small gaps between polygons by detecting any 0-alpha pixel surrounded by a majority of non 0-alpha pixels
*
* @param img
*/
public static BufferedImage postProcessImage(BufferedImage img) {
java.awt.Point[] ngbs = new java.awt.Point[8];
ngbs[0] = new java.awt.Point(-1, -1);
ngbs[1] = new java.awt.Point(0, -1);
ngbs[2] = new java.awt.Point(+1, -1);
ngbs[3] = new java.awt.Point(-1, 0);
ngbs[4] = new java.awt.Point(+1, 0);
ngbs[5] = new java.awt.Point(-1, +1);
ngbs[6] = new java.awt.Point(0, +1);
ngbs[7] = new java.awt.Point(+1, +1);
class ToSet {
int x;
int y;
int rgba;
}
ArrayList<ToSet> toSets = new ArrayList<>();
TIntArrayList ngbColours = new TIntArrayList();
// each pixel, apart from edge pixels, has 8 neighbours
int width = img.getWidth();
int height = img.getHeight();
for (int x = 0; x < width; x++) {
for (int y = 0; y < height; y++) {
// img.getData().geta
int rgba = img.getRGB(x, y);
int alpha = (rgba & (0xFF000000)) >> 24;
if (alpha == 0) {
// int ngbCount = 0;
int zeroAlphaNgbCount = 0;
int nonZeroAlphaNgbCount = 0;
ngbColours.clear();
for (java.awt.Point ngb : ngbs) {
int xi = x + ngb.x;
int yi = y + ngb.y;
if (xi < width && xi >= 0 && yi < height && yi >= 0) {
// ngbCount++;
int orgba = img.getRGB(xi, yi);
int oalpha = (orgba & (0xFF000000)) >> 24;
if (oalpha == 0) {
zeroAlphaNgbCount++;
} else {
nonZeroAlphaNgbCount++;
ngbColours.add(orgba);
}
}
}
if (nonZeroAlphaNgbCount > zeroAlphaNgbCount) {
// set to majority colour
ToSet toSet = new ToSet();
toSet.x = x;
toSet.y = y;
toSet.rgba = IntUtils.getModal(ngbColours);
toSets.add(toSet);
}
}
}
}
// take copy of input image and do the sets
BufferedImage correctedImg = ImageUtils.deepCopy(img);
for (ToSet toSet : toSets) {
correctedImg.setRGB(toSet.x, toSet.y, toSet.rgba);
}
// create the argb final image with a fade and then everything on top of it
// BufferedImage ret = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
// Graphics2D g = null;
// try {
// g = (Graphics2D) ret.getGraphics();
// g.setClip(0, 0, width, height);
// renderFade(g);
// g.drawImage(correctedImg, 0, 0, null);
// } finally {
// if (g != null) {
// g.dispose();
// }
// }
return correctedImg;
}
// public static void renderFade(Graphics2D g) {
// Color fade = new Color(255, 255, 255, 100);
// Rectangle bounds = g.getClipBounds();
// g.setColor(fade);
// g.fillRect(bounds.x, bounds.y, bounds.width, bounds.height);
// }
// public void renderObjects(Graphics2D g, Iterable<? extends DrawableObject> pnts, LatLongToScreen converter, long renderFlags) {
// renderObjects(g, pnts, converter, renderFlags, null);
// }
public synchronized void renderTexts(final Graphics2D g, Iterable<? extends DrawableObject> pnts, final LatLongToScreen converter, long renderflags) {
// sort objects by priority (lowest first), speeding up the sort by making use of the fact that priority will mostly be the same
int pntsCount=0;
TreeMap<Long, LinkedList<DrawableObject>> sortedMap = new TreeMap<>();
for(DrawableObject obj : pnts){
long priority = obj.getLabelPriority();
LinkedList<DrawableObject> list = sortedMap.get(priority);
if(list==null){
list = new LinkedList<>();
sortedMap.put(priority, list);
}
list.add(obj);
pntsCount++;
}
// now place back into a single collection and replace the input iterable with it
ArrayList<DrawableObject> sorted = new ArrayList<>(pntsCount);
for(LinkedList<DrawableObject> list:sortedMap.values()){
sorted.addAll(list);
}
pnts = sorted;
// check for label groups
StandardisedCache stdCache = new StandardisedCache();
HashMap<String, ArrayList<DrawableObject>> labelGroups = new HashMap<>();
for (DrawableObject obj : pnts) {
if (!Strings.isEmpty(obj.getLabelGroupKey())) {
String std = stdCache.std(obj.getLabelGroupKey());
ArrayList<DrawableObject> list = labelGroups.get(std);
if (list == null) {
list = new ArrayList<>();
labelGroups.put(std, list);
}
list.add(obj);
}
}
// process label groups to decide which object gets the label
HashMap<String, DrawableObject> labelGroupWinningObjects = new HashMap<>();
for (Map.Entry<String, ArrayList<DrawableObject>> entry : labelGroups.entrySet()) {
ArrayList<DrawableObject> group = entry.getValue();
// Simple rule - if they are all linestrings then pick the longest segment otherwise pick the most central.
// Ignore onscreen / offscreen for the moment (so label has a fixed position).
boolean allLineStrings = true;
Point2D.Double sum = new Point2D.Double();
long count = 0;
double longestLineStringLength = Double.NEGATIVE_INFINITY;
DrawableObject longestLineString = null;
int n = group.size();
ArrayList<Point2D> centroids = new ArrayList<>(n);
for (DrawableObject obj : group) {
if (obj.getGeometry() == null) {
Point2D centroid = converter.getOnScreenPixelPosition(obj);
centroids.add(centroid);
sum.x += centroid.getX();
sum.y += centroid.getY();
count++;
allLineStrings = false;
} else {
// check for linestrings and record the longest
ODLGeomImpl geom = obj.getGeometry();
if(geom.isLineString()){
OnscreenGeometry transformed = getCachedGeometry(obj.getGeometry(), converter, true);
double length = transformed.getLineStringLength();
if (length > longestLineStringLength) {
longestLineStringLength = length;
longestLineString = obj;
}
}else{
allLineStrings = false;
}
// add to sum
Point2D centroid = geom.getWorldBitmapCentroid(converter);
centroids.add(centroid);
if(centroid!=null){
sum.x += centroid.getX();
sum.y += centroid.getY();
count++;
}
}
}
if (allLineStrings) {
if (longestLineString != null) {
labelGroupWinningObjects.put(entry.getKey(), longestLineString);
}
} else {
// determine the most central object
if (count > 0) {
Point2D.Double centroid = new Point2D.Double(sum.x / count, sum.y / count);
double minDistSqd = Double.POSITIVE_INFINITY;
DrawableObject minDistObj = null;
for (int i = 0; i < n; i++) {
Point2D objCentroid = centroids.get(i);
if (objCentroid != null) {
double distSqd = centroid.distanceSq(objCentroid);
if (distSqd < minDistSqd) {
minDistSqd = distSqd;
minDistObj = group.get(i);
}
}
}
if (minDistObj != null) {
labelGroupWinningObjects.put(entry.getKey(), minDistObj);
}
}
}
}
final Quadtree textQuadtree = new Quadtree();
final Rectangle2D viewport = converter.getViewportWorldBitmapScreenPosition();
// call which checks for on-screen etc
final LowLevelTextRenderer lowLevelRenderer = new LowLevelTextRenderer();
class TextDrawer {
void render(DrawableObject obj) {
if(!isVisibleAtZoom(obj, converter.getZoomForObjectFiltering())){
return;
}
if (Strings.isEmpty(obj.getLabel()) == false) {
boolean visible = false;
if (obj.getGeometry() == null) {
visible = getPointIntersectsScreen(g, obj, converter.getOnScreenPixelPosition(obj));
} else {
Rectangle2D bounds = getRenderedWorldBitmapBounds(obj, converter);
if(bounds!=null){
visible = bounds.intersects(viewport);
}
// CachedGeometry transformed = getCachedGeometry(obj.getGeometry(), converter, true);
// if (transformed != null) {
// Rectangle2D bounds = transformed.getWorldBitmapBounds();
// visible = bounds.intersects(viewport);
// }
}
if (visible) {
lowLevelRenderer.renderDrawableText(g, converter, obj, textQuadtree);
}
}
}
}
TextDrawer drawer = new TextDrawer();
// Draw OSM copyright (and zoom too if this is on)
if ((renderflags & RenderProperties.DRAW_OSM_COPYRIGHT) == RenderProperties.DRAW_OSM_COPYRIGHT) {
lowLevelRenderer.renderInBottomCorner(AppConstants.OSM_COPYRIGHT + ". Z" + ZoomConverter.toExternal(converter.getZoomForObjectFiltering()), 9, g, textQuadtree, true);
// lowLevelRenderer.renderInBottomCorner("Zoom " + converter.getZoomForObjectFiltering(), 9, g, textQuadtree, false);
}
// render groups first, rendering the winning object only
// int speedTest=1000;
// for(int i =0 ; i < speedTest ; i++){
for (DrawableObject obj : pnts) {
if (!Strings.isEmpty(obj.getLabelGroupKey())) {
String std = stdCache.std(obj.getLabelGroupKey());
if (obj == labelGroupWinningObjects.get(std)) {
drawer.render(obj);
}
}
}
// then render non-groups (assumed to be less important than groups)
for (DrawableObject obj : pnts) {
if (Strings.isEmpty(obj.getLabelGroupKey())) {
drawer.render(obj);
}
}
// }
}
/**
* Render fade, objects, text and legend
*
* @param g
* @param pnts
* @param converter
* @param renderFlags
* @param selectedObjectIds
*/
public synchronized void renderAll(Graphics2D g, Iterable<? extends DrawableObject> pnts, LatLongToScreen converter, long renderFlags, TLongHashSet selectedObjectIds) {
// draw objects
for (DrawableObject pnt : pnts) {
if (pnt != null) {
boolean isSelected = selectedObjectIds != null ? selectedObjectIds.contains(pnt.getGlobalRowId()) : false;
renderObject(g, converter, pnt, isSelected,0);
}
}
// then text - earlier objects get text rendering priority
if ((renderFlags & SHOW_TEXT) == SHOW_TEXT) {
renderTexts(g, pnts, converter, renderFlags);
}
renderLegend(g, pnts, renderFlags);
}
void renderLegend(Graphics2D g, Iterable<? extends DrawableObject> pnts, long renderFlags) {
// finally legend (if flagged)
if ((renderFlags & (LEGEND_TOP_LEFT | LEGEND_TOP_RIGHT | LEGEND_BOTTOM_LEFT | LEGEND_BOTTOM_RIGHT | LEGEND_TOP | LEGEND_BOTTOM)) != 0) {
boolean horizontal = (renderFlags & (LEGEND_TOP | LEGEND_BOTTOM)) != 0;
BufferedImage legend = Legend.createLegendImageFromDrawables(pnts, Legend.DEFAULT_FONT_SIZE, horizontal ? LegendAlignment.HORIZONTAL : LegendAlignment.VERTICAL);
if (legend != null) {
int lw = legend.getWidth();
int lh = legend.getHeight();
int iw = g.getClipBounds().width;
int ih = g.getClipBounds().height;
if ((renderFlags & LEGEND_TOP_LEFT) == LEGEND_TOP_LEFT) {
g.drawImage(legend, 0, 0, null);
}
if ((renderFlags & LEGEND_TOP_RIGHT) == LEGEND_TOP_RIGHT) {
g.drawImage(legend, iw - lw, 0, null);
}
if ((renderFlags & LEGEND_BOTTOM_LEFT) == LEGEND_BOTTOM_LEFT) {
g.drawImage(legend, 0, ih - lh, null);
}
if ((renderFlags & LEGEND_BOTTOM_RIGHT) == LEGEND_BOTTOM_RIGHT) {
g.drawImage(legend, iw - lw, ih - lh, null);
}
if ((renderFlags & LEGEND_TOP) == LEGEND_TOP) {
g.drawImage(legend, iw / 2 - lw / 2, 0, null);
}
if ((renderFlags & LEGEND_BOTTOM) == LEGEND_BOTTOM) {
g.drawImage(legend, iw / 2 - lw / 2, ih - lh, null);
}
}
}
}
// /**
// * Render fade then objects
// *
// * @param g
// * @param pnts
// * @param converter
// * @param selectedObjectIds
// * @return
// */
// private void renderObjects(Graphics2D g, Iterable<? extends DrawableObject> pnts, LatLongToScreen converter, TLongHashSet selectedObjectIds) {
//
// for (DrawableObject pnt : pnts) {
// if (pnt != null) {
// boolean isSelected = selectedObjectIds != null ? selectedObjectIds.contains(pnt.getGlobalRowId()) : false;
// renderObject(g, converter, pnt, isSelected);
// }
// }
//
// }
public static TLongArrayList getWithinRectangle(Iterable<? extends DrawableObject> pnts, LatLongToScreen converter, Rectangle selRectOnScreen, boolean filterUnselectable) {
List<DrawableObject> list = getObjectsWithinRectangle(pnts, converter, selRectOnScreen,filterUnselectable);
TLongArrayList ret = new TLongArrayList(list.size());
for (DrawableObject obj : list) {
ret.add(obj.getGlobalRowId());
}
return ret;
}
public static boolean isVisibleAtZoom(DrawableObject o, int internalZoom){
int externalZoom = ZoomConverter.toExternal(internalZoom);
if(o.getMinZoom() > externalZoom){
return false;
}
if(o.getMaxZoom() < externalZoom){
return false;
}
return true;
}
public static List<DrawableObject> getObjectsWithinRectangle(Iterable<? extends DrawableObject> pnts, LatLongToScreen converter, Rectangle selRectOnScreen, boolean filterUnselectable) {
List<DrawableObject> ret = new ArrayList<>();
if(pnts == null){
return ret;
}
// create blank image with the bounds that we're testing to give us a valid graphics object
int w = Math.max(selRectOnScreen.width, 1);
int h = Math.max(selRectOnScreen.height, 1);
BufferedImage testImage = new BufferedImage(w, h, BufferedImage.TYPE_INT_RGB);
Graphics2D g = testImage.createGraphics();
Rectangle testRectangle = new Rectangle(0, 0, w, h);
g.setClip(0, 0, w, h);
// get the screen viewport's world bitmap bounds
Rectangle2D wbView = converter.getViewportWorldBitmapScreenPosition();
try {
for (DrawableObject pnt : pnts) {
if(filterUnselectable && pnt.getSelectable()==0){
continue;
}
if(!isVisibleAtZoom(pnt, converter.getZoomForObjectFiltering())){
continue;
}
if (pnt.getGeometry() == null) {
if (hasValidLatLong(pnt)) {
Point2D screenPos = converter.getOnScreenPixelPosition(pnt);
// create bounding box around the point
int pntWidth = (int) pnt.getPixelWidth();
Rectangle bounding = createRectangle(screenPos, pntWidth);
if (selRectOnScreen.intersects(bounding)) {
// if the bounding box is entirely within the rectangle then include it
boolean found = selRectOnScreen.contains(bounding);
if (!found) {
// otherwise do complex image test
// get position in our test image by offsetting the from the test rectangle position in screen space
Point2D imagePos = new Point2D.Double(screenPos.getX() - selRectOnScreen.x, screenPos.getY() - selRectOnScreen.y);
Shape shape = createShape(getSymbolType(pnt), imagePos, pntWidth);
if (g.hit(testRectangle, shape, false)) {
found = true;
// System.out.println(testRectangle + " hit interior");
} else if (g.hit(testRectangle, shape, true)) {
// System.out.println(testRectangle + " hit stroke");
found = true;
}
}
if (found) {
ret.add(pnt);
}
}
}
} else {
// get the on-screen bounds of the object
Rectangle2D wbb = getRenderedWorldBitmapBounds(pnt, converter);
if(wbb!=null){
Rectangle2D onScreenBounds = new Rectangle2D.Double(wbb.getMinX() - wbView.getMinX(), wbb.getMinY() - wbView.getMinY(), wbb.getWidth(), wbb.getHeight());
boolean found = false;
if (selRectOnScreen.contains(onScreenBounds)) {
// object is definitely contained, don't need to test further
found = true;
}
else if (onScreenBounds.intersects(selRectOnScreen)) {
// need to do geometry testing
OnscreenGeometry cachedGeometry = getCachedGeometry((ODLGeomImpl)pnt.getGeometry(), converter, true);
if(cachedGeometry!=null){
if (cachedGeometry.isDrawFilledBounds()) {
// the entire bounds will be drawn; just check for intersection
found = selRectOnScreen.intersects(onScreenBounds);
} else {
// complex render-based test...
found = renderOrHitTestJTSGeometry(g, pnt, cachedGeometry.getJTSGeometry(), null, null, wbView, selRectOnScreen, 0);
}
}
}
if (found) {
ret.add(pnt);
}
}
// if (cachedGeometry != null) {
//
// Rectangle2D wbb = cachedGeometry.getWorldBitmapBounds();
// Rectangle2D onScreenBounds = new Rectangle2D.Double(wbb.getMinX() - wbView.getMinX(), wbb.getMinY() - wbView.getMinY(), wbb.getWidth(), wbb.getHeight());
// if (onScreenBounds.intersects(selRectOnScreen)) {
// boolean found = false;
//
// if (selRectOnScreen.contains(onScreenBounds)) {
// // selection rectangle contains the bounds
// found = true;
// } else if (cachedGeometry.isDrawFilledBounds()) {
// // the entire bounds will be drawn; just check for intersection
// found = selRectOnScreen.intersects(onScreenBounds);
// } else {
//
// // complex render-based test...
// found = renderOrHitTestJTSGeometry(g, pnt, cachedGeometry.getJTSGeometry(), null, null, wbView, selRectOnScreen, 0);
// }
//
// if (found) {
// ret.add(pnt);
// }
// }
// }
}
}
} finally {
g.dispose();
}
// System.out.println("NbImageTests: " + nbImageTests);
return ret;
}
// /**
// * Fill the polygon, also widening it slightly so adjacent polygons on-screen will not show gaps between them.
// *
// * This method should be removed in the future. We currently practically-speaking never render without polygon borders,
// * which hide the gaps anyway.
// *
// * @param shape
// * @param exterior
// * @param col
// * @param g2d
// */
// @Deprecated
// private static void fillWidenedPolygon(Shape shape, Path2D exterior, final Color col, Graphics2D g2d, int viewportWidth, int viewportHeight) {
// Rectangle2D bounds = exterior.getBounds();
//
// // clip bounds to the viewport, remembering that shape is already relative to viewport
// Rectangle2D viewport = new Rectangle2D.Double(0, 0, viewportWidth, viewportHeight);
// bounds = viewport.createIntersection(bounds);
//
// // ensure image dimensions at least one
// int width = (int) Math.round(bounds.getWidth());
// width = Math.max(width, 1);
// int height = (int) Math.round(bounds.getHeight());
// height = Math.max(height, 1);
//
// double size = width * height;
// if (size < MAX_IMAGE_SIZE_LIMIT) {
// // draw on temporary image without alpha channel
// BufferedImage img = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
// Graphics2D gImage = img.createGraphics();
// try {
// gImage.setClip(0, 0, img.getWidth(), img.getHeight());
// gImage.translate(-bounds.getX(), -bounds.getY());
// gImage.setColor(Colours.setAlpha(col, 255));
// gImage.fill(shape);
//
// BasicStroke stroke = new BasicStroke(2, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND);
// gImage.setStroke(stroke);
// gImage.draw(exterior);
// } finally {
// gImage.dispose();
// }
//
// RGBImageFilter rgbFilter = new RGBImageFilter() {
//
// @Override
// public int filterRGB(int x, int y, int rgb) {
// if (rgb != 0) {
// return (rgb & 0x00FFFFFF) | (col.getAlpha() << 24);
// } else {
// return 0;
// }
// }
// };
//
// // create image with alpha channel
// ImageProducer ip = new FilteredImageSource(img.getSource(), rgbFilter);
// Image alphaImg = Toolkit.getDefaultToolkit().createImage(ip);
//
// // draw this to the output graphics object
// g2d.drawImage(alphaImg, (int) bounds.getX(), (int) bounds.getY(), null);
//
// } else {
// // don't bother widening, just draw (image too big)
// g2d.setColor(col);
// g2d.fill(shape);
// }
//
// // g2d.drawImage(ImageUtils.createBlankImage(img.getWidth(), img.getHeight(), Color.GREEN), bounds.x, bounds.y,null);
//
// }
private static class DrawnSymbol {
final boolean drawOutline;
final Color color;
final int pixelWidth;
final boolean selected;
// final Object objectKey;
BufferedImage image;
SymbolType symbolType;
DrawnSymbol(SymbolType symbolType, boolean drawOutline, Color color, int pixelWidth, boolean selected) {
this.symbolType = symbolType;
this.drawOutline = drawOutline;
this.color = color;
this.pixelWidth = pixelWidth;
this.selected = selected;
// this.objectKey = objectKey;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((color == null) ? 0 : color.hashCode());
result = prime * result + (drawOutline ? 1231 : 1237);
// result = prime * result + ((objectKey == null) ? 0 : objectKey.hashCode());
result = prime * result + pixelWidth;
result = prime * result + symbolType.ordinal();
result = prime * result + (selected ? 1231 : 1237);
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
DrawnSymbol other = (DrawnSymbol) obj;
if (color == null) {
if (other.color != null)
return false;
} else if (!color.equals(other.color))
return false;
if (drawOutline != other.drawOutline)
return false;
// if (objectKey == null) {
// if (other.objectKey != null)
// return false;
// } else if (!objectKey.equals(other.objectKey))
// return false;
if (pixelWidth != other.pixelWidth)
return false;
if (symbolType != other.symbolType) {
return false;
}
if (selected != other.selected)
return false;
return true;
}
void initSymbol() {
int dimension = 2 * (pixelWidth + symbols.getMaxOutline());
image = new BufferedImage(dimension, dimension, BufferedImage.TYPE_INT_ARGB);
Graphics2D g2 = image.createGraphics();
g2.setClip(0, 0, image.getWidth(), image.getHeight());
Point2D centre = new Point2D.Double(dimension / 2, dimension / 2);
if (selected) {
drawSymbol(g2, symbolType, centre, pixelWidth, color);
} else {
drawOutlinedSymbol(g2, symbolType, centre, pixelWidth, color, drawOutline);
}
g2.dispose();
}
}
private static Point2D toOnscreen(Coordinate worldBitmapCoord, Rectangle2D viewport) {
return new Point2D.Double(worldBitmapCoord.x - viewport.getX(), worldBitmapCoord.y - viewport.getY());
}
private static Path2D.Double toOnscreenPath(CoordinateSequence cs, Rectangle2D viewport) {
Path2D.Double path = new Path2D.Double();
int n = cs.size();
for (int i = 0; i < n; i++) {
Coordinate coord = cs.getCoordinate(i);
double x = coord.x - viewport.getX();
double y = coord.y - viewport.getY();
if (i == 0) {
path.moveTo(x, y);
} else {
path.lineTo(x, y);
}
}
return path;
}
static BufferedImage createBaseImage(int imageWidth, int imageHeight, long renderFlags) {
BufferedImage image = new BufferedImage(imageWidth, imageHeight, BufferedImage.TYPE_INT_ARGB);
if ((renderFlags & RenderProperties.SKIP_BACKGROUND_COLOUR_RENDERING) != RenderProperties.SKIP_BACKGROUND_COLOUR_RENDERING) {
ImageUtils.fillImage(image, new Color(200, 200, 255, 255));
}
return image;
}
static boolean renderOrHitTestJTSGeometry(Graphics2D g, DrawableObject obj, Geometry geometry, Color col, Color outlineCol, Rectangle2D viewport, Rectangle hitTestOnScreen, long renderFlags) {
boolean hit = false;
boolean skipBorders = (renderFlags & RenderProperties.SKIP_BORDER_RENDERING) == RenderProperties.SKIP_BORDER_RENDERING;
boolean bordersOnly = (renderFlags & RenderProperties.RENDER_BORDERS_ONLY) == RenderProperties.RENDER_BORDERS_ONLY;
if (geometry == null) {
throw new RuntimeException("Null geometry");
}
if (GeometryCollection.class.isInstance(geometry)) {
int ng = geometry.getNumGeometries();
for (int i = 0; i < ng; i++) {
hit |= renderOrHitTestJTSGeometry(g, obj, geometry.getGeometryN(i), col, outlineCol, viewport, hitTestOnScreen,renderFlags);
}
} else {
// Do further bounding box hit test; speeds up case where we have multigeometries
// and we've only done test on the whole of them. This is particularly important in the
// case of American Samoa and the mainland USA being the multipolygon, otherwise USA is
// always rendered as its bounding box spans the Earth...
// Ensure we ensure all bounds have at least one pixel width & size as points have zero width by default
// and this gives no intersection...
Envelope bb = geometry.getEnvelopeInternal();
Rectangle2D bounds = new Rectangle2D.Double(bb.getMinX(), bb.getMinY(), bb.getWidth() + obj.getPixelWidth(),bb.getHeight() + obj.getPixelWidth());
bounds = expandBoundsForSymbolRendering(obj,geometry, bounds);
if (bounds.intersects(viewport)) {
Stroke oldStroke = g.getStroke();
if (col != null) {
g.setColor(col);
}
if (Point.class.isInstance(geometry)) {
if(!bordersOnly){
Point2D onscreen = toOnscreen(((Point) geometry).getCoordinate(), viewport);
int width = (int) obj.getPixelWidth();
if (hitTestOnScreen == null) {
drawOutlinedSymbol(g, getSymbolType(obj), onscreen, width, col, obj.getDrawOutline() == 1);
} else {
hit |= g.hit(hitTestOnScreen, createShape(getSymbolType(obj), onscreen, width), false);
}
}
}
else{
// create world bitmap to screen position transform
AffineTransform transform = AffineTransform.getTranslateInstance(-viewport.getX(), -viewport.getY());
if (LineString.class.isInstance(geometry)) {
if(!bordersOnly){
// search for a dash config
float [] dash=null;
float dashPhase=0;
for(LineDashConfig dc : DASH_CONFIGS){
if((obj.getFlags() & dc.flag)==dc.flag){
dash = dc.dash;
dashPhase = dc.phase;
}
}
Shape path = new LiteShape(geometry,transform, false);
BasicStroke stroke = new BasicStroke(obj.getPixelWidth(), BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND,10.0f, dash, dashPhase);
g.setStroke(stroke);
if (hitTestOnScreen == null) {
g.draw(path);
} else {
hit |= g.hit(hitTestOnScreen, path, true);
}
}
} else if (Polygon.class.isInstance(geometry)) {
Polygon polygon = (Polygon) geometry;
Shape exterior = new LiteShape(polygon.getExteriorRing(), transform, false);
Shape shape = new LiteShape(polygon,transform, false);
if (hitTestOnScreen == null) {
// g.fill(shape);
//fillWidenedPolygon(shape, exterior, col, g, (int) Math.ceil(viewport.getWidth()), (int) Math.ceil(viewport.getHeight()));
if(!bordersOnly){
g.setColor(col);
g.fill(shape);
}
} else {
hit |= g.hit(hitTestOnScreen, shape, false);
hit |= shape.intersects(hitTestOnScreen);
}
// Draw the outline
if (obj.getDrawOutline() != 0 && skipBorders==false) {
float borderWidth = (renderFlags & RenderProperties.THIN_POLYGON_BORDERS)==RenderProperties.THIN_POLYGON_BORDERS? 1 : 2;
BasicStroke stroke = new BasicStroke(borderWidth, BasicStroke.CAP_ROUND, BasicStroke.JOIN_MITER);
g.setStroke(stroke);
if (outlineCol != null) {
g.setColor(outlineCol);
}
if (hitTestOnScreen == null) {
g.draw(exterior);
} else {
hit |= g.hit(hitTestOnScreen, exterior, true);
}
}
} else {
throw new UnsupportedOperationException("Unsupported geometry type: " + geometry.getClass());
}
}
g.setStroke(oldStroke);
}
}
// System.out.println(obj.getName() + " hit=" + hit);
return hit;
}
/**
* Expand the bounds to take account of symbol rendering.
* @param obj
* @param geometry
* @param geometryBounds
* @return
*/
private static Rectangle2D expandBoundsForSymbolRendering(DrawableObject obj,Geometry geometry, Rectangle2D geometryBounds){
if(geometry==null || JTSUtils.getGeomCount(geometry,AtomicGeomType.POINT)>0){
return expandBoundsForSymbolRendering(obj.getPixelWidth(), geometryBounds);
}
return geometryBounds;
}
private static Rectangle2D expandBoundsForSymbolRendering(long pixelWidth, Rectangle2D geometryBounds){
long width =pixelWidth+1; // 1 extra for good luck and rounding!
long halfWidth = width/2;
return new Rectangle2D.Double(geometryBounds.getMinX() - halfWidth, geometryBounds.getMinY() - halfWidth, geometryBounds.getWidth() + width, geometryBounds.getHeight() + width);
}
private static boolean hasPoint(DrawableObject o){
return o.getGeometry()==null || o.getGeometry().getAtomicGeomCount(AtomicGeomType.POINT)>0;
}
private boolean renderGeometry(Graphics2D g, final LatLongToScreen converter, DrawableObject pnt, boolean isSelected, long renderFlags) {
boolean rendered = false;
if(!isVisibleAtZoom(pnt, converter.getZoomForObjectFiltering())){
return false;
}
ODLGeomImpl geom = pnt.getGeometry();
if (geom == null) {
return false;
}
// Check for intersection with viewport. For geometry collections this checks for all at once
Rectangle2D viewableTestBounds = getRenderedWorldBitmapBounds(pnt, converter);
Rectangle2D viewport = converter.getViewportWorldBitmapScreenPosition();
if (viewableTestBounds==null || viewableTestBounds.intersects(viewport) == false) {
return false;
}
// get render colour
final Color renderCol = getRenderColour(pnt,isSelected);
// get geometry in world bitmap projection
OnscreenGeometry transformed = getCachedGeometry(geom, converter, true);
if (transformed == null) {
return false;
}
if (transformed.isDrawFilledBounds()) {
Rectangle2D objectBounds =geom.getWorldBitmapBounds(converter);
if(objectBounds==null){
return false;
}
g.setColor(renderCol);
int x = (int) Math.round(objectBounds.getMinX() - viewport.getMinX());
int y = (int) Math.round(objectBounds.getMinY() - viewport.getMinY());
g.fillRect(x, y, (int) Math.round(objectBounds.getWidth()), (int) Math.round(objectBounds.getHeight()));
} else {
renderOrHitTestJTSGeometry(g, pnt, transformed.getJTSGeometry(), renderCol, getPolygonBorderColour(renderCol), viewport, null,renderFlags);
}
rendered = true;
return rendered;
}
static public Color getDefaultPolygonBorderColour(Color polyCol) {
return Colours.multiplyNonAlpha(polyCol, SELECTION_DARKEN_OUTLINE_FACTOR);
}
protected Color getPolygonBorderColour(Color polyCol){
return getDefaultPolygonBorderColour(polyCol);
}
/**
* Get the rendered world bitmap bounds. This includes line width or symbol width
* @param obj
* @param converter
* @return
*/
public static Rectangle2D getRenderedWorldBitmapBounds(DrawableObject obj, LatLongToScreen converter){
Rectangle2D ret= null;
ODLGeomImpl g =obj.getGeometry();
if(g!=null){
// enable calculating of bounds without getting the cached geometry to speed up querying
ret= g.getWorldBitmapBounds(converter);
// include point or line width
if(ret!=null && (g.getAtomicGeomCount(AtomicGeomType.POINT)>0 || g.getAtomicGeomCount(AtomicGeomType.LINESTRING)>0)){
ret = expandBoundsForSymbolRendering(obj.getPixelWidth(), ret);
}
}
// assume its a point
if(ret==null){
ret= createRectangle(converter.getWorldBitmapPixelPosition(new LatLongImpl(obj.getLatitude(),obj.getLongitude())),(int) obj.getPixelWidth());
}
return ret;
}
public static OnscreenGeometry getCachedGeometry(ODLGeomImpl geom, LatLongToScreen converter, boolean createIfNotCached) {
RecentlyUsedCache cache = ApplicationCache.singleton().get(ApplicationCache.PROJECTED_RENDERER_GEOMETRY);
CachedGeomKey key = new CachedGeomKey(geom, converter.getZoomHashmapKey());
OnscreenGeometry transformed = (OnscreenGeometry) cache.get(key);
if (transformed == null && createIfNotCached) {
transformed =geom.createOnscreenGeometry(converter);
if(transformed!=null){
cache.put(key, transformed, transformed.getSizeInBytes());
}
}
return transformed;
}
static boolean hasValidLatLong(DrawableObject obj) {
if (Double.isNaN(obj.getLatitude()) || Double.isNaN(obj.getLongitude())) {
return false;
}
return true;
}
@Override
public boolean renderObject(Graphics2D g, LatLongToScreen converter, DrawableObject pnt, boolean isSelected, long renderFlags) {
boolean rendered = false;
if(!isVisibleAtZoom(pnt, converter.getZoomForObjectFiltering())){
return false;
}
if (pnt.getGeometry() == null) {
// get on-screen position
if (hasValidLatLong(pnt)) {
rendered = renderSymbol(g, pnt, converter.getOnScreenPixelPosition(pnt), isSelected);
}
} else {
// // speed test
// for(int i =0 ;i<100;i++){
// rendered = renderGeometry(g, converter, pnt, isSelected, cache);
// }
rendered = renderGeometry(g, converter, pnt, isSelected,renderFlags);
}
return rendered;
}
private static SymbolType getSymbolType(DrawableObject obj) {
return getSymbolType(obj.getSymbol());
}
public static SymbolType getSymbolType(String symbol) {
SymbolType ret = null;
if (!Strings.isEmpty(symbol)) {
ret = symbols.getType(symbol);
}
if (ret == null) {
ret = SymbolType.CIRCLE;
}
return ret;
}
boolean renderSymbol(Graphics2D g, DrawableObject pnt, Point2D screenPos, boolean isSelected) {
boolean rendered = false;
if (getPointIntersectsScreen(g, pnt, screenPos)) {
// get colour
DrawnSymbol image = new DrawnSymbol(getSymbolType(pnt), pnt.getDrawOutline() == 1, getRenderColour(pnt,isSelected), (int) pnt.getPixelWidth(), isSelected);
DrawnSymbol cached = null;
if (circleImageCache != null) {
synchronized (this) {
cached = circleImageCache.get(image);
}
}
if (cached != null) {
// use cached image
image = cached;
} else {
// create image
image.initSymbol();
if (circleImageCache != null) {
synchronized (this) {
circleImageCache.put(image, image);
}
}
}
g.drawImage(image.image, (int) (screenPos.getX() - image.image.getWidth() / 2), (int) (screenPos.getY() - image.image.getHeight() / 2), null);
rendered = true;
}
return rendered;
}
boolean getPointIntersectsScreen(Graphics2D g, DrawableObject pnt, Point2D screenPos) {
Rectangle rectangle = createRectangle(screenPos, (int) pnt.getPixelWidth());
boolean intersects = g.getClipBounds().intersects(rectangle);
return intersects;
}
// /**
// * Gets a bounding rectangle in world bitmap coords for the input point (i.e. using the lat longs)
// *
// * @param pnt
// * @param converter
// * @return
// */
// public static Rectangle getWorldBitmapPointBoundingRectangle(DrawableObject pnt, LatLongToScreen converter) {
// if (pnt.getGeometry() != null) {
// throw new IllegalArgumentException();
// }
// if (hasValidLatLong(pnt)) {
// return getPointBoundingRectangle(pnt, converter.getWorldBitmapPixelPosition(pnt));
// }
// return null;
// }
public static Color getRenderColour(DrawableObject pnt, boolean isSelected) {
if(isSelected){
return SELECTION_COLOUR;
}
Color col = getNoAlphaColour(pnt.getColour(), pnt.getColourKey());
double opaque = pnt.getOpaque();
opaque = MathUtil.clamp(opaque, 0, 1);
col = Colours.setAlpha(col, (int) Math.round((255 * opaque)));
return col;
}
public static Color getNoAlphaColour(Color colourFieldValue, String colourKey) {
if (!Strings.isEmpty(colourKey)) {
colourFieldValue = Colours.getRandomColour(colourKey);
}
if (colourFieldValue == null) {
colourFieldValue = DrawableObject.DEFAULT_COLOUR;
}
return colourFieldValue;
}
static void drawOutlinedSymbol(Graphics2D g, SymbolType symbolType, Point2D screenPos, int circumferenceInPixels, Color col, boolean outlined) {
int outer = symbolType.getOuterOutline();
int inner = symbolType.getInnerOutline();
// // no point outlining if the remaining shape is really small
// if(circumferenceInPixels - outer <=5){
// outlined = false;
// }
if (outlined) {
// draw in black with the total circumference
drawSymbol(g, symbolType, screenPos, circumferenceInPixels, Colours.setAlpha(Color.BLACK, col.getAlpha()));
// subtract the difference between the inner and outer outline (this is the width of the black line)
circumferenceInPixels -= (outer - inner);
// draw in white
drawSymbol(g, symbolType, screenPos, circumferenceInPixels, Colours.setAlpha(Color.WHITE, col.getAlpha()));
circumferenceInPixels -= inner;
// finally draw in correct colour
drawSymbol(g, symbolType, screenPos, circumferenceInPixels, col);
} else {
drawSymbol(g, symbolType, screenPos, circumferenceInPixels, col);
}
}
private static void drawSymbol(Graphics2D g, SymbolType shapeType, Point2D screenPos, int width, Color col) {
Shape shape = createShape(shapeType, screenPos, width);
g.setColor(col);
g.fill(shape);
}
private static Shape createShape(SymbolType shapeType, Point2D screenPos, int width) {
width = Math.max(1, width);
return symbols.get(shapeType, screenPos.getX(), screenPos.getY(), width);
}
private static Rectangle createRectangle(Point2D centre, int length) {
Rectangle rectangle = new Rectangle((int) centre.getX() - length / 2, (int) centre.getY() - length / 2, length, length);
return rectangle;
}
// public boolean isRenderFade() {
// return renderFade;
// }
//
// public void setRenderFade(boolean renderFade) {
// this.renderFade = renderFade;
// }
// public boolean isAllowDelayedGeometryRendering() {
// return allowDelayedGeometryRendering;
// }
//
// public void setAllowDelayedGeometryRendering(boolean allowDelayedGeometryRendering) {
// this.allowDelayedGeometryRendering = allowDelayedGeometryRendering;
// }
// @Override
// public void paint(Graphics2D g, Object object, int width, int height) {
// ODLDatastore<MapTable> ds = mds.prepareDBForRendering();
// renderLatLongPoints(g, ds, converter,false);
//
// }
}