Ticket #11487: 11487.3.patch
File 11487.3.patch, 35.9 KB (added by , 2 years ago) |
---|
-
new file src/org/openstreetmap/josm/data/osm/visitor/paint/ImageCache.java
Subject: [PATCH] #11487: Try to improve render performance --- IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 diff --git a/src/org/openstreetmap/josm/data/osm/visitor/paint/ImageCache.java b/src/org/openstreetmap/josm/data/osm/visitor/paint/ImageCache.java new file mode 100644
- + 1 // License: GPL. For details, see LICENSE file. 2 package org.openstreetmap.josm.data.osm.visitor.paint; 3 4 import java.awt.Image; 5 6 import javax.annotation.Nullable; 7 8 /** 9 * A record for keeping the image information for a tile. Used in conjunction with {@link TileZXY} for 10 * {@link org.openstreetmap.josm.data.cache.JCSCacheManager}. 11 * @since xxx 12 */ 13 public final class ImageCache { 14 private final boolean isDirty; 15 private final StyledTiledMapRenderer.TileLoader imageFuture; 16 private final Image image; 17 /** 18 * Create a new {@link ImageCache} object 19 * @param image The image to paint (optional; either this or {@link #imageFuture} must be specified) 20 * @param imageFuture The future for the image (optional; either this or {@link #image} must be specified) 21 * @param isDirty {@code true} if the tile needs to be repainted 22 */ 23 public ImageCache(Image image, StyledTiledMapRenderer.TileLoader imageFuture, boolean isDirty) { 24 this.image = image; 25 this.imageFuture = imageFuture; 26 this.isDirty = isDirty; 27 if (image == null && imageFuture == null) { 28 throw new IllegalArgumentException("Either image or imageFuture must be non-null"); 29 } 30 } 31 32 /** 33 * Check if this tile is dirty 34 * @return {@code true} if this is a dirty tile 35 */ 36 public boolean isDirty() { 37 return this.isDirty; 38 } 39 40 /** 41 * Mark this tile as dirty 42 * @return The tile to put in the cache 43 */ 44 public ImageCache becomeDirty() { 45 if (this.isDirty) { 46 return this; 47 } 48 return new ImageCache(this.image, this.imageFuture, true); 49 } 50 51 /** 52 * Get the image to paint 53 * @return The image (may be {@code null}) 54 */ 55 @Nullable 56 public Image image() { 57 return this.image; 58 } 59 60 /** 61 * Get the image future 62 * @return The image future (may be {@code null}) 63 */ 64 @Nullable 65 public StyledTiledMapRenderer.TileLoader imageFuture() { 66 return this.imageFuture; 67 } 68 } -
src/org/openstreetmap/josm/data/osm/visitor/paint/MapRendererFactory.java
IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 diff --git a/src/org/openstreetmap/josm/data/osm/visitor/paint/MapRendererFactory.java b/src/org/openstreetmap/josm/data/osm/visitor/paint/MapRendererFactory.java
a b 191 191 tr("Styled Map Renderer"), 192 192 tr("Renders the map using style rules in a set of style sheets.") 193 193 ); 194 register( 195 StyledTiledMapRenderer.class, 196 tr("Styled Map Renderer (tiled)"), 197 tr("Renders the map using style rules in a set of style sheets by tile.") 198 ); 194 199 } 195 200 196 201 /** -
new file src/org/openstreetmap/josm/data/osm/visitor/paint/StyledTiledMapRenderer.java
IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 diff --git a/src/org/openstreetmap/josm/data/osm/visitor/paint/StyledTiledMapRenderer.java b/src/org/openstreetmap/josm/data/osm/visitor/paint/StyledTiledMapRenderer.java new file mode 100644
- + 1 // License: GPL. For details, see LICENSE file. 2 package org.openstreetmap.josm.data.osm.visitor.paint; 3 4 import static org.openstreetmap.josm.tools.I18n.tr; 5 6 import java.awt.AlphaComposite; 7 import java.awt.Color; 8 import java.awt.Font; 9 import java.awt.Graphics2D; 10 import java.awt.Image; 11 import java.awt.Point; 12 import java.awt.Transparency; 13 import java.awt.event.MouseEvent; 14 import java.awt.geom.AffineTransform; 15 import java.awt.image.BufferedImage; 16 import java.util.Comparator; 17 import java.util.List; 18 import java.util.Set; 19 import java.util.function.Consumer; 20 import java.util.stream.Collectors; 21 22 import org.apache.commons.jcs3.access.CacheAccess; 23 import org.openstreetmap.josm.data.Bounds; 24 import org.openstreetmap.josm.data.coor.LatLon; 25 import org.openstreetmap.josm.data.osm.OsmData; 26 import org.openstreetmap.josm.data.projection.ProjectionRegistry; 27 import org.openstreetmap.josm.gui.MainApplication; 28 import org.openstreetmap.josm.gui.MapView; 29 import org.openstreetmap.josm.gui.NavigatableComponent; 30 import org.openstreetmap.josm.spi.preferences.Config; 31 import org.openstreetmap.josm.tools.Logging; 32 33 /** 34 * A styled render that does the rendering on a tile basis 35 */ 36 public class StyledTiledMapRenderer extends StyledMapRenderer { 37 private CacheAccess<TileZXY, ImageCache> cache; 38 private int zoom; 39 private Consumer<TileZXY> notifier; 40 41 /** 42 * Constructs a new {@code StyledMapRenderer}. 43 * 44 * @param g the graphics context. Must not be null. 45 * @param nc the map viewport. Must not be null. 46 * @param isInactiveMode if true, the paint visitor shall render OSM objects such that they 47 * look inactive. Example: rendering of data in an inactive layer using light gray as color only. 48 * @throws IllegalArgumentException if {@code g} is null 49 * @throws IllegalArgumentException if {@code nc} is null 50 */ 51 public StyledTiledMapRenderer(Graphics2D g, NavigatableComponent nc, boolean isInactiveMode) { 52 super(g, nc, isInactiveMode); 53 } 54 55 @Override 56 public void render(OsmData<?, ?, ?, ?> data, boolean renderVirtualNodes, Bounds bounds) { 57 // If there is no cache, fall back to old behavior 58 if (this.cache == null) { 59 super.render(data, renderVirtualNodes, bounds); 60 return; 61 } 62 final BufferedImage tempImage; 63 final Graphics2D tempG2d; 64 // I'd like to avoid two image copies, but there are some issues using the original g2d object 65 tempImage = nc.getGraphicsConfiguration().createCompatibleImage(this.nc.getWidth(), this.nc.getHeight(), Transparency.TRANSLUCENT); 66 tempG2d = tempImage.createGraphics(); 67 tempG2d.setComposite(AlphaComposite.DstAtop); // Avoid tile lines in large areas 68 69 final List<TileZXY> toRender = TileZXY.boundsToTiles(bounds.getMinLat(), bounds.getMinLon(), 70 bounds.getMaxLat(), bounds.getMaxLon(), zoom).collect(Collectors.toList()); 71 final Bounds box = new Bounds(bounds); 72 toRender.stream().map(TileZXY::tileToBounds).forEach(box::extend); 73 final int tileSize; 74 if (toRender.isEmpty()) { 75 tileSize = Config.getPref().getInt("mappaint.fast_render.tile_size", 256); // Mostly to keep the compiler happy 76 } else { 77 final TileZXY tile = toRender.get(0); 78 final Bounds box2 = TileZXY.tileToBounds(tile); 79 final Point min = this.nc.getPoint(box2.getMin()); 80 final Point max = this.nc.getPoint(box2.getMax()); 81 tileSize = max.x - min.x + 2; // Buffer by 2 pixels to avoid black lines 82 } 83 84 // Sort the tiles based off of proximity to the mouse pointer 85 if (nc instanceof MapView) { // Ideally this would either be an interface or a method in NavigableComponent 86 final MapView mv = (MapView) nc; 87 final MouseEvent mouseEvent = mv.lastMEvent; 88 final LatLon mousePosition = nc.getLatLon(mouseEvent.getX(), mouseEvent.getY()); 89 final TileZXY mouseTile = TileZXY.latLonToTile(mousePosition.lat(), mousePosition.lon(), zoom); 90 toRender.sort(Comparator.comparingInt(tile -> { 91 final int x = tile.x() - mouseTile.x(); 92 final int y = tile.y() - mouseTile.y(); 93 return x * x + y * y; 94 })); 95 } 96 97 // We want to prioritize where the mouse is, but having some in the queue will reduce overall paint time 98 int submittedTile = 5; 99 int painted = 0; 100 for (TileZXY tile : toRender) { 101 final Image tileImage; 102 // Needed to avoid having tiles that aren't rendered properly 103 final ImageCache tImg = this.cache.get(tile); 104 final boolean wasDirty = tImg != null && tImg.isDirty(); 105 if (tImg != null && !tImg.isDirty() && tImg.imageFuture() != null) { 106 submittedTile = 0; // Don't submit new tiles if there are futures already in the queue. Not perfect. 107 } 108 if (submittedTile > 0 && (tImg == null || tImg.isDirty())) { 109 // Ensure that we don't add a large number of render calls 110 if (tImg != null && tImg.imageFuture() != null) { 111 tImg.imageFuture().cancel(); 112 } 113 submittedTile--; 114 // Note that the paint code is *not* thread safe, so all tiles must be painted on the same thread. 115 // FIXME figure out how to make this thread safe? Probably not necessary, since UI isn't blocked, but it would be a nice to have 116 TileLoader loader = new TileLoader(data, tile, tileSize); 117 MainApplication.worker.submit(loader); 118 if (tImg == null) { 119 this.cache.put(tile, new ImageCache(null, loader, false)); 120 } else { 121 // This might cause some extra renders, but *probably* ok 122 this.cache.put(tile, new ImageCache(tImg.image(), loader, true)); 123 } 124 tileImage = tImg != null ? tImg.image() : null; 125 } else if (tImg != null) { 126 tileImage = tImg.image(); 127 } else { 128 tileImage = null; 129 } 130 final Point point = this.nc.getPoint(tile); 131 if (tileImage != null) { 132 // FIXME move to isTraceEnabled prior to commit 133 if ((wasDirty && Logging.isTraceEnabled()) || this.isInactiveMode) { 134 tempG2d.setColor(Color.DARK_GRAY); 135 tempG2d.fillRect(point.x, point.y, tileSize, tileSize); 136 } else { 137 painted++; 138 } 139 tempG2d.drawImage(tileImage, point.x, point.y, tileSize, tileSize, null, null); 140 } else { 141 Logging.trace("StyledMapRenderer did not paint tile {1}", tile); 142 } 143 } 144 final double percentDrawn = 100 * painted / (double) toRender.size(); 145 if (percentDrawn < 99.99) { 146 final int x = 0; 147 final int y = nc.getHeight() / 8; 148 final String message = tr("Rendering Status: {0}%", Math.floor(percentDrawn)); 149 tempG2d.setComposite(AlphaComposite.SrcOver); 150 tempG2d.setFont(new Font("sansserif", Font.BOLD, 13)); 151 tempG2d.setColor(Color.BLACK); 152 tempG2d.drawString(message, x + 1, y); 153 tempG2d.setColor(Color.LIGHT_GRAY); 154 tempG2d.drawString(message, x, y); 155 } 156 tempG2d.dispose(); 157 g.drawImage(tempImage, 0, 0, null); 158 } 159 160 /** 161 * Set the cache for this painter. If not set, this acts like {@link StyledMapRenderer}. 162 * @param box The box we will be rendering -- any jobs for tiles outside of this box will be cancelled 163 * @param cache The cache to use 164 * @param zoom The zoom level to use for creating the tiles 165 * @param notifier The method to call when a tile has been updated. This may or may not be called in the EDT. 166 */ 167 public void setCache(Bounds box, CacheAccess<TileZXY, ImageCache> cache, int zoom, Consumer<TileZXY> notifier) { 168 this.cache = cache; 169 this.zoom = zoom; 170 this.notifier = notifier != null ? notifier : tile -> { /* Do nothing */ }; 171 172 Set<TileZXY> tiles = TileZXY.boundsToTiles(box.getMinLat(), box.getMinLon(), box.getMaxLat(), box.getMaxLon(), zoom) 173 .collect(Collectors.toSet()); 174 cache.getMatching(".*").forEach((key, value) -> { 175 if (!tiles.contains(key)) { 176 cancelImageFuture(cache, key, value); 177 } 178 }); 179 } 180 181 private static void cancelImageFuture(CacheAccess<TileZXY, ImageCache> cache, TileZXY key, ImageCache value) { 182 if (value.imageFuture() != null) { 183 value.imageFuture().cancel(); 184 if (value.image() == null) { 185 cache.remove(key); 186 } else { 187 cache.put(key, new ImageCache(value.image(), null, value.isDirty())); 188 } 189 } 190 } 191 192 private BufferedImage generateTile(OsmData<?, ?, ?, ?> data, TileZXY tile, int tileSize) { 193 BufferedImage bufferedImage = nc.getGraphicsConfiguration().createCompatibleImage(tileSize, tileSize, Transparency.TRANSLUCENT); 194 Graphics2D g2d = bufferedImage.createGraphics(); 195 try { 196 // Render to the surrounding tiles for continuity -- this probably needs to be tweaked 197 final int buffer = 2; 198 Bounds bounds = TileZXY.tileToBounds(new TileZXY(zoom, tile.x() - buffer, tile.y() - buffer)); 199 bounds.extend(TileZXY.tileToBounds(new TileZXY(zoom, tile.x() + buffer, tile.y() + buffer))); 200 201 final int renderSize = tileSize * (2 * buffer + 1); 202 final NavigatableComponent temporaryView = new NavigatableComponent() { 203 @Override 204 public int getWidth() { 205 return renderSize; 206 } 207 208 @Override 209 public int getHeight() { 210 return renderSize; 211 } 212 }; 213 temporaryView.setSize(renderSize, renderSize); 214 temporaryView.zoomTo(TileZXY.tileToBounds(tile).getCenter().getEastNorth(ProjectionRegistry.getProjection()), mapState.getScale()); 215 g2d.setTransform(AffineTransform.getTranslateInstance(-buffer * (double) tileSize, -buffer * (double) tileSize)); 216 final AbstractMapRenderer tilePainter = MapRendererFactory.getInstance().createActiveRenderer(g2d, temporaryView, false); 217 tilePainter.render(data, true, bounds); 218 } finally { 219 g2d.dispose(); 220 } 221 return bufferedImage; 222 } 223 224 class TileLoader implements Runnable { 225 private final TileZXY tile; 226 private final int tileSize; 227 private final OsmData<?, ?, ?, ?> data; 228 private boolean cancel; 229 230 TileLoader(OsmData<?, ?, ?, ?> data, TileZXY tile, int tileSize) { 231 this.data = data; 232 this.tile = tile; 233 this.tileSize = tileSize; 234 } 235 236 @Override 237 public void run() { 238 if (!cancel) { 239 final BufferedImage tImage = generateTile(data, tile, tileSize); 240 cache.put(tile, new ImageCache(tImage, null, false)); 241 notifier.accept(tile); 242 } 243 } 244 245 /** 246 * Cancel this job without causing a {@link java.util.concurrent.CancellationException} 247 */ 248 void cancel() { 249 this.cancel = true; 250 } 251 } 252 } -
new file src/org/openstreetmap/josm/data/osm/visitor/paint/TileZXY.java
IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 diff --git a/src/org/openstreetmap/josm/data/osm/visitor/paint/TileZXY.java b/src/org/openstreetmap/josm/data/osm/visitor/paint/TileZXY.java new file mode 100644
- + 1 // License: GPL. For details, see LICENSE file. 2 package org.openstreetmap.josm.data.osm.visitor.paint; 3 4 import java.util.stream.IntStream; 5 import java.util.stream.Stream; 6 7 import org.openstreetmap.josm.data.Bounds; 8 import org.openstreetmap.josm.data.coor.ILatLon; 9 10 /** 11 * A record used for storing tile information for painting 12 * @since xxx 13 */ 14 public final class TileZXY implements ILatLon { 15 private final int zoom; 16 private final int x; 17 private final int y; 18 19 /** 20 * Create a new {@link TileZXY} object 21 * @param zoom The zoom for which this tile was created 22 * @param x The x coordinate at the specified zoom level 23 * @param y The y coordinate at the specified zoom level 24 */ 25 public TileZXY(int zoom, int x, int y) { 26 this.zoom = zoom; 27 this.x = x; 28 this.y = y; 29 } 30 31 /** 32 * Get the zoom level 33 * @return The zoom level for which this tile was created 34 */ 35 public int zoom() { 36 return this.zoom; 37 } 38 39 /** 40 * Get the x coordinate 41 * @return The x coordinate for this tile 42 */ 43 public int x() { 44 return this.x; 45 } 46 47 /** 48 * Get the y coordinate 49 * @return The y coordinate for this tile 50 */ 51 public int y() { 52 return this.y; 53 } 54 55 /** 56 * Get the latitude for upper-left corner of this tile 57 * @return The latitude 58 */ 59 @Override 60 public double lat() { 61 return yToLat(this.y(), this.zoom()); 62 } 63 64 /** 65 * Get the longitude for the upper-left corner of this tile 66 * @return The longitude 67 */ 68 @Override 69 public double lon() { 70 return xToLon(this.x(), this.zoom()); 71 } 72 73 /** 74 * Convert a bounds to a series of tiles that entirely cover the bounds 75 * @param minLat The minimum latitude 76 * @param minLon The minimum longitude 77 * @param maxLat The maximum latitude 78 * @param maxLon The maximum longitude 79 * @param zoom The zoom level to generate the tiles for 80 * @return The stream of tiles 81 */ 82 public static Stream<TileZXY> boundsToTiles(double minLat, double minLon, double maxLat, double maxLon, int zoom) { 83 return boundsToTiles(minLat, minLon, maxLat, maxLon, zoom, 0); 84 } 85 86 /** 87 * Convert a bounds to a series of tiles that entirely cover the bounds 88 * @param minLat The minimum latitude 89 * @param minLon The minimum longitude 90 * @param maxLat The maximum latitude 91 * @param maxLon The maximum longitude 92 * @param zoom The zoom level to generate the tiles for 93 * @param expansion The number of tiles to expand on the x/y axis (1 row north, 1 row south, 1 column left, 1 column right) 94 * @return The stream of tiles 95 */ 96 public static Stream<TileZXY> boundsToTiles(double minLat, double minLon, double maxLat, double maxLon, int zoom, int expansion) { 97 final TileZXY upperRight = latLonToTile(maxLat, maxLon, zoom); 98 final TileZXY lowerLeft = latLonToTile(minLat, minLon, zoom); 99 return IntStream.rangeClosed(lowerLeft.x() - expansion, upperRight.x() + expansion) 100 .mapToObj(x -> IntStream.rangeClosed(upperRight.y() - expansion, lowerLeft.y() + expansion) 101 .mapToObj(y -> new TileZXY(zoom, x, y))) 102 .flatMap(stream -> stream); 103 } 104 105 /** 106 * Convert a tile to the bounds for that tile 107 * @param tile The tile to get the bounds for 108 * @return The bounds 109 */ 110 public static Bounds tileToBounds(TileZXY tile) { 111 return new Bounds(yToLat(tile.y() + 1, tile.zoom()), xToLon(tile.x(), tile.zoom()), 112 yToLat(tile.y(), tile.zoom()), xToLon(tile.x() + 1, tile.zoom())); 113 } 114 115 /** 116 * Convert a x tile coordinate to a latitude 117 * @param x The x coordinate 118 * @param zoom The zoom level to use for the calculation 119 * @return The latitude for the x coordinate (upper-left of the tile) 120 */ 121 public static double xToLon(int x, int zoom) { 122 return (x / Math.pow(2, zoom)) * 360 - 180; 123 } 124 125 /** 126 * Convert a y tile coordinate to a latitude 127 * @param y The y coordinate 128 * @param zoom The zoom level to use for the calculation 129 * @return The latitude for the y coordinate (upper-left of the tile) 130 */ 131 public static double yToLat(int y, int zoom) { 132 double t = Math.PI - (2 * Math.PI * y) / Math.pow(2, zoom); 133 return 180 / Math.PI * Math.atan((Math.exp(t) - Math.exp(-t)) / 2); 134 } 135 136 /** 137 * Convert a lat, lon, and zoom to a tile coordiante 138 * @param lat The latitude 139 * @param lon The longitude 140 * @param zoom The zoom level 141 * @return The specified tile coordinates at the specified zoom 142 */ 143 public static TileZXY latLonToTile(double lat, double lon, int zoom) { 144 int xCoord = (int) Math.floor(Math.pow(2, zoom) * (180 + lon) / 360); 145 int yCoord = (int) Math.floor(Math.pow(2, zoom) * 146 (1 - Math.log(Math.tan(Math.toRadians(lat)) + 1 / Math.cos(Math.toRadians(lat))) / Math.PI) / 2); 147 return new TileZXY(zoom, xCoord, yCoord); 148 } 149 150 @Override 151 public String toString() { 152 return "TileZXY{" + zoom + "/" + x + "/" + y + "}"; 153 } 154 155 @Override 156 public int hashCode() { 157 // We only care about comparing zoom, x, and y 158 return Integer.hashCode(this.zoom) + 31 * (Integer.hashCode(this.x) + 31 * Integer.hashCode(this.y)); 159 } 160 161 @Override 162 public boolean equals(Object obj) { 163 if (obj instanceof TileZXY) { 164 TileZXY o = (TileZXY) obj; 165 return this.zoom == o.zoom && this.x == o.x && this.y == o.y; 166 } 167 return false; 168 } 169 } -
src/org/openstreetmap/josm/gui/layer/OsmDataLayer.java
IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 diff --git a/src/org/openstreetmap/josm/gui/layer/OsmDataLayer.java b/src/org/openstreetmap/josm/gui/layer/OsmDataLayer.java
a b 49 49 import javax.swing.JPanel; 50 50 import javax.swing.JScrollPane; 51 51 52 import org.apache.commons.jcs3.access.CacheAccess; 53 import org.openstreetmap.gui.jmapviewer.OsmMercator; 52 54 import org.openstreetmap.josm.actions.AutoScaleAction; 53 55 import org.openstreetmap.josm.actions.ExpertToggleAction; 54 56 import org.openstreetmap.josm.actions.RenameLayerAction; … … 58 60 import org.openstreetmap.josm.data.Data; 59 61 import org.openstreetmap.josm.data.ProjectionBounds; 60 62 import org.openstreetmap.josm.data.UndoRedoHandler; 63 import org.openstreetmap.josm.data.cache.JCSCacheManager; 61 64 import org.openstreetmap.josm.data.conflict.Conflict; 62 65 import org.openstreetmap.josm.data.conflict.ConflictCollection; 63 66 import org.openstreetmap.josm.data.coor.EastNorth; … … 70 73 import org.openstreetmap.josm.data.gpx.GpxTrackSegment; 71 74 import org.openstreetmap.josm.data.gpx.IGpxTrackSegment; 72 75 import org.openstreetmap.josm.data.gpx.WayPoint; 76 import org.openstreetmap.josm.data.osm.BBox; 73 77 import org.openstreetmap.josm.data.osm.DataIntegrityProblemException; 74 78 import org.openstreetmap.josm.data.osm.DataSelectionListener; 75 79 import org.openstreetmap.josm.data.osm.DataSet; … … 77 81 import org.openstreetmap.josm.data.osm.DatasetConsistencyTest; 78 82 import org.openstreetmap.josm.data.osm.DownloadPolicy; 79 83 import org.openstreetmap.josm.data.osm.HighlightUpdateListener; 84 import org.openstreetmap.josm.data.osm.INode; 80 85 import org.openstreetmap.josm.data.osm.IPrimitive; 86 import org.openstreetmap.josm.data.osm.IWay; 81 87 import org.openstreetmap.josm.data.osm.Node; 82 88 import org.openstreetmap.josm.data.osm.OsmPrimitive; 83 89 import org.openstreetmap.josm.data.osm.OsmPrimitiveComparator; … … 91 97 import org.openstreetmap.josm.data.osm.visitor.BoundingXYVisitor; 92 98 import org.openstreetmap.josm.data.osm.visitor.OsmPrimitiveVisitor; 93 99 import org.openstreetmap.josm.data.osm.visitor.paint.AbstractMapRenderer; 100 import org.openstreetmap.josm.data.osm.visitor.paint.ImageCache; 94 101 import org.openstreetmap.josm.data.osm.visitor.paint.MapRendererFactory; 102 import org.openstreetmap.josm.data.osm.visitor.paint.StyledTiledMapRenderer; 103 import org.openstreetmap.josm.data.osm.visitor.paint.TileZXY; 95 104 import org.openstreetmap.josm.data.osm.visitor.paint.relations.MultipolygonCache; 96 105 import org.openstreetmap.josm.data.preferences.BooleanProperty; 97 106 import org.openstreetmap.josm.data.preferences.IntegerProperty; … … 104 113 import org.openstreetmap.josm.gui.MapFrame; 105 114 import org.openstreetmap.josm.gui.MapView; 106 115 import org.openstreetmap.josm.gui.MapViewState.MapViewPoint; 116 import org.openstreetmap.josm.gui.NavigatableComponent; 117 import org.openstreetmap.josm.gui.PrimitiveHoverListener; 107 118 import org.openstreetmap.josm.gui.datatransfer.ClipboardUtils; 108 119 import org.openstreetmap.josm.gui.datatransfer.data.OsmLayerTransferData; 109 120 import org.openstreetmap.josm.gui.dialogs.LayerListDialog; … … 144 155 * @author imi 145 156 * @since 17 146 157 */ 147 public class OsmDataLayer extends AbstractOsmDataLayer implements Listener, DataSelectionListener, HighlightUpdateListener { 158 public class OsmDataLayer extends AbstractOsmDataLayer 159 implements Listener, DataSelectionListener, HighlightUpdateListener, PrimitiveHoverListener { 160 private static final int MAX_ZOOM = 30; 161 private static final int OVER_ZOOM = 2; 148 162 private static final int HATCHED_SIZE = 15; 149 163 // U+2205 EMPTY SET 150 164 private static final String IS_EMPTY_SYMBOL = "\u2205"; … … 155 169 private boolean requiresUploadToServer; 156 170 /** Flag used to know if the layer is being uploaded */ 157 171 private final AtomicBoolean isUploadInProgress = new AtomicBoolean(false); 172 /** 173 * A cache used for painting 174 */ 175 private final CacheAccess<TileZXY, ImageCache> cache = JCSCacheManager.getCache("osmDataLayer:" + System.identityHashCode(this)); 176 /** The map paint index that was painted (used to invalidate {@link #cache}) */ 177 private int lastDataIdx; 178 /** The last zoom level (we invalidate all tiles when switching layers) */ 179 private int lastZoom; 180 private boolean hoverListenerAdded; 158 181 159 182 /** 160 183 * List of validation errors in this layer. … … 497 520 * Draw nodes last to overlap the ways they belong to. 498 521 */ 499 522 @Override public void paint(final Graphics2D g, final MapView mv, Bounds box) { 523 if (!hoverListenerAdded) { 524 MainApplication.getMap().mapView.addPrimitiveHoverListener(this); 525 hoverListenerAdded = true; 526 } 500 527 boolean active = mv.getLayerManager().getActiveLayer() == this; 501 528 boolean inactive = !active && Config.getPref().getBoolean("draw.data.inactive_color", true); 502 529 boolean virtual = !inactive && mv.isVirtualNodesEnabled(); … … 537 564 } 538 565 } 539 566 567 // Used to invalidate cache 568 int zoom = getZoom(mv); 569 if (zoom != lastZoom) { 570 // We just mark the previous zoom as dirty before moving in. 571 // It means we don't have to traverse up/down z-levels marking tiles as dirty. 572 this.cache.getMatching("TileZXY\\{" + lastZoom + "/.*") 573 .forEach((tile, imageCache) -> this.cache.put(tile, imageCache.becomeDirty())); 574 lastZoom = zoom; 575 } 540 576 AbstractMapRenderer painter = MapRendererFactory.getInstance().createActiveRenderer(g, mv, inactive); 541 painter.enableSlowOperations(mv.getMapMover() == null || !mv.getMapMover().movementInProgress() 542 || !PROPERTY_HIDE_LABELS_WHILE_DRAGGING.get()); 543 painter.render(data, virtual, box); 577 if (!(painter instanceof StyledTiledMapRenderer) || zoom - OVER_ZOOM > Config.getPref().getInt("mappaint.fast_render.zlevel", 16)) { 578 painter.enableSlowOperations(mv.getMapMover() == null || !mv.getMapMover().movementInProgress() 579 || !PROPERTY_HIDE_LABELS_WHILE_DRAGGING.get()); 580 } else { 581 StyledTiledMapRenderer renderer = (StyledTiledMapRenderer) painter; 582 renderer.setCache(box, this.cache, zoom, (tile) -> { 583 /* This causes "bouncing". I'm not certain why. 584 if (oldState.equalsInWindow(mv.getState())) { (oldstate = mv.getState()) 585 final Point upperLeft = mv.getPoint(tile); 586 final Point lowerRight = mv.getPoint(new TileZXY(tile.zoom(), tile.x() + 1, tile.y() + 1)); 587 GuiHelper.runInEDT(() -> mv.repaint(0, upperLeft.x, upperLeft.y, lowerRight.x - upperLeft.x, lowerRight.y - upperLeft.y)); 588 } 589 */ 590 // Invalidate doesn't trigger an instant repaint, but putting this off lets us batch the repaints needed for multiple tiles 591 MainApplication.worker.submit(this::invalidate); 592 }); 593 594 if (this.data.getMappaintCacheIndex() != this.lastDataIdx) { 595 this.cache.clear(); 596 this.lastDataIdx = this.data.getMappaintCacheIndex(); 597 Logging.trace("OsmDataLayer {0} paint cache cleared", this.getName()); 598 } 599 } 600 painter.render(this.data, virtual, box); 544 601 MainApplication.getMap().conflictDialog.paintConflicts(g, mv); 545 602 } 546 603 … … 1147 1204 validationErrors.clear(); 1148 1205 removeClipboardDataFor(this); 1149 1206 recentRelations.clear(); 1207 if (hoverListenerAdded) { 1208 hoverListenerAdded = false; 1209 MainApplication.getMap().mapView.removePrimitiveHoverListener(this); 1210 } 1150 1211 } 1151 1212 1152 1213 protected static void removeClipboardDataFor(OsmDataLayer osm) { … … 1165 1226 1166 1227 @Override 1167 1228 public void processDatasetEvent(AbstractDatasetChangedEvent event) { 1229 resetTiles(event.getPrimitives()); 1168 1230 invalidate(); 1169 1231 setRequiresSaveToFile(true); 1170 1232 setRequiresUploadToServer(event.getDataset().requiresUploadToServer()); … … 1172 1234 1173 1235 @Override 1174 1236 public void selectionChanged(SelectionChangeEvent event) { 1237 Set<IPrimitive> primitives = new HashSet<>(event.getAdded()); 1238 primitives.addAll(event.getRemoved()); 1239 resetTiles(primitives); 1175 1240 invalidate(); 1176 1241 } 1177 1242 1243 private void resetTiles(Iterable<? extends IPrimitive> primitives) { 1244 for (IPrimitive primitive : primitives) { 1245 if (primitive == null || primitive.getDataSet() != this.getDataSet()) continue; 1246 final Collection<? extends IPrimitive> referrers = primitive.getReferrers(); 1247 // We can usually avoid invalidating a bunch of tiles when the way is not an area or part of a multipolygon 1248 if (primitive instanceof IWay<?> && !((IWay<?>) primitive).isClosed() && referrers.stream().noneMatch(IPrimitive::isMultipolygon)) { 1249 double lastLat = Double.NaN; 1250 double lastLon = Double.NaN; 1251 for (INode n : ((IWay<?>) primitive).getNodes()) { 1252 final double lat = n.lat(); 1253 final double lon = n.lon(); 1254 if (!Double.isNaN(lastLat)) { 1255 resetBounds(Math.min(lat, lastLat), Math.min(lon, lastLon), 1256 Math.max(lat, lastLat), Math.max(lon, lastLon)); 1257 } else { // Just in case there is a 1 node way (illegal data) 1258 resetBounds(lat, lon, lat, lon); 1259 } 1260 lastLat = lat; 1261 lastLon = lon; 1262 } 1263 } else { 1264 final BBox box = primitive.getBBox(); 1265 resetBounds(box.getMinLat(), box.getMinLon(), box.getMaxLat(), box.getMaxLon()); 1266 if (!referrers.isEmpty()) { 1267 resetTiles(referrers); 1268 } 1269 } 1270 } 1271 } 1272 1273 private void resetBounds(double minLat, double minLon, double maxLat, double maxLon) { 1274 // Get the current zoom. Hopefully we aren't painting with a different navigatable component 1275 final int currentZoom = lastZoom; 1276 TileZXY.boundsToTiles(minLat, minLon, maxLat, maxLon, currentZoom, 1).forEach(tile -> { 1277 final ImageCache imageCache = this.cache.get(tile); 1278 if (imageCache != null && !imageCache.isDirty()) { 1279 this.cache.put(tile, imageCache.becomeDirty()); 1280 } 1281 }); 1282 } 1283 1284 private static int getZoom(NavigatableComponent navigatableComponent) { 1285 final double scale = navigatableComponent.getScale(); 1286 // We might have to fall back to the old method if user is reprojecting 1287 // 256 is the "target" size, (TODO check HiDPI!) 1288 final int targetSize = Config.getPref().getInt("mappaint.fast_render.tile_size", 256); 1289 final double topResolution = 2 * Math.PI * OsmMercator.EARTH_RADIUS / targetSize; 1290 int zoom; 1291 for (zoom = 0; zoom < MAX_ZOOM; zoom++) { // Use something like imagery.{generic|tms}.max_zoom_lvl (20 is a bit too low for our needs) 1292 if (scale > topResolution / Math.pow(2, zoom)) { 1293 zoom = zoom > 0 ? zoom - 1 : zoom; 1294 break; 1295 } 1296 } 1297 // We paint at a few levels higher, note that the tiles are appropriately sized (if 256 is the "target" size, the tiles should be 1298 // 64px square). 1299 zoom += OVER_ZOOM; 1300 return zoom; 1301 } 1302 1178 1303 @Override 1179 1304 public void projectionChanged(Projection oldValue, Projection newValue) { 1180 1305 // No reprojection required. The dataset itself is registered as projection … … 1307 1432 invalidate(); 1308 1433 } 1309 1434 1435 @Override 1436 public void primitiveHovered(PrimitiveHoverEvent e) { 1437 resetTiles(Arrays.asList(e.getHoveredPrimitive(), e.getPreviousPrimitive())); 1438 this.invalidate(); 1439 } 1440 1310 1441 @Override 1311 1442 public void setName(String name) { 1312 1443 if (data != null) { -
src/org/openstreetmap/josm/gui/MapView.java
IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 diff --git a/src/org/openstreetmap/josm/gui/MapView.java b/src/org/openstreetmap/josm/gui/MapView.java
a b 9 9 import java.awt.Point; 10 10 import java.awt.Rectangle; 11 11 import java.awt.Shape; 12 import java.awt.Transparency; 12 13 import java.awt.event.ComponentAdapter; 13 14 import java.awt.event.ComponentEvent; 14 15 import java.awt.event.KeyEvent; … … 540 541 && nonChangedLayers.equals(visibleLayers.subList(0, nonChangedLayers.size())); 541 542 542 543 if (null == offscreenBuffer || offscreenBuffer.getWidth() != width || offscreenBuffer.getHeight() != height) { 543 offscreenBuffer = new BufferedImage(width, height, BufferedImage.TYPE_3BYTE_BGR);544 offscreenBuffer = this.getGraphicsConfiguration().createCompatibleImage(width, height, Transparency.OPAQUE); 544 545 } 545 546 546 547 if (!canUseBuffer || nonChangedLayersBuffer == null) { 547 548 if (null == nonChangedLayersBuffer 548 549 || nonChangedLayersBuffer.getWidth() != width || nonChangedLayersBuffer.getHeight() != height) { 549 nonChangedLayersBuffer = new BufferedImage(width, height, BufferedImage.TYPE_3BYTE_BGR);550 nonChangedLayersBuffer = this.getGraphicsConfiguration().createCompatibleImage(width, height, Transparency.OPAQUE); 550 551 } 551 552 Graphics2D g2 = nonChangedLayersBuffer.createGraphics(); 552 553 g2.setClip(scaledClip);