package org.activityinfo.server.report.renderer.image; /* * #%L * ActivityInfo Server * %% * Copyright (C) 2009 - 2013 UNICEF * %% * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public * License along with this program. If not, see * <http://www.gnu.org/licenses/gpl-3.0.html>. * #L% */ import com.google.code.appengine.awt.*; import com.google.code.appengine.awt.color.ColorSpace; import com.google.code.appengine.awt.font.LineMetrics; import com.google.code.appengine.awt.geom.Ellipse2D; import com.google.code.appengine.awt.geom.GeneralPath; import com.google.code.appengine.awt.geom.Rectangle2D; import com.google.code.appengine.awt.image.BufferedImage; import com.google.code.appengine.imageio.ImageIO; import com.google.inject.Inject; import org.activityinfo.model.type.geo.AiLatLng; import org.activityinfo.legacy.shared.model.BaseMap; import org.activityinfo.legacy.shared.model.TileBaseMap; import org.activityinfo.legacy.shared.reports.content.*; import org.activityinfo.legacy.shared.reports.model.MapReportElement; import org.activityinfo.server.geo.AdminGeo; import org.activityinfo.server.geo.AdminGeometryProvider; import org.activityinfo.server.report.generator.MapIconPath; import org.activityinfo.server.report.generator.map.TileProvider; import org.activityinfo.server.report.generator.map.TiledMap; import org.activityinfo.server.report.util.ColorUtil; import org.activityinfo.server.util.logging.LogSlow; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStream; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.logging.Level; import java.util.logging.Logger; /** * Renders a MapElement and its generated MapContent */ public class ImageMapRenderer { private static final Logger LOGGER = Logger.getLogger(ImageMapRenderer.class.getName()); /** * Provides tile images from a URL */ private final class RemoteTileProvider implements TileProvider { private final TileBaseMap baseMap; private RemoteTileProvider(TileBaseMap baseMap) { this.baseMap = baseMap; } @Override public String getImageUrl(int zoom, int tileX, int tileY) { String tileUrl = baseMap.getTileUrl(zoom, tileX, tileY); if (tileUrl.startsWith("//")) { return "http:" + tileUrl; } return tileUrl; } } private final String mapIconRoot; private final Map<String, BufferedImage> iconImages = new HashMap<String, BufferedImage>(); private final AdminGeometryProvider geometryProvider; @Inject public ImageMapRenderer(AdminGeometryProvider geometryProvider, @MapIconPath String mapIconPath) { this.geometryProvider = geometryProvider; this.mapIconRoot = mapIconPath; } public String getMapIconRoot() { return mapIconRoot; } public void renderToFile(MapReportElement element, File file) throws IOException { FileOutputStream os = new FileOutputStream(file); render(element, os); os.close(); } public void render(MapReportElement element, OutputStream os) throws IOException { BufferedImage image = new BufferedImage(element.getWidth(), element.getHeight(), ColorSpace.TYPE_RGB); Graphics2D g2d = image.createGraphics(); render(element, g2d); ImageIO.write(image, "PNG", os); } public void render(MapReportElement element, Graphics2D g2d) { drawBasemap(g2d, element); // Draw markers drawOverlays(element, g2d); } protected void drawOverlays(MapReportElement element, Graphics2D g2d) { TiledMap map = createTileMap(element); for (AdminOverlay overlay : element.getContent().getAdminOverlays()) { drawAdminOverlay(map, g2d, overlay); } for (MapMarker marker : element.getContent().getMarkers()) { if (marker instanceof IconMapMarker) { drawIcon(g2d, (IconMapMarker) marker); } else if (marker instanceof PieMapMarker) { drawPieMarker(g2d, (PieMapMarker) marker); } else if (marker instanceof BubbleMapMarker) { drawBubbleMarker(g2d, (BubbleMapMarker) marker); } } } @LogSlow(threshold = 50) protected void drawAdminOverlay(TiledMap map, Graphics2D g2d, AdminOverlay overlay) { List<AdminGeo> geometry = geometryProvider.getGeometries(overlay.getAdminLevelId()); Color strokeColor = ColorUtil.colorFromString(overlay.getOutlineColor()); g2d.setStroke(new BasicStroke(1, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND)); for (AdminGeo adminGeo : geometry) { AdminMarker polygon = overlay.getPolygon(adminGeo.getId()); if (polygon != null) { GeneralPath path = PathUtils.toPath(map, adminGeo.getGeometry()); g2d.setColor(bubbleFillColor(ColorUtil.colorFromString(polygon.getColor()))); g2d.fill(path); g2d.setColor(strokeColor); g2d.draw(path); } } } public static void drawPieMarker(Graphics2D g2d, PieMapMarker marker) { // Determine the total area Rectangle area = new Rectangle(); area.setRect(marker.getX() - marker.getRadius(), marker.getY() - marker.getRadius(), marker.getRadius() * 2, marker.getRadius() * 2); // Get total value of all slices double total = 0.0D; for (PieMapMarker.SliceValue slice : marker.getSlices()) { total += slice.getValue(); } // Draw each pie slice double curValue = 0.0D; int startAngle = 0; int i = 0; for (PieMapMarker.SliceValue slice : marker.getSlices()) { // Compute the start and stop angles startAngle = (int) (curValue * 360 / total); int arcAngle = (int) (slice.getValue() * 360 / total); // Ensure that rounding errors do not leave a gap between the first // and last slice if (i++ == marker.getSlices().size() - 1) { arcAngle = 360 - startAngle; } // Set the color and draw a filled arc g2d.setColor(bubbleFillColor(ColorUtil.colorFromString(slice.getColor()))); g2d.fillArc(area.x, area.y, area.width, area.height, startAngle, arcAngle); curValue += slice.getValue(); } if (marker.getLabel() != null) { drawLabel(g2d, marker); } } private static void drawLabel(Graphics2D g2d, BubbleMapMarker marker) { Font font = new Font(Font.SANS_SERIF, Font.BOLD, (int) (marker.getRadius() * 2f * 0.8f)); // measure the bounds of the string so we can center it within // the bubble. // QUICK FIX: labeling set inappropriately as default for new map layers // and no method in UI to turn them off. Rectangle2D rect = font.getStringBounds(marker.getLabel(), g2d.getFontRenderContext()); LineMetrics lm = font.getLineMetrics(marker.getLabel(), g2d.getFontRenderContext()); g2d.setColor(Color.WHITE); g2d.setFont(font); g2d.drawString(marker.getLabel(), (int) (marker.getX() - (rect.getWidth() / 2)), (int) (marker.getY() + (lm.getAscent() * 1.25))); } protected void drawIcon(Graphics2D g2d, IconMapMarker marker) { BufferedImage image = getIconImage(marker); if (image != null) { g2d.drawImage(image, null, marker.getX() - marker.getIcon().getAnchorX(), marker.getY() - marker.getIcon().getAnchorY()); } } private BufferedImage getIconImage(IconMapMarker marker) { return getIconImage(marker.getIcon().getName()); } protected ItextGraphic renderSlice(ImageCreator imageCreator, Color color, int size) { ItextGraphic result = imageCreator.create(size, size); Graphics2D g2d = result.getGraphics(); g2d.setPaint(color); g2d.fillRect(0, 0, size, size); // // Ellipse2D.Double ellipse = new Ellipse2D.Double( // size/2, size/2, size/2, size/2); // // g2d.setPaint(color); // g2d.fill(ellipse); // return result; } private BufferedImage getIconImage(String name) { BufferedImage image = iconImages.get(name); if (image == null) { try { image = ImageIO.read(getImageFile(name)); iconImages.put(name, image); } catch (IOException e) { LOGGER.log(Level.WARNING, "Exception reading icon '" + name + "'", e); } } return image; } public File getImageFile(String name) { return new File(mapIconRoot + "/" + name + ".png"); } public void drawBasemap(Graphics2D g2d, MapReportElement element) { // Draw white backgrond first, in case we run out of tiles g2d.setPaint(Color.WHITE); g2d.fillRect(0, 0, element.getWidth(), element.getHeight()); G2dTileHandler tileHandler = new G2dTileHandler(g2d); drawBasemap(element, tileHandler); } protected void drawBasemap(MapReportElement element, TileHandler tileHandler) { TiledMap map = createTileMap(element); BaseMap baseMap = element.getContent().getBaseMap(); try { if (baseMap instanceof TileBaseMap) { drawTiledBaseMap(tileHandler, map, baseMap); } else if (baseMap instanceof GoogleBaseMap) { drawTiledBaseMap(tileHandler, map, MapboxLayers.toTileBaseMap(baseMap)); } } catch (Exception e) { LOGGER.log(Level.WARNING, "Exception drawing basemap", e); } } protected TiledMap createTileMap(MapReportElement element) { AiLatLng center = element.getCenter() != null ? element.getCenter() : element.getContent().getCenter(); TiledMap map = new TiledMap(element.getWidth(), element.getHeight(), center, element.getContent().getZoomLevel()); return map; } private void drawTiledBaseMap(TileHandler handler, TiledMap map, BaseMap baseMap) { TileProvider tileProvider = new RemoteTileProvider((TileBaseMap) baseMap); map.drawLayer(handler, tileProvider); } public static Color bubbleFillColor(Color colorRgb) { return new Color(colorRgb.getRed(), colorRgb.getGreen(), colorRgb.getBlue(), 179); // 179=0.7*255 } public static Color bubbleStrokeColor(int colorRgb) { return new Color(colorRgb).darker(); } public static void drawBubbleMarker(Graphics2D g2d, BubbleMapMarker marker) { drawBubble(g2d, ColorUtil.colorFromString(marker.getColor()), marker.getX(), marker.getY(), marker.getRadius()); if (marker.getLabel() != null) { drawLabel(g2d, marker); } } public static void drawBubble(Graphics2D g2d, Color colorRgb, int x, int y, int radius) { Ellipse2D.Double ellipse = new Ellipse2D.Double(x - radius, y - radius, radius * 2, radius * 2); g2d.setPaint(bubbleFillColor(colorRgb)); g2d.fill(ellipse); g2d.setPaint(bubbleFillColor(colorRgb)); g2d.setStroke(new BasicStroke(1.5f)); g2d.draw(ellipse); } }