001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.gui.mappaint; 003 004import static org.openstreetmap.josm.tools.I18n.tr; 005 006import java.awt.Color; 007import java.awt.Dimension; 008import java.awt.Graphics2D; 009import java.awt.Point; 010import java.awt.RenderingHints; 011import java.awt.image.BufferedImage; 012import java.io.IOException; 013import java.util.Collection; 014import java.util.HashMap; 015import java.util.Map; 016import java.util.Optional; 017 018import org.openstreetmap.josm.data.Bounds; 019import org.openstreetmap.josm.data.ProjectionBounds; 020import org.openstreetmap.josm.data.osm.DataSet; 021import org.openstreetmap.josm.data.osm.visitor.paint.StyledMapRenderer; 022import org.openstreetmap.josm.data.projection.Projection; 023import org.openstreetmap.josm.data.projection.ProjectionRegistry; 024import org.openstreetmap.josm.gui.NavigatableComponent; 025import org.openstreetmap.josm.gui.mappaint.mapcss.MapCSSStyleSource; 026import org.openstreetmap.josm.io.IllegalDataException; 027import org.openstreetmap.josm.tools.CheckParameterUtil; 028import org.openstreetmap.josm.tools.Logging; 029 030/** 031 * Class to render osm data to a file. 032 * @since 12963 033 */ 034public class RenderingHelper { 035 036 private final DataSet ds; 037 private final Bounds bounds; 038 private final ProjectionBounds projBounds; 039 private final double scale; 040 private final Collection<StyleData> styles; 041 private Color backgroundColor; 042 private boolean fillBackground = true; 043 044 /** 045 * Data class to save style settings along with the corresponding style URL. 046 */ 047 public static class StyleData { 048 public String styleUrl; 049 public Map<String, String> settings = new HashMap<>(); 050 } 051 052 /** 053 * Construct a new {@code RenderingHelper}. 054 * @param ds the dataset to render 055 * @param bounds the bounds of the are to render 056 * @param scale the scale to render at (east/north units per pixel) 057 * @param styles the styles to use for rendering 058 */ 059 public RenderingHelper(DataSet ds, Bounds bounds, double scale, Collection<StyleData> styles) { 060 CheckParameterUtil.ensureParameterNotNull(ds, "ds"); 061 CheckParameterUtil.ensureParameterNotNull(bounds, "bounds"); 062 CheckParameterUtil.ensureParameterNotNull(styles, "styles"); 063 this.ds = ds; 064 this.bounds = bounds; 065 this.scale = scale; 066 this.styles = styles; 067 Projection proj = ProjectionRegistry.getProjection(); 068 projBounds = new ProjectionBounds(); 069 projBounds.extend(proj.latlon2eastNorth(bounds.getMin())); 070 projBounds.extend(proj.latlon2eastNorth(bounds.getMax())); 071 } 072 073 /** 074 * Set the background color to use for rendering. 075 * 076 * @param backgroundColor the background color to use, {@code} means 077 * to determine the background color automatically from the style 078 * @see #setFillBackground(boolean) 079 * @since 12966 080 */ 081 public void setBackgroundColor(Color backgroundColor) { 082 this.backgroundColor = backgroundColor; 083 } 084 085 /** 086 * Decide if background should be filled or left transparent. 087 * @param fillBackground true, if background should be filled 088 * @see #setBackgroundColor(java.awt.Color) 089 * @since 12966 090 */ 091 public void setFillBackground(boolean fillBackground) { 092 this.fillBackground = fillBackground; 093 } 094 095 Dimension getImageSize() { 096 double widthEn = projBounds.maxEast - projBounds.minEast; 097 double heightEn = projBounds.maxNorth - projBounds.minNorth; 098 int widthPx = (int) Math.round(widthEn / scale); 099 int heightPx = (int) Math.round(heightEn / scale); 100 return new Dimension(widthPx, heightPx); 101 } 102 103 /** 104 * Invoke the renderer. 105 * 106 * @return the rendered image 107 * @throws IOException in case of an IOException 108 * @throws IllegalDataException when illegal data is encountered (style has errors, etc.) 109 */ 110 public BufferedImage render() throws IOException, IllegalDataException { 111 // load the styles 112 ElemStyles elemStyles = new ElemStyles(); 113 MapCSSStyleSource.STYLE_SOURCE_LOCK.writeLock().lock(); 114 try { 115 for (StyleData sd : styles) { 116 MapCSSStyleSource source = new MapCSSStyleSource(sd.styleUrl, "cliRenderingStyle", "cli rendering style '" + sd.styleUrl + "'"); 117 source.loadStyleSource(); 118 elemStyles.add(source); 119 if (!source.getErrors().isEmpty()) { 120 throw new IllegalDataException("Failed to load style file. Errors: " + source.getErrors()); 121 } 122 for (String key : sd.settings.keySet()) { 123 StyleSetting.BooleanStyleSetting match = source.settings.stream() 124 .filter(s -> s instanceof StyleSetting.BooleanStyleSetting) 125 .map(s -> (StyleSetting.BooleanStyleSetting) s) 126 .filter(bs -> bs.prefKey.endsWith(":" + key)) 127 .findFirst().orElse(null); 128 if (match == null) { 129 Logging.warn(tr("Style setting not found: ''{0}''", key)); 130 } else { 131 boolean value = Boolean.parseBoolean(sd.settings.get(key)); 132 Logging.trace("setting applied: ''{0}:{1}''", key, value); 133 match.setValue(value); 134 } 135 } 136 if (!sd.settings.isEmpty()) { 137 source.loadStyleSource(); // reload to apply settings 138 } 139 } 140 } finally { 141 MapCSSStyleSource.STYLE_SOURCE_LOCK.writeLock().unlock(); 142 } 143 144 Dimension imgDimPx = getImageSize(); 145 NavigatableComponent nc = new NavigatableComponent() { 146 { 147 setBounds(0, 0, imgDimPx.width, imgDimPx.height); 148 updateLocationState(); 149 } 150 151 @Override 152 protected boolean isVisibleOnScreen() { 153 return true; 154 } 155 156 @Override 157 public Point getLocationOnScreen() { 158 return new Point(0, 0); 159 } 160 }; 161 nc.zoomTo(projBounds.getCenter(), scale); 162 163 // render the data 164 BufferedImage image = new BufferedImage(imgDimPx.width, imgDimPx.height, BufferedImage.TYPE_INT_ARGB); 165 Graphics2D g = image.createGraphics(); 166 167 // Force all render hints to be defaults - do not use platform values 168 g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); 169 g.setRenderingHint(RenderingHints.KEY_ALPHA_INTERPOLATION, RenderingHints.VALUE_ALPHA_INTERPOLATION_QUALITY); 170 g.setRenderingHint(RenderingHints.KEY_COLOR_RENDERING, RenderingHints.VALUE_COLOR_RENDER_QUALITY); 171 g.setRenderingHint(RenderingHints.KEY_DITHERING, RenderingHints.VALUE_DITHER_DISABLE); 172 g.setRenderingHint(RenderingHints.KEY_FRACTIONALMETRICS, RenderingHints.VALUE_FRACTIONALMETRICS_ON); 173 g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR); 174 g.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY); 175 g.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, RenderingHints.VALUE_STROKE_NORMALIZE); 176 g.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON); 177 178 if (fillBackground) { 179 g.setColor(Optional.ofNullable(backgroundColor).orElse(elemStyles.getBackgroundColor())); 180 g.fillRect(0, 0, imgDimPx.width, imgDimPx.height); 181 } 182 StyledMapRenderer smr = new StyledMapRenderer(g, nc, false); 183 smr.setStyles(elemStyles); 184 smr.render(ds, false, bounds); 185 return image; 186 } 187 188}