Ticket #11487: 11487.4.patch
File 11487.4.patch, 45.3 KB (added by , 10 months ago) |
---|
-
new file src/org/openstreetmap/josm/actions/TiledRenderToggleAction.java
Subject: [PATCH] #11487 --- IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 diff --git a/src/org/openstreetmap/josm/actions/TiledRenderToggleAction.java b/src/org/openstreetmap/josm/actions/TiledRenderToggleAction.java new file mode 100644
- + 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 8 import org.openstreetmap.josm.data.osm.visitor.paint.MapRendererFactory; 9 import org.openstreetmap.josm.data.osm.visitor.paint.StyledMapRenderer; 10 import org.openstreetmap.josm.data.osm.visitor.paint.StyledTiledMapRenderer; 11 import org.openstreetmap.josm.gui.MainApplication; 12 import org.openstreetmap.josm.gui.layer.OsmDataLayer; 13 14 /** 15 * This class enables and disables tiled rendering mode. 16 * This is intended to be short-term until the tiled rendering 17 * has not significant issues at high zoom levels. 18 * @since xxx 19 */ 20 public class TiledRenderToggleAction extends ToggleAction implements ExpertToggleAction.ExpertModeChangeListener { 21 /** 22 * Create a new action for toggling render methods 23 */ 24 public TiledRenderToggleAction() { 25 super(tr("Tiled Rendering"), 26 null, 27 tr("Enable/disable rendering the map in tiles"), 28 null, 29 false /* register toolbar */ 30 ); 31 setToolbarId("tiledRendering"); 32 MainApplication.getToolbar().register(this); 33 setSelected(false); // Always start disabled 34 ExpertToggleAction.addExpertModeChangeListener(this, true); 35 } 36 37 @Override 38 protected boolean listenToSelectionChange() { 39 return false; 40 } 41 42 @Override 43 public void expertChanged(boolean isExpert) { 44 this.updateEnabledState(); 45 } 46 47 @Override 48 protected void updateEnabledState() { 49 setEnabled(getLayerManager().getActiveData() != null && ExpertToggleAction.isExpert()); 50 } 51 52 @Override 53 public void actionPerformed(ActionEvent e) { 54 toggleSelectedState(e); 55 if (isSelected()) { 56 MapRendererFactory.getInstance().activate(StyledTiledMapRenderer.class); 57 } else { 58 MapRendererFactory.getInstance().activate(StyledMapRenderer.class); 59 } 60 61 notifySelectedState(); 62 getLayerManager().getLayersOfType(OsmDataLayer.class).forEach(OsmDataLayer::invalidate); 63 } 64 } -
new file src/org/openstreetmap/josm/data/osm/visitor/paint/ImageCache.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/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 jakarta.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.ArrayList; 17 import java.util.Collection; 18 import java.util.Comparator; 19 import java.util.IntSummaryStatistics; 20 import java.util.List; 21 import java.util.Objects; 22 import java.util.Optional; 23 import java.util.Set; 24 import java.util.concurrent.Executor; 25 import java.util.function.Consumer; 26 import java.util.stream.Collectors; 27 28 import org.apache.commons.jcs3.access.CacheAccess; 29 import org.openstreetmap.josm.data.Bounds; 30 import org.openstreetmap.josm.data.coor.LatLon; 31 import org.openstreetmap.josm.data.osm.OsmData; 32 import org.openstreetmap.josm.data.projection.ProjectionRegistry; 33 import org.openstreetmap.josm.gui.MainApplication; 34 import org.openstreetmap.josm.gui.MapView; 35 import org.openstreetmap.josm.gui.NavigatableComponent; 36 import org.openstreetmap.josm.spi.preferences.Config; 37 import org.openstreetmap.josm.tools.Logging; 38 39 /** 40 * A styled render that does the rendering on a tile basis. Note: this is currently experimental! 41 * It may be extracted to an interface at a later date. 42 * @since xxx 43 */ 44 public final class StyledTiledMapRenderer extends StyledMapRenderer { 45 // Render to the surrounding tiles for continuity -- this probably needs to be tweaked 46 private static final int BUFFER_TILES = 2; 47 // The number of extra pixels to render per tile (avoids black lines in render result) 48 private static final int BUFFER_PIXELS = 16; 49 private CacheAccess<TileZXY, ImageCache> cache; 50 private int zoom; 51 private Consumer<TileZXY> notifier; 52 53 /** 54 * Constructs a new {@code StyledMapRenderer}. 55 * 56 * @param g the graphics context. Must not be null. 57 * @param nc the map viewport. Must not be null. 58 * @param isInactiveMode if true, the paint visitor shall render OSM objects such that they 59 * look inactive. Example: rendering of data in an inactive layer using light gray as color only. 60 * @throws IllegalArgumentException if {@code g} is null 61 * @throws IllegalArgumentException if {@code nc} is null 62 */ 63 public StyledTiledMapRenderer(Graphics2D g, NavigatableComponent nc, boolean isInactiveMode) { 64 super(g, nc, isInactiveMode); 65 } 66 67 @Override 68 public void render(OsmData<?, ?, ?, ?> data, boolean renderVirtualNodes, Bounds bounds) { 69 // If there is no cache, fall back to old behavior 70 if (this.cache == null) { 71 super.render(data, renderVirtualNodes, bounds); 72 return; 73 } 74 final Executor worker = MainApplication.worker; 75 final BufferedImage tempImage; 76 final Graphics2D tempG2d; 77 // I'd like to avoid two image copies, but there are some issues using the original g2d object 78 tempImage = nc.getGraphicsConfiguration().createCompatibleImage(this.nc.getWidth(), this.nc.getHeight(), Transparency.TRANSLUCENT); 79 tempG2d = tempImage.createGraphics(); 80 tempG2d.setComposite(AlphaComposite.DstAtop); // Avoid tile lines in large areas 81 82 final List<TileZXY> toRender = TileZXY.boundsToTiles(bounds.getMinLat(), bounds.getMinLon(), 83 bounds.getMaxLat(), bounds.getMaxLon(), zoom).collect(Collectors.toList()); 84 final Bounds box = new Bounds(bounds); 85 toRender.stream().map(TileZXY::tileToBounds).forEach(box::extend); 86 final int tileSize; 87 if (toRender.isEmpty()) { 88 tileSize = Config.getPref().getInt("mappaint.fast_render.tile_size", 256); // Mostly to keep the compiler happy 89 } else { 90 final TileZXY tile = toRender.get(0); 91 final Bounds box2 = TileZXY.tileToBounds(tile); 92 final Point min = this.nc.getPoint(box2.getMin()); 93 final Point max = this.nc.getPoint(box2.getMax()); 94 tileSize = max.x - min.x + BUFFER_PIXELS; 95 } 96 97 // Sort the tiles based off of proximity to the mouse pointer 98 if (nc instanceof MapView) { // Ideally this would either be an interface or a method in NavigableComponent 99 final MapView mv = (MapView) nc; 100 final MouseEvent mouseEvent = mv.lastMEvent; 101 final LatLon mousePosition = nc.getLatLon(mouseEvent.getX(), mouseEvent.getY()); 102 final TileZXY mouseTile = TileZXY.latLonToTile(mousePosition.lat(), mousePosition.lon(), zoom); 103 toRender.sort(Comparator.comparingInt(tile -> { 104 final int x = tile.x() - mouseTile.x(); 105 final int y = tile.y() - mouseTile.y(); 106 return x * x + y * y; 107 })); 108 } 109 110 // We want to prioritize where the mouse is, but having some in the queue will reduce overall paint time 111 int submittedTile = 5; 112 int painted = 0; 113 for (TileZXY tile : toRender) { 114 final Image tileImage; 115 // Needed to avoid having tiles that aren't rendered properly 116 final ImageCache tImg = this.cache.get(tile); 117 final boolean wasDirty = tImg != null && tImg.isDirty(); 118 if (tImg != null && !tImg.isDirty() && tImg.imageFuture() != null) { 119 submittedTile = 0; // Don't submit new tiles if there are futures already in the queue. Not perfect. 120 } 121 if (submittedTile > 0 && (tImg == null || tImg.isDirty())) { 122 // Ensure that we don't add a large number of render calls 123 if (tImg != null && tImg.imageFuture() != null) { 124 tImg.imageFuture().cancel(); 125 } 126 submittedTile--; 127 // Note that the paint code is *not* thread safe, so all tiles must be painted on the same thread. 128 // 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 129 TileLoader loader = new TileLoader(data, tile, tileSize, new ArrayList<>()); 130 worker.execute(loader); 131 if (tImg == null) { 132 this.cache.put(tile, new ImageCache(null, loader, false)); 133 } else { 134 // This might cause some extra renders, but *probably* ok 135 this.cache.put(tile, new ImageCache(tImg.image(), loader, true)); 136 } 137 tileImage = tImg != null ? tImg.image() : null; 138 } else if (tImg != null) { 139 tileImage = tImg.image(); 140 } else { 141 tileImage = null; 142 } 143 final Point point = this.nc.getPoint(tile); 144 if (tileImage != null) { 145 // FIXME move to isTraceEnabled prior to commit 146 if ((wasDirty && Logging.isTraceEnabled()) || this.isInactiveMode) { 147 tempG2d.setColor(Color.DARK_GRAY); 148 tempG2d.fillRect(point.x, point.y, tileSize, tileSize); 149 } else { 150 painted++; 151 } 152 // There seems to be an off-by-one error somewhere. 153 tempG2d.drawImage(tileImage, point.x + 1, point.y + 1, null, null); 154 } else { 155 Logging.trace("StyledMapRenderer did not paint tile {1}", tile); 156 } 157 } 158 // Force another render pass if there may be more tiles to render 159 if (submittedTile <= 0) { 160 worker.execute(nc::invalidate); 161 } 162 final double percentDrawn = 100 * painted / (double) toRender.size(); 163 if (percentDrawn < 99.99) { 164 final int x = 0; 165 final int y = nc.getHeight() / 8; 166 final String message = tr("Rendering Status: {0}%", Math.floor(percentDrawn)); 167 tempG2d.setComposite(AlphaComposite.SrcOver); 168 tempG2d.setFont(new Font("sansserif", Font.BOLD, 13)); 169 tempG2d.setColor(Color.BLACK); 170 tempG2d.drawString(message, x + 1, y); 171 tempG2d.setColor(Color.LIGHT_GRAY); 172 tempG2d.drawString(message, x, y); 173 } 174 tempG2d.dispose(); 175 g.drawImage(tempImage, 0, 0, null); 176 } 177 178 /** 179 * Set the cache for this painter. If not set, this acts like {@link StyledMapRenderer}. 180 * @param box The box we will be rendering -- any jobs for tiles outside of this box will be cancelled 181 * @param cache The cache to use 182 * @param zoom The zoom level to use for creating the tiles 183 * @param notifier The method to call when a tile has been updated. This may or may not be called in the EDT. 184 */ 185 public void setCache(Bounds box, CacheAccess<TileZXY, ImageCache> cache, int zoom, Consumer<TileZXY> notifier) { 186 this.cache = cache; 187 this.zoom = zoom; 188 this.notifier = notifier != null ? notifier : tile -> { /* Do nothing */ }; 189 190 Set<TileZXY> tiles = TileZXY.boundsToTiles(box.getMinLat(), box.getMinLon(), box.getMaxLat(), box.getMaxLon(), zoom) 191 .collect(Collectors.toSet()); 192 cache.getMatching(".*").forEach((key, value) -> { 193 if (!tiles.contains(key)) { 194 cancelImageFuture(cache, key, value); 195 } 196 }); 197 } 198 199 /** 200 * Cancel a job for a tile 201 * @param cache The cache with the job 202 * @param key The tile key 203 * @param value The {@link ImageCache} to remove and cancel 204 */ 205 private static void cancelImageFuture(CacheAccess<TileZXY, ImageCache> cache, TileZXY key, ImageCache value) { 206 if (value.imageFuture() != null) { 207 value.imageFuture().cancel(); 208 if (value.image() == null) { 209 cache.remove(key); 210 } else { 211 cache.put(key, new ImageCache(value.image(), null, value.isDirty())); 212 } 213 } 214 } 215 216 /** 217 * Generate tile images 218 * @param data The data to generate tiles from 219 * @param tiles The collection of tiles to generate (note: there is currently a bug with multiple tiles) 220 * @param tileSize The size of the tile image 221 * @return The image for the tiles passed in 222 */ 223 private BufferedImage generateTiles(OsmData<?, ?, ?, ?> data, Collection<TileZXY> tiles, int tileSize) { 224 if (tiles.isEmpty()) { 225 throw new IllegalArgumentException("tiles cannot be empty"); 226 } 227 // We need to know how large of an area we are rendering; we get the min x/y and max x/y in order to get the 228 // number of tiles in the x/y directions we are rendering. 229 final IntSummaryStatistics xStats = tiles.stream().mapToInt(TileZXY::x).distinct().summaryStatistics(); 230 final IntSummaryStatistics yStats = tiles.stream().mapToInt(TileZXY::y).distinct().summaryStatistics(); 231 final int xCount = xStats.getMax() - xStats.getMin() + 1; // inclusive 232 final int yCount = yStats.getMax() - yStats.getMin() + 1; // inclusive 233 final int width = tileSize * (2 * BUFFER_TILES + xCount); 234 final int height = tileSize * (2 * BUFFER_TILES + yCount); 235 // getWidth and getHeight are called in the constructor; Java 22 will let us call super after we set variables. 236 final NavigatableComponent temporaryView = new NavigatableComponent() { 237 @Override 238 public int getWidth() { 239 return width; 240 } 241 242 @Override 243 public int getHeight() { 244 return height; 245 } 246 }; 247 // These bounds are used to set the render area; it includes the buffer area. 248 final Bounds bounds = generateRenderArea(tiles); 249 250 temporaryView.zoomTo(bounds.getCenter().getEastNorth(ProjectionRegistry.getProjection()), mapState.getScale()); 251 BufferedImage bufferedImage = Optional.ofNullable(nc.getGraphicsConfiguration()) 252 .map(gc -> gc.createCompatibleImage(tileSize * xCount + xCount, tileSize * yCount + xCount, Transparency.TRANSLUCENT)) 253 .orElseGet(() -> new BufferedImage(tileSize * xCount + xCount, tileSize * yCount + xCount, BufferedImage.TYPE_INT_ARGB)); 254 Graphics2D g2d = bufferedImage.createGraphics(); 255 try { 256 g2d.setTransform(AffineTransform.getTranslateInstance(-BUFFER_TILES * (double) tileSize, -BUFFER_TILES * (double) tileSize)); 257 final AbstractMapRenderer tilePainter = MapRendererFactory.getInstance().createActiveRenderer(g2d, temporaryView, false); 258 tilePainter.render(data, true, bounds); 259 } finally { 260 g2d.dispose(); 261 } 262 return bufferedImage; 263 } 264 265 /** 266 * Generate the area for rendering 267 * @param tiles The tiles that we want to render 268 * @return The generated render area with {@link #BUFFER_TILES} on all sides. 269 */ 270 private static Bounds generateRenderArea(Collection<TileZXY> tiles) { 271 Bounds bounds = null; 272 for (TileZXY tile : tiles) { 273 if (bounds == null) { 274 bounds = TileZXY.tileToBounds(tile); 275 } 276 bounds.extend(TileZXY.tileToBounds(new TileZXY(tile.zoom(), tile.x() - BUFFER_TILES, tile.y() - BUFFER_TILES))); 277 bounds.extend(TileZXY.tileToBounds(new TileZXY(tile.zoom(), tile.x() + BUFFER_TILES, tile.y() + BUFFER_TILES))); 278 } 279 return Objects.requireNonNull(bounds); 280 } 281 282 /** 283 * A loader for tiles 284 */ 285 class TileLoader implements Runnable { 286 private final TileZXY tile; 287 private final int tileSize; 288 private final OsmData<?, ?, ?, ?> data; 289 private boolean cancel; 290 private final Collection<TileLoader> tileCollection; 291 private boolean done; 292 293 /** 294 * Create a new tile loader 295 * @param data The data to use for painting 296 * @param tile The tile this tile loader is for 297 * @param tileSize The expected size of this tile 298 * @param tileCollection The collection of tiles that this tile is being rendered with (for batching) 299 */ 300 TileLoader(OsmData<?, ?, ?, ?> data, TileZXY tile, int tileSize, Collection<TileLoader> tileCollection) { 301 this.data = data; 302 this.tile = tile; 303 this.tileSize = tileSize; 304 this.tileCollection = tileCollection; 305 this.tileCollection.add(this); 306 } 307 308 @Override 309 public void run() { 310 if (!cancel) { 311 synchronized (tileCollection) { 312 if (!done) { 313 final BufferedImage tImage = generateTiles(data, 314 tileCollection.stream().map(t -> t.tile).collect(Collectors.toList()), tileSize); 315 final int minX = tileCollection.stream().map(t -> t.tile).mapToInt(TileZXY::x).min().orElse(this.tile.x()); 316 final int minY = tileCollection.stream().map(t -> t.tile).mapToInt(TileZXY::y).min().orElse(this.tile.y()); 317 for (TileLoader loader : tileCollection) { 318 final TileZXY txy = loader.tile; 319 final int x = (txy.x() - minX) * (tileSize - BUFFER_PIXELS) + BUFFER_PIXELS / 2; 320 final int y = (txy.y() - minY) * (tileSize - BUFFER_PIXELS) + BUFFER_PIXELS / 2; 321 final int wh = tileSize - BUFFER_PIXELS / 2; 322 323 final BufferedImage tileImage = tImage.getSubimage(x, y, wh, wh); 324 loader.cacheTile(tileImage); 325 } 326 } 327 } 328 } 329 } 330 331 /** 332 * Finish a tile generation job 333 * @param tImage The tile image for this job 334 */ 335 private synchronized void cacheTile(BufferedImage tImage) { 336 cache.put(tile, new ImageCache(tImage, null, false)); 337 done = true; 338 notifier.accept(tile); 339 } 340 341 /** 342 * Cancel this job without causing a {@link java.util.concurrent.CancellationException} 343 */ 344 void cancel() { 345 this.cancel = true; 346 } 347 } 348 } -
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; … … 91 95 import org.openstreetmap.josm.data.osm.visitor.BoundingXYVisitor; 92 96 import org.openstreetmap.josm.data.osm.visitor.OsmPrimitiveVisitor; 93 97 import org.openstreetmap.josm.data.osm.visitor.paint.AbstractMapRenderer; 98 import org.openstreetmap.josm.data.osm.visitor.paint.ImageCache; 94 99 import org.openstreetmap.josm.data.osm.visitor.paint.MapRendererFactory; 100 import org.openstreetmap.josm.data.osm.visitor.paint.StyledTiledMapRenderer; 101 import org.openstreetmap.josm.data.osm.visitor.paint.TileZXY; 95 102 import org.openstreetmap.josm.data.osm.visitor.paint.relations.MultipolygonCache; 96 103 import org.openstreetmap.josm.data.preferences.BooleanProperty; 97 104 import org.openstreetmap.josm.data.preferences.IntegerProperty; … … 104 111 import org.openstreetmap.josm.gui.MapFrame; 105 112 import org.openstreetmap.josm.gui.MapView; 106 113 import org.openstreetmap.josm.gui.MapViewState.MapViewPoint; 114 import org.openstreetmap.josm.gui.NavigatableComponent; 115 import org.openstreetmap.josm.gui.PrimitiveHoverListener; 107 116 import org.openstreetmap.josm.gui.datatransfer.ClipboardUtils; 108 117 import org.openstreetmap.josm.gui.datatransfer.data.OsmLayerTransferData; 109 118 import org.openstreetmap.josm.gui.dialogs.LayerListDialog; … … 144 153 * @author imi 145 154 * @since 17 146 155 */ 147 public class OsmDataLayer extends AbstractOsmDataLayer implements Listener, DataSelectionListener, HighlightUpdateListener { 156 public class OsmDataLayer extends AbstractOsmDataLayer 157 implements Listener, DataSelectionListener, HighlightUpdateListener, PrimitiveHoverListener { 158 private static final int MAX_ZOOM = 30; 159 private static final int OVER_ZOOM = 2; 148 160 private static final int HATCHED_SIZE = 15; 149 161 // U+2205 EMPTY SET 150 162 private static final String IS_EMPTY_SYMBOL = "\u2205"; … … 155 167 private boolean requiresUploadToServer; 156 168 /** Flag used to know if the layer is being uploaded */ 157 169 private final AtomicBoolean isUploadInProgress = new AtomicBoolean(false); 170 /** 171 * A cache used for painting 172 */ 173 private final CacheAccess<TileZXY, ImageCache> cache = JCSCacheManager.getCache("osmDataLayer:" + System.identityHashCode(this)); 174 /** The map paint index that was painted (used to invalidate {@link #cache}) */ 175 private int lastDataIdx; 176 /** The last zoom level (we invalidate all tiles when switching layers) */ 177 private int lastZoom; 178 private boolean hoverListenerAdded; 158 179 159 180 /** 160 181 * List of validation errors in this layer. … … 497 518 * Draw nodes last to overlap the ways they belong to. 498 519 */ 499 520 @Override public void paint(final Graphics2D g, final MapView mv, Bounds box) { 521 if (!hoverListenerAdded) { 522 MainApplication.getMap().mapView.addPrimitiveHoverListener(this); 523 hoverListenerAdded = true; 524 } 500 525 boolean active = mv.getLayerManager().getActiveLayer() == this; 501 526 boolean inactive = !active && Config.getPref().getBoolean("draw.data.inactive_color", true); 502 527 boolean virtual = !inactive && mv.isVirtualNodesEnabled(); … … 537 562 } 538 563 } 539 564 565 // Used to invalidate cache 566 int zoom = getZoom(mv); 567 if (zoom != lastZoom) { 568 // We just mark the previous zoom as dirty before moving in. 569 // It means we don't have to traverse up/down z-levels marking tiles as dirty (this can get *very* expensive). 570 this.cache.getMatching("TileZXY\\{" + lastZoom + "/.*") 571 .forEach((tile, imageCache) -> this.cache.put(tile, imageCache.becomeDirty())); 572 } 573 lastZoom = zoom; 540 574 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); 575 if (!(painter instanceof StyledTiledMapRenderer) || zoom - OVER_ZOOM > Config.getPref().getInt("mappaint.fast_render.zlevel", 16)) { 576 painter.enableSlowOperations(mv.getMapMover() == null || !mv.getMapMover().movementInProgress() 577 || !PROPERTY_HIDE_LABELS_WHILE_DRAGGING.get()); 578 } else { 579 StyledTiledMapRenderer renderer = (StyledTiledMapRenderer) painter; 580 renderer.setCache(box, this.cache, zoom, (tile) -> { 581 /* This causes "bouncing". I'm not certain why. 582 if (oldState.equalsInWindow(mv.getState())) { (oldstate = mv.getState()) 583 final Point upperLeft = mv.getPoint(tile); 584 final Point lowerRight = mv.getPoint(new TileZXY(tile.zoom(), tile.x() + 1, tile.y() + 1)); 585 GuiHelper.runInEDT(() -> mv.repaint(0, upperLeft.x, upperLeft.y, lowerRight.x - upperLeft.x, lowerRight.y - upperLeft.y)); 586 } 587 */ 588 // Invalidate doesn't trigger an instant repaint, but putting this off lets us batch the repaints needed for multiple tiles 589 MainApplication.worker.submit(this::invalidate); 590 }); 591 592 if (this.data.getMappaintCacheIndex() != this.lastDataIdx) { 593 this.cache.clear(); 594 this.lastDataIdx = this.data.getMappaintCacheIndex(); 595 Logging.trace("OsmDataLayer {0} paint cache cleared", this.getName()); 596 } 597 } 598 painter.render(this.data, virtual, box); 544 599 MainApplication.getMap().conflictDialog.paintConflicts(g, mv); 545 600 } 546 601 … … 1147 1202 validationErrors.clear(); 1148 1203 removeClipboardDataFor(this); 1149 1204 recentRelations.clear(); 1205 if (hoverListenerAdded) { 1206 hoverListenerAdded = false; 1207 MainApplication.getMap().mapView.removePrimitiveHoverListener(this); 1208 } 1150 1209 } 1151 1210 1152 1211 protected static void removeClipboardDataFor(OsmDataLayer osm) { … … 1165 1224 1166 1225 @Override 1167 1226 public void processDatasetEvent(AbstractDatasetChangedEvent event) { 1227 resetTiles(event.getPrimitives()); 1168 1228 invalidate(); 1169 1229 setRequiresSaveToFile(true); 1170 1230 setRequiresUploadToServer(event.getDataset().requiresUploadToServer()); … … 1172 1232 1173 1233 @Override 1174 1234 public void selectionChanged(SelectionChangeEvent event) { 1235 Set<IPrimitive> primitives = new HashSet<>(event.getAdded()); 1236 primitives.addAll(event.getRemoved()); 1237 resetTiles(primitives); 1175 1238 invalidate(); 1176 1239 } 1177 1240 1241 private void resetTiles(Collection<? extends IPrimitive> primitives) { 1242 if (primitives.size() == this.data.allPrimitives().size()) { 1243 this.data.getDataSourceBounds().forEach(b -> resetBounds(b.getMinLat(), b.getMinLon(), b.getMaxLat(), b.getMaxLon())); 1244 return; 1245 } 1246 // Most of the time, a selection is going to be a big box. 1247 // So we want to optimize for that case. 1248 BBox box = null; 1249 for (IPrimitive primitive : primitives) { 1250 if (primitive == null || primitive.getDataSet() != this.getDataSet()) continue; 1251 final Collection<? extends IPrimitive> referrers = primitive.getReferrers(); 1252 if (box == null) { 1253 box = new BBox(primitive.getBBox()); 1254 } else { 1255 box.addPrimitive(primitive, 0); 1256 } 1257 for (IPrimitive referrer : referrers) { 1258 box.addPrimitive(referrer, 0); 1259 } 1260 } 1261 if (box != null) { 1262 resetBounds(box.getMinLat(), box.getMinLon(), box.getMaxLat(), box.getMaxLon()); 1263 } 1264 } 1265 1266 private void resetBounds(double minLat, double minLon, double maxLat, double maxLon) { 1267 // Get the current zoom. Hopefully we aren't painting with a different navigatable component 1268 final int currentZoom = lastZoom; 1269 TileZXY.boundsToTiles(minLat, minLon, maxLat, maxLon, currentZoom, 1).forEach(tile -> { 1270 final ImageCache imageCache = this.cache.get(tile); 1271 if (imageCache != null && !imageCache.isDirty()) { 1272 this.cache.put(tile, imageCache.becomeDirty()); 1273 } 1274 }); 1275 } 1276 1277 /** 1278 * Get the zoom for a {@link NavigatableComponent} 1279 * @param navigatableComponent The component to get the zoom from 1280 * @return The zoom for the navigatable component 1281 */ 1282 private static int getZoom(NavigatableComponent navigatableComponent) { 1283 final double scale = navigatableComponent.getScale(); 1284 // We might have to fall back to the old method if user is reprojecting 1285 // 256 is the "target" size, (TODO check HiDPI!) 1286 final int targetSize = Config.getPref().getInt("mappaint.fast_render.tile_size", 256); 1287 final double topResolution = 2 * Math.PI * OsmMercator.EARTH_RADIUS / targetSize; 1288 int zoom; 1289 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) 1290 if (scale > topResolution / Math.pow(2, zoom)) { 1291 zoom = zoom > 0 ? zoom - 1 : zoom; 1292 break; 1293 } 1294 } 1295 // We paint at a few levels higher, note that the tiles are appropriately sized (if 256 is the "target" size, the tiles should be 1296 // 64px square). 1297 zoom += OVER_ZOOM; 1298 return zoom; 1299 } 1300 1178 1301 @Override 1179 1302 public void projectionChanged(Projection oldValue, Projection newValue) { 1180 1303 // No reprojection required. The dataset itself is registered as projection … … 1307 1430 invalidate(); 1308 1431 } 1309 1432 1433 @Override 1434 public void primitiveHovered(PrimitiveHoverEvent e) { 1435 resetTiles(Arrays.asList(e.getHoveredPrimitive(), e.getPreviousPrimitive())); 1436 this.invalidate(); 1437 } 1438 1310 1439 @Override 1311 1440 public void setName(String name) { 1312 1441 if (data != null) { -
src/org/openstreetmap/josm/gui/MainMenu.java
IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 diff --git a/src/org/openstreetmap/josm/gui/MainMenu.java b/src/org/openstreetmap/josm/gui/MainMenu.java
a b 103 103 import org.openstreetmap.josm.actions.SimplifyWayAction; 104 104 import org.openstreetmap.josm.actions.SplitWayAction; 105 105 import org.openstreetmap.josm.actions.TaggingPresetSearchAction; 106 import org.openstreetmap.josm.actions.TiledRenderToggleAction; 106 107 import org.openstreetmap.josm.actions.UnGlueAction; 107 108 import org.openstreetmap.josm.actions.UnJoinNodeWayAction; 108 109 import org.openstreetmap.josm.actions.UndoAction; … … 248 249 /* View menu */ 249 250 /** View / Wireframe View */ 250 251 public final WireframeToggleAction wireFrameToggleAction = new WireframeToggleAction(); 252 /** View / Tiled Rendering */ 253 public final TiledRenderToggleAction tiledRenderToggleAction = new TiledRenderToggleAction(); 251 254 /** View / Hatch area outside download */ 252 255 public final DrawBoundariesOfDownloadedDataAction drawBoundariesOfDownloadedDataAction = new DrawBoundariesOfDownloadedDataAction(); 253 256 /** View / Advanced info */ … … 799 802 viewMenu.add(wireframe); 800 803 wireframe.setAccelerator(wireFrameToggleAction.getShortcut().getKeyStroke()); 801 804 wireFrameToggleAction.addButtonModel(wireframe.getModel()); 805 // -- tiled render toggle action 806 final JCheckBoxMenuItem tiledRender = new JCheckBoxMenuItem(tiledRenderToggleAction); 807 viewMenu.add(tiledRender); 808 tiledRenderToggleAction.addButtonModel(tiledRender.getModel()); 809 ExpertToggleAction.addVisibilitySwitcher(tiledRender); 810 // -- hatch toggle action 802 811 final JCheckBoxMenuItem hatchAreaOutsideDownloadMenuItem = drawBoundariesOfDownloadedDataAction.getCheckbox(); 803 812 viewMenu.add(hatchAreaOutsideDownloadMenuItem); 804 813 ExpertToggleAction.addVisibilitySwitcher(hatchAreaOutsideDownloadMenuItem); -
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 4 4 import java.awt.AlphaComposite; 5 5 import java.awt.BasicStroke; 6 6 import java.awt.Color; 7 import java.awt.Component; 7 8 import java.awt.Dimension; 8 9 import java.awt.Graphics; 9 10 import java.awt.Graphics2D; 11 import java.awt.GraphicsEnvironment; 10 12 import java.awt.Point; 11 13 import java.awt.Rectangle; 12 14 import java.awt.Shape; 13 15 import java.awt.Stroke; 16 import java.awt.Transparency; 14 17 import java.awt.event.ComponentAdapter; 15 18 import java.awt.event.ComponentEvent; 16 19 import java.awt.event.KeyEvent; … … 328 331 return Arrays.asList(zoomSlider, scaler); 329 332 } 330 333 334 private static BufferedImage getAcceleratedImage(Component mv, int width, int height) { 335 if (GraphicsEnvironment.isHeadless()) { 336 return new BufferedImage(width, height, BufferedImage.TYPE_3BYTE_BGR); 337 } 338 return mv.getGraphicsConfiguration().createCompatibleImage(width, height, Transparency.OPAQUE); 339 } 340 331 341 // remebered geometry of the component 332 342 private Dimension oldSize; 333 343 private Point oldLoc; … … 546 556 && nonChangedLayers.equals(visibleLayers.subList(0, nonChangedLayers.size())); 547 557 548 558 if (null == offscreenBuffer || offscreenBuffer.getWidth() != width || offscreenBuffer.getHeight() != height) { 549 offscreenBuffer = new BufferedImage(width, height, BufferedImage.TYPE_3BYTE_BGR);559 offscreenBuffer = getAcceleratedImage(this, width, height); 550 560 } 551 561 552 562 if (!canUseBuffer || nonChangedLayersBuffer == null) { 553 563 if (null == nonChangedLayersBuffer 554 564 || nonChangedLayersBuffer.getWidth() != width || nonChangedLayersBuffer.getHeight() != height) { 555 nonChangedLayersBuffer = new BufferedImage(width, height, BufferedImage.TYPE_3BYTE_BGR);565 nonChangedLayersBuffer = getAcceleratedImage(this, width, height); 556 566 } 557 567 Graphics2D g2 = nonChangedLayersBuffer.createGraphics(); 558 568 g2.setClip(scaledClip);