Ticket #8464: josm-map-grid.patch
| File josm-map-grid.patch, 118.2 KB (added by , 16 hours ago) |
|---|
-
new file resources/images/grid.svg
diff --git resources/images/grid.svg resources/images/grid.svg new file mode 100644 index 0000000000..a68166e9cc
- + 1 <?xml version="1.0" encoding="UTF-8"?> 2 <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"> 3 <rect x="2.5" y="2.5" width="19" height="19" rx="1.5" fill="#f4f4f4" stroke="#606060"/> 4 <path d="M9 2.5v19M15.5 2.5v19M2.5 9h19M2.5 15.5h19" fill="none" stroke="#3d7ac2"/> 5 </svg> -
new file resources/images/gridorigin.svg
diff --git resources/images/gridorigin.svg resources/images/gridorigin.svg new file mode 100644 index 0000000000..f92f0fb38e
- + 1 <?xml version="1.0" encoding="UTF-8"?> 2 <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"> 3 <rect x="2.5" y="2.5" width="19" height="19" rx="1.5" fill="#f4f4f4" stroke="#606060"/> 4 <path d="M12 2.5v19M2.5 12h19" fill="none" stroke="#3d7ac2"/> 5 <rect x="9" y="9" width="6" height="6" fill="#ffffff" stroke="#df421e" stroke-width="2"/><!-- origin at a grid crossing --> 6 </svg> -
new file resources/images/gridrotate.svg
diff --git resources/images/gridrotate.svg resources/images/gridrotate.svg new file mode 100644 index 0000000000..527ad84175
- + 1 <?xml version="1.0" encoding="UTF-8"?> 2 <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"> 3 <g transform="rotate(-30 12 12)" fill="none" stroke="#3d7ac2" stroke-width="1.5"><!-- grid turned to the way below --> 4 <rect x="4.5" y="4.5" width="15" height="15"/> 5 <path d="M12 4.5v15"/> 6 </g> 7 <path d="M2.4 17.7 21.6 6.6" fill="none" stroke="#df421e" stroke-width="2.5"/><!-- the selected way --> 8 </svg> -
new file resources/images/preferences/grid.svg
diff --git resources/images/preferences/grid.svg resources/images/preferences/grid.svg new file mode 100644 index 0000000000..9107df1f5d
- + 1 <?xml version="1.0" encoding="UTF-8"?> 2 <svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48"> 3 <rect x="4" y="4" width="40" height="40" rx="3" fill="#f4f4f4" stroke="#606060" stroke-width="2"/> 4 <path d="M17.3 4v40M30.7 4v40M4 17.3h40M4 30.7h40" fill="none" stroke="#3d7ac2" stroke-width="2"/> 5 </svg> -
new file src/org/openstreetmap/josm/actions/AlignGridRotationAction.java
diff --git src/org/openstreetmap/josm/actions/AlignGridRotationAction.java src/org/openstreetmap/josm/actions/AlignGridRotationAction.java new file mode 100644 index 0000000000..b3b0eb616c
- + 1 // License: GPL. For details, see LICENSE file. 2 package org.openstreetmap.josm.actions; 3 4 import static org.openstreetmap.josm.tools.I18n.tr; 5 6 import java.awt.event.ActionEvent; 7 import java.util.Collection; 8 import java.util.Iterator; 9 10 import org.openstreetmap.josm.data.coor.EastNorth; 11 import org.openstreetmap.josm.data.osm.DataSet; 12 import org.openstreetmap.josm.data.osm.Node; 13 import org.openstreetmap.josm.data.osm.OsmPrimitive; 14 import org.openstreetmap.josm.data.osm.Way; 15 import org.openstreetmap.josm.gui.MainApplication; 16 import org.openstreetmap.josm.gui.layer.MapGridPaintable; 17 import org.openstreetmap.josm.gui.layer.MapGridPaintable.GridType; 18 19 /** 20 * Rotates the grid drawn over the map so that its lines run parallel to the current selection: either a single way 21 * (the direction from its first to its last node) or exactly two nodes. Switches the grid to projected coordinates, 22 * since a latitude/longitude grid cannot be rotated, and enables it. 23 * @see MapGridPaintable 24 * @see SetGridOriginAction 25 * @since xxx 26 */ 27 public class AlignGridRotationAction extends JosmAction { 28 29 /** 30 * Constructs a new {@code AlignGridRotationAction}. 31 */ 32 public AlignGridRotationAction() { 33 super(tr("Align rotation to selection"), "gridrotate", 34 tr("Rotate the grid so that its lines are parallel to the selected way " 35 + "(first to last node) or to the line between the two selected nodes. Switches to a projected grid."), 36 null, false); 37 } 38 39 /** 40 * Determines the direction given by the selection: a single way with at least two nodes, or exactly two nodes. 41 * @param ds the data set, may be null 42 * @return start and end point, or {@code null} if the selection does not define a direction 43 */ 44 static EastNorth[] getSelectedDirection(DataSet ds) { 45 if (ds == null) { 46 return null; 47 } 48 Collection<Way> ways = ds.getSelectedWays(); 49 Collection<Node> nodes = ds.getSelectedNodes(); 50 Node a = null; 51 Node b = null; 52 if (ways.size() == 1 && nodes.isEmpty()) { 53 Way w = ways.iterator().next(); 54 if (w.getNodesCount() >= 2) { 55 a = w.firstNode(); 56 b = w.isClosed() ? w.getNode(1) : w.lastNode(); 57 } 58 } else if (nodes.size() == 2 && ways.isEmpty()) { 59 Iterator<Node> it = nodes.iterator(); 60 a = it.next(); 61 b = it.next(); 62 } 63 if (a == null || b == null || !a.isLatLonKnown() || !b.isLatLonKnown()) { 64 return null; 65 } 66 EastNorth[] result = {a.getEastNorth(), b.getEastNorth()}; 67 return result[0].equalsEpsilon(result[1], 1e-9) ? null : result; 68 } 69 70 /** 71 * Computes the grid rotation (counter clockwise, in degrees, in the range [0, 90)) for which a grid line is 72 * parallel to the given direction. 73 * @param from start point 74 * @param to end point 75 * @return the rotation in degrees 76 */ 77 public static double rotationOf(EastNorth from, EastNorth to) { 78 double angle = Math.toDegrees(Math.atan2(to.north() - from.north(), to.east() - from.east())); 79 angle %= 90; 80 if (angle < 0) { 81 angle += 90; 82 } 83 return angle >= 90 - 1e-9 ? 0 : angle; 84 } 85 86 @Override 87 protected void updateEnabledState() { 88 setEnabled(getSelectedDirection(getLayerManager().getEditDataSet()) != null); 89 } 90 91 @Override 92 protected void updateEnabledState(Collection<? extends OsmPrimitive> selection) { 93 updateEnabledState(); 94 } 95 96 @Override 97 public void actionPerformed(ActionEvent e) { 98 EastNorth[] direction = getSelectedDirection(getLayerManager().getEditDataSet()); 99 if (direction == null) { 100 return; 101 } 102 MapGridPaintable.ROTATION.put(rotationOf(direction[0], direction[1])); 103 MapGridPaintable.TYPE.put(GridType.PROJECTED); 104 MapGridPaintable.ENABLED.put(true); 105 if (MainApplication.isDisplayingMapView()) { 106 MainApplication.getMap().mapView.repaint(); 107 } 108 } 109 } -
new file src/org/openstreetmap/josm/actions/SetGridOriginAction.java
diff --git src/org/openstreetmap/josm/actions/SetGridOriginAction.java src/org/openstreetmap/josm/actions/SetGridOriginAction.java new file mode 100644 index 0000000000..d45b72ce5a
- + 1 // License: GPL. For details, see LICENSE file. 2 package org.openstreetmap.josm.actions; 3 4 import static org.openstreetmap.josm.tools.I18n.tr; 5 6 import java.awt.event.ActionEvent; 7 import java.util.Collection; 8 9 import org.openstreetmap.josm.data.coor.EastNorth; 10 import org.openstreetmap.josm.data.coor.LatLon; 11 import org.openstreetmap.josm.data.osm.DataSet; 12 import org.openstreetmap.josm.data.osm.Node; 13 import org.openstreetmap.josm.data.osm.OsmPrimitive; 14 import org.openstreetmap.josm.data.osm.visitor.AllNodesVisitor; 15 import org.openstreetmap.josm.data.projection.ProjectionRegistry; 16 import org.openstreetmap.josm.gui.MainApplication; 17 import org.openstreetmap.josm.gui.layer.MapGridPaintable; 18 import org.openstreetmap.josm.gui.layer.MapGridPaintable.GridType; 19 import org.openstreetmap.josm.tools.Logging; 20 21 /** 22 * Moves the origin of the grid drawn over the map to the current selection, so that grid lines pass through it, 23 * and enables the grid. The origin is the position of the selected node, or the centroid (arithmetic mean 24 * position) of all nodes reachable from the current selection (nodes of selected ways, node members of selected 25 * relations) if more than one node is involved. 26 * @see MapGridPaintable 27 * @see AlignGridRotationAction 28 * @since xxx 29 */ 30 public class SetGridOriginAction extends JosmAction { 31 32 /** 33 * Constructs a new {@code SetGridOriginAction}. 34 */ 35 public SetGridOriginAction() { 36 super(tr("Set origin to selection"), "gridorigin", 37 tr("Move the grid so that a grid line passes through the selected node, " 38 + "or through the centroid of the current selection, and show the grid."), 39 null, false); 40 } 41 42 @Override 43 protected void updateEnabledState() { 44 setEnabled(getCentroid(getLayerManager().getEditDataSet()) != null); 45 } 46 47 @Override 48 protected void updateEnabledState(Collection<? extends OsmPrimitive> selection) { 49 updateEnabledState(); 50 } 51 52 @Override 53 public void actionPerformed(ActionEvent e) { 54 EastNorth centroid = getCentroid(getLayerManager().getEditDataSet()); 55 if (centroid == null) { 56 return; 57 } 58 setOrigin(centroid); 59 if (MainApplication.isDisplayingMapView()) { 60 MainApplication.getMap().mapView.repaint(); 61 } 62 } 63 64 /** 65 * Computes the centroid of the current selection: the (arithmetic mean) position of every node reachable from 66 * the selected primitives (the selected nodes themselves, the nodes of selected ways, and the node members of 67 * selected relations). For a single selected node this is simply that node's position. 68 * @param ds the data set, may be {@code null} 69 * @return the centroid, or {@code null} if the selection contains no node with known coordinates 70 */ 71 static EastNorth getCentroid(DataSet ds) { 72 if (ds == null) { 73 return null; 74 } 75 double sumEast = 0; 76 double sumNorth = 0; 77 int count = 0; 78 for (Node n : AllNodesVisitor.getAllNodes(ds.getSelected())) { 79 if (n.isLatLonKnown()) { 80 EastNorth en = n.getEastNorth(); 81 sumEast += en.east(); 82 sumNorth += en.north(); 83 count++; 84 } 85 } 86 return count == 0 ? null : new EastNorth(sumEast / count, sumNorth / count); 87 } 88 89 /** 90 * Sets the grid origin (in the coordinates of the current grid type) and enables the grid. 91 * @param position the new grid origin, in projected coordinates 92 */ 93 public static void setOrigin(EastNorth position) { 94 if (MapGridPaintable.TYPE.get() == GridType.PROJECTED) { 95 MapGridPaintable.ORIGIN_X.put(position.east()); 96 MapGridPaintable.ORIGIN_Y.put(position.north()); 97 } else { 98 final LatLon ll; 99 try { 100 ll = ProjectionRegistry.getProjection().eastNorth2latlon(position); 101 } catch (IllegalArgumentException e) { 102 // the position is outside the domain of the projection, leave the grid as it is 103 Logging.warn("Cannot use {0} as grid origin: {1}", position, e.getMessage()); 104 Logging.trace(e); 105 return; 106 } 107 MapGridPaintable.ORIGIN_X.put(ll.lon()); 108 MapGridPaintable.ORIGIN_Y.put(ll.lat()); 109 } 110 MapGridPaintable.ENABLED.put(true); 111 } 112 } -
new file src/org/openstreetmap/josm/actions/ShowGridAction.java
diff --git src/org/openstreetmap/josm/actions/ShowGridAction.java src/org/openstreetmap/josm/actions/ShowGridAction.java new file mode 100644 index 0000000000..4f1343ff37
- + 1 // License: GPL. For details, see LICENSE file. 2 package org.openstreetmap.josm.actions; 3 4 import static org.openstreetmap.josm.gui.help.HelpUtil.ht; 5 import static org.openstreetmap.josm.tools.I18n.tr; 6 7 import java.awt.event.ActionEvent; 8 9 import org.openstreetmap.josm.gui.MainApplication; 10 import org.openstreetmap.josm.gui.layer.MapGridPaintable; 11 import org.openstreetmap.josm.tools.ImageProvider; 12 13 /** 14 * This action toggles the display of the grid over the map view. 15 * @see MapGridPaintable 16 * @since xxx 17 */ 18 public class ShowGridAction extends PreferenceToggleAction { 19 20 /** 21 * Constructs a new {@link ShowGridAction}. 22 */ 23 public ShowGridAction() { 24 super(tr("Show"), 25 new ImageProvider("grid"), 26 tr("Enable/disable the grid drawn over the map. Its spacing and orientation are set in the display preferences."), 27 MapGridPaintable.ENABLED 28 ); 29 setHelpId(ht("/MapView#Grid")); 30 } 31 32 @Override 33 protected boolean listenToSelectionChange() { 34 return false; 35 } 36 37 @Override 38 protected void updateEnabledState() { 39 setEnabled(MainApplication.isDisplayingMapView()); 40 } 41 42 @Override 43 public void actionPerformed(ActionEvent e) { 44 super.actionPerformed(e); 45 if (MainApplication.isDisplayingMapView()) { 46 MainApplication.getMap().mapView.repaint(); 47 } 48 } 49 } -
src/org/openstreetmap/josm/gui/MainMenu.java
diff --git src/org/openstreetmap/josm/gui/MainMenu.java src/org/openstreetmap/josm/gui/MainMenu.java index 3a089782ba..eeb0d37368 100644
import javax.swing.event.MenuListener; 30 30 31 31 import org.openstreetmap.josm.actions.AboutAction; 32 32 import org.openstreetmap.josm.actions.AddNodeAction; 33 import org.openstreetmap.josm.actions.AlignGridRotationAction; 33 34 import org.openstreetmap.josm.actions.AlignInCircleAction; 34 35 import org.openstreetmap.josm.actions.AlignInLineAction; 35 36 import org.openstreetmap.josm.actions.AutoScaleAction; … … import org.openstreetmap.josm.actions.SelectNonBranchingWaySequencesAction; 100 101 import org.openstreetmap.josm.actions.SelectSharedChildObjectsAction; 101 102 import org.openstreetmap.josm.actions.SessionSaveAction; 102 103 import org.openstreetmap.josm.actions.SessionSaveAsAction; 104 import org.openstreetmap.josm.actions.SetGridOriginAction; 105 import org.openstreetmap.josm.actions.ShowGridAction; 103 106 import org.openstreetmap.josm.actions.ShowStatusReportAction; 104 107 import org.openstreetmap.josm.actions.SimplifyWayAction; 105 108 import org.openstreetmap.josm.actions.SplitWayAction; … … import org.openstreetmap.josm.gui.layer.MainLayerManager.ActiveLayerChangeEvent; 135 138 import org.openstreetmap.josm.gui.layer.MainLayerManager.ActiveLayerChangeListener; 136 139 import org.openstreetmap.josm.gui.layer.geoimage.WikimediaCommonsLoader.WikimediaCommonsLoadImagesAction; 137 140 import org.openstreetmap.josm.gui.mappaint.MapPaintMenu; 141 import org.openstreetmap.josm.gui.preferences.display.GridPreference; 138 142 import org.openstreetmap.josm.gui.preferences.imagery.ImageryPreference; 139 143 import org.openstreetmap.josm.gui.tagging.presets.TaggingPresetSearchPrimitiveDialog; 140 144 import org.openstreetmap.josm.spi.preferences.Config; … … public class MainMenu extends JMenuBar { 256 260 public final TiledRenderToggleAction tiledRenderToggleAction = new TiledRenderToggleAction(); 257 261 /** View / Hatch area outside download */ 258 262 public final DrawBoundariesOfDownloadedDataAction drawBoundariesOfDownloadedDataAction = new DrawBoundariesOfDownloadedDataAction(); 263 /** View / Grid submenu: the grid drawn over the map and its placement */ 264 public final JMenu gridMenu = new JMenu(tr("Grid")); 265 /** View / Grid / Show */ 266 public final ShowGridAction showGridAction = new ShowGridAction(); 267 /** View / Grid / Set origin to selection */ 268 public final SetGridOriginAction setGridOriginAction = new SetGridOriginAction(); 269 /** View / Grid / Align rotation to selection */ 270 public final AlignGridRotationAction alignGridRotationAction = new AlignGridRotationAction(); 259 271 /** View / Advanced info */ 260 272 public final InfoAction info = new InfoAction(); 261 273 /** View / Advanced info (web) */ … … public class MainMenu extends JMenuBar { 815 827 final JCheckBoxMenuItem hatchAreaOutsideDownloadMenuItem = drawBoundariesOfDownloadedDataAction.getCheckbox(); 816 828 viewMenu.add(hatchAreaOutsideDownloadMenuItem); 817 829 ExpertToggleAction.addVisibilitySwitcher(hatchAreaOutsideDownloadMenuItem); 830 // -- Grid submenu 831 gridMenu.setIcon(ImageProvider.get("grid", ImageProvider.ImageSizes.MENU)); 832 gridMenu.add(showGridAction.getCheckbox()); 833 add(gridMenu, setGridOriginAction); 834 add(gridMenu, alignGridRotationAction); 835 gridMenu.addSeparator(); 836 add(gridMenu, PreferencesAction.forPreferenceTab(tr("Grid preferences..."), 837 tr("Click to open the grid tab in the preferences"), GridPreference.class)); 838 viewMenu.add(gridMenu); 818 839 819 840 viewMenu.add(new MapPaintMenu()); 820 841 viewMenu.addSeparator(); -
src/org/openstreetmap/josm/gui/MapFrame.java
diff --git src/org/openstreetmap/josm/gui/MapFrame.java src/org/openstreetmap/josm/gui/MapFrame.java index f3e81c9953..341371d0c4 100644
import org.openstreetmap.josm.gui.dialogs.UserListDialog; 73 73 import org.openstreetmap.josm.gui.dialogs.ValidatorDialog; 74 74 import org.openstreetmap.josm.gui.dialogs.properties.PropertiesDialog; 75 75 import org.openstreetmap.josm.gui.layer.Layer; 76 import org.openstreetmap.josm.gui.layer.MapGridPaintable; 76 77 import org.openstreetmap.josm.gui.layer.LayerManager.LayerAddEvent; 77 78 import org.openstreetmap.josm.gui.layer.LayerManager.LayerChangeListener; 78 79 import org.openstreetmap.josm.gui.layer.LayerManager.LayerOrderChangeEvent; … … public class MapFrame extends JPanel implements Destroyable, ActiveLayerChangeLi 122 123 * The view control displayed. 123 124 */ 124 125 public final MapView mapView; 126 /** The grid drawn over the map view, see {@link MapGridPaintable} */ 127 private final MapGridPaintable gridOverlay = new MapGridPaintable(); 125 128 126 129 /** 127 130 * This object allows to detect key press and release events … … public class MapFrame extends JPanel implements Destroyable, ActiveLayerChangeLi 204 207 setLayout(new BorderLayout()); 205 208 206 209 mapView = new MapView(MainApplication.getLayerManager(), viewportData); 210 mapView.addTemporaryLayer(gridOverlay); 207 211 208 212 splitPane = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, true); 209 213 … … public class MapFrame extends JPanel implements Destroyable, ActiveLayerChangeLi 366 370 toolBarToggle.removeAll(); 367 371 368 372 statusLine.destroy(); 373 mapView.removeTemporaryLayer(gridOverlay); 374 gridOverlay.destroy(); 369 375 mapView.destroy(); 370 376 keyDetector.unregister(); 371 377 -
new file src/org/openstreetmap/josm/gui/draw/BlendComposite.java
diff --git src/org/openstreetmap/josm/gui/draw/BlendComposite.java src/org/openstreetmap/josm/gui/draw/BlendComposite.java new file mode 100644 index 0000000000..e00e14ca3d
- + 1 // License: GPL. For details, see LICENSE file. 2 package org.openstreetmap.josm.gui.draw; 3 4 import java.awt.Composite; 5 import java.awt.CompositeContext; 6 import java.awt.RenderingHints; 7 import java.awt.image.ColorModel; 8 import java.awt.image.Raster; 9 import java.awt.image.WritableRaster; 10 import java.util.EnumMap; 11 import java.util.Map; 12 13 /** 14 * A {@link Composite} implementing the common "blend modes" of image editors (multiply, burn, hard light, 15 * difference, divide), which the standard {@link java.awt.AlphaComposite} does not provide. 16 * <p> 17 * The source alpha (including the coverage produced by antialiasing) controls how strongly the blended color 18 * replaces the destination, following the "source over" rule of the W3C compositing model, so that a 19 * translucent destination is handled correctly as well. It works on any color model, but is implemented per 20 * pixel and thus only meant for drawing thin shapes such as grid lines. It must be used on 21 * {@link java.awt.image.BufferedImage} backed graphics, since hardware accelerated pipelines do not support 22 * custom composites. 23 * @since xxx 24 */ 25 public final class BlendComposite implements Composite { 26 27 /** 28 * The supported blend modes. 29 */ 30 public enum Mode { 31 /** Normal alpha blending (like {@link java.awt.AlphaComposite#SrcOver}) */ 32 NORMAL, 33 /** Multiplies source and destination: darkens, white is neutral */ 34 MULTIPLY, 35 /** Color burn: darkens and increases contrast, white is neutral */ 36 BURN, 37 /** Hard light: multiplies for dark sources, screens for light sources; strong contrast */ 38 HARD_LIGHT, 39 /** Absolute difference between source and destination: always visible on any background */ 40 DIFFERENCE, 41 /** Divides destination by source: lightens, white is neutral, dark sources give bright lines */ 42 DIVIDE 43 } 44 45 private static final Map<Mode, BlendComposite> INSTANCES = new EnumMap<>(Mode.class); 46 47 private final Mode mode; 48 49 private BlendComposite(Mode mode) { 50 this.mode = mode; 51 } 52 53 /** 54 * Returns the composite for the given mode. 55 * @param mode the blend mode 56 * @return the composite 57 */ 58 public static synchronized BlendComposite getInstance(Mode mode) { 59 return INSTANCES.computeIfAbsent(mode, BlendComposite::new); 60 } 61 62 /** 63 * Returns the blend mode of this composite. 64 * @return the blend mode 65 */ 66 public Mode getMode() { 67 return mode; 68 } 69 70 @Override 71 public CompositeContext createContext(ColorModel srcColorModel, ColorModel dstColorModel, RenderingHints hints) { 72 return new BlendContext(mode, srcColorModel, dstColorModel); 73 } 74 75 /** 76 * Blends one color channel. 77 * @param mode blend mode 78 * @param s source channel value (0-255) 79 * @param d destination channel value (0-255) 80 * @return blended value (0-255) 81 */ 82 static int blend(Mode mode, int s, int d) { 83 switch (mode) { 84 case MULTIPLY: 85 return s * d / 255; 86 case BURN: 87 return s == 0 ? 0 : 255 - Math.min(255, (255 - d) * 255 / s); 88 case HARD_LIGHT: 89 return s < 128 ? 2 * s * d / 255 : 255 - (255 - d) * (510 - 2 * s) / 255; 90 case DIFFERENCE: 91 return Math.abs(s - d); 92 case DIVIDE: 93 return s == 0 ? 255 : Math.min(255, d * 255 / s); 94 case NORMAL: 95 default: 96 return s; 97 } 98 } 99 100 /** 101 * Composes one pixel: blends the source color with the destination and combines the two with the 102 * "source over" rule, so that both the source alpha (including the coverage produced by antialiasing) 103 * and a translucent destination are handled correctly. 104 * @param mode blend mode 105 * @param s source color (ARGB, not premultiplied) 106 * @param d destination color (ARGB, not premultiplied) 107 * @return the resulting color (ARGB, not premultiplied) 108 */ 109 static int composePixel(Mode mode, int s, int d) { 110 int sa = s >>> 24; 111 if (sa == 0) { 112 return d; 113 } 114 int da = d >>> 24; 115 if (da == 0xff) { 116 // opaque destination (the map view): the result is the destination moved towards the blended color 117 return 0xff000000 118 | (mix(blend(mode, (s >> 16) & 0xff, (d >> 16) & 0xff), (d >> 16) & 0xff, sa) << 16) 119 | (mix(blend(mode, (s >> 8) & 0xff, (d >> 8) & 0xff), (d >> 8) & 0xff, sa) << 8) 120 | mix(blend(mode, s & 0xff, d & 0xff), d & 0xff, sa); 121 } 122 int a = sa + da * (0xff - sa) / 0xff; 123 if (a == 0) { 124 return 0; 125 } 126 return (a << 24) 127 | (composeChannel(mode, (s >> 16) & 0xff, (d >> 16) & 0xff, sa, da, a) << 16) 128 | (composeChannel(mode, (s >> 8) & 0xff, (d >> 8) & 0xff, sa, da, a) << 8) 129 | composeChannel(mode, s & 0xff, d & 0xff, sa, da, a); 130 } 131 132 /** 133 * Composes one color channel of a translucent destination, see 134 * <a href="https://www.w3.org/TR/compositing-1/#blending">the W3C compositing model</a>: 135 * {@code co = as*(1-ab)*Cs + as*ab*B(Cb,Cs) + (1-as)*ab*Cb} and {@code Co = co/ao}. 136 * @param mode blend mode 137 * @param cs source channel value (0-255) 138 * @param cb destination (backdrop) channel value (0-255) 139 * @param sa source alpha (0-255) 140 * @param da destination alpha (0-255) 141 * @param a the resulting alpha (0-255), must not be 0 142 * @return the resulting channel value (0-255) 143 */ 144 private static int composeChannel(Mode mode, int cs, int cb, int sa, int da, int a) { 145 int co = sa * (0xff - da) * cs + sa * da * blend(mode, cs, cb) + (0xff - sa) * da * cb; 146 return co / (0xff * a); 147 } 148 149 /** linear interpolation between d (alpha 0) and s (alpha 255) */ 150 private static int mix(int s, int d, int alpha) { 151 return d + (s - d) * alpha / 0xff; 152 } 153 154 private static final class BlendContext implements CompositeContext { 155 private final Mode mode; 156 private final ColorModel srcColorModel; 157 private final ColorModel dstColorModel; 158 159 BlendContext(Mode mode, ColorModel srcColorModel, ColorModel dstColorModel) { 160 this.mode = mode; 161 this.srcColorModel = srcColorModel; 162 this.dstColorModel = dstColorModel; 163 } 164 165 @Override 166 public void compose(Raster src, Raster dstIn, WritableRaster dstOut) { 167 int w = Math.min(Math.min(src.getWidth(), dstIn.getWidth()), dstOut.getWidth()); 168 int h = Math.min(Math.min(src.getHeight(), dstIn.getHeight()), dstOut.getHeight()); 169 Object srcPixel = null; 170 Object dstPixel = null; 171 Object outPixel = null; 172 for (int y = 0; y < h; y++) { 173 for (int x = 0; x < w; x++) { 174 srcPixel = src.getDataElements(x, y, srcPixel); 175 dstPixel = dstIn.getDataElements(x, y, dstPixel); 176 int result = composePixel(mode, srcColorModel.getRGB(srcPixel), dstColorModel.getRGB(dstPixel)); 177 outPixel = dstColorModel.getDataElements(result, outPixel); 178 dstOut.setDataElements(x, y, outPixel); 179 } 180 } 181 } 182 183 @Override 184 public void dispose() { 185 // nothing to dispose 186 } 187 } 188 } -
src/org/openstreetmap/josm/gui/layer/AbstractTileSourceLayer.java
diff --git src/org/openstreetmap/josm/gui/layer/AbstractTileSourceLayer.java src/org/openstreetmap/josm/gui/layer/AbstractTileSourceLayer.java index 510fad35d1..f5f96bca4c 100644
package org.openstreetmap.josm.gui.layer; 4 4 import static org.openstreetmap.josm.tools.I18n.marktr; 5 5 import static org.openstreetmap.josm.tools.I18n.tr; 6 6 7 import java.awt.BasicStroke; 7 8 import java.awt.Color; 8 9 import java.awt.Component; 9 10 import java.awt.Dimension; … … import java.awt.GridBagLayout; 15 16 import java.awt.Image; 16 17 import java.awt.Shape; 17 18 import java.awt.Toolkit; 19 import java.awt.Stroke; 18 20 import java.awt.event.ActionEvent; 19 21 import java.awt.event.MouseAdapter; 20 22 import java.awt.event.MouseEvent; … … import org.openstreetmap.josm.data.imagery.vectortile.VectorTile; 93 95 import org.openstreetmap.josm.data.osm.visitor.BoundingXYVisitor; 94 96 import org.openstreetmap.josm.data.preferences.BooleanProperty; 95 97 import org.openstreetmap.josm.data.preferences.IntegerProperty; 98 import org.openstreetmap.josm.data.preferences.NamedColorProperty; 96 99 import org.openstreetmap.josm.data.projection.Projection; 97 100 import org.openstreetmap.josm.data.projection.ProjectionRegistry; 98 101 import org.openstreetmap.josm.data.projection.Projections; … … import org.openstreetmap.josm.gui.layer.imagery.LoadErroneousTilesAction; 116 119 import org.openstreetmap.josm.gui.layer.imagery.MVTLayer; 117 120 import org.openstreetmap.josm.gui.layer.imagery.ReprojectionTile; 118 121 import org.openstreetmap.josm.gui.layer.imagery.ShowErrorsAction; 122 import org.openstreetmap.josm.gui.layer.imagery.ShowTileBordersAction; 119 123 import org.openstreetmap.josm.gui.layer.imagery.TileAnchor; 120 124 import org.openstreetmap.josm.gui.layer.imagery.TileCoordinateConverter; 121 125 import org.openstreetmap.josm.gui.layer.imagery.TilePosition; … … implements ImageObserver, TileLoaderListener, ZoomChangeListener, FilterChangeLi 181 185 public static final IntegerProperty ZOOM_OFFSET = new IntegerProperty(PREFERENCE_PREFIX + ".zoom_offset", 0); 182 186 183 187 private static final BooleanProperty POPUP_MENU_ENABLED = new BooleanProperty(PREFERENCE_PREFIX + ".popupmenu", true); 188 /** Color of the border drawn around each tile if enabled, see {@link TileSourceDisplaySettings#isShowTileBorders()} */ 189 private static final NamedColorProperty TILE_BORDER_COLOR = new NamedColorProperty(marktr("imagery tile border"), new Color(0, 0, 0, 96)); 190 private static final Stroke TILE_BORDER_STROKE = new BasicStroke(1f); 184 191 private static final String ERROR_STRING = marktr("Error"); 185 192 186 193 /* … … implements ImageObserver, TileLoaderListener, ZoomChangeListener, FilterChangeLi 1226 1233 //texty += 1 + fontHeight; 1227 1234 } 1228 1235 1229 if (Logging.isDebugEnabled()) { 1236 if (getDisplaySettings().isShowTileBorders()) { 1237 // draw a thin border around the tile 1238 Color oldColor = g.getColor(); 1239 Stroke oldStroke = g.getStroke(); 1240 g.setColor(TILE_BORDER_COLOR.get()); 1241 g.setStroke(TILE_BORDER_STROKE); 1242 g.draw(coordinateConverter.getTileShapeScreen(tile)); 1243 g.setStroke(oldStroke); 1244 g.setColor(oldColor); 1245 } else if (Logging.isDebugEnabled()) { 1230 1246 // draw tile outline in semi-transparent red 1231 1247 g.setColor(new Color(255, 0, 0, 50)); 1232 1248 g.draw(coordinateConverter.getTileShapeScreen(tile)); … … implements ImageObserver, TileLoaderListener, ZoomChangeListener, FilterChangeLi 1853 1869 new AutoLoadTilesAction(this), 1854 1870 new AutoZoomAction(this), 1855 1871 new ShowErrorsAction(this), 1872 new ShowTileBordersAction(this), 1856 1873 new IncreaseZoomAction(this), 1857 1874 new DecreaseZoomAction(this), 1858 1875 new ZoomToBestAction(this), -
new file src/org/openstreetmap/josm/gui/layer/MapGridPaintable.java
diff --git src/org/openstreetmap/josm/gui/layer/MapGridPaintable.java src/org/openstreetmap/josm/gui/layer/MapGridPaintable.java new file mode 100644 index 0000000000..4a2d8fdd15
- + 1 // License: GPL. For details, see LICENSE file. 2 package org.openstreetmap.josm.gui.layer; 3 4 import static org.openstreetmap.josm.tools.I18n.marktr; 5 6 import java.awt.BasicStroke; 7 import java.awt.Color; 8 import java.awt.Graphics2D; 9 import java.awt.RenderingHints; 10 import java.awt.geom.Line2D; 11 import java.awt.geom.Path2D; 12 import java.awt.geom.Point2D; 13 import java.util.ArrayList; 14 import java.util.List; 15 16 import org.openstreetmap.josm.data.Bounds; 17 import org.openstreetmap.josm.data.ProjectionBounds; 18 import org.openstreetmap.josm.data.coor.EastNorth; 19 import org.openstreetmap.josm.data.coor.ILatLon; 20 import org.openstreetmap.josm.data.coor.LatLon; 21 import org.openstreetmap.josm.data.preferences.BooleanProperty; 22 import org.openstreetmap.josm.data.preferences.DoubleProperty; 23 import org.openstreetmap.josm.data.preferences.EnumProperty; 24 import org.openstreetmap.josm.data.preferences.NamedColorProperty; 25 import org.openstreetmap.josm.data.projection.Projection; 26 import org.openstreetmap.josm.gui.MapView; 27 import org.openstreetmap.josm.gui.draw.BlendComposite; 28 import org.openstreetmap.josm.spi.preferences.Config; 29 import org.openstreetmap.josm.spi.preferences.PreferenceChangeEvent; 30 import org.openstreetmap.josm.spi.preferences.PreferenceChangedListener; 31 import org.openstreetmap.josm.tools.Destroyable; 32 import org.openstreetmap.josm.tools.Logging; 33 34 /** 35 * A grid drawn over the whole map view, on top of all layers. 36 * <p> 37 * The grid is a pure visual aid (there is no snapping to it). It is either aligned to latitude/longitude, with a 38 * spacing in degrees, or to the projected coordinates, with a spacing in metres (true distance, measured at the 39 * grid origin), an optional rotation and an origin offset. When the grid cells would become 40 * smaller than a minimal size on screen, the spacing is multiplied by 10 until the cells are large enough, so the 41 * grid stays readable at every zoom level while remaining aligned to the configured one. 42 * <p> 43 * All settings are preferences (prefix {@code draw.grid.}), see {@link org.openstreetmap.josm.gui.preferences.display.GridPreference}. 44 * <p> 45 * An instance registers itself as a preference listener, so {@link #destroy()} must be called when it is no 46 * longer used ({@link org.openstreetmap.josm.gui.MapFrame} does this), otherwise the listener is leaked. 47 * @since xxx 48 */ 49 public class MapGridPaintable extends AbstractMapViewPaintable implements PreferenceChangedListener, Destroyable { 50 51 /** 52 * The kind of coordinates a grid is aligned to. 53 */ 54 public enum GridType { 55 /** lines of constant latitude and longitude, spacing in degrees */ 56 LATLON, 57 /** lines of constant projected east/north coordinate (optionally rotated), spacing in metres */ 58 PROJECTED 59 } 60 61 private static final String PREFIX = "draw.grid."; 62 63 /** Whether the grid is shown */ 64 public static final BooleanProperty ENABLED = new BooleanProperty(PREFIX + "enabled", false); 65 /** The kind of grid */ 66 public static final EnumProperty<GridType> TYPE = new EnumProperty<>(PREFIX + "type", GridType.class, GridType.PROJECTED); 67 /** Spacing of the vertical lines (longitude resp. east), in degrees resp. metres (true distance at the origin) */ 68 public static final DoubleProperty SPACING_X = new DoubleProperty(PREFIX + "spacing-x", 1000); 69 /** Spacing of the horizontal lines (latitude resp. north), in degrees resp. metres (true distance at the origin) */ 70 public static final DoubleProperty SPACING_Y = new DoubleProperty(PREFIX + "spacing-y", 1000); 71 /** Rotation of a projected grid, in degrees counter clockwise */ 72 public static final DoubleProperty ROTATION = new DoubleProperty(PREFIX + "rotation", 0); 73 /** Origin of the grid: a grid line passes through this coordinate (longitude resp. east) */ 74 public static final DoubleProperty ORIGIN_X = new DoubleProperty(PREFIX + "origin-x", 0); 75 /** Origin of the grid: a grid line passes through this coordinate (latitude resp. north) */ 76 public static final DoubleProperty ORIGIN_Y = new DoubleProperty(PREFIX + "origin-y", 0); 77 /** Below this distance between lines (in pixels) the spacing is multiplied by 10 */ 78 public static final DoubleProperty MIN_PIXEL_SPACING = new DoubleProperty(PREFIX + "min-pixel-spacing", 25); 79 /** The blend mode used to draw the lines */ 80 public static final EnumProperty<BlendComposite.Mode> BLEND_MODE 81 = new EnumProperty<>(PREFIX + "blend-mode", BlendComposite.Mode.class, BlendComposite.Mode.HARD_LIGHT); 82 /** Width of the lines in pixels */ 83 public static final DoubleProperty LINE_WIDTH = new DoubleProperty(PREFIX + "line-width", 2); 84 /** Color of the lines of constant east coordinate resp. longitude (the lines running north-south) */ 85 public static final NamedColorProperty COLOR_EAST = new NamedColorProperty(marktr("grid east lines"), Color.YELLOW); 86 /** Color of the lines of constant north coordinate resp. latitude (the lines running east-west) */ 87 public static final NamedColorProperty COLOR_NORTH = new NamedColorProperty(marktr("grid north lines"), Color.YELLOW); 88 89 /** 90 * A straight line of a projected grid. 91 * @since xxx 92 */ 93 public static final class ProjectedGridLine { 94 /** start point */ 95 public final EastNorth start; 96 /** end point */ 97 public final EastNorth end; 98 /** {@code true} for a line of constant (rotated) east coordinate, {@code false} for constant north */ 99 public final boolean constantEast; 100 101 ProjectedGridLine(EastNorth start, EastNorth end, boolean constantEast) { 102 this.start = start; 103 this.end = end; 104 this.constantEast = constantEast; 105 } 106 107 @Override 108 public String toString() { 109 return (constantEast ? "east " : "north ") + start + " -> " + end; 110 } 111 } 112 113 /** 114 * A (curved) line of a latitude/longitude grid. 115 * @since xxx 116 */ 117 public static final class LatLonGridLine { 118 /** the points of the polyline */ 119 public final List<LatLon> points; 120 /** {@code true} for a meridian (constant longitude), {@code false} for a parallel (constant latitude) */ 121 public final boolean meridian; 122 123 LatLonGridLine(List<LatLon> points, boolean meridian) { 124 this.points = points; 125 this.meridian = meridian; 126 } 127 128 @Override 129 public String toString() { 130 return (meridian ? "meridian " : "parallel ") + points; 131 } 132 } 133 134 /** number of segments used to draw a curved (lat/lon) grid line across the view */ 135 private static final int CURVE_SEGMENTS = 32; 136 /** hard limit for the number of lines in one direction, whatever the settings are */ 137 private static final int MAX_LINES = 500; 138 139 /** written by {@link #paint} and read by {@link #preferenceChanged}, which may run on another thread */ 140 private volatile MapView mapView; 141 142 /** 143 * Constructs a new {@code MapGridPaintable}. 144 */ 145 public MapGridPaintable() { 146 Config.getPref().addPreferenceChangeListener(this); 147 } 148 149 @Override 150 public void paint(Graphics2D g, MapView mv, Bounds bbox) { 151 mapView = mv; 152 if (!Boolean.TRUE.equals(ENABLED.get()) || mv.getWidth() <= 0 || mv.getHeight() <= 0) { 153 return; 154 } 155 Graphics2D g2 = (Graphics2D) g.create(); 156 try { 157 g2.setStroke(new BasicStroke((float) Math.max(0.1, LINE_WIDTH.get()))); 158 g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, 159 Config.getPref().getBoolean("mappaint.use-antialiasing", true) 160 ? RenderingHints.VALUE_ANTIALIAS_ON : RenderingHints.VALUE_ANTIALIAS_OFF); 161 BlendComposite.Mode mode = BLEND_MODE.get(); 162 if (mode != null && mode != BlendComposite.Mode.NORMAL) { 163 g2.setComposite(BlendComposite.getInstance(mode)); 164 } 165 if (TYPE.get() == GridType.PROJECTED) { 166 paintProjectedGrid(g2, mv); 167 } else { 168 paintLatLonGrid(g2, mv); 169 } 170 } finally { 171 g2.dispose(); 172 } 173 } 174 175 private static void paintProjectedGrid(Graphics2D g, MapView mv) { 176 double sx = SPACING_X.get(); 177 double sy = SPACING_Y.get(); 178 if (sx <= 0 || sy <= 0) { 179 return; 180 } 181 // the spacing is a true distance in metres, measured at the grid origin 182 EastNorth origin = new EastNorth(ORIGIN_X.get(), ORIGIN_Y.get()); 183 double unitsPerMetre = projectionUnitsPerMetre(mv.getProjection(), origin); 184 sx *= unitsPerMetre; 185 sy *= unitsPerMetre; 186 // thin out the grid until the cells are large enough on screen 187 double factor = thinningFactor(Math.min(sx, sy) / mv.getScale()); 188 List<ProjectedGridLine> lines = getProjectedGridLines(mv.getProjectionBounds(), sx * factor, sy * factor, 189 ROTATION.get(), origin.east(), origin.north()); 190 for (ProjectedGridLine line : lines) { 191 g.setColor(line.constantEast ? COLOR_EAST.get() : COLOR_NORTH.get()); 192 g.draw(new Line2D.Double(mv.getPoint2D(line.start), mv.getPoint2D(line.end))); 193 } 194 } 195 196 private static void paintLatLonGrid(Graphics2D g, MapView mv) { 197 double sx = SPACING_X.get(); 198 double sy = SPACING_Y.get(); 199 if (sx <= 0 || sy <= 0) { 200 return; 201 } 202 Bounds view = getVisibleLatLonBounds(mv); 203 if (view == null) { 204 return; 205 } 206 // pixel size of one cell at the center of the view 207 LatLon center = view.getCenter(); 208 Point2D c = mv.getPoint2D(center); 209 double factor = thinningFactor(Math.min(probePixels(mv, c, center, sx, true), probePixels(mv, c, center, sy, false))); 210 List<LatLonGridLine> lines = getLatLonGridLines(view, mv.getProjection().getWorldBoundsLatLon(), 211 sx * factor, sy * factor, ORIGIN_X.get(), ORIGIN_Y.get(), CURVE_SEGMENTS); 212 for (LatLonGridLine line : lines) { 213 g.setColor(line.meridian ? COLOR_EAST.get() : COLOR_NORTH.get()); 214 Path2D.Double path = new Path2D.Double(); 215 boolean first = true; 216 for (LatLon ll : line.points) { 217 Point2D p = mv.getPoint2D(ll); 218 if (first) { 219 path.moveTo(p.getX(), p.getY()); 220 first = false; 221 } else { 222 path.lineTo(p.getX(), p.getY()); 223 } 224 } 225 g.draw(path); 226 } 227 } 228 229 /** 230 * Computes the latitude/longitude bounds of the part of the world which is visible in the given view. 231 * <p> 232 * Contrary to {@link org.openstreetmap.josm.gui.NavigatableComponent#getRealBounds()} this does not simply 233 * convert the corners of the view: as soon as the view is larger than the world, those lie outside the world 234 * and their longitude wraps around, which yields a range much narrower than what is really visible (and one 235 * which jumps around while zooming or panning). The view is therefore first clipped to the world in 236 * projected coordinates. 237 * <p> 238 * The clipped area is kept a hair inside the world, because a point exactly on the antimeridian converts to 239 * an ambiguous longitude: {@link Projection#eastNorth2latlon} normalizes it to -180, which would turn the 240 * visible range of a view showing e.g. 60° E to 180° into 180° W to 60° E, i.e. the other half of the world. 241 * @param mv the map view 242 * @return the visible bounds, or {@code null} if no part of the world is visible 243 */ 244 static Bounds getVisibleLatLonBounds(MapView mv) { 245 Projection projection = mv.getProjection(); 246 ProjectionBounds view = mv.getProjectionBounds(); 247 ProjectionBounds world = projection.getWorldBoundsBoxEastNorth(); 248 double minEast = Math.max(view.minEast, world.minEast); 249 double maxEast = Math.min(view.maxEast, world.maxEast); 250 double minNorth = Math.max(view.minNorth, world.minNorth); 251 double maxNorth = Math.min(view.maxNorth, world.maxNorth); 252 if (minEast >= maxEast || minNorth >= maxNorth) { 253 return null; 254 } 255 // stay inside the world, but never collapse the area (the inset is a fraction of a millimetre on the ground) 256 double insetEast = Math.min((maxEast - minEast) / 4, (world.maxEast - world.minEast) * 1e-9); 257 double insetNorth = Math.min((maxNorth - minNorth) / 4, (world.maxNorth - world.minNorth) * 1e-9); 258 minEast += insetEast; 259 maxEast -= insetEast; 260 minNorth += insetNorth; 261 maxNorth -= insetNorth; 262 Bounds bounds = null; 263 for (double east : new double[] {minEast, maxEast}) { 264 for (double north : new double[] {minNorth, maxNorth}) { 265 LatLon ll = projection.eastNorth2latlon(new EastNorth(east, north)); 266 if (ll.isValid()) { 267 if (bounds == null) { 268 bounds = new Bounds(ll, false); 269 } else { 270 bounds.extend(ll); 271 } 272 } 273 } 274 } 275 return bounds != null ? bounds : mv.getRealBounds(); 276 } 277 278 /** 279 * Measures the distance on screen which corresponds to one grid spacing at the center of the view. The probe 280 * is placed on whichever side of the center stays inside the valid coordinate range, and it is shortened if 281 * the spacing itself does not fit, so that the result is a usable length at every zoom level. 282 * @param mv the map view 283 * @param center the center of the view, on screen 284 * @param at the center of the view 285 * @param spacing the grid spacing, in degrees 286 * @param lon {@code true} to probe along the longitude, {@code false} along the latitude 287 * @return the distance in pixels; 0 if it cannot be measured 288 */ 289 private static double probePixels(MapView mv, Point2D center, LatLon at, double spacing, boolean lon) { 290 double max = lon ? 180 : 89; 291 double delta = Math.min(spacing, max); 292 double value = lon ? at.lon() : at.lat(); 293 // probe towards the pole resp. the antimeridian, or backwards if that would leave the valid range 294 double probe = value + delta <= max ? value + delta : value - delta; 295 if (probe < -max || probe > max) { 296 return 0; 297 } 298 Point2D p = mv.getPoint2D(lon ? new LatLon(at.lat(), probe) : new LatLon(probe, at.lon())); 299 return center.distance(p) * spacing / delta; 300 } 301 302 /** 303 * Computes how many projection units correspond to one metre on the ground at the given position. For 304 * conformal projections (e.g. Mercator) this is the local scale factor, for a Mercator grid at 60° latitude 305 * one metre is two projection units. 306 * @param projection the projection 307 * @param at the position (projected coordinates) 308 * @return projection units per metre; 1 if it cannot be determined (position outside the world) 309 */ 310 public static double projectionUnitsPerMetre(Projection projection, EastNorth at) { 311 try { 312 if (!projection.getWorldBoundsBoxEastNorth().contains(at)) { 313 return 1; 314 } 315 double step = 100; 316 LatLon a = projection.eastNorth2latlon(at); 317 LatLon b = projection.eastNorth2latlon(new EastNorth(at.east() + step, at.north())); 318 if (!a.isValid() || !b.isValid()) { 319 return 1; 320 } 321 double metres = a.greatCircleDistance((ILatLon) b); 322 return metres > 0 && Double.isFinite(metres) ? step / metres : 1; 323 } catch (IllegalArgumentException e) { 324 Logging.trace(e); 325 return 1; 326 } 327 } 328 329 /** 330 * Computes the factor (a power of 10) by which the spacing must be multiplied so that the lines are at least 331 * {@link #MIN_PIXEL_SPACING} apart. 332 * @param pixelSpacing the distance between two lines on screen, in pixels 333 * @return the factor (at least 1) 334 */ 335 static double thinningFactor(double pixelSpacing) { 336 double min = Math.max(1, MIN_PIXEL_SPACING.get()); 337 double factor = 1; 338 if (pixelSpacing <= 0 || Double.isNaN(pixelSpacing)) { 339 return factor; 340 } 341 while (pixelSpacing * factor < min && factor < 1e15) { 342 factor *= 10; 343 } 344 return factor; 345 } 346 347 /** 348 * Computes the lines of a (possibly rotated) grid in projected coordinates which cross the given area. 349 * @param area the area to cover 350 * @param spacingX distance between the lines running in the "north" direction of the grid (before rotation) 351 * @param spacingY distance between the lines running in the "east" direction of the grid (before rotation) 352 * @param rotationDegrees rotation of the grid, counter clockwise 353 * @param originX east coordinate of the grid origin 354 * @param originY north coordinate of the grid origin 355 * @return the lines; empty if the spacing is invalid or there would be too many lines 356 */ 357 public static List<ProjectedGridLine> getProjectedGridLines(ProjectionBounds area, double spacingX, double spacingY, 358 double rotationDegrees, double originX, double originY) { 359 List<ProjectedGridLine> lines = new ArrayList<>(); 360 if (!(spacingX > 0) || !(spacingY > 0)) { 361 return lines; 362 } 363 double angle = Math.toRadians(rotationDegrees); 364 double c = Math.cos(angle); 365 double s = Math.sin(angle); 366 // grid coordinates (u along the rotated east axis, v along the rotated north axis) of the area corners 367 double[] xs = {area.minEast, area.maxEast, area.maxEast, area.minEast}; 368 double[] ys = {area.minNorth, area.minNorth, area.maxNorth, area.maxNorth}; 369 double uMin = Double.POSITIVE_INFINITY; 370 double uMax = Double.NEGATIVE_INFINITY; 371 double vMin = Double.POSITIVE_INFINITY; 372 double vMax = Double.NEGATIVE_INFINITY; 373 for (int i = 0; i < 4; i++) { 374 double dx = xs[i] - originX; 375 double dy = ys[i] - originY; 376 double u = c * dx + s * dy; 377 double v = -s * dx + c * dy; 378 uMin = Math.min(uMin, u); 379 uMax = Math.max(uMax, u); 380 vMin = Math.min(vMin, v); 381 vMax = Math.max(vMax, v); 382 } 383 long kuMin = (long) Math.ceil(uMin / spacingX); 384 long kuMax = (long) Math.floor(uMax / spacingX); 385 long kvMin = (long) Math.ceil(vMin / spacingY); 386 long kvMax = (long) Math.floor(vMax / spacingY); 387 if (kuMax - kuMin > MAX_LINES || kvMax - kvMin > MAX_LINES) { 388 return lines; 389 } 390 // lines of constant u run along the v axis 391 for (long k = kuMin; k <= kuMax; k++) { 392 double u = k * spacingX; 393 lines.add(new ProjectedGridLine(gridToEastNorth(u, vMin, c, s, originX, originY), 394 gridToEastNorth(u, vMax, c, s, originX, originY), true)); 395 } 396 for (long k = kvMin; k <= kvMax; k++) { 397 double v = k * spacingY; 398 lines.add(new ProjectedGridLine(gridToEastNorth(uMin, v, c, s, originX, originY), 399 gridToEastNorth(uMax, v, c, s, originX, originY), false)); 400 } 401 return lines; 402 } 403 404 private static EastNorth gridToEastNorth(double u, double v, double c, double s, double originX, double originY) { 405 return new EastNorth(originX + c * u - s * v, originY + s * u + c * v); 406 } 407 408 /** 409 * Computes the lines of a latitude/longitude grid which cross the given area. Since these lines are curves in 410 * most projections, each line is returned as a polyline. 411 * @param area the area to cover 412 * @param world the bounds of the world in the current projection, the lines are clamped to it 413 * @param spacingLon distance between the meridians, in degrees 414 * @param spacingLat distance between the parallels, in degrees 415 * @param originLon longitude of a meridian of the grid 416 * @param originLat latitude of a parallel of the grid 417 * @param segments number of segments of each polyline 418 * @return the lines; empty if the spacing is invalid or there would be too many lines 419 */ 420 public static List<LatLonGridLine> getLatLonGridLines(Bounds area, Bounds world, double spacingLon, double spacingLat, 421 double originLon, double originLat, int segments) { 422 List<LatLonGridLine> lines = new ArrayList<>(); 423 if (!(spacingLon > 0) || !(spacingLat > 0) || segments < 1) { 424 return lines; 425 } 426 double minLat = Math.max(area.getMinLat(), world.getMinLat()); 427 double maxLat = Math.min(area.getMaxLat(), world.getMaxLat()); 428 double minLon = area.getMinLon(); 429 double maxLon = area.getMaxLon(); 430 if (maxLon < minLon) { 431 maxLon += 360; // the view crosses the antimeridian 432 } 433 if (minLat >= maxLat || minLon >= maxLon) { 434 return lines; 435 } 436 long kLonMin = (long) Math.ceil((minLon - originLon) / spacingLon); 437 long kLonMax = (long) Math.floor((maxLon - originLon) / spacingLon); 438 long kLatMin = (long) Math.ceil((minLat - originLat) / spacingLat); 439 long kLatMax = (long) Math.floor((maxLat - originLat) / spacingLat); 440 if (kLonMax - kLonMin > MAX_LINES || kLatMax - kLatMin > MAX_LINES) { 441 return lines; 442 } 443 // for a projection which does not span the whole globe, meridians outside its longitude range are 444 // skipped like the parallels below; a full range must not be checked since longitudes wrap around 445 boolean limitedLon = world.getMinLon() > -180 || world.getMaxLon() < 180; 446 for (long k = kLonMin; k <= kLonMax; k++) { 447 double lon = LatLon.toIntervalLon(originLon + k * spacingLon); 448 if (limitedLon && (lon < world.getMinLon() || lon > world.getMaxLon())) { 449 continue; 450 } 451 List<LatLon> line = new ArrayList<>(segments + 1); 452 for (int i = 0; i <= segments; i++) { 453 line.add(new LatLon(minLat + (maxLat - minLat) * i / segments, lon)); 454 } 455 lines.add(new LatLonGridLine(line, true)); 456 } 457 for (long k = kLatMin; k <= kLatMax; k++) { 458 double lat = originLat + k * spacingLat; 459 if (lat < world.getMinLat() || lat > world.getMaxLat()) { 460 continue; 461 } 462 addParallel(lines, lat, minLon, maxLon, segments); 463 } 464 return lines; 465 } 466 467 /** 468 * Adds a parallel running from one longitude to another. A parallel which crosses the antimeridian is added 469 * as two lines, one on each side of it: wrapping the longitudes of a single polyline would instead make it 470 * jump right across the view. 471 * @param lines the list to add to 472 * @param lat the latitude of the parallel 473 * @param minLon the longitude to start at, in [-180, 180] 474 * @param maxLon the longitude to end at, may be larger than 180 if the area crosses the antimeridian 475 * @param segments number of segments of each polyline 476 */ 477 private static void addParallel(List<LatLonGridLine> lines, double lat, double minLon, double maxLon, int segments) { 478 if (maxLon > 180) { 479 addParallel(lines, lat, minLon, 180, segments); 480 addParallel(lines, lat, -180, maxLon - 360, segments); 481 return; 482 } 483 if (!(maxLon > minLon)) { 484 return; 485 } 486 List<LatLon> line = new ArrayList<>(segments + 1); 487 for (int i = 0; i <= segments; i++) { 488 line.add(new LatLon(lat, minLon + (maxLon - minLon) * i / segments)); 489 } 490 lines.add(new LatLonGridLine(line, false)); 491 } 492 493 @Override 494 public void preferenceChanged(PreferenceChangeEvent e) { 495 if (e.getKey().startsWith(PREFIX) || e.getKey().equals(COLOR_EAST.getKey()) || e.getKey().equals(COLOR_NORTH.getKey())) { 496 invalidate(); 497 if (mapView != null) { 498 mapView.repaint(); 499 } 500 } 501 } 502 503 @Override 504 public void destroy() { 505 Config.getPref().removePreferenceChangeListener(this); 506 mapView = null; 507 } 508 } -
new file src/org/openstreetmap/josm/gui/layer/imagery/ShowTileBordersAction.java
diff --git src/org/openstreetmap/josm/gui/layer/imagery/ShowTileBordersAction.java src/org/openstreetmap/josm/gui/layer/imagery/ShowTileBordersAction.java new file mode 100644 index 0000000000..849829d807
- + 1 // License: GPL. For details, see LICENSE file. 2 package org.openstreetmap.josm.gui.layer.imagery; 3 4 import static org.openstreetmap.josm.tools.I18n.tr; 5 6 import java.awt.Component; 7 import java.awt.event.ActionEvent; 8 import java.util.List; 9 10 import javax.swing.AbstractAction; 11 import javax.swing.JCheckBoxMenuItem; 12 13 import org.openstreetmap.josm.gui.layer.AbstractTileSourceLayer; 14 import org.openstreetmap.josm.gui.layer.Layer; 15 import org.openstreetmap.josm.gui.layer.Layer.LayerAction; 16 17 /** 18 * Toggles the drawing of a thin border around each tile of an imagery layer. 19 * @since xxx 20 */ 21 public class ShowTileBordersAction extends AbstractAction implements LayerAction { 22 23 private final AbstractTileSourceLayer<?> layer; 24 25 /** 26 * Constructs a new {@code ShowTileBordersAction}. 27 * @param layer imagery layer 28 */ 29 public ShowTileBordersAction(AbstractTileSourceLayer<?> layer) { 30 super(tr("Show tile borders")); 31 this.layer = layer; 32 } 33 34 @Override 35 public void actionPerformed(ActionEvent ae) { 36 TileSourceDisplaySettings settings = layer.getDisplaySettings(); 37 boolean show = !settings.isShowTileBorders(); 38 settings.setShowTileBorders(show); 39 // remember the choice, so that it also applies to layers created later and after a restart. Only the 40 // action does this, not the setter, so that loading a session does not overwrite the preference. 41 TileSourceDisplaySettings.PROP_SHOW_TILE_BORDERS.put(show); 42 } 43 44 @Override 45 public Component createMenuComponent() { 46 JCheckBoxMenuItem item = new JCheckBoxMenuItem(this); 47 item.setSelected(layer.getDisplaySettings().isShowTileBorders()); 48 return item; 49 } 50 51 @Override 52 public boolean supportLayers(List<Layer> layers) { 53 return AbstractTileSourceLayer.actionSupportLayers(layers); 54 } 55 } -
src/org/openstreetmap/josm/gui/layer/imagery/TileSourceDisplaySettings.java
diff --git src/org/openstreetmap/josm/gui/layer/imagery/TileSourceDisplaySettings.java src/org/openstreetmap/josm/gui/layer/imagery/TileSourceDisplaySettings.java index 4496fb6027..13497a3ac6 100644
public class TileSourceDisplaySettings implements SessionAwareReadApply { 45 45 */ 46 46 private static final String SHOW_ERRORS = "show-errors"; 47 47 48 private static final String SHOW_TILE_BORDERS = "show-tile-borders"; 49 48 50 private static final String DISPLACEMENT = "displacement"; 49 51 50 52 private static final String PREFERENCE_PREFIX = "imagery.generic"; … … public class TileSourceDisplaySettings implements SessionAwareReadApply { 59 61 */ 60 62 public static final BooleanProperty PROP_AUTO_ZOOM = new BooleanProperty(PREFERENCE_PREFIX + ".default_autozoom", true); 61 63 64 /** 65 * The default tile borders property, remembered whenever the user toggles them, so that the choice also 66 * applies to layers created later and after a restart 67 * @since xxx 68 */ 69 public static final BooleanProperty PROP_SHOW_TILE_BORDERS 70 = new BooleanProperty(PREFERENCE_PREFIX + ".default_showtileborders", false); 71 62 72 63 73 /** if layers changes automatically, when user zooms in */ 64 74 private boolean autoZoom; … … public class TileSourceDisplaySettings implements SessionAwareReadApply { 66 76 private boolean autoLoad; 67 77 /** if layer should show errors on tiles */ 68 78 private boolean showErrors; 79 /** if layer should draw a border around each tile */ 80 private boolean showTileBorders; 69 81 70 82 private OffsetBookmark previousOffsetBookmark; 71 83 private OffsetBookmark offsetBookmark; … … public class TileSourceDisplaySettings implements SessionAwareReadApply { 97 109 autoZoom = getProperty(prefixes, "default_autozoom", PROP_AUTO_ZOOM.getDefaultValue()); 98 110 autoLoad = getProperty(prefixes, "default_autoload", PROP_AUTO_LOAD.getDefaultValue()); 99 111 showErrors = getProperty(prefixes, "default_showerrors", Boolean.TRUE); 112 showTileBorders = getProperty(prefixes, "default_showtileborders", PROP_SHOW_TILE_BORDERS.getDefaultValue()); 100 113 } 101 114 102 115 private static boolean getProperty(String[] prefixes, String name, Boolean def) { 103 // iterate through all values to force the preferences to receive the default value. 104 // we only support a default value of true. 105 boolean value = true; 116 // iterate through all values to force the preferences to receive the default value 117 boolean value = def; 106 118 for (String p : prefixes) { 107 119 String key = p + "." + name; 108 boolean currentValue = Config.getPref().getBoolean(key, true);120 boolean currentValue = Config.getPref().getBoolean(key, def); 109 121 if (!Config.getPref().get(key, def.toString()).isEmpty()) { 110 122 value = currentValue; 111 123 } … … public class TileSourceDisplaySettings implements SessionAwareReadApply { 170 182 fireSettingsChange(SHOW_ERRORS); 171 183 } 172 184 185 /** 186 * If the layer should draw a thin border around each tile. 187 * @return <code>true</code> to draw tile borders. 188 * @since xxx 189 */ 190 public boolean isShowTileBorders() { 191 return showTileBorders; 192 } 193 194 /** 195 * Sets the show tile borders property. Fires a change event. 196 * @param showTileBorders {@code true} if the layer should draw a thin border around each tile 197 * @see #isShowTileBorders() 198 * @since xxx 199 */ 200 public void setShowTileBorders(boolean showTileBorders) { 201 this.showTileBorders = showTileBorders; 202 fireSettingsChange(SHOW_TILE_BORDERS); 203 } 204 173 205 /** 174 206 * Gets the displacement in x (east) direction 175 207 * @return The displacement. … … public class TileSourceDisplaySettings implements SessionAwareReadApply { 290 322 data.put(AUTO_LOAD, Boolean.toString(autoLoad)); 291 323 data.put(AUTO_ZOOM, Boolean.toString(autoZoom)); 292 324 data.put(SHOW_ERRORS, Boolean.toString(showErrors)); 325 data.put(SHOW_TILE_BORDERS, Boolean.toString(showTileBorders)); 293 326 return data; 294 327 } 295 328 … … public class TileSourceDisplaySettings implements SessionAwareReadApply { 317 350 if (doShowErrors != null) { 318 351 setShowErrors(Boolean.parseBoolean(doShowErrors)); 319 352 } 353 354 String doShowTileBorders = data.get(SHOW_TILE_BORDERS); 355 if (doShowTileBorders != null) { 356 setShowTileBorders(Boolean.parseBoolean(doShowTileBorders)); 357 } 320 358 } catch (JosmRuntimeException | IllegalArgumentException | IllegalStateException e) { 321 359 throw BugReport.intercept(e).put("data", data); 322 360 } … … public class TileSourceDisplaySettings implements SessionAwareReadApply { 324 362 325 363 @Override 326 364 public int hashCode() { 327 return Objects.hash(autoLoad, autoZoom, showErrors );365 return Objects.hash(autoLoad, autoZoom, showErrors, showTileBorders); 328 366 } 329 367 330 368 @Override … … public class TileSourceDisplaySettings implements SessionAwareReadApply { 336 374 TileSourceDisplaySettings other = (TileSourceDisplaySettings) obj; 337 375 return autoLoad == other.autoLoad 338 376 && autoZoom == other.autoZoom 339 && showErrors == other.showErrors; 377 && showErrors == other.showErrors 378 && showTileBorders == other.showTileBorders; 340 379 } 341 380 342 381 @Override 343 382 public String toString() { 344 383 return "TileSourceDisplaySettings [autoZoom=" + autoZoom + ", autoLoad=" + autoLoad + ", showErrors=" 345 + showErrors + ']';384 + showErrors + ", showTileBorders=" + showTileBorders + ']'; 346 385 } 347 386 348 387 /** -
src/org/openstreetmap/josm/gui/preferences/PreferenceTabbedPane.java
diff --git src/org/openstreetmap/josm/gui/preferences/PreferenceTabbedPane.java src/org/openstreetmap/josm/gui/preferences/PreferenceTabbedPane.java index 61b8c82993..d1d8431b26 100644
import org.openstreetmap.josm.gui.preferences.display.ColorPreference; 51 51 import org.openstreetmap.josm.gui.preferences.display.DisplayPreference; 52 52 import org.openstreetmap.josm.gui.preferences.display.DrawingPreference; 53 53 import org.openstreetmap.josm.gui.preferences.display.GPXPreference; 54 import org.openstreetmap.josm.gui.preferences.display.GridPreference; 54 55 import org.openstreetmap.josm.gui.preferences.display.LafPreference; 55 56 import org.openstreetmap.josm.gui.preferences.display.LanguagePreference; 56 57 import org.openstreetmap.josm.gui.preferences.imagery.ImageryPreference; … … public final class PreferenceTabbedPane extends JTabbedPane implements ExpertMod 610 611 SETTINGS_FACTORIES.add(new ServerAccessPreference.Factory()); 611 612 SETTINGS_FACTORIES.add(new ProxyPreference.Factory()); 612 613 SETTINGS_FACTORIES.add(new ProjectionPreference.Factory()); 614 SETTINGS_FACTORIES.add(new GridPreference.Factory()); 613 615 SETTINGS_FACTORIES.add(new MapPaintPreference.Factory()); 614 616 SETTINGS_FACTORIES.add(new TaggingPresetPreference.Factory()); 615 617 SETTINGS_FACTORIES.add(new BackupPreference.Factory()); -
new file src/org/openstreetmap/josm/gui/preferences/display/GridPreference.java
diff --git src/org/openstreetmap/josm/gui/preferences/display/GridPreference.java src/org/openstreetmap/josm/gui/preferences/display/GridPreference.java new file mode 100644 index 0000000000..f2bc5198f3
- + 1 // License: GPL. For details, see LICENSE file. 2 package org.openstreetmap.josm.gui.preferences.display; 3 4 import static org.openstreetmap.josm.tools.I18n.tr; 5 6 import java.awt.Component; 7 import java.awt.GridBagLayout; 8 import java.text.DecimalFormat; 9 import java.text.DecimalFormatSymbols; 10 import java.text.ParseException; 11 import java.util.Locale; 12 import java.util.function.Function; 13 14 import javax.swing.DefaultListCellRenderer; 15 import javax.swing.JCheckBox; 16 import javax.swing.JLabel; 17 import javax.swing.JList; 18 import javax.swing.JPanel; 19 20 import org.openstreetmap.josm.data.preferences.DoubleProperty; 21 import org.openstreetmap.josm.gui.draw.BlendComposite; 22 import org.openstreetmap.josm.gui.help.HelpUtil; 23 import org.openstreetmap.josm.gui.layer.MapGridPaintable; 24 import org.openstreetmap.josm.gui.layer.MapGridPaintable.GridType; 25 import org.openstreetmap.josm.gui.preferences.DefaultTabPreferenceSetting; 26 import org.openstreetmap.josm.gui.preferences.PreferenceSetting; 27 import org.openstreetmap.josm.gui.preferences.PreferenceSettingFactory; 28 import org.openstreetmap.josm.gui.preferences.PreferenceTabbedPane; 29 import org.openstreetmap.josm.gui.widgets.JosmComboBox; 30 import org.openstreetmap.josm.gui.widgets.JosmTextField; 31 import org.openstreetmap.josm.tools.GBC; 32 import org.openstreetmap.josm.tools.Logging; 33 34 /** 35 * Settings of the grid drawn over the map, see {@link MapGridPaintable}. 36 * @since xxx 37 */ 38 public class GridPreference extends DefaultTabPreferenceSetting { 39 40 /** 41 * Factory used to create a new {@code GridPreference}. 42 */ 43 public static class Factory implements PreferenceSettingFactory { 44 @Override 45 public PreferenceSetting createPreferenceSetting() { 46 return new GridPreference(); 47 } 48 } 49 50 private static final DecimalFormat FORMAT = new DecimalFormat("0.#########", DecimalFormatSymbols.getInstance(Locale.ROOT)); 51 52 private final JCheckBox enabled = new JCheckBox(tr("Show grid")); 53 private final JosmComboBox<GridType> type = new JosmComboBox<>(GridType.values()); 54 private final JosmTextField spacingX = new JosmTextField(10); 55 private final JosmTextField spacingY = new JosmTextField(10); 56 private final JosmTextField rotation = new JosmTextField(10); 57 private final JosmTextField originX = new JosmTextField(10); 58 private final JosmTextField originY = new JosmTextField(10); 59 private final JosmTextField minPixelSpacing = new JosmTextField(10); 60 private final JosmTextField lineWidth = new JosmTextField(10); 61 private final JosmComboBox<BlendComposite.Mode> blendMode = new JosmComboBox<>(BlendComposite.Mode.values()); 62 private final JLabel spacingXLabel = new JLabel(); 63 private final JLabel spacingYLabel = new JLabel(); 64 private final JLabel originXLabel = new JLabel(); 65 private final JLabel originYLabel = new JLabel(); 66 67 GridPreference() { 68 super("preferences/grid", tr("Grid"), tr("Settings of the grid drawn over the map.")); 69 } 70 71 @Override 72 public void addGui(PreferenceTabbedPane gui) { 73 JPanel panel = new JPanel(new GridBagLayout()); 74 75 enabled.setSelected(Boolean.TRUE.equals(MapGridPaintable.ENABLED.get())); 76 enabled.setToolTipText(tr("Draw a grid over the map (View menu: Show grid). " 77 + "The grid is a visual aid only, there is no snapping to it.")); 78 type.setSelectedItem(MapGridPaintable.TYPE.get()); 79 type.setRenderer(new TranslatedRenderer<>(t -> t == GridType.PROJECTED ? tr("Projected coordinates") : tr("Latitude/longitude"))); 80 type.setToolTipText("<html>" + tr("Latitude/longitude: lines of constant latitude and longitude, spacing in degrees.<br>" 81 + "Projected: lines of constant east/north coordinate of the map projection, " 82 + "spacing in metres, optionally rotated.") + "</html>"); 83 type.addActionListener(e -> updateLabels()); 84 set(spacingX, MapGridPaintable.SPACING_X); 85 set(spacingY, MapGridPaintable.SPACING_Y); 86 set(rotation, MapGridPaintable.ROTATION); 87 rotation.setToolTipText(tr("Rotation of a projected grid in degrees, counter clockwise. Ignored for a latitude/longitude grid.")); 88 set(originX, MapGridPaintable.ORIGIN_X); 89 set(originY, MapGridPaintable.ORIGIN_Y); 90 set(minPixelSpacing, MapGridPaintable.MIN_PIXEL_SPACING); 91 set(lineWidth, MapGridPaintable.LINE_WIDTH); 92 lineWidth.setToolTipText(tr("Width of the grid lines in pixels. The colors of the east and north lines are set in the Colors tab.")); 93 minPixelSpacing.setToolTipText(tr("When the grid lines get closer than this on screen, the spacing is multiplied by 10 " 94 + "so that the grid stays readable when zooming out.")); 95 blendMode.setSelectedItem(MapGridPaintable.BLEND_MODE.get()); 96 blendMode.setRenderer(new TranslatedRenderer<>(GridPreference::blendModeName)); 97 blendMode.setToolTipText(tr("How the lines are combined with the map: normal transparency, multiply (darkens), " 98 + "burn, hard light, difference (visible on any background) or divide. The colors are set in the Colors tab.")); 99 updateLabels(); 100 101 panel.add(enabled, GBC.eol().insets(0, 0, 0, 10)); 102 panel.add(new JLabel(tr("Grid type")), GBC.std().insets(5, 0, 5, 5)); 103 panel.add(type, GBC.eol().fill(GBC.HORIZONTAL).insets(0, 0, 0, 5)); 104 panel.add(spacingXLabel, GBC.std().insets(5, 0, 5, 5)); 105 panel.add(spacingX, GBC.eol().insets(0, 0, 0, 5)); 106 panel.add(spacingYLabel, GBC.std().insets(5, 0, 5, 5)); 107 panel.add(spacingY, GBC.eol().insets(0, 0, 0, 5)); 108 panel.add(new JLabel(tr("Rotation (degrees)")), GBC.std().insets(5, 0, 5, 5)); 109 panel.add(rotation, GBC.eol().insets(0, 0, 0, 5)); 110 panel.add(originXLabel, GBC.std().insets(5, 0, 5, 5)); 111 panel.add(originX, GBC.eol().insets(0, 0, 0, 5)); 112 panel.add(originYLabel, GBC.std().insets(5, 0, 5, 5)); 113 panel.add(originY, GBC.eol().insets(0, 0, 0, 5)); 114 panel.add(new JLabel(tr("Minimum line distance on screen (pixels)")), GBC.std().insets(5, 0, 5, 5)); 115 panel.add(minPixelSpacing, GBC.eol().insets(0, 0, 0, 5)); 116 panel.add(new JLabel(tr("Line width (pixels)")), GBC.std().insets(5, 0, 5, 5)); 117 panel.add(lineWidth, GBC.eol().insets(0, 0, 0, 5)); 118 panel.add(new JLabel(tr("Blend mode")), GBC.std().insets(5, 0, 5, 5)); 119 panel.add(blendMode, GBC.eol().fill(GBC.HORIZONTAL).insets(0, 0, 0, 5)); 120 panel.add(GBC.glue(0, 0), GBC.eol().fill(GBC.BOTH)); 121 122 createPreferenceTabWithScrollPane(gui, panel); 123 } 124 125 private static String blendModeName(BlendComposite.Mode mode) { 126 switch (mode) { 127 case MULTIPLY: 128 return tr("Multiply"); 129 case BURN: 130 return tr("Burn"); 131 case HARD_LIGHT: 132 return tr("Hard light"); 133 case DIFFERENCE: 134 return tr("Difference"); 135 case DIVIDE: 136 return tr("Divide"); 137 case NORMAL: 138 default: 139 return tr("Normal"); 140 } 141 } 142 143 /** Renders enum values with a translated name */ 144 private static final class TranslatedRenderer<T> extends DefaultListCellRenderer { 145 private final Function<T, String> name; 146 147 TranslatedRenderer(Function<T, String> name) { 148 this.name = name; 149 } 150 151 @Override 152 @SuppressWarnings("unchecked") 153 public Component getListCellRendererComponent(JList<?> list, Object value, int index, boolean isSelected, boolean cellHasFocus) { 154 String text = value == null ? "" : name.apply((T) value); 155 return super.getListCellRendererComponent(list, text, index, isSelected, cellHasFocus); 156 } 157 } 158 159 private void updateLabels() { 160 boolean latlon = type.getSelectedItem() != GridType.PROJECTED; 161 spacingXLabel.setText(latlon ? tr("Longitude spacing (degrees)") : tr("East spacing (metres)")); 162 spacingYLabel.setText(latlon ? tr("Latitude spacing (degrees)") : tr("North spacing (metres)")); 163 String spacingTip = latlon 164 ? tr("Distance between the grid lines in degrees.") 165 : tr("Distance between the grid lines as a true distance in metres (as measured by the parallel way tool " 166 + "and the status bar), measured at the grid origin. Place the origin in the area of interest."); 167 spacingX.setToolTipText(spacingTip); 168 spacingY.setToolTipText(spacingTip); 169 originXLabel.setText(latlon ? tr("Origin longitude") : tr("Origin east")); 170 originYLabel.setText(latlon ? tr("Origin latitude") : tr("Origin north")); 171 rotation.setEnabled(!latlon); 172 } 173 174 private static void set(JosmTextField field, DoubleProperty property) { 175 field.setText(FORMAT.format(property.get())); 176 } 177 178 private static void save(JosmTextField field, DoubleProperty property, boolean positive) { 179 try { 180 double value = FORMAT.parse(field.getText().trim()).doubleValue(); 181 if (positive && !(value > 0)) { 182 Logging.warn("Ignoring invalid grid setting {0}: {1}", property.getKey(), field.getText()); 183 return; 184 } 185 property.put(value); 186 } catch (ParseException e) { 187 Logging.warn("Ignoring invalid grid setting {0}: {1}", property.getKey(), field.getText()); 188 Logging.trace(e); 189 } 190 } 191 192 @Override 193 public boolean ok() { 194 MapGridPaintable.ENABLED.put(enabled.isSelected()); 195 MapGridPaintable.TYPE.put((GridType) type.getSelectedItem()); 196 save(spacingX, MapGridPaintable.SPACING_X, true); 197 save(spacingY, MapGridPaintable.SPACING_Y, true); 198 save(rotation, MapGridPaintable.ROTATION, false); 199 save(originX, MapGridPaintable.ORIGIN_X, false); 200 save(originY, MapGridPaintable.ORIGIN_Y, false); 201 save(minPixelSpacing, MapGridPaintable.MIN_PIXEL_SPACING, true); 202 save(lineWidth, MapGridPaintable.LINE_WIDTH, true); 203 MapGridPaintable.BLEND_MODE.put((BlendComposite.Mode) blendMode.getSelectedItem()); 204 return false; 205 } 206 207 @Override 208 public boolean isExpert() { 209 return false; 210 } 211 212 @Override 213 public String getHelpContext() { 214 return HelpUtil.ht("/Preferences/Grid"); 215 } 216 } -
new file test/unit/org/openstreetmap/josm/actions/GridActionsTest.java
diff --git test/unit/org/openstreetmap/josm/actions/GridActionsTest.java test/unit/org/openstreetmap/josm/actions/GridActionsTest.java new file mode 100644 index 0000000000..37e9d667c6
- + 1 // License: GPL. For details, see LICENSE file. 2 package org.openstreetmap.josm.actions; 3 4 import static org.junit.jupiter.api.Assertions.assertEquals; 5 import static org.junit.jupiter.api.Assertions.assertFalse; 6 import static org.junit.jupiter.api.Assertions.assertNull; 7 import static org.junit.jupiter.api.Assertions.assertTrue; 8 9 import java.awt.event.ActionEvent; 10 11 import org.junit.jupiter.api.AfterEach; 12 import org.junit.jupiter.api.Test; 13 import org.openstreetmap.josm.data.coor.EastNorth; 14 import org.openstreetmap.josm.data.coor.LatLon; 15 import org.openstreetmap.josm.data.osm.DataSet; 16 import org.openstreetmap.josm.data.osm.Node; 17 import org.openstreetmap.josm.data.osm.Relation; 18 import org.openstreetmap.josm.data.osm.RelationMember; 19 import org.openstreetmap.josm.data.osm.Way; 20 import org.openstreetmap.josm.gui.MainApplication; 21 import org.openstreetmap.josm.gui.layer.MapGridPaintable; 22 import org.openstreetmap.josm.gui.layer.MapGridPaintable.GridType; 23 import org.openstreetmap.josm.gui.layer.OsmDataLayer; 24 import org.openstreetmap.josm.spi.preferences.Config; 25 import org.openstreetmap.josm.testutils.annotations.Main; 26 import org.openstreetmap.josm.testutils.annotations.Projection; 27 28 /** 29 * Unit tests of {@link SetGridOriginAction} and {@link AlignGridRotationAction}. 30 */ 31 @Main 32 @Projection 33 class GridActionsTest { 34 35 @AfterEach 36 void reset() { 37 for (String key : new String[] {"enabled", "type", "rotation", "origin-x", "origin-y"}) { 38 Config.getPref().put("draw.grid." + key, null); 39 } 40 } 41 42 /** 43 * The origin is the position of a single selected node, stored in the coordinates of the grid type, and the 44 * grid gets enabled. Without a selection the action is disabled. 45 */ 46 @Test 47 void testSetOriginSingleNode() { 48 DataSet ds = new DataSet(); 49 OsmDataLayer layer = new OsmDataLayer(ds, "GridActionsTest", null); 50 MainApplication.getLayerManager().addLayer(layer); 51 try { 52 Node n = new Node(new LatLon(50, 10)); 53 ds.addPrimitive(n); 54 55 assertFalse(new SetGridOriginAction().isEnabled()); 56 57 ds.setSelected(n); 58 EastNorth en = n.getEastNorth(); 59 MapGridPaintable.TYPE.put(GridType.LATLON); 60 SetGridOriginAction action = new SetGridOriginAction(); 61 assertTrue(action.isEnabled()); 62 action.actionPerformed(new ActionEvent(this, 0, "")); 63 assertEquals(10, MapGridPaintable.ORIGIN_X.get(), 1e-7); 64 assertEquals(50, MapGridPaintable.ORIGIN_Y.get(), 1e-7); 65 assertTrue(MapGridPaintable.ENABLED.get()); 66 67 MapGridPaintable.TYPE.put(GridType.PROJECTED); 68 action.actionPerformed(new ActionEvent(this, 0, "")); 69 assertEquals(en.east(), MapGridPaintable.ORIGIN_X.get(), 1e-6); 70 assertEquals(en.north(), MapGridPaintable.ORIGIN_Y.get(), 1e-6); 71 } finally { 72 MainApplication.getLayerManager().removeLayer(layer); 73 } 74 } 75 76 /** 77 * With more than one node reachable from the selection, the origin is the arithmetic mean of their positions: 78 * the centroid of two nodes of a way is their midpoint, not weighted by any polygon area. 79 */ 80 @Test 81 void testSetOriginCentroid() { 82 DataSet ds = new DataSet(); 83 OsmDataLayer layer = new OsmDataLayer(ds, "GridActionsTest", null); 84 MainApplication.getLayerManager().addLayer(layer); 85 try { 86 Node a = new Node(new EastNorth(0, 0)); 87 Node b = new Node(new EastNorth(100, 0)); 88 Way w = new Way(); 89 w.addNode(a); 90 w.addNode(b); 91 ds.addPrimitive(a); 92 ds.addPrimitive(b); 93 ds.addPrimitive(w); 94 95 ds.setSelected(w); 96 MapGridPaintable.TYPE.put(GridType.PROJECTED); 97 SetGridOriginAction action = new SetGridOriginAction(); 98 assertTrue(action.isEnabled()); 99 action.actionPerformed(new ActionEvent(this, 0, "")); 100 assertEquals(50, MapGridPaintable.ORIGIN_X.get(), 1e-6); 101 assertEquals(0, MapGridPaintable.ORIGIN_Y.get(), 1e-6); 102 103 // a relation contributes its node members 104 Node c = new Node(new EastNorth(0, 100)); 105 ds.addPrimitive(c); 106 Relation r = new Relation(); 107 r.addMember(new RelationMember("", c)); 108 ds.addPrimitive(r); 109 ds.setSelected(r); 110 action = new SetGridOriginAction(); 111 assertTrue(action.isEnabled()); 112 action.actionPerformed(new ActionEvent(this, 0, "")); 113 assertEquals(0, MapGridPaintable.ORIGIN_X.get(), 1e-6); 114 assertEquals(100, MapGridPaintable.ORIGIN_Y.get(), 1e-6); 115 116 ds.setSelected(); 117 assertNull(SetGridOriginAction.getCentroid(ds)); 118 assertFalse(new SetGridOriginAction().isEnabled()); 119 } finally { 120 MainApplication.getLayerManager().removeLayer(layer); 121 } 122 } 123 124 /** 125 * The rotation is reduced to [0, 90) and is the same for all four directions of a square grid 126 */ 127 @Test 128 void testRotationOf() { 129 EastNorth o = new EastNorth(0, 0); 130 assertEquals(0, AlignGridRotationAction.rotationOf(o, new EastNorth(10, 0)), 1e-9); 131 assertEquals(0, AlignGridRotationAction.rotationOf(o, new EastNorth(0, 10)), 1e-9); 132 assertEquals(0, AlignGridRotationAction.rotationOf(o, new EastNorth(-10, 0)), 1e-9); 133 assertEquals(45, AlignGridRotationAction.rotationOf(o, new EastNorth(10, 10)), 1e-9); 134 assertEquals(45, AlignGridRotationAction.rotationOf(o, new EastNorth(-10, 10)), 1e-9); 135 double c30 = Math.cos(Math.toRadians(30)); 136 double s30 = Math.sin(Math.toRadians(30)); 137 assertEquals(30, AlignGridRotationAction.rotationOf(o, new EastNorth(c30, s30)), 1e-9); 138 assertEquals(30, AlignGridRotationAction.rotationOf(o, new EastNorth(-c30, -s30)), 1e-9); 139 // a road at compass heading 115 degrees is a grid rotated by 65 degrees 140 double heading = Math.toRadians(115); 141 assertEquals(65, AlignGridRotationAction.rotationOf(o, new EastNorth(Math.sin(heading), Math.cos(heading))), 1e-9); 142 } 143 144 /** 145 * The direction comes from a single selected way or two selected nodes; anything else disables the action. 146 * The action reacts live to selection changes, since it now lives in a persistent menu. 147 */ 148 @Test 149 void testAlignToSelection() { 150 DataSet ds = new DataSet(); 151 OsmDataLayer layer = new OsmDataLayer(ds, "GridActionsTest", null); 152 MainApplication.getLayerManager().addLayer(layer); 153 try { 154 Node a = new Node(new LatLon(50, 10)); 155 Node b = new Node(new LatLon(50.01, 10.01)); 156 Node c = new Node(new LatLon(50.02, 10)); 157 Way w = new Way(); 158 w.addNode(a); 159 w.addNode(c); 160 w.addNode(b); 161 ds.addPrimitive(a); 162 ds.addPrimitive(b); 163 ds.addPrimitive(c); 164 ds.addPrimitive(w); 165 166 ds.setSelected(); 167 assertNull(AlignGridRotationAction.getSelectedDirection(ds)); 168 AlignGridRotationAction action = new AlignGridRotationAction(); 169 assertFalse(action.isEnabled()); 170 ds.setSelected(a); 171 assertNull(AlignGridRotationAction.getSelectedDirection(ds)); 172 assertFalse(action.isEnabled()); 173 ds.setSelected(a, b, c); 174 assertNull(AlignGridRotationAction.getSelectedDirection(ds)); 175 assertFalse(action.isEnabled()); 176 177 ds.setSelected(a, b); 178 assertTrue(action.isEnabled()); 179 MapGridPaintable.TYPE.put(GridType.LATLON); 180 action.actionPerformed(new ActionEvent(this, 0, "")); 181 double expected = AlignGridRotationAction.rotationOf(a.getEastNorth(), b.getEastNorth()); 182 assertEquals(expected, MapGridPaintable.ROTATION.get(), 1e-9); 183 assertEquals(GridType.PROJECTED, MapGridPaintable.TYPE.get()); 184 assertTrue(MapGridPaintable.ENABLED.get()); 185 186 // the way: first to last node (a to b), not the first segment 187 ds.setSelected(w); 188 assertTrue(action.isEnabled()); 189 EastNorth[] dir = AlignGridRotationAction.getSelectedDirection(ds); 190 assertEquals(a.getEastNorth(), dir[0]); 191 assertEquals(b.getEastNorth(), dir[1]); 192 } finally { 193 MainApplication.getLayerManager().removeLayer(layer); 194 } 195 } 196 } -
new file test/unit/org/openstreetmap/josm/gui/draw/BlendCompositeTest.java
diff --git test/unit/org/openstreetmap/josm/gui/draw/BlendCompositeTest.java test/unit/org/openstreetmap/josm/gui/draw/BlendCompositeTest.java new file mode 100644 index 0000000000..9498b08f82
- + 1 // License: GPL. For details, see LICENSE file. 2 package org.openstreetmap.josm.gui.draw; 3 4 import static org.junit.jupiter.api.Assertions.assertEquals; 5 import static org.junit.jupiter.api.Assertions.assertSame; 6 7 import java.awt.Color; 8 import java.awt.Graphics2D; 9 import java.awt.image.BufferedImage; 10 11 import org.junit.jupiter.params.ParameterizedTest; 12 import org.junit.jupiter.params.provider.ValueSource; 13 import org.junit.jupiter.api.Test; 14 import org.openstreetmap.josm.gui.draw.BlendComposite.Mode; 15 16 /** 17 * Unit tests of {@link BlendComposite}. 18 */ 19 class BlendCompositeTest { 20 21 private static int paint(int imageType, Color background, Color paint, Mode mode) { 22 BufferedImage img = new BufferedImage(4, 4, imageType); 23 Graphics2D g = img.createGraphics(); 24 g.setColor(background); 25 g.fillRect(0, 0, 4, 4); 26 g.setComposite(BlendComposite.getInstance(mode)); 27 g.setColor(paint); 28 g.fillRect(1, 1, 2, 2); 29 g.dispose(); 30 // the untouched pixel keeps the background 31 assertEquals(background.getRGB() & 0xffffff, img.getRGB(0, 0) & 0xffffff); 32 return img.getRGB(1, 1) & 0xffffff; 33 } 34 35 /** 36 * Blend modes on an opaque source, on both an image without and with alpha channel. 37 * @param imageType image type 38 */ 39 @ParameterizedTest 40 @ValueSource(ints = {BufferedImage.TYPE_3BYTE_BGR, BufferedImage.TYPE_INT_RGB, BufferedImage.TYPE_INT_ARGB}) 41 void testModes(int imageType) { 42 Color bg = new Color(200, 100, 50); 43 Color fg = new Color(128, 255, 0); 44 assertEquals(0x80ff00, paint(imageType, bg, fg, Mode.NORMAL)); 45 assertEquals(new Color(200 * 128 / 255, 100, 0).getRGB() & 0xffffff, paint(imageType, bg, fg, Mode.MULTIPLY)); 46 assertEquals(new Color(146, 100, 0).getRGB() & 0xffffff, paint(imageType, bg, fg, Mode.BURN)); 47 assertEquals(new Color(201, 255, 0).getRGB() & 0xffffff, paint(imageType, bg, fg, Mode.HARD_LIGHT)); 48 assertEquals(new Color(72, 155, 50).getRGB() & 0xffffff, paint(imageType, bg, fg, Mode.DIFFERENCE)); 49 assertEquals(new Color(255, 100, 255).getRGB() & 0xffffff, paint(imageType, bg, fg, Mode.DIVIDE)); 50 } 51 52 /** 53 * A translucent source only partially applies the blended color. 54 */ 55 @Test 56 void testAlpha() { 57 Color bg = new Color(200, 200, 200); 58 // half transparent black, multiply: 200 -> 0 at full alpha -> 100 at half alpha 59 int rgb = paint(BufferedImage.TYPE_INT_RGB, bg, new Color(0, 0, 0, 128), Mode.MULTIPLY); 60 assertEquals(100, (rgb >> 16) & 0xff, 1); 61 assertEquals(100, rgb & 0xff, 1); 62 // fully transparent: nothing changes 63 assertEquals(bg.getRGB() & 0xffffff, paint(BufferedImage.TYPE_INT_RGB, bg, new Color(0, 0, 0, 0), Mode.DIFFERENCE)); 64 } 65 66 /** 67 * White is neutral for multiply, burn and divide; black is neutral for difference 68 */ 69 @Test 70 void testNeutralColors() { 71 Color bg = new Color(12, 34, 56); 72 assertEquals(bg.getRGB() & 0xffffff, paint(BufferedImage.TYPE_INT_RGB, bg, Color.WHITE, Mode.MULTIPLY)); 73 assertEquals(bg.getRGB() & 0xffffff, paint(BufferedImage.TYPE_INT_RGB, bg, Color.WHITE, Mode.BURN)); 74 assertEquals(bg.getRGB() & 0xffffff, paint(BufferedImage.TYPE_INT_RGB, bg, Color.WHITE, Mode.DIVIDE)); 75 assertEquals(bg.getRGB() & 0xffffff, paint(BufferedImage.TYPE_INT_RGB, bg, Color.BLACK, Mode.DIFFERENCE)); 76 // black burns everything to black, white hard light gives white 77 assertEquals(0, paint(BufferedImage.TYPE_INT_RGB, bg, Color.BLACK, Mode.BURN)); 78 assertEquals(0xffffff, paint(BufferedImage.TYPE_INT_RGB, bg, Color.WHITE, Mode.HARD_LIGHT)); 79 } 80 81 /** 82 * A translucent destination is composed with the "source over" rule: the result alpha is 83 * {@code as + ab*(1-as)} and the blend only applies where the backdrop is actually present. 84 */ 85 @Test 86 void testTranslucentDestination() { 87 // opaque source over a half transparent destination: the source wins, the result is opaque 88 int argb = BlendComposite.composePixel(Mode.MULTIPLY, 0xff808080, 0x80ffffff); 89 assertEquals(0xff, argb >>> 24); 90 assertEquals(0x80, (argb >> 16) & 0xff, 1); 91 92 // half transparent source over a fully transparent destination: the source shows unblended 93 argb = BlendComposite.composePixel(Mode.MULTIPLY, 0x80123456, 0x00000000); 94 assertEquals(0x80, argb >>> 24); 95 assertEquals(0x12, (argb >> 16) & 0xff, 1); 96 assertEquals(0x34, (argb >> 8) & 0xff, 1); 97 assertEquals(0x56, argb & 0xff, 1); 98 99 // half transparent source over a half transparent destination: alpha is 128 + 128*(1-128/255) 100 argb = BlendComposite.composePixel(Mode.NORMAL, 0x80ffffff, 0x80000000); 101 assertEquals(128 + 128 * (255 - 128) / 255, argb >>> 24); 102 103 // a transparent source never changes the destination, whatever the mode 104 for (Mode mode : Mode.values()) { 105 assertEquals(0x8012ab34, BlendComposite.composePixel(mode, 0x00ffffff, 0x8012ab34), mode::toString); 106 } 107 108 // an opaque destination keeps its alpha and is only moved towards the blended color 109 assertEquals(0xff000000, BlendComposite.composePixel(Mode.MULTIPLY, 0xff000000, 0xffffffff)); 110 } 111 112 /** 113 * Instances are shared per mode 114 */ 115 @Test 116 void testInstances() { 117 assertSame(BlendComposite.getInstance(Mode.MULTIPLY), BlendComposite.getInstance(Mode.MULTIPLY)); 118 assertEquals(Mode.DIVIDE, BlendComposite.getInstance(Mode.DIVIDE).getMode()); 119 } 120 } -
new file test/unit/org/openstreetmap/josm/gui/layer/MapGridPaintableTest.java
diff --git test/unit/org/openstreetmap/josm/gui/layer/MapGridPaintableTest.java test/unit/org/openstreetmap/josm/gui/layer/MapGridPaintableTest.java new file mode 100644 index 0000000000..be8250620b
- + 1 // License: GPL. For details, see LICENSE file. 2 package org.openstreetmap.josm.gui.layer; 3 4 import static org.junit.jupiter.api.Assertions.assertEquals; 5 import static org.junit.jupiter.api.Assertions.assertTrue; 6 7 import java.awt.Color; 8 import java.awt.Point; 9 import java.awt.Rectangle; 10 import java.awt.image.BufferedImage; 11 import java.util.List; 12 import java.util.stream.Collectors; 13 14 import org.junit.jupiter.api.Test; 15 import org.openstreetmap.josm.data.Bounds; 16 import org.openstreetmap.josm.data.ProjectionBounds; 17 import org.openstreetmap.josm.data.coor.EastNorth; 18 import org.openstreetmap.josm.data.coor.LatLon; 19 import org.openstreetmap.josm.data.projection.ProjectionRegistry; 20 import org.openstreetmap.josm.gui.MainApplication; 21 import org.openstreetmap.josm.gui.MapView; 22 import org.openstreetmap.josm.gui.draw.BlendComposite; 23 import org.openstreetmap.josm.gui.layer.MapGridPaintable.GridType; 24 import org.openstreetmap.josm.gui.layer.MapGridPaintable.LatLonGridLine; 25 import org.openstreetmap.josm.gui.layer.MapGridPaintable.ProjectedGridLine; 26 import org.openstreetmap.josm.gui.util.GuiHelper; 27 import org.openstreetmap.josm.testutils.annotations.Main; 28 import org.openstreetmap.josm.testutils.annotations.Projection; 29 30 /** 31 * Unit tests of {@link MapGridPaintable}. 32 */ 33 @Main 34 @Projection 35 class MapGridPaintableTest { 36 37 private static long count(List<ProjectedGridLine> lines, boolean vertical) { 38 return lines.stream().filter(l -> vertical == (Math.abs(l.start.east() - l.end.east()) < 1e-9)) 39 .peek(l -> assertEquals(vertical, l.constantEast, "wrong direction flag: " + l)).count(); 40 } 41 42 /** 43 * An axis aligned grid covers the area with lines at multiples of the spacing. 44 */ 45 @Test 46 void testProjectedGridAxisAligned() { 47 ProjectionBounds area = new ProjectionBounds(new EastNorth(-250, -120), new EastNorth(1010, 380)); 48 List<ProjectedGridLine> lines = MapGridPaintable.getProjectedGridLines(area, 100, 50, 0, 0, 0); 49 // vertical lines at -200 .. 1000 (13), horizontal at -100 .. 350 (10) 50 assertEquals(13, count(lines, true)); 51 assertEquals(10, count(lines, false)); 52 assertEquals(23, lines.size()); 53 for (ProjectedGridLine line : lines) { 54 if (line.constantEast) { 55 assertEquals(0, line.start.east() % 100, 1e-9, "vertical line not on the grid: " + line); 56 assertEquals(area.minNorth, Math.min(line.start.north(), line.end.north()), 1e-9); 57 assertEquals(area.maxNorth, Math.max(line.start.north(), line.end.north()), 1e-9); 58 } else { 59 assertEquals(0, line.start.north() % 50, 1e-9, "horizontal line not on the grid: " + line); 60 assertEquals(area.minEast, Math.min(line.start.east(), line.end.east()), 1e-9); 61 assertEquals(area.maxEast, Math.max(line.start.east(), line.end.east()), 1e-9); 62 } 63 } 64 } 65 66 /** 67 * The origin shifts the grid, the rotation turns it around the origin. 68 */ 69 @Test 70 void testProjectedGridOriginAndRotation() { 71 ProjectionBounds area = new ProjectionBounds(new EastNorth(0, 0), new EastNorth(100, 100)); 72 List<ProjectedGridLine> lines = MapGridPaintable.getProjectedGridLines(area, 30, 30, 0, 5, 10); 73 // vertical lines at 5, 35, 65, 95; horizontal at 10, 40, 70, 100 74 assertEquals(4, count(lines, true)); 75 assertEquals(4, count(lines, false)); 76 assertTrue(lines.stream().anyMatch(l -> Math.abs(l.start.east() - 95) < 1e-9 && Math.abs(l.end.east() - 95) < 1e-9)); 77 assertTrue(lines.stream().anyMatch(l -> Math.abs(l.start.north() - 100) < 1e-9 && Math.abs(l.end.north() - 100) < 1e-9)); 78 79 lines = MapGridPaintable.getProjectedGridLines(area, 30, 30, 45, 0, 0); 80 assertTrue(lines.size() > 4, lines.toString()); 81 for (ProjectedGridLine line : lines) { 82 double dx = line.end.east() - line.start.east(); 83 double dy = line.end.north() - line.start.north(); 84 // every line runs at +45° or -45° 85 assertEquals(Math.abs(dx), Math.abs(dy), 1e-6, "line not rotated by 45°: " + line); 86 // the lines pass through the grid points (u, v) = (k*30, m*30) rotated by 45°: check the distance of the 87 // origin to the line is a multiple of 30 88 double len = Math.hypot(dx, dy); 89 double dist = Math.abs(dx * (0 - line.start.north()) - dy * (0 - line.start.east())) / len; 90 assertEquals(0, dist % 30 < 1e-6 ? 0 : Math.abs(dist % 30 - 30), 1e-6, "line not on the rotated grid: " + dist); 91 } 92 } 93 94 /** 95 * Invalid spacings and huge line counts yield nothing. 96 */ 97 @Test 98 void testProjectedGridLimits() { 99 ProjectionBounds area = new ProjectionBounds(new EastNorth(0, 0), new EastNorth(100, 100)); 100 assertTrue(MapGridPaintable.getProjectedGridLines(area, 0, 10, 0, 0, 0).isEmpty()); 101 assertTrue(MapGridPaintable.getProjectedGridLines(area, 10, -1, 0, 0, 0).isEmpty()); 102 assertTrue(MapGridPaintable.getProjectedGridLines(area, 0.01, 0.01, 0, 0, 0).isEmpty()); 103 } 104 105 /** 106 * Latitude/longitude lines: multiples of the spacing (shifted by the origin), clamped to the world, curved 107 * lines with the requested number of segments. 108 */ 109 @Test 110 void testLatLonGrid() { 111 Bounds world = new Bounds(-85, -180, 85, 180); 112 Bounds area = new Bounds(50.2, 7.9, 52.1, 10.3); 113 List<LatLonGridLine> gridLines = MapGridPaintable.getLatLonGridLines(area, world, 1, 0.5, 0, 0, 4); 114 // meridians 8, 9, 10; parallels 50.5, 51, 51.5, 52 115 assertEquals(7, gridLines.size()); 116 assertEquals(3, gridLines.stream().filter(l -> l.meridian).count()); 117 for (LatLonGridLine gridLine : gridLines) { 118 List<LatLon> line = gridLine.points; 119 assertEquals(5, line.size()); 120 assertEquals(gridLine.meridian, line.get(0).lon() == line.get(4).lon()); 121 if (gridLine.meridian) { 122 assertEquals(0, line.get(0).lon() % 1, 1e-9); 123 assertEquals(50.2, line.get(0).lat(), 1e-9); 124 assertEquals(52.1, line.get(4).lat(), 1e-9); 125 } else { 126 assertEquals(0, line.get(0).lat() % 0.5, 1e-9); 127 assertEquals(7.9, line.get(0).lon(), 1e-9); 128 assertEquals(10.3, line.get(4).lon(), 1e-9); 129 } 130 } 131 // origin offset 132 List<List<LatLon>> lines = points(MapGridPaintable.getLatLonGridLines(area, world, 1, 1, 0.5, 0.25, 1)); 133 assertTrue(lines.stream().anyMatch(l -> l.get(0).lon() == 8.5 && l.get(1).lon() == 8.5), lines.toString()); 134 assertTrue(lines.stream().anyMatch(l -> l.get(0).lat() == 51.25 && l.get(1).lat() == 51.25), lines.toString()); 135 136 // clamped to the world bounds: no parallel at 90 137 lines = points(MapGridPaintable.getLatLonGridLines(new Bounds(80, 0, 89.9, 10), world, 10, 10, 0, 0, 1)); 138 assertTrue(lines.stream().noneMatch(l -> l.get(0).lat() > 85), lines.toString()); 139 assertTrue(lines.stream().anyMatch(l -> l.get(0).lat() == 80 && l.get(1).lat() == 80), lines.toString()); 140 141 // across the antimeridian: meridians 170 .. 180, -170; longitudes stay in [-180, 180] 142 gridLines = MapGridPaintable.getLatLonGridLines(new Bounds(0, 165, 10, -165), world, 10, 90, 0, 0, 1); 143 assertEquals(3, gridLines.stream().filter(l -> l.meridian).count(), gridLines.toString()); 144 assertTrue(gridLines.stream().allMatch(l -> l.points.stream().allMatch(ll -> ll.lon() >= -180 && ll.lon() <= 180))); 145 } 146 147 /** 148 * A projection which does not span the whole globe clamps the meridians to its longitude range, the same way 149 * the parallels are clamped to its latitude range. 150 */ 151 @Test 152 void testLatLonGridLimitedWorld() { 153 // a projection valid for 6 degrees of longitude only, e.g. a UTM zone 154 Bounds world = new Bounds(0, 6, 84, 12); 155 Bounds area = new Bounds(45, 2, 50, 16); 156 List<LatLonGridLine> lines = MapGridPaintable.getLatLonGridLines(area, world, 2, 2, 0, 0, 1); 157 assertTrue(lines.stream().filter(l -> l.meridian).findAny().isPresent(), lines.toString()); 158 assertTrue(lines.stream().filter(l -> l.meridian) 159 .allMatch(l -> l.points.get(0).lon() >= 6 && l.points.get(0).lon() <= 12), lines.toString()); 160 // meridians 6, 8, 10, 12 are inside the projection, 2, 4, 14, 16 are not 161 assertEquals(4, lines.stream().filter(l -> l.meridian).count(), lines.toString()); 162 163 // a world spanning all longitudes keeps every meridian, including across the antimeridian 164 Bounds whole = new Bounds(-85, -180, 85, 180); 165 assertEquals(8, MapGridPaintable.getLatLonGridLines(area, whole, 2, 2, 0, 0, 1) 166 .stream().filter(l -> l.meridian).count()); 167 } 168 169 /** 170 * A parallel which crosses the antimeridian is split in two, one line on each side of it. Non-regression 171 * test: wrapping the longitudes of a single polyline instead made it jump right across the view. 172 */ 173 @Test 174 void testLatLonGridAcrossAntimeridian() { 175 Bounds world = new Bounds(-85, -180, 85, 180); 176 List<LatLonGridLine> lines = MapGridPaintable.getLatLonGridLines(new Bounds(0, 165, 10, -165), world, 10, 90, 0, 0, 4); 177 List<List<LatLon>> parallels = points(lines.stream().filter(l -> !l.meridian).collect(Collectors.toList())); 178 // the parallel at latitude 0 runs from 165 to 180 and from -180 to -165 179 assertEquals(2, parallels.size(), parallels.toString()); 180 assertTrue(parallels.stream().anyMatch(l -> l.get(0).lon() == 165 && l.get(4).lon() == 180), parallels.toString()); 181 assertTrue(parallels.stream().anyMatch(l -> l.get(0).lon() == -180 && l.get(4).lon() == -165), parallels.toString()); 182 for (List<LatLon> line : parallels) { 183 assertEquals(0, line.get(0).lat(), 1e-9); 184 for (int i = 1; i < line.size(); i++) { 185 // no jump: every step is a quarter of the 15 degrees the line spans 186 assertEquals(3.75, line.get(i).lon() - line.get(i - 1).lon(), 1e-9, line.toString()); 187 } 188 } 189 } 190 191 /** 192 * The visible bounds cover the whole world once the view is larger than it. Non-regression test: the corners 193 * of such a view lie outside the world, so their longitude wraps around and simply converting them - as 194 * {@link MapView#getRealBounds()} does - yields a far too narrow range which jumps around while zooming. 195 */ 196 @Test 197 void testVisibleLatLonBoundsAtWorldZoom() { 198 SizedMapView mv = new SizedMapView(); 199 mv.setBounds(new Rectangle(713, 570)); 200 GuiHelper.runInEDTAndWait(() -> { /* let the component listener update the view state */ }); 201 mv.updateState(); 202 mv.zoomTo(new LatLon(0, 0)); 203 Bounds world = mv.getProjection().getWorldBoundsLatLon(); 204 try { 205 // zoomed in: the whole view shows the world, the bounds are those of the view 206 mv.zoomTo(mv.getCenter(), 1000); 207 Bounds bounds = MapGridPaintable.getVisibleLatLonBounds(mv); 208 assertEquals(mv.getRealBounds().getMinLon(), bounds.getMinLon(), 1e-6); 209 assertEquals(mv.getRealBounds().getMaxLon(), bounds.getMaxLon(), 1e-6); 210 211 // zoomed out until the world is narrower than the view: the bounds are those of the world 212 for (double scale : new double[] {45000, 60000, 77000, 200000}) { 213 mv.zoomTo(mv.getCenter(), scale); 214 bounds = MapGridPaintable.getVisibleLatLonBounds(mv); 215 double worldPixels = mv.getPoint2D(new LatLon(0, 180)).getX() - mv.getPoint2D(new LatLon(0, -180)).getX(); 216 if (worldPixels < mv.getWidth()) { 217 assertEquals(-180, bounds.getMinLon(), 1e-6, "scale " + scale); 218 assertEquals(180, bounds.getMaxLon(), 1e-6, "scale " + scale); 219 assertTrue(bounds.getMaxLon() - bounds.getMinLon() > mv.getRealBounds().getMaxLon() - mv.getRealBounds().getMinLon(), 220 "scale " + scale + ": " + bounds + " not wider than " + mv.getRealBounds()); 221 } 222 // Bounds.extend rounds to the OSM precision, so allow for that 223 assertTrue(bounds.getMinLat() >= world.getMinLat() - 1e-6 && bounds.getMaxLat() <= world.getMaxLat() + 1e-6, 224 "scale " + scale + ": " + bounds + " outside " + world); 225 } 226 } finally { 227 mv.destroy(); 228 } 229 } 230 231 /** 232 * Panning past the antimeridian, so that there is blank space beside the world, keeps the grid on screen. 233 * Non-regression test: the clipped edge falls exactly on the antimeridian, whose longitude is ambiguous and 234 * is normalized to -180, which turned a visible range of e.g. 6 W .. 180 into 180 W .. 6 W - the other half 235 * of the world, drawn completely off screen, so that the grid seemed to disappear. 236 */ 237 @Test 238 void testVisibleLatLonBoundsPastTheAntimeridian() { 239 SizedMapView mv = new SizedMapView(); 240 mv.setBounds(new Rectangle(713, 570)); 241 GuiHelper.runInEDTAndWait(() -> { /* let the component listener update the view state */ }); 242 mv.updateState(); 243 Bounds world = mv.getProjection().getWorldBoundsLatLon(); 244 double halfWorld = mv.getProjection().getWorldBoundsBoxEastNorth().maxEast; 245 try { 246 for (double fraction : new double[] {-0.8, -0.5, 0.5, 0.8}) { 247 mv.zoomTo(new EastNorth(fraction * halfWorld, 0), 30000); 248 boolean eastwards = fraction > 0; 249 assertTrue(eastwards ? mv.getProjectionBounds().maxEast > halfWorld : mv.getProjectionBounds().minEast < -halfWorld, 250 "no blank space beside the world at " + fraction); 251 Bounds bounds = MapGridPaintable.getVisibleLatLonBounds(mv); 252 String at = "at " + fraction + ": " + bounds; 253 assertTrue(bounds.getMinLon() < bounds.getMaxLon(), at); 254 // the range reaches the antimeridian on the blank side and the view edge on the other one 255 assertEquals(eastwards ? 180 : -180, eastwards ? bounds.getMaxLon() : bounds.getMinLon(), 1e-5, at); 256 assertEquals(mv.getProjection().eastNorth2latlon( 257 new EastNorth(eastwards ? mv.getProjectionBounds().minEast : mv.getProjectionBounds().maxEast, 0)).lon(), 258 eastwards ? bounds.getMinLon() : bounds.getMaxLon(), 1e-5, at); 259 // and the meridians really are drawn inside the view 260 List<LatLonGridLine> lines = MapGridPaintable.getLatLonGridLines(bounds, world, 10, 10, 0, 0, 2); 261 assertTrue(lines.stream().filter(l -> l.meridian) 262 .anyMatch(l -> mv.getPoint2D(l.points.get(0)).getX() >= 0 && mv.getPoint2D(l.points.get(0)).getX() <= mv.getWidth()), 263 at + ", meridians off screen: " + lines); 264 } 265 } finally { 266 mv.destroy(); 267 } 268 } 269 270 private static List<List<LatLon>> points(List<LatLonGridLine> lines) { 271 return lines.stream().map(l -> l.points).collect(Collectors.toList()); 272 } 273 274 /** 275 * In Mercator one metre is 1/cos(latitude) projection units. 276 */ 277 @Test 278 void testProjectionUnitsPerMetre() { 279 org.openstreetmap.josm.data.projection.Projection proj = ProjectionRegistry.getProjection(); 280 assertEquals(1, MapGridPaintable.projectionUnitsPerMetre(proj, proj.latlon2eastNorth(new LatLon(0, 10))), 1e-3); 281 assertEquals(1 / Math.cos(Math.toRadians(50)), 282 MapGridPaintable.projectionUnitsPerMetre(proj, proj.latlon2eastNorth(new LatLon(50, 10))), 1e-3); 283 assertEquals(1 / Math.cos(Math.toRadians(20.6)), 284 MapGridPaintable.projectionUnitsPerMetre(proj, proj.latlon2eastNorth(new LatLon(20.6, 87.8))), 1e-3); 285 // outside the world: no scaling 286 assertEquals(1, MapGridPaintable.projectionUnitsPerMetre(proj, new EastNorth(0, 1e12)), 1e-9); 287 } 288 289 /** 290 * The thinning factor is a power of ten bringing the spacing above the minimum. 291 */ 292 @Test 293 void testThinningFactor() { 294 MapGridPaintable.MIN_PIXEL_SPACING.put(25.0); 295 try { 296 assertEquals(1, MapGridPaintable.thinningFactor(30)); 297 assertEquals(1, MapGridPaintable.thinningFactor(25)); 298 assertEquals(10, MapGridPaintable.thinningFactor(24.9)); 299 assertEquals(100, MapGridPaintable.thinningFactor(0.3)); 300 assertEquals(1, MapGridPaintable.thinningFactor(0)); 301 assertEquals(1, MapGridPaintable.thinningFactor(Double.NaN)); 302 } finally { 303 MapGridPaintable.MIN_PIXEL_SPACING.remove(); 304 } 305 } 306 307 /** 308 * Painting on a map view draws something when enabled and nothing when disabled, for both grid types and 309 * with a blend mode. 310 */ 311 @Test 312 void testPaint() { 313 SizedMapView mv = new SizedMapView(); 314 mv.setBounds(new Rectangle(400, 300)); 315 GuiHelper.runInEDTAndWait(() -> { /* let the component listener update the view state */ }); 316 mv.updateState(); 317 mv.zoomTo(new LatLon(50, 10)); 318 assertTrue(mv.getRealBounds().getMaxLat() > mv.getRealBounds().getMinLat(), "map view has no size: " + mv.getRealBounds()); 319 MapGridPaintable grid = new MapGridPaintable(); 320 try { 321 MapGridPaintable.ENABLED.put(false); 322 assertEquals(0, paintedPixels(grid, mv), "grid drawn although disabled"); 323 MapGridPaintable.ENABLED.put(true); 324 MapGridPaintable.TYPE.put(GridType.LATLON); 325 MapGridPaintable.SPACING_X.put(0.001); 326 MapGridPaintable.SPACING_Y.put(0.001); 327 assertTrue(paintedPixels(grid, mv) > 100, "lat/lon grid not drawn"); 328 MapGridPaintable.TYPE.put(GridType.PROJECTED); 329 MapGridPaintable.SPACING_X.put(100.0); 330 MapGridPaintable.SPACING_Y.put(100.0); 331 MapGridPaintable.ROTATION.put(30.0); 332 MapGridPaintable.BLEND_MODE.put(BlendComposite.Mode.MULTIPLY); 333 assertTrue(paintedPixels(grid, mv) > 100, "projected grid not drawn"); 334 } finally { 335 grid.destroy(); 336 mv.destroy(); 337 for (String key : new String[] {"enabled", "type", "spacing-x", "spacing-y", "rotation", "blend-mode"}) { 338 org.openstreetmap.josm.spi.preferences.Config.getPref().put("draw.grid." + key, null); 339 } 340 } 341 } 342 343 /** A map view which believes it is shown on screen, so that its view state gets a size */ 344 private static final class SizedMapView extends MapView { 345 SizedMapView() { 346 super(MainApplication.getLayerManager(), null); 347 } 348 349 @Override 350 public Point getLocationOnScreen() { 351 return new Point(0, 0); 352 } 353 354 @Override 355 protected boolean isVisibleOnScreen() { 356 return true; 357 } 358 359 void updateState() { 360 updateLocationState(); 361 } 362 } 363 364 private static int paintedPixels(MapGridPaintable grid, MapView mv) { 365 BufferedImage img = new BufferedImage(mv.getWidth(), mv.getHeight(), BufferedImage.TYPE_3BYTE_BGR); 366 java.awt.Graphics2D g = img.createGraphics(); 367 g.setColor(Color.WHITE); 368 g.fillRect(0, 0, img.getWidth(), img.getHeight()); 369 grid.paint(g, mv, mv.getRealBounds()); 370 g.dispose(); 371 int count = 0; 372 for (int y = 0; y < img.getHeight(); y++) { 373 for (int x = 0; x < img.getWidth(); x++) { 374 if ((img.getRGB(x, y) & 0xffffff) != 0xffffff) { 375 count++; 376 } 377 } 378 } 379 return count; 380 } 381 } -
new file test/unit/org/openstreetmap/josm/gui/layer/imagery/ShowTileBordersActionTest.java
diff --git test/unit/org/openstreetmap/josm/gui/layer/imagery/ShowTileBordersActionTest.java test/unit/org/openstreetmap/josm/gui/layer/imagery/ShowTileBordersActionTest.java new file mode 100644 index 0000000000..45c30cb2a0
- + 1 // License: GPL. For details, see LICENSE file. 2 package org.openstreetmap.josm.gui.layer.imagery; 3 4 import static org.junit.jupiter.api.Assertions.assertFalse; 5 import static org.junit.jupiter.api.Assertions.assertTrue; 6 7 import java.awt.event.ActionEvent; 8 import java.util.Collections; 9 import java.util.HashMap; 10 import java.util.Map; 11 12 import javax.swing.JCheckBoxMenuItem; 13 14 import org.junit.jupiter.api.AfterEach; 15 import org.junit.jupiter.api.Test; 16 import org.openstreetmap.josm.gui.layer.TMSLayer; 17 import org.openstreetmap.josm.gui.layer.TMSLayerTest; 18 import org.openstreetmap.josm.spi.preferences.Config; 19 import org.openstreetmap.josm.testutils.annotations.Main; 20 import org.openstreetmap.josm.testutils.annotations.Projection; 21 22 /** 23 * Unit tests of {@link ShowTileBordersAction}. 24 */ 25 @Main 26 @Projection 27 class ShowTileBordersActionTest { 28 29 @AfterEach 30 void resetPreferences() { 31 Config.getPref().put("imagery.generic.default_showtileborders", null); 32 } 33 34 /** 35 * Toggling the tile borders remembers the choice, so that a layer created later - in this session or after 36 * a restart - shows them too. Non-regression test for the setting being forgotten between sessions. 37 */ 38 @Test 39 void testTogglePersistsTheChoice() { 40 TMSLayer layer = TMSLayerTest.createTmsLayer(); 41 assertFalse(layer.getDisplaySettings().isShowTileBorders()); 42 ShowTileBordersAction action = new ShowTileBordersAction(layer); 43 44 action.actionPerformed(new ActionEvent(this, 0, "")); 45 assertTrue(layer.getDisplaySettings().isShowTileBorders()); 46 assertTrue(TileSourceDisplaySettings.PROP_SHOW_TILE_BORDERS.get()); 47 // a layer created afterwards picks the choice up, which is what happens after a restart 48 assertTrue(TMSLayerTest.createTmsLayer().getDisplaySettings().isShowTileBorders()); 49 assertTrue(new JCheckBoxMenuItem(action).getModel().isEnabled()); 50 assertTrue(((JCheckBoxMenuItem) action.createMenuComponent()).isSelected()); 51 52 action.actionPerformed(new ActionEvent(this, 0, "")); 53 assertFalse(layer.getDisplaySettings().isShowTileBorders()); 54 assertFalse(TileSourceDisplaySettings.PROP_SHOW_TILE_BORDERS.get()); 55 assertFalse(TMSLayerTest.createTmsLayer().getDisplaySettings().isShowTileBorders()); 56 assertFalse(((JCheckBoxMenuItem) action.createMenuComponent()).isSelected()); 57 } 58 59 /** 60 * Loading a session applies the setting to that layer only, it must not change the remembered default. 61 */ 62 @Test 63 void testSessionDoesNotOverwriteThePreference() { 64 TMSLayer layer = TMSLayerTest.createTmsLayer(); 65 Map<String, String> session = new HashMap<>(Collections.singletonMap("show-tile-borders", "true")); 66 layer.getDisplaySettings().applyFromPropertiesMap(session); 67 68 assertTrue(layer.getDisplaySettings().isShowTileBorders()); 69 assertFalse(TileSourceDisplaySettings.PROP_SHOW_TILE_BORDERS.get()); 70 assertFalse(TMSLayerTest.createTmsLayer().getDisplaySettings().isShowTileBorders()); 71 } 72 } -
new file test/unit/org/openstreetmap/josm/gui/layer/imagery/TileSourceDisplaySettingsTest.java
diff --git test/unit/org/openstreetmap/josm/gui/layer/imagery/TileSourceDisplaySettingsTest.java test/unit/org/openstreetmap/josm/gui/layer/imagery/TileSourceDisplaySettingsTest.java new file mode 100644 index 0000000000..6adbe9eaaa
- + 1 // License: GPL. For details, see LICENSE file. 2 package org.openstreetmap.josm.gui.layer.imagery; 3 4 import static org.junit.jupiter.api.Assertions.assertEquals; 5 import static org.junit.jupiter.api.Assertions.assertFalse; 6 import static org.junit.jupiter.api.Assertions.assertNotEquals; 7 import static org.junit.jupiter.api.Assertions.assertTrue; 8 9 import java.util.ArrayList; 10 import java.util.List; 11 import java.util.Map; 12 13 import org.junit.jupiter.api.AfterEach; 14 import org.junit.jupiter.api.Test; 15 import org.openstreetmap.josm.spi.preferences.Config; 16 import org.openstreetmap.josm.testutils.annotations.BasicPreferences; 17 18 /** 19 * Unit tests of {@link TileSourceDisplaySettings}. 20 */ 21 @BasicPreferences 22 class TileSourceDisplaySettingsTest { 23 24 @AfterEach 25 void resetPreferences() { 26 Config.getPref().put("imagery.generic.default_showtileborders", null); 27 Config.getPref().put("imagery.tms.default_showtileborders", null); 28 } 29 30 /** 31 * The tile borders setting fires a change event and survives a round trip through the session properties. 32 */ 33 @Test 34 void testShowTileBorders() { 35 TileSourceDisplaySettings settings = new TileSourceDisplaySettings(); 36 assertFalse(settings.isShowTileBorders()); 37 List<String> changes = new ArrayList<>(); 38 settings.addSettingsChangeListener(e -> changes.add(e.getChangedSetting())); 39 settings.setShowTileBorders(true); 40 assertTrue(settings.isShowTileBorders()); 41 assertEquals(1, changes.size()); 42 43 Map<String, String> data = settings.toPropertiesMap(); 44 assertEquals("true", data.get("show-tile-borders")); 45 TileSourceDisplaySettings copy = new TileSourceDisplaySettings(); 46 assertNotEquals(settings, copy); 47 copy.applyFromPropertiesMap(data); 48 assertTrue(copy.isShowTileBorders()); 49 assertEquals(settings, copy); 50 assertEquals(settings.hashCode(), copy.hashCode()); 51 52 // a session without the setting keeps the current value 53 data.remove("show-tile-borders"); 54 copy.applyFromPropertiesMap(data); 55 assertTrue(copy.isShowTileBorders()); 56 } 57 58 /** 59 * The default of a new layer comes from the preferences, read with the same prefix logic as the sibling 60 * settings (auto load, auto zoom, show errors): an explicitly set value is honoured, and a layer specific 61 * prefix takes precedence over the generic one. 62 */ 63 @Test 64 void testShowTileBordersDefault() { 65 assertFalse(new TileSourceDisplaySettings().isShowTileBorders()); 66 assertFalse(new TileSourceDisplaySettings("imagery.tms").isShowTileBorders()); 67 68 Config.getPref().putBoolean("imagery.generic.default_showtileborders", true); 69 assertTrue(new TileSourceDisplaySettings().isShowTileBorders()); 70 71 Config.getPref().putBoolean("imagery.tms.default_showtileborders", true); 72 assertTrue(new TileSourceDisplaySettings("imagery.tms").isShowTileBorders()); 73 74 Config.getPref().putBoolean("imagery.tms.default_showtileborders", false); 75 assertFalse(new TileSourceDisplaySettings("imagery.tms").isShowTileBorders()); 76 } 77 } -
new file test/unit/org/openstreetmap/josm/gui/preferences/display/GridPreferenceTest.java
diff --git test/unit/org/openstreetmap/josm/gui/preferences/display/GridPreferenceTest.java test/unit/org/openstreetmap/josm/gui/preferences/display/GridPreferenceTest.java new file mode 100644 index 0000000000..b52d26c915
- + 1 // License: GPL. For details, see LICENSE file. 2 package org.openstreetmap.josm.gui.preferences.display; 3 4 import static org.junit.jupiter.api.Assertions.assertEquals; 5 import static org.junit.jupiter.api.Assertions.assertNotNull; 6 7 import org.junit.jupiter.api.Test; 8 import org.openstreetmap.josm.gui.layer.MapGridPaintable; 9 import org.openstreetmap.josm.gui.preferences.PreferencesTestUtils; 10 import org.openstreetmap.josm.testutils.annotations.BasicPreferences; 11 import org.openstreetmap.josm.testutils.annotations.Main; 12 13 /** 14 * Unit tests of {@link GridPreference} class. 15 */ 16 @BasicPreferences 17 @Main 18 class GridPreferenceTest { 19 /** 20 * Unit test of {@link GridPreference#GridPreference}. 21 */ 22 @Test 23 void testGridPreference() { 24 assertNotNull(new GridPreference.Factory().createPreferenceSetting()); 25 } 26 27 /** 28 * Unit test of {@link GridPreference#addGui}: the settings survive a round trip through the panel. 29 */ 30 @Test 31 void testAddGui() { 32 MapGridPaintable.SPACING_X.put(0.25); 33 MapGridPaintable.ROTATION.put(-12.5); 34 try { 35 PreferencesTestUtils.doTestPreferenceSettingAddGui(new GridPreference.Factory(), null); 36 assertEquals(0.25, MapGridPaintable.SPACING_X.get(), 1e-12); 37 assertEquals(-12.5, MapGridPaintable.ROTATION.get(), 1e-12); 38 } finally { 39 MapGridPaintable.SPACING_X.remove(); 40 MapGridPaintable.ROTATION.remove(); 41 } 42 } 43 }
