Subject: [PATCH] #11487: Try to improve render performance
---
Index: 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/src/org/openstreetmap/josm/gui/layer/OsmDataLayer.java	(revision 18720)
+++ b/src/org/openstreetmap/josm/gui/layer/OsmDataLayer.java	(date 1683031879495)
@@ -11,11 +11,13 @@
 import java.awt.Composite;
 import java.awt.Graphics2D;
 import java.awt.GridBagLayout;
+import java.awt.Point;
 import java.awt.Rectangle;
 import java.awt.TexturePaint;
 import java.awt.datatransfer.Transferable;
 import java.awt.datatransfer.UnsupportedFlavorException;
 import java.awt.event.ActionEvent;
+import java.awt.geom.AffineTransform;
 import java.awt.geom.Area;
 import java.awt.geom.Path2D;
 import java.awt.geom.Rectangle2D;
@@ -49,6 +51,9 @@
 import javax.swing.JPanel;
 import javax.swing.JScrollPane;
 
+import org.apache.commons.jcs3.access.CacheAccess;
+import org.openstreetmap.gui.jmapviewer.OsmMercator;
+import org.openstreetmap.gui.jmapviewer.TileXY;
 import org.openstreetmap.josm.actions.AutoScaleAction;
 import org.openstreetmap.josm.actions.ExpertToggleAction;
 import org.openstreetmap.josm.actions.RenameLayerAction;
@@ -58,6 +63,7 @@
 import org.openstreetmap.josm.data.Data;
 import org.openstreetmap.josm.data.ProjectionBounds;
 import org.openstreetmap.josm.data.UndoRedoHandler;
+import org.openstreetmap.josm.data.cache.JCSCacheManager;
 import org.openstreetmap.josm.data.conflict.Conflict;
 import org.openstreetmap.josm.data.conflict.ConflictCollection;
 import org.openstreetmap.josm.data.coor.EastNorth;
@@ -155,6 +161,12 @@
     private boolean requiresUploadToServer;
     /** Flag used to know if the layer is being uploaded */
     private final AtomicBoolean isUploadInProgress = new AtomicBoolean(false);
+    /** A cache used for painting */
+    private final CacheAccess<String, BufferedImage> cache = JCSCacheManager.getCache("osmDataLayer:" + this);
+    /** The last zoom that was painted (used to invalidate {@link #cache}) */
+    private int lastZoom;
+    /** The map paint index that was painted (used to invalidate {@link #cache}) */
+    private int lastDataIdx;
 
     /**
      * List of validation errors in this layer.
@@ -540,10 +552,83 @@
         AbstractMapRenderer painter = MapRendererFactory.getInstance().createActiveRenderer(g, mv, inactive);
         painter.enableSlowOperations(mv.getMapMover() == null || !mv.getMapMover().movementInProgress()
                 || !PROPERTY_HIDE_LABELS_WHILE_DRAGGING.get());
-        painter.render(data, virtual, box);
+        final double scale = mv.getScale();
+        final int tileSize = 256;
+        // We might have to fall back to the old method if user is reprojecting
+        final double topResolution = 2 * Math.PI * OsmMercator.EARTH_RADIUS / tileSize;
+        // Use to invalidate cache in the future
+        int zoom;
+        for (zoom = 0; zoom < 30; zoom++) {// Use something like imagery.{generic|tms}.max_zoom_lvl (20 is a bit too low for our needs)
+            if (scale > topResolution / (Math.pow(2, zoom))) {
+                zoom = zoom > 0 ? zoom - 1 : zoom;
+                break;
+            }
+        }
+        if (zoom != lastZoom || data.getMappaintCacheIndex() != lastDataIdx
+                || !data.selectionEmpty()
+                || !data.getHighlightedVirtualNodes().isEmpty()
+                || !data.getHighlightedWaySegments().isEmpty()) {
+            cache.clear();
+            lastZoom = zoom;
+            lastDataIdx = data.getMappaintCacheIndex();
+            Logging.trace("OsmDataLayer {0} paint cache cleared", this.getName());
+        }
+        final List<TileXY> toRender = new ArrayList<>();
+        final TileXY upperRight = latLonToTile(box.getMaxLat(), box.getMaxLon(), zoom);
+        final TileXY lowerLeft = latLonToTile(box.getMinLat(), box.getMinLon(), zoom);
+        for (int x = lowerLeft.getXIndex(); x <= upperRight.getXIndex(); x++) {
+            for (int y = upperRight.getYIndex(); y <= lowerLeft.getYIndex(); y++) {
+                toRender.add(new TileXY(x, y));
+            }
+        }
+
+        for (TileXY tile : toRender) {
+            final int actualZoom = zoom;
+            final double lat = yToLat(tile.getYIndex(), zoom);
+            final double lon = xToLon(tile.getXIndex(), zoom);
+            final Point point = mv.getPoint(new LatLon(lat, lon));
+            BufferedImage tileImage = cache.get(tile.toString(),
+                    () -> generateTile(data, mv, point, inactive, virtual, tile, tileSize, actualZoom));
+            g.drawImage(tileImage, point.x, point.y, tileSize, tileSize, null, null);
+        }
+        //painter.render(data, virtual, box);
         MainApplication.getMap().conflictDialog.paintConflicts(g, mv);
     }
 
+    private static BufferedImage generateTile(DataSet data, MapView mv, Point point, boolean inactive, boolean virtual, TileXY tile,
+                                              int tileSize, int zoom) {
+        BufferedImage bufferedImage = new BufferedImage(tileSize, tileSize, BufferedImage.TYPE_4BYTE_ABGR);
+        Graphics2D g2d = bufferedImage.createGraphics();
+        g2d.setTransform(AffineTransform.getTranslateInstance(-point.x, -point.y));
+        try {
+            AbstractMapRenderer tilePainter = MapRendererFactory.getInstance().createActiveRenderer(g2d, mv, inactive);
+            tilePainter.render(data, virtual, tileToBounds(tile, zoom));
+        } finally {
+            g2d.dispose();
+        }
+        return bufferedImage;
+    }
+
+    private static Bounds tileToBounds(TileXY tile, int zoom) {
+        return new Bounds(yToLat(tile.getYIndex() + 1, zoom), xToLon(tile.getXIndex(), zoom),
+                yToLat(tile.getYIndex(), zoom), xToLon(tile.getXIndex() + 1, zoom));
+    }
+
+    private static double xToLon(int x, int zoom) {
+        return (x / Math.pow(2, zoom)) * 360 - 180;
+    }
+
+    private static double yToLat(int y, int zoom) {
+        double t = Math.PI - (2 * Math.PI * y) / (Math.pow(2, zoom));
+        return 180 / Math.PI * Math.atan((Math.exp(t) - Math.exp(-t)) / 2);
+    }
+
+    private static TileXY latLonToTile(double lat, double lon, int zoom) {
+        int xCoord = (int) Math.floor(Math.pow(2, zoom) * (180 + lon) / 360);
+        int yCoord = (int) Math.floor(Math.pow(2, zoom) * (1 - Math.log(Math.tan(Math.toRadians(lat)) + 1 / Math.cos(Math.toRadians(lat))) / Math.PI) / 2);
+        return new TileXY(xCoord, yCoord);
+    }
+
     @Override public String getToolTipText() {
         DataCountVisitor counter = new DataCountVisitor();
         for (final OsmPrimitive osm : data.allPrimitives()) {
@@ -1165,6 +1250,7 @@
 
     @Override
     public void processDatasetEvent(AbstractDatasetChangedEvent event) {
+        cache.clear();
         invalidate();
         setRequiresSaveToFile(true);
         setRequiresUploadToServer(event.getDataset().requiresUploadToServer());
@@ -1172,6 +1258,7 @@
 
     @Override
     public void selectionChanged(SelectionChangeEvent event) {
+        cache.clear();
         invalidate();
     }
 
