/******************************************************************************* * 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 java.awt.Color; import java.awt.Graphics2D; import java.awt.Image; import java.awt.Toolkit; import java.awt.geom.Rectangle2D; import java.awt.image.BufferedImage; import java.awt.image.FilteredImageSource; import java.awt.image.ImageProducer; import java.awt.image.RGBImageFilter; import com.opendoorlogistics.api.geometry.LatLongToScreen; import com.opendoorlogistics.core.geometry.ODLGeomImpl; import com.opendoorlogistics.core.geometry.ODLGeomImpl.AtomicGeomType; import com.opendoorlogistics.core.gis.map.data.DrawableObject; import com.opendoorlogistics.core.utils.Colours; import com.opendoorlogistics.core.utils.images.ImageUtils; public class CachedGeomImageRenderer implements ObjectRenderer{ private final RecentImageCache geomCache = new RecentImageCache(RecentImageCache.ZipType.LZ4, 64*1024*1024); private final DatastoreRenderer renderer = new DatastoreRenderer(); @Override public boolean renderObject(Graphics2D g, LatLongToScreen converter, DrawableObject obj, boolean isSelected, long renderFlags){ if(!DatastoreRenderer.isVisibleAtZoom(obj, converter.getZoomForObjectFiltering())){ return false; } // test if we have points boolean hasPoint = obj.getGeometry()==null; if(!hasPoint){ hasPoint = obj.getGeometry().getAtomicGeomCount(AtomicGeomType.POINT)>0; } // if we have one or more points than draw as normal datastore renderer instead of caching if(hasPoint){ // draw using normal datastore renderer if(obj.getGeometry()!=null){ renderer.renderObject(g, converter, obj, isSelected,0); }else if (DatastoreRenderer.hasValidLatLong(obj)) { renderer.renderSymbol(g, obj, converter.getOnScreenPixelPosition(obj), isSelected); } }else{ // do intersection check to see if we should draw Rectangle2D geomBounds =obj.getGeometry().getWorldBitmapBounds(converter); Rectangle2D wBBBounds = converter.getViewportWorldBitmapScreenPosition(); if(wBBBounds==null || geomBounds.intersects(wBBBounds)==false){ return false; } // get transformed geometry OnscreenGeometry cachedGeometry = DatastoreRenderer.getCachedGeometry(obj.getGeometry(), converter,true); if(cachedGeometry==null){ return false; } // get the region to be drawn Rectangle2D drawRegion = wBBBounds.createIntersection(geomBounds); // just draw a filled rectangle if its really small if(cachedGeometry.isDrawFilledBounds() || (drawRegion.getWidth()<=1 && drawRegion.getHeight()<=1)){ int minX = (int)Math.floor(drawRegion.getMinX()); int minY = (int)Math.floor(drawRegion.getMinY()); int maxX = (int)Math.ceil(drawRegion.getMaxX()); int maxY = (int)Math.ceil(drawRegion.getMaxY()); g.fillRect(minX,minY,maxX - minX + 1, maxY - minY+ 1); }else{ // check cache for this image in black and white BWCacheKey cacheKey = new BWCacheKey(wBBBounds, converter.getZoomHashmapKey(), obj, isSelected); Image bwImage=geomCache.get(cacheKey); // if no image available, create image it (for the intersection between the object and the tile) // and cache it. final Color renderCol = DatastoreRenderer.getRenderColour(obj,isSelected); if(bwImage==null){ BufferedImage bufBwImage = createBWImage(obj, cachedGeometry, renderCol, drawRegion); bwImage = bufBwImage; geomCache.put(cacheKey, bufBwImage); } BufferedImage colourImage = createColouredImage(bwImage, renderCol); // draw the colour image in the corrent place int x = (int) Math.round(drawRegion.getMinX() - wBBBounds.getMinX()); int y = (int) Math.round(drawRegion.getMinY() - wBBBounds.getMinY()); g.drawImage(colourImage, x, y, null); } } return true; } private static class BWCacheKey{ private Rectangle2D wBBBounds; private Object zoom; private ODLGeomImpl geom; private boolean drawOutline; private int pixelWidth; private boolean selected; // static final int ESTIMATED_SIZE_IN_BYTES = (8*4) + 8 + 8 + 4 + 4 + 4; BWCacheKey(Rectangle2D wBBBounds, Object zoom,DrawableObject obj, boolean isSelected) { this.wBBBounds = wBBBounds; this.zoom =zoom; this.geom = obj.getGeometry(); this.drawOutline = obj.getDrawOutline()!=0; this.pixelWidth =(int) obj.getPixelWidth(); this.selected = isSelected; } @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + (drawOutline ? 1231 : 1237); result = prime * result + ((geom == null) ? 0 : geom.hashCode()); result = prime * result + pixelWidth; result = prime * result + (selected ? 1231 : 1237); result = prime * result + ((wBBBounds == null) ? 0 : wBBBounds.hashCode()); result = prime * result + ((zoom == null) ? 0 : zoom.hashCode()); return result; } @Override public boolean equals(Object obj) { if (this == obj) return true; if (obj == null) return false; if (getClass() != obj.getClass()) return false; BWCacheKey other = (BWCacheKey) obj; if (drawOutline != other.drawOutline) return false; if (geom == null) { if (other.geom != null) return false; } else if (!geom.equals(other.geom)) return false; if (pixelWidth != other.pixelWidth) return false; if (selected != other.selected) return false; if (wBBBounds == null) { if (other.wBBBounds != null) return false; } else if (!wBBBounds.equals(other.wBBBounds)) return false; if (zoom == null) { if (other.zoom != null) return false; } else if (!zoom.equals(other.zoom)) return false; return true; } } private BufferedImage createColouredImage(Image bwImage, final Color renderCol) { // create coloured image RGBImageFilter rgbFilter = new RGBImageFilter() { @Override public int filterRGB(int x, int y, int rgb) { Color current = new Color(rgb); int red = current.getRed(); if (current.getAlpha() == 0 || red == 0) { return 0; } double factor = red / 255.0; int newRed = Colours.to0To255Int(renderCol.getRed() * factor); int newBlue = Colours.to0To255Int(renderCol.getBlue() * factor); int newGreen = Colours.to0To255Int(renderCol.getGreen() * factor); Color newCol = new Color(newRed, newGreen, newBlue, renderCol.getAlpha()); return newCol.getRGB(); } }; // transform to colour and save in colour ImageProducer ip = new FilteredImageSource(bwImage.getSource(), rgbFilter); Image coloured = Toolkit.getDefaultToolkit().createImage(ip); BufferedImage colourImage = ImageUtils.toBufferedImage(coloured); return colourImage; } /** * Create a black and white image of the geometry to be coloured later-on * @param obj * @param cachedGeometry * @param renderCol * @param drawRegion * @return */ private BufferedImage createBWImage(DrawableObject obj, OnscreenGeometry cachedGeometry, final Color renderCol, Rectangle2D drawRegion) { // ensure image size is at least one int imgWidth = (int)Math.max(1, Math.ceil(drawRegion.getWidth())); int imgHeight = (int)Math.max(1, Math.ceil(drawRegion.getHeight())); // create black and white image BufferedImage buffBwImage = new BufferedImage(imgWidth, imgHeight, BufferedImage.TYPE_INT_ARGB); Graphics2D gImage = buffBwImage.createGraphics(); try { gImage.setClip(0, 0,imgWidth,imgHeight); Color bwCol = new Color(255, 255, 255, renderCol.getAlpha()); if (cachedGeometry.isDrawFilledBounds()) { gImage.setColor(bwCol); gImage.fillRect(0, 0, imgWidth, imgHeight); } else { DatastoreRenderer.renderOrHitTestJTSGeometry(gImage, obj, cachedGeometry.getJTSGeometry(), bwCol, DatastoreRenderer.getDefaultPolygonBorderColour(bwCol), drawRegion, null,0); } } finally{ gImage.dispose(); } return buffBwImage; } }