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}