Ticket #8464: josm-map-grid.patch
| File josm-map-grid.patch, 107.1 KB (added by , 20 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..221c6041f9
- + 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 = mv.getRealBounds(); 203 // pixel size of one cell at the center of the view 204 LatLon center = view.getCenter(); 205 Point2D c = mv.getPoint2D(center); 206 Point2D cx = mv.getPoint2D(new LatLon(center.lat(), Math.min(180, center.lon() + sx))); 207 Point2D cy = mv.getPoint2D(new LatLon(Math.min(89, center.lat() + sy), center.lon())); 208 double factor = thinningFactor(Math.min(c.distance(cx), c.distance(cy))); 209 List<LatLonGridLine> lines = getLatLonGridLines(view, mv.getProjection().getWorldBoundsLatLon(), 210 sx * factor, sy * factor, ORIGIN_X.get(), ORIGIN_Y.get(), CURVE_SEGMENTS); 211 for (LatLonGridLine line : lines) { 212 g.setColor(line.meridian ? COLOR_EAST.get() : COLOR_NORTH.get()); 213 Path2D.Double path = new Path2D.Double(); 214 boolean first = true; 215 for (LatLon ll : line.points) { 216 Point2D p = mv.getPoint2D(ll); 217 if (first) { 218 path.moveTo(p.getX(), p.getY()); 219 first = false; 220 } else { 221 path.lineTo(p.getX(), p.getY()); 222 } 223 } 224 g.draw(path); 225 } 226 } 227 228 /** 229 * Computes how many projection units correspond to one metre on the ground at the given position. For 230 * conformal projections (e.g. Mercator) this is the local scale factor, for a Mercator grid at 60° latitude 231 * one metre is two projection units. 232 * @param projection the projection 233 * @param at the position (projected coordinates) 234 * @return projection units per metre; 1 if it cannot be determined (position outside the world) 235 */ 236 public static double projectionUnitsPerMetre(Projection projection, EastNorth at) { 237 try { 238 if (!projection.getWorldBoundsBoxEastNorth().contains(at)) { 239 return 1; 240 } 241 double step = 100; 242 LatLon a = projection.eastNorth2latlon(at); 243 LatLon b = projection.eastNorth2latlon(new EastNorth(at.east() + step, at.north())); 244 if (!a.isValid() || !b.isValid()) { 245 return 1; 246 } 247 double metres = a.greatCircleDistance((ILatLon) b); 248 return metres > 0 && Double.isFinite(metres) ? step / metres : 1; 249 } catch (IllegalArgumentException e) { 250 Logging.trace(e); 251 return 1; 252 } 253 } 254 255 /** 256 * Computes the factor (a power of 10) by which the spacing must be multiplied so that the lines are at least 257 * {@link #MIN_PIXEL_SPACING} apart. 258 * @param pixelSpacing the distance between two lines on screen, in pixels 259 * @return the factor (at least 1) 260 */ 261 static double thinningFactor(double pixelSpacing) { 262 double min = Math.max(1, MIN_PIXEL_SPACING.get()); 263 double factor = 1; 264 if (pixelSpacing <= 0 || Double.isNaN(pixelSpacing)) { 265 return factor; 266 } 267 while (pixelSpacing * factor < min && factor < 1e15) { 268 factor *= 10; 269 } 270 return factor; 271 } 272 273 /** 274 * Computes the lines of a (possibly rotated) grid in projected coordinates which cross the given area. 275 * @param area the area to cover 276 * @param spacingX distance between the lines running in the "north" direction of the grid (before rotation) 277 * @param spacingY distance between the lines running in the "east" direction of the grid (before rotation) 278 * @param rotationDegrees rotation of the grid, counter clockwise 279 * @param originX east coordinate of the grid origin 280 * @param originY north coordinate of the grid origin 281 * @return the lines; empty if the spacing is invalid or there would be too many lines 282 */ 283 public static List<ProjectedGridLine> getProjectedGridLines(ProjectionBounds area, double spacingX, double spacingY, 284 double rotationDegrees, double originX, double originY) { 285 List<ProjectedGridLine> lines = new ArrayList<>(); 286 if (!(spacingX > 0) || !(spacingY > 0)) { 287 return lines; 288 } 289 double angle = Math.toRadians(rotationDegrees); 290 double c = Math.cos(angle); 291 double s = Math.sin(angle); 292 // grid coordinates (u along the rotated east axis, v along the rotated north axis) of the area corners 293 double[] xs = {area.minEast, area.maxEast, area.maxEast, area.minEast}; 294 double[] ys = {area.minNorth, area.minNorth, area.maxNorth, area.maxNorth}; 295 double uMin = Double.POSITIVE_INFINITY; 296 double uMax = Double.NEGATIVE_INFINITY; 297 double vMin = Double.POSITIVE_INFINITY; 298 double vMax = Double.NEGATIVE_INFINITY; 299 for (int i = 0; i < 4; i++) { 300 double dx = xs[i] - originX; 301 double dy = ys[i] - originY; 302 double u = c * dx + s * dy; 303 double v = -s * dx + c * dy; 304 uMin = Math.min(uMin, u); 305 uMax = Math.max(uMax, u); 306 vMin = Math.min(vMin, v); 307 vMax = Math.max(vMax, v); 308 } 309 long kuMin = (long) Math.ceil(uMin / spacingX); 310 long kuMax = (long) Math.floor(uMax / spacingX); 311 long kvMin = (long) Math.ceil(vMin / spacingY); 312 long kvMax = (long) Math.floor(vMax / spacingY); 313 if (kuMax - kuMin > MAX_LINES || kvMax - kvMin > MAX_LINES) { 314 return lines; 315 } 316 // lines of constant u run along the v axis 317 for (long k = kuMin; k <= kuMax; k++) { 318 double u = k * spacingX; 319 lines.add(new ProjectedGridLine(gridToEastNorth(u, vMin, c, s, originX, originY), 320 gridToEastNorth(u, vMax, c, s, originX, originY), true)); 321 } 322 for (long k = kvMin; k <= kvMax; k++) { 323 double v = k * spacingY; 324 lines.add(new ProjectedGridLine(gridToEastNorth(uMin, v, c, s, originX, originY), 325 gridToEastNorth(uMax, v, c, s, originX, originY), false)); 326 } 327 return lines; 328 } 329 330 private static EastNorth gridToEastNorth(double u, double v, double c, double s, double originX, double originY) { 331 return new EastNorth(originX + c * u - s * v, originY + s * u + c * v); 332 } 333 334 /** 335 * Computes the lines of a latitude/longitude grid which cross the given area. Since these lines are curves in 336 * most projections, each line is returned as a polyline. 337 * @param area the area to cover 338 * @param world the bounds of the world in the current projection, the lines are clamped to it 339 * @param spacingLon distance between the meridians, in degrees 340 * @param spacingLat distance between the parallels, in degrees 341 * @param originLon longitude of a meridian of the grid 342 * @param originLat latitude of a parallel of the grid 343 * @param segments number of segments of each polyline 344 * @return the lines; empty if the spacing is invalid or there would be too many lines 345 */ 346 public static List<LatLonGridLine> getLatLonGridLines(Bounds area, Bounds world, double spacingLon, double spacingLat, 347 double originLon, double originLat, int segments) { 348 List<LatLonGridLine> lines = new ArrayList<>(); 349 if (!(spacingLon > 0) || !(spacingLat > 0) || segments < 1) { 350 return lines; 351 } 352 double minLat = Math.max(area.getMinLat(), world.getMinLat()); 353 double maxLat = Math.min(area.getMaxLat(), world.getMaxLat()); 354 double minLon = area.getMinLon(); 355 double maxLon = area.getMaxLon(); 356 if (maxLon < minLon) { 357 maxLon += 360; // the view crosses the antimeridian 358 } 359 if (minLat >= maxLat || minLon >= maxLon) { 360 return lines; 361 } 362 long kLonMin = (long) Math.ceil((minLon - originLon) / spacingLon); 363 long kLonMax = (long) Math.floor((maxLon - originLon) / spacingLon); 364 long kLatMin = (long) Math.ceil((minLat - originLat) / spacingLat); 365 long kLatMax = (long) Math.floor((maxLat - originLat) / spacingLat); 366 if (kLonMax - kLonMin > MAX_LINES || kLatMax - kLatMin > MAX_LINES) { 367 return lines; 368 } 369 // for a projection which does not span the whole globe, meridians outside its longitude range are 370 // skipped like the parallels below; a full range must not be checked since longitudes wrap around 371 boolean limitedLon = world.getMinLon() > -180 || world.getMaxLon() < 180; 372 for (long k = kLonMin; k <= kLonMax; k++) { 373 double lon = LatLon.toIntervalLon(originLon + k * spacingLon); 374 if (limitedLon && (lon < world.getMinLon() || lon > world.getMaxLon())) { 375 continue; 376 } 377 List<LatLon> line = new ArrayList<>(segments + 1); 378 for (int i = 0; i <= segments; i++) { 379 line.add(new LatLon(minLat + (maxLat - minLat) * i / segments, lon)); 380 } 381 lines.add(new LatLonGridLine(line, true)); 382 } 383 for (long k = kLatMin; k <= kLatMax; k++) { 384 double lat = originLat + k * spacingLat; 385 if (lat < world.getMinLat() || lat > world.getMaxLat()) { 386 continue; 387 } 388 List<LatLon> line = new ArrayList<>(segments + 1); 389 for (int i = 0; i <= segments; i++) { 390 line.add(new LatLon(lat, LatLon.toIntervalLon(minLon + (maxLon - minLon) * i / segments))); 391 } 392 lines.add(new LatLonGridLine(line, false)); 393 } 394 return lines; 395 } 396 397 @Override 398 public void preferenceChanged(PreferenceChangeEvent e) { 399 if (e.getKey().startsWith(PREFIX) || e.getKey().equals(COLOR_EAST.getKey()) || e.getKey().equals(COLOR_NORTH.getKey())) { 400 invalidate(); 401 if (mapView != null) { 402 mapView.repaint(); 403 } 404 } 405 } 406 407 @Override 408 public void destroy() { 409 Config.getPref().removePreferenceChangeListener(this); 410 mapView = null; 411 } 412 } -
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..dc99e73d39
- + 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 private static List<List<LatLon>> points(List<LatLonGridLine> lines) { 170 return lines.stream().map(l -> l.points).collect(Collectors.toList()); 171 } 172 173 /** 174 * In Mercator one metre is 1/cos(latitude) projection units. 175 */ 176 @Test 177 void testProjectionUnitsPerMetre() { 178 org.openstreetmap.josm.data.projection.Projection proj = ProjectionRegistry.getProjection(); 179 assertEquals(1, MapGridPaintable.projectionUnitsPerMetre(proj, proj.latlon2eastNorth(new LatLon(0, 10))), 1e-3); 180 assertEquals(1 / Math.cos(Math.toRadians(50)), 181 MapGridPaintable.projectionUnitsPerMetre(proj, proj.latlon2eastNorth(new LatLon(50, 10))), 1e-3); 182 assertEquals(1 / Math.cos(Math.toRadians(20.6)), 183 MapGridPaintable.projectionUnitsPerMetre(proj, proj.latlon2eastNorth(new LatLon(20.6, 87.8))), 1e-3); 184 // outside the world: no scaling 185 assertEquals(1, MapGridPaintable.projectionUnitsPerMetre(proj, new EastNorth(0, 1e12)), 1e-9); 186 } 187 188 /** 189 * The thinning factor is a power of ten bringing the spacing above the minimum. 190 */ 191 @Test 192 void testThinningFactor() { 193 MapGridPaintable.MIN_PIXEL_SPACING.put(25.0); 194 try { 195 assertEquals(1, MapGridPaintable.thinningFactor(30)); 196 assertEquals(1, MapGridPaintable.thinningFactor(25)); 197 assertEquals(10, MapGridPaintable.thinningFactor(24.9)); 198 assertEquals(100, MapGridPaintable.thinningFactor(0.3)); 199 assertEquals(1, MapGridPaintable.thinningFactor(0)); 200 assertEquals(1, MapGridPaintable.thinningFactor(Double.NaN)); 201 } finally { 202 MapGridPaintable.MIN_PIXEL_SPACING.remove(); 203 } 204 } 205 206 /** 207 * Painting on a map view draws something when enabled and nothing when disabled, for both grid types and 208 * with a blend mode. 209 */ 210 @Test 211 void testPaint() { 212 SizedMapView mv = new SizedMapView(); 213 mv.setBounds(new Rectangle(400, 300)); 214 GuiHelper.runInEDTAndWait(() -> { /* let the component listener update the view state */ }); 215 mv.updateState(); 216 mv.zoomTo(new LatLon(50, 10)); 217 assertTrue(mv.getRealBounds().getMaxLat() > mv.getRealBounds().getMinLat(), "map view has no size: " + mv.getRealBounds()); 218 MapGridPaintable grid = new MapGridPaintable(); 219 try { 220 MapGridPaintable.ENABLED.put(false); 221 assertEquals(0, paintedPixels(grid, mv), "grid drawn although disabled"); 222 MapGridPaintable.ENABLED.put(true); 223 MapGridPaintable.TYPE.put(GridType.LATLON); 224 MapGridPaintable.SPACING_X.put(0.001); 225 MapGridPaintable.SPACING_Y.put(0.001); 226 assertTrue(paintedPixels(grid, mv) > 100, "lat/lon grid not drawn"); 227 MapGridPaintable.TYPE.put(GridType.PROJECTED); 228 MapGridPaintable.SPACING_X.put(100.0); 229 MapGridPaintable.SPACING_Y.put(100.0); 230 MapGridPaintable.ROTATION.put(30.0); 231 MapGridPaintable.BLEND_MODE.put(BlendComposite.Mode.MULTIPLY); 232 assertTrue(paintedPixels(grid, mv) > 100, "projected grid not drawn"); 233 } finally { 234 grid.destroy(); 235 mv.destroy(); 236 for (String key : new String[] {"enabled", "type", "spacing-x", "spacing-y", "rotation", "blend-mode"}) { 237 org.openstreetmap.josm.spi.preferences.Config.getPref().put("draw.grid." + key, null); 238 } 239 } 240 } 241 242 /** A map view which believes it is shown on screen, so that its view state gets a size */ 243 private static final class SizedMapView extends MapView { 244 SizedMapView() { 245 super(MainApplication.getLayerManager(), null); 246 } 247 248 @Override 249 public Point getLocationOnScreen() { 250 return new Point(0, 0); 251 } 252 253 @Override 254 protected boolean isVisibleOnScreen() { 255 return true; 256 } 257 258 void updateState() { 259 updateLocationState(); 260 } 261 } 262 263 private static int paintedPixels(MapGridPaintable grid, MapView mv) { 264 BufferedImage img = new BufferedImage(mv.getWidth(), mv.getHeight(), BufferedImage.TYPE_3BYTE_BGR); 265 java.awt.Graphics2D g = img.createGraphics(); 266 g.setColor(Color.WHITE); 267 g.fillRect(0, 0, img.getWidth(), img.getHeight()); 268 grid.paint(g, mv, mv.getRealBounds()); 269 g.dispose(); 270 int count = 0; 271 for (int y = 0; y < img.getHeight(); y++) { 272 for (int x = 0; x < img.getWidth(); x++) { 273 if ((img.getRGB(x, y) & 0xffffff) != 0xffffff) { 274 count++; 275 } 276 } 277 } 278 return count; 279 } 280 } -
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 }
