[12906] | 1 | // License: GPL. For details, see LICENSE file.
|
---|
| 2 | package org.openstreetmap.josm.gui.mappaint;
|
---|
| 3 |
|
---|
| 4 | import static org.openstreetmap.josm.tools.I18n.tr;
|
---|
| 5 |
|
---|
[12963] | 6 | import java.awt.Dimension;
|
---|
[12966] | 7 | import java.awt.image.BufferedImage;
|
---|
| 8 | import java.io.File;
|
---|
[12906] | 9 | import java.io.FileNotFoundException;
|
---|
| 10 | import java.io.IOException;
|
---|
[13204] | 11 | import java.nio.file.Files;
|
---|
| 12 | import java.nio.file.Paths;
|
---|
[12906] | 13 | import java.util.ArrayList;
|
---|
| 14 | import java.util.List;
|
---|
| 15 | import java.util.Locale;
|
---|
| 16 | import java.util.Optional;
|
---|
[13849] | 17 | import java.util.function.DoubleSupplier;
|
---|
[12906] | 18 | import java.util.logging.Level;
|
---|
| 19 |
|
---|
[12966] | 20 | import javax.imageio.ImageIO;
|
---|
| 21 |
|
---|
[12906] | 22 | import org.openstreetmap.gui.jmapviewer.OsmMercator;
|
---|
[14118] | 23 | import org.openstreetmap.josm.cli.CLIModule;
|
---|
[12906] | 24 | import org.openstreetmap.josm.data.Bounds;
|
---|
| 25 | import org.openstreetmap.josm.data.ProjectionBounds;
|
---|
| 26 | import org.openstreetmap.josm.data.coor.EastNorth;
|
---|
| 27 | import org.openstreetmap.josm.data.coor.LatLon;
|
---|
| 28 | import org.openstreetmap.josm.data.coor.conversion.LatLonParser;
|
---|
| 29 | import org.openstreetmap.josm.data.osm.DataSet;
|
---|
[13021] | 30 | import org.openstreetmap.josm.data.preferences.JosmBaseDirectories;
|
---|
[14119] | 31 | import org.openstreetmap.josm.data.preferences.JosmUrls;
|
---|
[12906] | 32 | import org.openstreetmap.josm.data.projection.Projection;
|
---|
[14120] | 33 | import org.openstreetmap.josm.data.projection.ProjectionRegistry;
|
---|
[12906] | 34 | import org.openstreetmap.josm.data.projection.Projections;
|
---|
[12963] | 35 | import org.openstreetmap.josm.gui.mappaint.RenderingHelper.StyleData;
|
---|
[12906] | 36 | import org.openstreetmap.josm.io.IllegalDataException;
|
---|
| 37 | import org.openstreetmap.josm.io.OsmReader;
|
---|
| 38 | import org.openstreetmap.josm.spi.preferences.Config;
|
---|
| 39 | import org.openstreetmap.josm.spi.preferences.MemoryPreferences;
|
---|
[13050] | 40 | import org.openstreetmap.josm.tools.I18n;
|
---|
| 41 | import org.openstreetmap.josm.tools.JosmDecimalFormatSymbolsProvider;
|
---|
[12906] | 42 | import org.openstreetmap.josm.tools.Logging;
|
---|
| 43 | import org.openstreetmap.josm.tools.RightAndLefthandTraffic;
|
---|
| 44 |
|
---|
| 45 | import gnu.getopt.Getopt;
|
---|
| 46 | import gnu.getopt.LongOpt;
|
---|
| 47 |
|
---|
| 48 | /**
|
---|
| 49 | * Command line interface for rendering osm data to an image file.
|
---|
| 50 | *
|
---|
| 51 | * @since 12906
|
---|
| 52 | */
|
---|
| 53 | public class RenderingCLI implements CLIModule {
|
---|
| 54 |
|
---|
| 55 | /**
|
---|
| 56 | * The singleton instance of this class.
|
---|
| 57 | */
|
---|
| 58 | public static final RenderingCLI INSTANCE = new RenderingCLI();
|
---|
| 59 |
|
---|
| 60 | private static final double PIXEL_PER_METER = 96 / 2.54 * 100; // standard value of 96 dpi display resolution
|
---|
| 61 | private static final int DEFAULT_MAX_IMAGE_SIZE = 20000;
|
---|
| 62 |
|
---|
| 63 | private boolean argDebug;
|
---|
| 64 | private boolean argTrace;
|
---|
| 65 | private String argInput;
|
---|
| 66 | private String argOutput;
|
---|
| 67 | private List<StyleData> argStyles;
|
---|
| 68 | private Integer argZoom;
|
---|
| 69 | private Double argScale;
|
---|
| 70 | private Bounds argBounds;
|
---|
| 71 | private LatLon argAnchor;
|
---|
| 72 | private Double argWidthM;
|
---|
| 73 | private Double argHeightM;
|
---|
| 74 | private Integer argWidthPx;
|
---|
| 75 | private Integer argHeightPx;
|
---|
| 76 | private String argProjection;
|
---|
| 77 | private Integer argMaxImageSize;
|
---|
| 78 |
|
---|
| 79 | private enum Option {
|
---|
| 80 | HELP(false, 'h'),
|
---|
| 81 | DEBUG(false, '*'),
|
---|
| 82 | TRACE(false, '*'),
|
---|
| 83 | INPUT(true, 'i'),
|
---|
| 84 | STYLE(true, 's'),
|
---|
| 85 | SETTING(true, '*'),
|
---|
| 86 | OUTPUT(true, 'o'),
|
---|
| 87 | ZOOM(true, 'z'),
|
---|
| 88 | SCALE(true, '*'),
|
---|
| 89 | BOUNDS(true, 'b'),
|
---|
| 90 | ANCHOR(true, '*'),
|
---|
| 91 | WIDTH_M(true, '*'),
|
---|
| 92 | HEIGHT_M(true, '*'),
|
---|
| 93 | WIDTH_PX(true, '*'),
|
---|
| 94 | HEIGHT_PX(true, '*'),
|
---|
| 95 | PROJECTION(true, '*'),
|
---|
| 96 | MAX_IMAGE_SIZE(true, '*');
|
---|
| 97 |
|
---|
| 98 | private final String name;
|
---|
| 99 | private final boolean requiresArg;
|
---|
| 100 | private final char shortOption;
|
---|
| 101 |
|
---|
| 102 | Option(boolean requiresArgument, char shortOption) {
|
---|
| 103 | this.name = name().toLowerCase(Locale.US).replace('_', '-');
|
---|
| 104 | this.requiresArg = requiresArgument;
|
---|
| 105 | this.shortOption = shortOption;
|
---|
| 106 | }
|
---|
| 107 |
|
---|
| 108 | /**
|
---|
| 109 | * Replies the option name
|
---|
| 110 | * @return The option name, in lowercase
|
---|
| 111 | */
|
---|
| 112 | public String getName() {
|
---|
| 113 | return name;
|
---|
| 114 | }
|
---|
| 115 |
|
---|
| 116 | /**
|
---|
| 117 | * Determines if this option requires an argument.
|
---|
| 118 | * @return {@code true} if this option requires an argument, {@code false} otherwise
|
---|
| 119 | */
|
---|
| 120 | public boolean requiresArgument() {
|
---|
| 121 | return requiresArg;
|
---|
| 122 | }
|
---|
| 123 |
|
---|
| 124 | /**
|
---|
| 125 | * Replies the short option (single letter) associated with this option.
|
---|
| 126 | * @return the short option or '*' if there is no short option
|
---|
| 127 | */
|
---|
| 128 | public char getShortOption() {
|
---|
| 129 | return shortOption;
|
---|
| 130 | }
|
---|
| 131 |
|
---|
| 132 | LongOpt toLongOpt() {
|
---|
| 133 | return new LongOpt(getName(), requiresArgument() ? LongOpt.REQUIRED_ARGUMENT : LongOpt.NO_ARGUMENT, null, getShortOption());
|
---|
| 134 | }
|
---|
| 135 | }
|
---|
| 136 |
|
---|
| 137 | /**
|
---|
| 138 | * Data class to hold return values for {@link #determineRenderingArea(DataSet)}.
|
---|
| 139 | *
|
---|
| 140 | * Package private access for unit tests.
|
---|
| 141 | */
|
---|
| 142 | static class RenderingArea {
|
---|
| 143 | public Bounds bounds;
|
---|
| 144 | public double scale; // in east-north units per pixel (unlike the --scale option, which is in meter per meter)
|
---|
| 145 | }
|
---|
| 146 |
|
---|
| 147 | RenderingCLI() {
|
---|
| 148 | // hide constructor (package private access for unit tests)
|
---|
| 149 | }
|
---|
| 150 |
|
---|
| 151 | @Override
|
---|
| 152 | public String getActionKeyword() {
|
---|
| 153 | return "render";
|
---|
| 154 | }
|
---|
| 155 |
|
---|
| 156 | @Override
|
---|
| 157 | public void processArguments(String[] argArray) {
|
---|
| 158 | try {
|
---|
| 159 | parseArguments(argArray);
|
---|
| 160 | initialize();
|
---|
[12963] | 161 | DataSet ds = loadDataset();
|
---|
| 162 | RenderingArea area = determineRenderingArea(ds);
|
---|
| 163 | RenderingHelper rh = new RenderingHelper(ds, area.bounds, area.scale, argStyles);
|
---|
| 164 | checkPreconditions(rh);
|
---|
[12966] | 165 | BufferedImage image = rh.render();
|
---|
| 166 | writeImageToFile(image);
|
---|
[12906] | 167 | } catch (FileNotFoundException e) {
|
---|
| 168 | if (Logging.isDebugEnabled()) {
|
---|
| 169 | e.printStackTrace();
|
---|
| 170 | }
|
---|
| 171 | System.err.println(tr("Error - file not found: ''{0}''", e.getMessage()));
|
---|
| 172 | System.exit(1);
|
---|
| 173 | } catch (IllegalArgumentException | IllegalDataException | IOException e) {
|
---|
| 174 | if (Logging.isDebugEnabled()) {
|
---|
| 175 | e.printStackTrace();
|
---|
| 176 | }
|
---|
| 177 | if (e.getMessage() != null) {
|
---|
| 178 | System.err.println(tr("Error: {0}", e.getMessage()));
|
---|
| 179 | }
|
---|
| 180 | System.exit(1);
|
---|
| 181 | }
|
---|
| 182 | System.exit(0);
|
---|
| 183 | }
|
---|
| 184 |
|
---|
| 185 | /**
|
---|
| 186 | * Parse command line arguments and do some low-level error checking.
|
---|
| 187 | * @param argArray the arguments array
|
---|
| 188 | */
|
---|
| 189 | void parseArguments(String[] argArray) {
|
---|
| 190 | Getopt.setI18nHandler(I18n::tr);
|
---|
| 191 | Logging.setLogLevel(Level.INFO);
|
---|
| 192 |
|
---|
| 193 | LongOpt[] opts = new LongOpt[Option.values().length];
|
---|
| 194 | StringBuilder optString = new StringBuilder();
|
---|
| 195 | for (Option o : Option.values()) {
|
---|
| 196 | opts[o.ordinal()] = o.toLongOpt();
|
---|
| 197 | if (o.getShortOption() != '*') {
|
---|
| 198 | optString.append(o.getShortOption());
|
---|
| 199 | if (o.requiresArgument()) {
|
---|
| 200 | optString.append(':');
|
---|
| 201 | }
|
---|
| 202 | }
|
---|
| 203 | }
|
---|
| 204 |
|
---|
| 205 | Getopt getopt = new Getopt("JOSM rendering", argArray, optString.toString(), opts);
|
---|
| 206 |
|
---|
| 207 | StyleData currentStyle = new StyleData();
|
---|
| 208 | argStyles = new ArrayList<>();
|
---|
| 209 |
|
---|
| 210 | int c;
|
---|
| 211 | while ((c = getopt.getopt()) != -1) {
|
---|
| 212 | switch (c) {
|
---|
| 213 | case 'h':
|
---|
| 214 | showHelp();
|
---|
| 215 | System.exit(0);
|
---|
| 216 | case 'i':
|
---|
| 217 | argInput = getopt.getOptarg();
|
---|
| 218 | break;
|
---|
| 219 | case 's':
|
---|
| 220 | if (currentStyle.styleUrl != null) {
|
---|
| 221 | argStyles.add(currentStyle);
|
---|
| 222 | currentStyle = new StyleData();
|
---|
| 223 | }
|
---|
| 224 | currentStyle.styleUrl = getopt.getOptarg();
|
---|
| 225 | break;
|
---|
| 226 | case 'o':
|
---|
| 227 | argOutput = getopt.getOptarg();
|
---|
| 228 | break;
|
---|
| 229 | case 'z':
|
---|
| 230 | try {
|
---|
[13399] | 231 | argZoom = Integer.valueOf(getopt.getOptarg());
|
---|
[12906] | 232 | } catch (NumberFormatException nfe) {
|
---|
| 233 | throw new IllegalArgumentException(
|
---|
[13207] | 234 | tr("Expected integer number for option {0}, but got ''{1}''", "--zoom", getopt.getOptarg()), nfe);
|
---|
[12906] | 235 | }
|
---|
| 236 | if (argZoom < 0)
|
---|
| 237 | throw new IllegalArgumentException(
|
---|
| 238 | tr("Expected integer number >= 0 for option {0}, but got ''{1}''", "--zoom", getopt.getOptarg()));
|
---|
| 239 | break;
|
---|
| 240 | case 'b':
|
---|
[13849] | 241 | if (!"auto".equals(getopt.getOptarg())) {
|
---|
[12906] | 242 | try {
|
---|
| 243 | argBounds = new Bounds(getopt.getOptarg(), ",", Bounds.ParseMethod.LEFT_BOTTOM_RIGHT_TOP, false);
|
---|
[12907] | 244 | } catch (IllegalArgumentException iae) { // NOPMD
|
---|
[13207] | 245 | throw new IllegalArgumentException(tr("Unable to parse {0} parameter: {1}", "--bounds", iae.getMessage()), iae);
|
---|
[12906] | 246 | }
|
---|
| 247 | }
|
---|
| 248 | break;
|
---|
| 249 | case '*':
|
---|
| 250 | switch (Option.values()[getopt.getLongind()]) {
|
---|
| 251 | case DEBUG:
|
---|
| 252 | argDebug = true;
|
---|
| 253 | break;
|
---|
| 254 | case TRACE:
|
---|
| 255 | argTrace = true;
|
---|
| 256 | break;
|
---|
| 257 | case SETTING:
|
---|
| 258 | String keyval = getopt.getOptarg();
|
---|
| 259 | String[] comp = keyval.split(":");
|
---|
| 260 | if (comp.length != 2)
|
---|
| 261 | throw new IllegalArgumentException(
|
---|
| 262 | tr("Expected key and value, separated by '':'' character for option {0}, but got ''{1}''",
|
---|
| 263 | "--setting", getopt.getOptarg()));
|
---|
| 264 | currentStyle.settings.put(comp[0].trim(), comp[1].trim());
|
---|
| 265 | break;
|
---|
| 266 | case SCALE:
|
---|
| 267 | try {
|
---|
[13050] | 268 | argScale = JosmDecimalFormatSymbolsProvider.parseDouble(getopt.getOptarg());
|
---|
[12906] | 269 | } catch (NumberFormatException nfe) {
|
---|
| 270 | throw new IllegalArgumentException(
|
---|
[13207] | 271 | tr("Expected floating point number for option {0}, but got ''{1}''", "--scale", getopt.getOptarg()), nfe);
|
---|
[12906] | 272 | }
|
---|
| 273 | break;
|
---|
| 274 | case ANCHOR:
|
---|
| 275 | String[] parts = getopt.getOptarg().split(",");
|
---|
| 276 | if (parts.length != 2)
|
---|
| 277 | throw new IllegalArgumentException(
|
---|
| 278 | tr("Expected two coordinates, separated by comma, for option {0}, but got ''{1}''",
|
---|
| 279 | "--anchor", getopt.getOptarg()));
|
---|
| 280 | try {
|
---|
| 281 | double lon = LatLonParser.parseCoordinate(parts[0]);
|
---|
| 282 | double lat = LatLonParser.parseCoordinate(parts[1]);
|
---|
| 283 | argAnchor = new LatLon(lat, lon);
|
---|
[12907] | 284 | } catch (IllegalArgumentException iae) { // NOPMD
|
---|
[13207] | 285 | throw new IllegalArgumentException(tr("In option {0}: {1}", "--anchor", iae.getMessage()), iae);
|
---|
[12906] | 286 | }
|
---|
| 287 | break;
|
---|
| 288 | case WIDTH_M:
|
---|
| 289 | try {
|
---|
[13050] | 290 | argWidthM = JosmDecimalFormatSymbolsProvider.parseDouble(getopt.getOptarg());
|
---|
[12906] | 291 | } catch (NumberFormatException nfe) {
|
---|
| 292 | throw new IllegalArgumentException(
|
---|
[13207] | 293 | tr("Expected floating point number for option {0}, but got ''{1}''", "--width-m", getopt.getOptarg()), nfe);
|
---|
[12906] | 294 | }
|
---|
| 295 | if (argWidthM <= 0) throw new IllegalArgumentException(
|
---|
| 296 | tr("Expected floating point number > 0 for option {0}, but got ''{1}''", "--width-m", getopt.getOptarg()));
|
---|
| 297 | break;
|
---|
| 298 | case HEIGHT_M:
|
---|
| 299 | try {
|
---|
[13050] | 300 | argHeightM = JosmDecimalFormatSymbolsProvider.parseDouble(getopt.getOptarg());
|
---|
[12906] | 301 | } catch (NumberFormatException nfe) {
|
---|
| 302 | throw new IllegalArgumentException(
|
---|
[13207] | 303 | tr("Expected floating point number for option {0}, but got ''{1}''", "--height-m", getopt.getOptarg()), nfe);
|
---|
[12906] | 304 | }
|
---|
| 305 | if (argHeightM <= 0) throw new IllegalArgumentException(
|
---|
| 306 | tr("Expected floating point number > 0 for option {0}, but got ''{1}''", "--width-m", getopt.getOptarg()));
|
---|
| 307 | break;
|
---|
| 308 | case WIDTH_PX:
|
---|
| 309 | try {
|
---|
[13399] | 310 | argWidthPx = Integer.valueOf(getopt.getOptarg());
|
---|
[12906] | 311 | } catch (NumberFormatException nfe) {
|
---|
| 312 | throw new IllegalArgumentException(
|
---|
[13207] | 313 | tr("Expected integer number for option {0}, but got ''{1}''", "--width-px", getopt.getOptarg()), nfe);
|
---|
[12906] | 314 | }
|
---|
| 315 | if (argWidthPx <= 0) throw new IllegalArgumentException(
|
---|
| 316 | tr("Expected integer number > 0 for option {0}, but got ''{1}''", "--width-px", getopt.getOptarg()));
|
---|
| 317 | break;
|
---|
| 318 | case HEIGHT_PX:
|
---|
| 319 | try {
|
---|
[13399] | 320 | argHeightPx = Integer.valueOf(getopt.getOptarg());
|
---|
[12906] | 321 | } catch (NumberFormatException nfe) {
|
---|
| 322 | throw new IllegalArgumentException(
|
---|
[13207] | 323 | tr("Expected integer number for option {0}, but got ''{1}''", "--height-px", getopt.getOptarg()), nfe);
|
---|
[12906] | 324 | }
|
---|
| 325 | if (argHeightPx <= 0) throw new IllegalArgumentException(
|
---|
| 326 | tr("Expected integer number > 0 for option {0}, but got ''{1}''", "--height-px", getopt.getOptarg()));
|
---|
| 327 | break;
|
---|
| 328 | case PROJECTION:
|
---|
| 329 | argProjection = getopt.getOptarg();
|
---|
| 330 | break;
|
---|
| 331 | case MAX_IMAGE_SIZE:
|
---|
| 332 | try {
|
---|
[13399] | 333 | argMaxImageSize = Integer.valueOf(getopt.getOptarg());
|
---|
[12906] | 334 | } catch (NumberFormatException nfe) {
|
---|
| 335 | throw new IllegalArgumentException(
|
---|
[13207] | 336 | tr("Expected integer number for option {0}, but got ''{1}''", "--max-image-size", getopt.getOptarg()), nfe);
|
---|
[12906] | 337 | }
|
---|
| 338 | if (argMaxImageSize < 0) throw new IllegalArgumentException(
|
---|
[12936] | 339 | tr("Expected integer number >= 0 for option {0}, but got ''{1}''", "--max-image-size", getopt.getOptarg()));
|
---|
[12906] | 340 | break;
|
---|
| 341 | default:
|
---|
| 342 | throw new AssertionError("Unexpected option index: " + getopt.getLongind());
|
---|
| 343 | }
|
---|
| 344 | break;
|
---|
| 345 | case '?':
|
---|
| 346 | throw new IllegalArgumentException(); // getopt error
|
---|
| 347 | default:
|
---|
| 348 | throw new AssertionError("Unrecognized option: " + c);
|
---|
| 349 | }
|
---|
| 350 | }
|
---|
| 351 | if (currentStyle.styleUrl != null) {
|
---|
| 352 | argStyles.add(currentStyle);
|
---|
| 353 | }
|
---|
| 354 | }
|
---|
| 355 |
|
---|
| 356 | /**
|
---|
| 357 | * Displays help on the console
|
---|
| 358 | */
|
---|
| 359 | public static void showHelp() {
|
---|
| 360 | System.out.println(getHelp());
|
---|
| 361 | }
|
---|
| 362 |
|
---|
| 363 | private static String getHelp() {
|
---|
| 364 | return tr("JOSM rendering command line interface")+"\n\n"+
|
---|
| 365 | tr("Usage")+":\n"+
|
---|
| 366 | "\tjava -jar josm.jar render <options>\n\n"+
|
---|
| 367 | tr("Description")+":\n"+
|
---|
| 368 | tr("Renders data and saves the result to an image file.")+"\n\n"+
|
---|
| 369 | tr("Options")+":\n"+
|
---|
| 370 | "\t--help|-h "+tr("Show this help")+"\n"+
|
---|
| 371 | "\t--input|-i <file> "+tr("Input data file name (.osm)")+"\n"+
|
---|
| 372 | "\t--output|-o <file> "+tr("Output image file name (.png); defaults to ''{0}''", "out.png")+"\n"+
|
---|
| 373 | "\t--style|-s <file> "+tr("Style file to use for rendering (.mapcss or .zip)")+"\n"+
|
---|
| 374 | "\t "+tr("This option can be repeated to load multiple styles.")+"\n"+
|
---|
| 375 | "\t--setting <key>:<value> "+tr("Style setting (in JOSM accessible in the style list dialog right click menu)")+"\n"+
|
---|
| 376 | "\t "+tr("Applies to the last style loaded with the {0} option.", "--style")+"\n"+
|
---|
| 377 | "\t--zoom|-z <lvl> "+tr("Select zoom level to render. (integer value, 0=entire earth, 18=street level)")+"\n"+
|
---|
| 378 | "\t--scale <scale> "+tr("Select the map scale")+"\n"+
|
---|
| 379 | "\t "+tr("A value of 10000 denotes a scale of 1:10000 (1 cm on the map equals 100 m on the ground; "
|
---|
| 380 | + "display resolution: 96 dpi)")+"\n"+
|
---|
| 381 | "\t "+tr("Options {0} and {1} are mutually exclusive.", "--zoom", "--scale")+"\n"+
|
---|
| 382 | "\t--bounds|-b auto|<min_lon>,<min_lat>,<max_lon>,<max_lat>\n"+
|
---|
| 383 | "\t "+tr("Area to render, default value is ''{0}''", "auto")+"\n"+
|
---|
| 384 | "\t "+tr("With keyword ''{0}'', the downloaded area in the .osm input file will be used (if recorded).",
|
---|
| 385 | "auto")+"\n"+
|
---|
| 386 | "\t--anchor <lon>,<lat> "+tr("Specify bottom left corner of the rendering area")+"\n"+
|
---|
| 387 | "\t "+tr("Used in combination with width and height options to determine the area to render.")+"\n"+
|
---|
| 388 | "\t--width-m <number> "+tr("Width of the rendered area, in meter")+"\n"+
|
---|
| 389 | "\t--height-m <number> "+tr("Height of the rendered area, in meter")+"\n"+
|
---|
| 390 | "\t--width-px <number> "+tr("Width of the target image, in pixel")+"\n"+
|
---|
| 391 | "\t--height-px <number> "+tr("Height of the target image, in pixel")+"\n"+
|
---|
| 392 | "\t--projection <code> "+tr("Projection to use, default value ''{0}'' (web-Mercator)", "epsg:3857")+"\n"+
|
---|
| 393 | "\t--max-image-size <number> "+tr("Maximum image width/height in pixel (''{0}'' means no limit), default value: {1}",
|
---|
| 394 | 0, Integer.toString(DEFAULT_MAX_IMAGE_SIZE))+"\n"+
|
---|
| 395 | "\n"+
|
---|
| 396 | tr("To specify the rendered area and scale, the options can be combined in various ways")+":\n"+
|
---|
| 397 | " * --bounds (--zoom|--scale|--width-px|--height-px)\n"+
|
---|
| 398 | " * --anchor (--width-m|--width-px) (--height-m|--height-px) (--zoom|--scale)\n"+
|
---|
| 399 | " * --anchor --width-m --height-m (--width-px|--height-px)\n"+
|
---|
| 400 | " * --anchor --width-px --height-px (--width-m|--height-m)\n"+
|
---|
| 401 | tr("If neither ''{0}'' nor ''{1}'' is given, the default value {2} takes effect "
|
---|
| 402 | + "and the bounds of the download area in the .osm input file are used.",
|
---|
| 403 | "bounds", "anchor", "--bounds=auto")+"\n\n"+
|
---|
| 404 | tr("Examples")+":\n"+
|
---|
| 405 | " java -jar josm.jar render -i data.osm -s style.mapcss -z 16\n"+
|
---|
| 406 | " josm render -i data.osm -s style.mapcss --scale 5000\n"+
|
---|
| 407 | " josm render -i data.osm -s style.mapcss -z 16 -o image.png\n"+
|
---|
| 408 | " josm render -i data.osm -s elemstyles.mapcss --setting hide_icons:false -z 16\n"+
|
---|
| 409 | " josm render -i data.osm -s style.mapcss -s another_style.mapcss -z 16 -o image.png\n"+
|
---|
| 410 | " josm render -i data.osm -s style.mapcss --bounds 21.151,51.401,21.152,51.402 -z 16\n"+
|
---|
| 411 | " josm render -i data.osm -s style.mapcss --anchor 21.151,51.401 --width-m 500 --height-m 300 -z 16\n"+
|
---|
| 412 | " josm render -i data.osm -s style.mapcss --anchor 21.151,51.401 --width-m 500 --height-m 300 --width-px 1800\n"+
|
---|
| 413 | " josm render -i data.osm -s style.mapcss --scale 5000 --projection epsg:4326\n";
|
---|
| 414 | }
|
---|
| 415 |
|
---|
| 416 | /**
|
---|
| 417 | * Initialization.
|
---|
| 418 | *
|
---|
| 419 | * Requires arguments to be parsed already ({@link #parseArguments(java.lang.String[])}).
|
---|
| 420 | */
|
---|
| 421 | void initialize() {
|
---|
| 422 | Logging.setLogLevel(getLogLevel());
|
---|
| 423 |
|
---|
[13021] | 424 | Config.setBaseDirectoriesProvider(JosmBaseDirectories.getInstance()); // for right-left-hand traffic cache file
|
---|
[12906] | 425 | Config.setPreferencesInstance(new MemoryPreferences());
|
---|
[14119] | 426 | Config.setUrlsProvider(JosmUrls.getInstance());
|
---|
[12906] | 427 | Config.getPref().putBoolean("mappaint.auto_reload_local_styles", false); // unnecessary to listen for external changes
|
---|
| 428 | String projCode = Optional.ofNullable(argProjection).orElse("epsg:3857");
|
---|
[14120] | 429 | ProjectionRegistry.setProjection(Projections.getProjectionByCode(projCode.toUpperCase(Locale.US)));
|
---|
[12906] | 430 |
|
---|
| 431 | RightAndLefthandTraffic.initialize();
|
---|
| 432 | }
|
---|
| 433 |
|
---|
| 434 | private Level getLogLevel() {
|
---|
| 435 | if (argTrace) {
|
---|
| 436 | return Logging.LEVEL_TRACE;
|
---|
| 437 | } else if (argDebug) {
|
---|
| 438 | return Logging.LEVEL_DEBUG;
|
---|
| 439 | } else {
|
---|
| 440 | return Logging.LEVEL_INFO;
|
---|
| 441 | }
|
---|
| 442 | }
|
---|
| 443 |
|
---|
| 444 | /**
|
---|
| 445 | * Find the area to render and the scale, given certain command line options and the dataset.
|
---|
| 446 | * @param ds the dataset
|
---|
| 447 | * @return area to render and the scale
|
---|
| 448 | */
|
---|
| 449 | RenderingArea determineRenderingArea(DataSet ds) {
|
---|
| 450 |
|
---|
[14120] | 451 | Projection proj = ProjectionRegistry.getProjection();
|
---|
[12906] | 452 | Double scale = null; // scale in east-north units per pixel
|
---|
| 453 | if (argZoom != null) {
|
---|
| 454 | scale = OsmMercator.EARTH_RADIUS * Math.PI * 2 / Math.pow(2, argZoom) / OsmMercator.DEFAUL_TILE_SIZE / proj.getMetersPerUnit();
|
---|
| 455 | }
|
---|
| 456 | Bounds bounds = argBounds;
|
---|
| 457 | ProjectionBounds pb = null;
|
---|
| 458 |
|
---|
| 459 | if (bounds == null) {
|
---|
| 460 | if (argAnchor != null) {
|
---|
| 461 | EastNorth projAnchor = proj.latlon2eastNorth(argAnchor);
|
---|
| 462 |
|
---|
[13849] | 463 | double enPerMeter = Double.NaN;
|
---|
| 464 | DoubleSupplier getEnPerMeter = () -> {
|
---|
[12906] | 465 | double shiftMeter = 10;
|
---|
| 466 | EastNorth projAnchorShifted = projAnchor.add(
|
---|
| 467 | shiftMeter / proj.getMetersPerUnit(), shiftMeter / proj.getMetersPerUnit());
|
---|
| 468 | LatLon anchorShifted = proj.eastNorth2latlon(projAnchorShifted);
|
---|
| 469 | return projAnchor.distance(projAnchorShifted) / argAnchor.greatCircleDistance(anchorShifted);
|
---|
| 470 | };
|
---|
| 471 |
|
---|
| 472 | if (scale == null) {
|
---|
| 473 | if (argScale != null) {
|
---|
[13849] | 474 | enPerMeter = getEnPerMeter.getAsDouble();
|
---|
[12906] | 475 | scale = argScale * enPerMeter / PIXEL_PER_METER;
|
---|
| 476 | } else if (argWidthM != null && argWidthPx != null) {
|
---|
[13849] | 477 | enPerMeter = getEnPerMeter.getAsDouble();
|
---|
[12906] | 478 | scale = argWidthM / argWidthPx * enPerMeter;
|
---|
| 479 | } else if (argHeightM != null && argHeightPx != null) {
|
---|
[13849] | 480 | enPerMeter = getEnPerMeter.getAsDouble();
|
---|
[12906] | 481 | scale = argHeightM / argHeightPx * enPerMeter;
|
---|
| 482 | } else {
|
---|
| 483 | throw new IllegalArgumentException(
|
---|
| 484 | tr("Argument {0} given, but scale cannot be determined from remaining arguments", "--anchor"));
|
---|
| 485 | }
|
---|
| 486 | }
|
---|
| 487 |
|
---|
| 488 | double widthEn;
|
---|
| 489 | if (argWidthM != null) {
|
---|
[13850] | 490 | if (Double.isNaN(enPerMeter)) {
|
---|
[13849] | 491 | enPerMeter = getEnPerMeter.getAsDouble();
|
---|
| 492 | }
|
---|
[12906] | 493 | widthEn = argWidthM * enPerMeter;
|
---|
| 494 | } else if (argWidthPx != null) {
|
---|
| 495 | widthEn = argWidthPx * scale;
|
---|
| 496 | } else {
|
---|
| 497 | throw new IllegalArgumentException(
|
---|
| 498 | tr("Argument {0} given, expected {1} or {2}", "--anchor", "--width-m", "--width-px"));
|
---|
| 499 | }
|
---|
| 500 |
|
---|
| 501 | double heightEn;
|
---|
| 502 | if (argHeightM != null) {
|
---|
[13850] | 503 | if (Double.isNaN(enPerMeter)) {
|
---|
[13849] | 504 | enPerMeter = getEnPerMeter.getAsDouble();
|
---|
| 505 | }
|
---|
[12906] | 506 | heightEn = argHeightM * enPerMeter;
|
---|
| 507 | } else if (argHeightPx != null) {
|
---|
| 508 | heightEn = argHeightPx * scale;
|
---|
| 509 | } else {
|
---|
| 510 | throw new IllegalArgumentException(
|
---|
| 511 | tr("Argument {0} given, expected {1} or {2}", "--anchor", "--height-m", "--height-px"));
|
---|
| 512 | }
|
---|
| 513 | pb = new ProjectionBounds(projAnchor);
|
---|
| 514 | pb.extend(new EastNorth(projAnchor.east() + widthEn, projAnchor.north() + heightEn));
|
---|
| 515 | bounds = new Bounds(proj.eastNorth2latlon(pb.getMin()), false);
|
---|
| 516 | bounds.extend(proj.eastNorth2latlon(pb.getMax()));
|
---|
| 517 | } else {
|
---|
| 518 | if (ds.getDataSourceBounds().isEmpty()) {
|
---|
| 519 | throw new IllegalArgumentException(tr("{0} mode, but no bounds found in osm data input file", "--bounds=auto"));
|
---|
| 520 | }
|
---|
| 521 | bounds = ds.getDataSourceBounds().get(0);
|
---|
| 522 | }
|
---|
| 523 | }
|
---|
| 524 |
|
---|
| 525 | if (pb == null) {
|
---|
| 526 | pb = new ProjectionBounds();
|
---|
| 527 | pb.extend(proj.latlon2eastNorth(bounds.getMin()));
|
---|
| 528 | pb.extend(proj.latlon2eastNorth(bounds.getMax()));
|
---|
| 529 | }
|
---|
| 530 |
|
---|
| 531 | if (scale == null) {
|
---|
| 532 | if (argScale != null) {
|
---|
| 533 | double enPerMeter = pb.getMin().distance(pb.getMax()) / bounds.getMin().greatCircleDistance(bounds.getMax());
|
---|
| 534 | scale = argScale * enPerMeter / PIXEL_PER_METER;
|
---|
| 535 | } else if (argWidthPx != null) {
|
---|
| 536 | scale = (pb.maxEast - pb.minEast) / argWidthPx;
|
---|
| 537 | } else if (argHeightPx != null) {
|
---|
| 538 | scale = (pb.maxNorth - pb.minNorth) / argHeightPx;
|
---|
| 539 | } else {
|
---|
| 540 | throw new IllegalArgumentException(
|
---|
| 541 | tr("Unable to determine scale, one of the options {0}, {1}, {2} or {3} expected",
|
---|
| 542 | "--zoom", "--scale", "--width-px", "--height-px"));
|
---|
| 543 | }
|
---|
| 544 | }
|
---|
| 545 |
|
---|
| 546 | RenderingArea ra = new RenderingArea();
|
---|
| 547 | ra.bounds = bounds;
|
---|
| 548 | ra.scale = scale;
|
---|
| 549 | return ra;
|
---|
| 550 | }
|
---|
| 551 |
|
---|
[13204] | 552 | private DataSet loadDataset() throws IOException, IllegalDataException {
|
---|
[12906] | 553 | if (argInput == null) {
|
---|
| 554 | throw new IllegalArgumentException(tr("Missing argument - input data file ({0})", "--input|-i"));
|
---|
| 555 | }
|
---|
| 556 | try {
|
---|
[13204] | 557 | return OsmReader.parseDataSet(Files.newInputStream(Paths.get(argInput)), null);
|
---|
[12906] | 558 | } catch (IllegalDataException e) {
|
---|
[13207] | 559 | throw new IllegalDataException(tr("In .osm data file ''{0}'' - ", argInput) + e.getMessage(), e);
|
---|
[12906] | 560 | }
|
---|
[12963] | 561 | }
|
---|
[12906] | 562 |
|
---|
[12963] | 563 | private void checkPreconditions(RenderingHelper rh) {
|
---|
| 564 | if (argStyles.isEmpty())
|
---|
| 565 | throw new IllegalArgumentException(tr("Missing argument - at least one style expected ({0})", "--style"));
|
---|
[12906] | 566 |
|
---|
[12963] | 567 | Dimension imgSize = rh.getImageSize();
|
---|
| 568 | Logging.debug("image size (px): {0}x{1}", imgSize.width, imgSize.height);
|
---|
[12906] | 569 | int maxSize = Optional.ofNullable(argMaxImageSize).orElse(DEFAULT_MAX_IMAGE_SIZE);
|
---|
[12963] | 570 | if (maxSize != 0 && (imgSize.width > maxSize || imgSize.height > maxSize)) {
|
---|
[12906] | 571 | throw new IllegalArgumentException(
|
---|
| 572 | tr("Image dimensions ({0}x{1}) exceeds maximum image size {2} (use option {3} to change limit)",
|
---|
[12963] | 573 | imgSize.width, imgSize.height, maxSize, "--max-image-size"));
|
---|
[12906] | 574 | }
|
---|
| 575 | }
|
---|
[12966] | 576 |
|
---|
| 577 | private void writeImageToFile(BufferedImage image) throws IOException {
|
---|
| 578 | String output = Optional.ofNullable(argOutput).orElse("out.png");
|
---|
| 579 | ImageIO.write(image, "png", new File(output));
|
---|
| 580 | }
|
---|
[12906] | 581 | }
|
---|