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}