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