Ticket #11487: 11487.2.patch
| File 11487.2.patch, 16.4 KB (added by , 3 years ago) |
|---|
-
src/org/openstreetmap/josm/gui/layer/OsmDataLayer.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/gui/layer/OsmDataLayer.java b/src/org/openstreetmap/josm/gui/layer/OsmDataLayer.java
a b 11 11 import java.awt.Composite; 12 12 import java.awt.Graphics2D; 13 13 import java.awt.GridBagLayout; 14 import java.awt.Point; 14 15 import java.awt.Rectangle; 15 16 import java.awt.TexturePaint; 16 17 import java.awt.datatransfer.Transferable; 17 18 import java.awt.datatransfer.UnsupportedFlavorException; 18 19 import java.awt.event.ActionEvent; 20 import java.awt.geom.AffineTransform; 19 21 import java.awt.geom.Area; 20 22 import java.awt.geom.Path2D; 21 23 import java.awt.geom.Rectangle2D; … … 37 39 import java.util.concurrent.CopyOnWriteArrayList; 38 40 import java.util.concurrent.atomic.AtomicBoolean; 39 41 import java.util.concurrent.atomic.AtomicInteger; 42 import java.util.function.Supplier; 40 43 import java.util.regex.Pattern; 41 44 import java.util.stream.Collectors; 42 45 import java.util.stream.Stream; … … 49 52 import javax.swing.JPanel; 50 53 import javax.swing.JScrollPane; 51 54 55 import org.apache.commons.jcs3.access.CacheAccess; 56 import org.openstreetmap.gui.jmapviewer.OsmMercator; 57 import org.openstreetmap.gui.jmapviewer.TileXY; 52 58 import org.openstreetmap.josm.actions.AutoScaleAction; 53 59 import org.openstreetmap.josm.actions.ExpertToggleAction; 54 60 import org.openstreetmap.josm.actions.RenameLayerAction; … … 58 64 import org.openstreetmap.josm.data.Data; 59 65 import org.openstreetmap.josm.data.ProjectionBounds; 60 66 import org.openstreetmap.josm.data.UndoRedoHandler; 67 import org.openstreetmap.josm.data.cache.JCSCacheManager; 61 68 import org.openstreetmap.josm.data.conflict.Conflict; 62 69 import org.openstreetmap.josm.data.conflict.ConflictCollection; 63 70 import org.openstreetmap.josm.data.coor.EastNorth; … … 70 77 import org.openstreetmap.josm.data.gpx.GpxTrackSegment; 71 78 import org.openstreetmap.josm.data.gpx.IGpxTrackSegment; 72 79 import org.openstreetmap.josm.data.gpx.WayPoint; 80 import org.openstreetmap.josm.data.osm.BBox; 73 81 import org.openstreetmap.josm.data.osm.DataIntegrityProblemException; 74 82 import org.openstreetmap.josm.data.osm.DataSelectionListener; 75 83 import org.openstreetmap.josm.data.osm.DataSet; … … 77 85 import org.openstreetmap.josm.data.osm.DatasetConsistencyTest; 78 86 import org.openstreetmap.josm.data.osm.DownloadPolicy; 79 87 import org.openstreetmap.josm.data.osm.HighlightUpdateListener; 88 import org.openstreetmap.josm.data.osm.INode; 80 89 import org.openstreetmap.josm.data.osm.IPrimitive; 90 import org.openstreetmap.josm.data.osm.IWay; 81 91 import org.openstreetmap.josm.data.osm.Node; 82 92 import org.openstreetmap.josm.data.osm.OsmPrimitive; 83 93 import org.openstreetmap.josm.data.osm.OsmPrimitiveComparator; … … 104 114 import org.openstreetmap.josm.gui.MapFrame; 105 115 import org.openstreetmap.josm.gui.MapView; 106 116 import org.openstreetmap.josm.gui.MapViewState.MapViewPoint; 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 { 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 * TODO: add dirty bit (to avoid "blank" spaces) 173 */ 174 private final CacheAccess<String, BufferedImage> cache = JCSCacheManager.getCache("osmDataLayer:" + this); 175 /** The last zoom that was painted (used to invalidate {@link #cache}, TODO investigate if it is worth it to cache multiple zoom levels) */ 176 private int lastZoom; 177 /** The map paint index that was painted (used to invalidate {@link #cache}) */ 178 private int lastDataIdx; 179 private boolean hoverListenerAdded; 158 180 159 181 /** 160 182 * List of validation errors in this layer. … … 497 519 * Draw nodes last to overlap the ways they belong to. 498 520 */ 499 521 @Override public void paint(final Graphics2D g, final MapView mv, Bounds box) { 522 if (!hoverListenerAdded) { 523 MainApplication.getMap().mapView.addPrimitiveHoverListener(this); 524 hoverListenerAdded = true; 525 } 500 526 boolean active = mv.getLayerManager().getActiveLayer() == this; 501 527 boolean inactive = !active && Config.getPref().getBoolean("draw.data.inactive_color", true); 502 528 boolean virtual = !inactive && mv.isVirtualNodesEnabled(); … … 537 563 } 538 564 } 539 565 540 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); 566 final double scale = mv.getScale(); 567 // We might have to fall back to the old method if user is reprojecting 568 final double topResolution = 2 * Math.PI * OsmMercator.EARTH_RADIUS / 256; // 256 is the "target" size (TODO check HiDPI!) 569 // Used to invalidate cache 570 int zoom; 571 for (zoom = 0; zoom < 30; zoom++) { // Use something like imagery.{generic|tms}.max_zoom_lvl (20 is a bit too low for our needs) 572 if (scale > topResolution / Math.pow(2, zoom)) { 573 zoom = zoom > 0 ? zoom - 1 : zoom; 574 break; 575 } 576 } 577 // We paint at a few levels higher, note that the tiles are appropriately sized (if 256 is the "target" size, the tiles should be 578 // 64px square) 579 zoom += 2; 580 if (!Config.getPref().getBoolean("mappaint.fast_render", false) || zoom > Config.getPref().getInt("mappaint.fast_render.zlevel", 16)) { 581 AbstractMapRenderer painter = MapRendererFactory.getInstance().createActiveRenderer(g, mv, inactive); 582 painter.enableSlowOperations(mv.getMapMover() == null || !mv.getMapMover().movementInProgress() 583 || !PROPERTY_HIDE_LABELS_WHILE_DRAGGING.get()); 584 painter.render(this.data, virtual, box); 585 } else { 586 if (zoom != this.lastZoom || this.data.getMappaintCacheIndex() != this.lastDataIdx) { 587 this.cache.clear(); 588 this.lastZoom = zoom; 589 this.lastDataIdx = this.data.getMappaintCacheIndex(); 590 Logging.trace("OsmDataLayer {0} paint cache cleared", this.getName()); 591 } 592 final List<TileXY> toRender = boundsToTiles(box.getMinLat(), box.getMinLon(), box.getMaxLat(), box.getMaxLon(), zoom); 593 toRender.stream().map(tile -> tileToBounds(tile, lastZoom)).forEach(box::extend); // TODO is the box sent in reused? 594 final int tileSize; 595 if (toRender.isEmpty()) { 596 tileSize = 256; // Mostly to keep the compiler happy 597 } else { 598 final TileXY tile = toRender.get(0); 599 final Bounds bounds = tileToBounds(tile, zoom); 600 final Point min = mv.getPoint(bounds.getMin()); 601 final Point max = mv.getPoint(bounds.getMax()); 602 tileSize = max.x - min.x; 603 } 604 for (TileXY tile : toRender) { 605 final int actualZoom = zoom; 606 final double lat = yToLat(tile.getYIndex(), zoom); 607 final double lon = xToLon(tile.getXIndex(), zoom); 608 final Point point = mv.getPoint(new LatLon(lat, lon)); 609 final BufferedImage tileImage; 610 // Needed to avoid having tiles that aren't rendered properly 611 final String tileString = tile.toString(); 612 final Supplier<BufferedImage> tileSupplier = 613 () -> generateTile(this.data, mv, point, inactive, virtual, tile, tileSize, actualZoom); 614 final BufferedImage tImg = this.cache.get(tileString); 615 // 16.66 ms gives us 60 fps, but we want to ''always'' render at ''least'' one tile 616 if (tImg == null) { 617 // We could do this render in a separate thread. Worker thread maybe? Caveat: mv changes will kind of mess it up. 618 // TODO do we want to use the worker thread, or do we need/want a dedicated thread(s)? 619 // Note that the paint code is *not* thread safe, so all tiles must be painted on the same thread. 620 // FIXME figure out how to make this thread safe? Probably not necessary, since UI isn't blocked. 621 MainApplication.worker.execute(() -> { 622 // These checkpoints ensure that the mapview is the same as when we scheduled the tile 623 // This prevents tearing. This check occurs first to avoid a pointless render. 624 final Point checkPoint = mv.getPoint(new LatLon(lat, lon)); 625 if (checkPoint.equals(point)) { 626 this.cache.put(tileString, tileSupplier.get()); 627 // A second check just in case the map moved during render. 628 final Point checkPoint2 = mv.getPoint(new LatLon(lat, lon)); 629 if (checkPoint2.equals(checkPoint)) { 630 GuiHelper.runInEDT(this::invalidate); 631 } else { 632 this.cache.remove(tileString); 633 } 634 } 635 }); 636 tileImage = null; 637 } else { 638 tileImage = tImg; 639 } 640 if (tileImage != null) { 641 // Get the lowerright point of the tile to avoid render gaps 642 // This does very slightly stretch a tile though. There is something like a 1px border if we use the tile size. 643 final double lat2 = yToLat(tile.getYIndex() + 1, zoom); 644 final double lon2 = xToLon(tile.getXIndex() + 1, zoom); 645 final Point point2 = mv.getPoint(new LatLon(lat2, lon2)); 646 g.drawImage(tileImage, point.x, point.y, point2.x - point.x, point2.y - point.y, null, null); 647 } 648 } 649 } 544 650 MainApplication.getMap().conflictDialog.paintConflicts(g, mv); 545 651 } 546 652 653 private static List<TileXY> boundsToTiles(double minLat, double minLon, double maxLat, double maxLon, int zoom) { 654 final List<TileXY> tiles = new ArrayList<>(); 655 final TileXY upperRight = latLonToTile(maxLat, maxLon, zoom); 656 final TileXY lowerLeft = latLonToTile(minLat, minLon, zoom); 657 for (int x = lowerLeft.getXIndex(); x <= upperRight.getXIndex(); x++) { 658 for (int y = upperRight.getYIndex(); y <= lowerLeft.getYIndex(); y++) { 659 tiles.add(new TileXY(x, y)); 660 } 661 } 662 return tiles; 663 } 664 665 private static BufferedImage generateTile(DataSet data, MapView mv, Point point, boolean inactive, boolean virtual, TileXY tile, 666 int tileSize, int zoom) { 667 BufferedImage bufferedImage = new BufferedImage(tileSize, tileSize, BufferedImage.TYPE_4BYTE_ABGR); 668 Graphics2D g2d = bufferedImage.createGraphics(); 669 g2d.setTransform(AffineTransform.getTranslateInstance(-point.x, -point.y)); 670 try { 671 AbstractMapRenderer tilePainter = MapRendererFactory.getInstance().createActiveRenderer(g2d, mv, inactive); 672 // Render to the surrounding tiles for continuity -- this probably needs to be tweaked 673 int buffer = 2; 674 Bounds bounds = tileToBounds(new TileXY(tile.getXIndex() - buffer, tile.getYIndex() - buffer), zoom); 675 bounds.extend(tileToBounds(new TileXY(tile.getXIndex() + buffer, tile.getYIndex() + buffer), zoom)); 676 tilePainter.render(data, virtual, bounds); 677 } finally { 678 g2d.dispose(); 679 } 680 return bufferedImage; 681 } 682 683 private static Bounds tileToBounds(TileXY tile, int zoom) { 684 return new Bounds(yToLat(tile.getYIndex() + 1, zoom), xToLon(tile.getXIndex(), zoom), 685 yToLat(tile.getYIndex(), zoom), xToLon(tile.getXIndex() + 1, zoom)); 686 } 687 688 private static double xToLon(int x, int zoom) { 689 return (x / Math.pow(2, zoom)) * 360 - 180; 690 } 691 692 private static double yToLat(int y, int zoom) { 693 double t = Math.PI - (2 * Math.PI * y) / Math.pow(2, zoom); 694 return 180 / Math.PI * Math.atan((Math.exp(t) - Math.exp(-t)) / 2); 695 } 696 697 private static TileXY latLonToTile(double lat, double lon, int zoom) { 698 int xCoord = (int) Math.floor(Math.pow(2, zoom) * (180 + lon) / 360); 699 int yCoord = (int) Math.floor(Math.pow(2, zoom) * 700 (1 - Math.log(Math.tan(Math.toRadians(lat)) + 1 / Math.cos(Math.toRadians(lat))) / Math.PI) / 2); 701 return new TileXY(xCoord, yCoord); 702 } 703 547 704 @Override public String getToolTipText() { 548 705 DataCountVisitor counter = new DataCountVisitor(); 549 706 for (final OsmPrimitive osm : data.allPrimitives()) { … … 1147 1304 validationErrors.clear(); 1148 1305 removeClipboardDataFor(this); 1149 1306 recentRelations.clear(); 1307 if (hoverListenerAdded) { 1308 hoverListenerAdded = false; 1309 MainApplication.getMap().mapView.removePrimitiveHoverListener(this); 1310 } 1150 1311 } 1151 1312 1152 1313 protected static void removeClipboardDataFor(OsmDataLayer osm) { … … 1165 1326 1166 1327 @Override 1167 1328 public void processDatasetEvent(AbstractDatasetChangedEvent event) { 1329 resetTiles(event.getPrimitives()); 1168 1330 invalidate(); 1169 1331 setRequiresSaveToFile(true); 1170 1332 setRequiresUploadToServer(event.getDataset().requiresUploadToServer()); … … 1172 1334 1173 1335 @Override 1174 1336 public void selectionChanged(SelectionChangeEvent event) { 1337 Set<IPrimitive> primitives = new HashSet<>(event.getAdded()); 1338 primitives.addAll(event.getRemoved()); 1339 resetTiles(primitives); 1175 1340 invalidate(); 1176 1341 } 1177 1342 1343 private void resetTiles(Iterable<? extends IPrimitive> primitives) { 1344 for (IPrimitive primitive : primitives) { 1345 final BBox bounds = primitive.getBBox(); 1346 for (TileXY tile : boundsToTiles(bounds.getMinLat(), bounds.getMinLon(), bounds.getMaxLat(), bounds.getMaxLon(), lastZoom)) { 1347 this.cache.remove(tile.toString()); 1348 } 1349 } 1350 } 1351 1178 1352 @Override 1179 1353 public void projectionChanged(Projection oldValue, Projection newValue) { 1180 1354 // No reprojection required. The dataset itself is registered as projection … … 1307 1481 invalidate(); 1308 1482 } 1309 1483 1484 @Override 1485 public void primitiveHovered(PrimitiveHoverEvent e) { 1486 for (IPrimitive primitive : Arrays.asList(e.getHoveredPrimitive(), e.getPreviousPrimitive())) { 1487 if (primitive == null || primitive.getDataSet() != this.getDataSet()) continue; 1488 if (primitive instanceof IWay<?>) { 1489 for (INode n : ((IWay<?>) primitive).getNodes()) { 1490 final TileXY tile = latLonToTile(n.lat(), n.lon(), lastZoom); 1491 this.cache.remove(tile.toString()); 1492 } 1493 } else { 1494 final BBox box = primitive.getBBox(); 1495 for (TileXY tile : boundsToTiles(box.getMinLat(), box.getMinLon(), box.getMaxLat(), box.getMaxLon(), lastZoom)) { 1496 this.cache.remove(tile.toString()); 1497 } 1498 } 1499 } 1500 this.invalidate(); 1501 } 1502 1310 1503 @Override 1311 1504 public void setName(String name) { 1312 1505 if (data != null) {
