/******************************************************************************* * 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 3.0 * which accompanies this distribution, and is available at * http://www.gnu.org/licenses/lgpl.html * ******************************************************************************/ package com.opendoorlogistics.core.gis.map.tiled; import gnu.trove.impl.Constants; import gnu.trove.map.hash.TIntObjectHashMap; import gnu.trove.map.hash.TObjectIntHashMap; import gnu.trove.set.hash.TLongHashSet; 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.BufferedImageOp; import java.awt.image.ConvolveOp; import java.awt.image.FilteredImageSource; import java.awt.image.ImageProducer; import java.awt.image.Kernel; import java.awt.image.RGBImageFilter; import com.opendoorlogistics.api.geometry.LatLongToScreen; import com.opendoorlogistics.api.geometry.ODLGeom; import com.opendoorlogistics.core.gis.map.DatastoreRenderer; import com.opendoorlogistics.core.gis.map.ObjectRenderer; import com.opendoorlogistics.core.gis.map.RenderProperties; import com.opendoorlogistics.core.gis.map.data.DrawableObject; import com.opendoorlogistics.core.gis.map.data.DrawableObjectDecorator; import com.opendoorlogistics.core.utils.images.CompressedImage; import com.opendoorlogistics.core.utils.images.CompressedImage.CompressedType; import com.opendoorlogistics.core.utils.images.ImageUtils; /** * Class to handle optimised rendering of a non-overlapping polygon layer * @author Phil * */ public class NOVLPolyLayerTile { private final DrawableObjectLayer layer; private final TObjectIntHashMap<ODLGeom> ids; private final CompressedImage filled; private final CompressedImage borders; private final int h; private final int w; long getSizeInBytes(){ return ids.size() * 8 + filled.getSizeBytes() + borders.getSizeBytes(); } private static class ColourDecorator extends DrawableObjectDecorator{ private Color color; private long drawOutline; public ColourDecorator(DrawableObject decorated, Color color, long drawOutline) { super(decorated); this.color = color; this.drawOutline = drawOutline; } @Override public Color getColour() { return color; } @Override public String getColourKey() { return null; } @Override public double getOpaque() { return 1; } @Override public long getDrawOutline() { return drawOutline; } @Override public long getMinZoom() { // Ensure always visible as visibility filtering is done afterwards return Long.MIN_VALUE; } @Override public long getMaxZoom() { return Long.MAX_VALUE; } } DrawableObjectLayer getLayer(){ return layer; } NOVLPolyLayerTile(LatLongToScreen converter,ObjectRenderer renderer,DrawableObjectLayer layer){ this.layer =layer; // assign a unique id to each geom int nextId =1; ids = new TObjectIntHashMap<>(layer.size(), Constants.DEFAULT_LOAD_FACTOR, -1); for(DrawableObject obj:layer){ ODLGeom geom = obj.getGeometry(); if(geom==null){ continue; } if(ids.containsKey(geom)){ continue; } ids.put(geom, nextId++); } Rectangle2D viewable = converter.getViewportWorldBitmapScreenPosition(); w = (int)viewable.getWidth(); h = (int)viewable.getHeight(); // draw all objects on the tile with different colour for each based on id and not showing borders filled = createDrawnImage(converter, renderer,false); // now do the same for the borders (doing separately allows coping with opaque) borders = createDrawnImage(converter, renderer,true); } /** * @param converter * @param renderer * @param nonOverlappingPolys * @param ret * @return */ private CompressedImage createDrawnImage(LatLongToScreen converter, ObjectRenderer renderer,boolean isBorders) { // private BufferedImage createDrawnImage(LatLongToScreen converter, ObjectRenderer renderer,boolean bordersOnly) { BufferedImage img = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB); Graphics2D g = img.createGraphics(); try { g.setClip(0, 0, w, h); for(DrawableObject obj:layer){ if(obj.getGeometry()!=null){ int id = ids.get(obj.getGeometry()); Color dummyIdColour = createColourId(id); g.setColor(dummyIdColour); renderer.renderObject(g, converter, new ColourDecorator(obj, dummyIdColour, isBorders?1:0), false, isBorders?RenderProperties.RENDER_BORDERS_ONLY | RenderProperties.THIN_POLYGON_BORDERS:RenderProperties.SKIP_BORDER_RENDERING); } } } finally { g.dispose(); } return new CompressedImage(img, CompressedType.LZ4); // return img; } /** * @param id * @return */ private static Color createColourId(int id) { // int ia = (0xFF000000) >> 24; int ir = (id & 0x00FF0000) >> 16; int ig = (id & 0x0000FF00) >> 8; int ib = id & 0x000000FF; // value = ((a & 0xFF) << 24) | // ((r & 0xFF) << 16) | // ((g & 0xFF) << 8) | // ((b & 0xFF) << 0); return new Color(ir,ig,ib,255); } //static final HashSet<Integer> distinctRgbs = new HashSet<>(); boolean draw(Iterable<DrawableObject> polys, Graphics2D g2d,TLongHashSet selectedIds, int x, int y, int zoom){ // get colour state of each geom and put this in a map using our ids final TIntObjectHashMap< Color> colours = new TIntObjectHashMap<>(); for(DrawableObject obj :polys){ if(obj.getGeometry()==null){ continue; } int id = ids.get(obj.getGeometry()); if(id==-1){ // unknown id.. rendering went wrong return false; } // Check visible at zoom, get colour if so if(DatastoreRenderer.isVisibleAtZoom(obj, zoom)){ Color col = DatastoreRenderer.getRenderColour(obj, selectedIds.contains(obj.getGlobalRowId())); colours.put(id, col); } } BufferedImage baseImg = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB); Graphics2D gTmp = baseImg.createGraphics(); try { BufferedImage img =null; img = applyColourFiltering(filled.get(), createRGBFilter(colours,false)); gTmp.drawImage(img, x, y,null); // draw borders img = applyColourFiltering(borders.get(), createRGBFilter(colours,true)); // soften borders with a blur float[] blurKernel = { 0, 1 / 8f, 0, 1/8f, 1 / 2f, 1/8f, 0, 1 / 8f, 0f }; BufferedImageOp blur = new ConvolveOp(new Kernel(3, 3, blurKernel), ConvolveOp.EDGE_NO_OP, null); BufferedImage blurredBorders = blur.filter(img, new BufferedImage(baseImg.getWidth(), baseImg.getHeight(), baseImg.getType())); // draw blurred borders first gTmp.drawImage(blurredBorders, x, y,null); // then draw sharper ones on-top (like anti-aliasing) gTmp.drawImage(img, x, y,null); } finally { gTmp.dispose(); } // databuf = new BufferedImage(mshi.getWidth(null), // mshi.getHeight(null), // BufferedImage.TYPE_INT_BGR); // // Graphics g = databuf.getGraphics(); // g.drawImage(mshi, 455, 255, null); // float[] blurKernel = { // 1 / 9f, 1 / 9f, 1 / 9f, // 1 / 9f, 1 / 9f, 1 / 9f, // 1 / 9f, 1 / 9f, 1 / 9f // }; g2d.drawImage(baseImg, x, y,null); return true; } /** * @param colours * @return */ private RGBImageFilter createRGBFilter(final TIntObjectHashMap<Color> colours,final boolean isBorderRendering) { return new RGBImageFilter() { int lastInputRGB; int lastOutputRGB; @Override public int filterRGB(int x, int y, int rgb) { // remove alpha channel as can't get it to work right.... may give max 16 million objects rgb = rgb & 0x00FFFFFF; if(rgb!=0){ // Do a speedup to stop to many hashtable queries. // Often we will call this method for the same object twice in a row if(rgb == lastInputRGB){ return lastOutputRGB; } // Get colour from input map using rgb as id Color col = colours.get(rgb); if(col!=null){ if(isBorderRendering){ col = DatastoreRenderer.getDefaultPolygonBorderColour(col); } lastInputRGB = rgb; rgb = col.getRGB(); lastOutputRGB = rgb; } else{ // Hide it as not visible rgb = 0; } } return rgb; } }; } private static BufferedImage applyColourFiltering(Image uncompressed, RGBImageFilter rgbFilter) { ImageProducer ip = new FilteredImageSource(uncompressed.getSource(), rgbFilter); Image coloured = Toolkit.getDefaultToolkit().createImage(ip); return ImageUtils.toBufferedImage(coloured); } // public static void main(String [] args){ // int val = 1; // Random random = new Random(123); // while(val < Integer.MAX_VALUE / 2){ // Color col = createColourId(val); // System.out.println("val = " + val + " -> " + col.getRGB() + " -> " + new Color(val).getRGB()); // val += (int)Math.round(val * random.nextDouble()); // } // } }