Ticket #11487: 11487.2.patch

File 11487.2.patch, 16.4 KB (added by taylor.smock, 3 years ago)

WIP -- DO NOT APPLY -- Allows for arbitrary zoom levels, but do note that pixelation may occur; render code checks that the mapview hasn't changed between paint request, paint, and store; selection/highlight now only rerenders tiles with the selection/highlight change (once); use old render path at high zoom (configurable)

  • 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  
    1111import java.awt.Composite;
    1212import java.awt.Graphics2D;
    1313import java.awt.GridBagLayout;
     14import java.awt.Point;
    1415import java.awt.Rectangle;
    1516import java.awt.TexturePaint;
    1617import java.awt.datatransfer.Transferable;
    1718import java.awt.datatransfer.UnsupportedFlavorException;
    1819import java.awt.event.ActionEvent;
     20import java.awt.geom.AffineTransform;
    1921import java.awt.geom.Area;
    2022import java.awt.geom.Path2D;
    2123import java.awt.geom.Rectangle2D;
     
    3739import java.util.concurrent.CopyOnWriteArrayList;
    3840import java.util.concurrent.atomic.AtomicBoolean;
    3941import java.util.concurrent.atomic.AtomicInteger;
     42import java.util.function.Supplier;
    4043import java.util.regex.Pattern;
    4144import java.util.stream.Collectors;
    4245import java.util.stream.Stream;
     
    4952import javax.swing.JPanel;
    5053import javax.swing.JScrollPane;
    5154
     55import org.apache.commons.jcs3.access.CacheAccess;
     56import org.openstreetmap.gui.jmapviewer.OsmMercator;
     57import org.openstreetmap.gui.jmapviewer.TileXY;
    5258import org.openstreetmap.josm.actions.AutoScaleAction;
    5359import org.openstreetmap.josm.actions.ExpertToggleAction;
    5460import org.openstreetmap.josm.actions.RenameLayerAction;
     
    5864import org.openstreetmap.josm.data.Data;
    5965import org.openstreetmap.josm.data.ProjectionBounds;
    6066import org.openstreetmap.josm.data.UndoRedoHandler;
     67import org.openstreetmap.josm.data.cache.JCSCacheManager;
    6168import org.openstreetmap.josm.data.conflict.Conflict;
    6269import org.openstreetmap.josm.data.conflict.ConflictCollection;
    6370import org.openstreetmap.josm.data.coor.EastNorth;
     
    7077import org.openstreetmap.josm.data.gpx.GpxTrackSegment;
    7178import org.openstreetmap.josm.data.gpx.IGpxTrackSegment;
    7279import org.openstreetmap.josm.data.gpx.WayPoint;
     80import org.openstreetmap.josm.data.osm.BBox;
    7381import org.openstreetmap.josm.data.osm.DataIntegrityProblemException;
    7482import org.openstreetmap.josm.data.osm.DataSelectionListener;
    7583import org.openstreetmap.josm.data.osm.DataSet;
     
    7785import org.openstreetmap.josm.data.osm.DatasetConsistencyTest;
    7886import org.openstreetmap.josm.data.osm.DownloadPolicy;
    7987import org.openstreetmap.josm.data.osm.HighlightUpdateListener;
     88import org.openstreetmap.josm.data.osm.INode;
    8089import org.openstreetmap.josm.data.osm.IPrimitive;
     90import org.openstreetmap.josm.data.osm.IWay;
    8191import org.openstreetmap.josm.data.osm.Node;
    8292import org.openstreetmap.josm.data.osm.OsmPrimitive;
    8393import org.openstreetmap.josm.data.osm.OsmPrimitiveComparator;
     
    104114import org.openstreetmap.josm.gui.MapFrame;
    105115import org.openstreetmap.josm.gui.MapView;
    106116import org.openstreetmap.josm.gui.MapViewState.MapViewPoint;
     117import org.openstreetmap.josm.gui.PrimitiveHoverListener;
    107118import org.openstreetmap.josm.gui.datatransfer.ClipboardUtils;
    108119import org.openstreetmap.josm.gui.datatransfer.data.OsmLayerTransferData;
    109120import org.openstreetmap.josm.gui.dialogs.LayerListDialog;
     
    144155 * @author imi
    145156 * @since 17
    146157 */
    147 public class OsmDataLayer extends AbstractOsmDataLayer implements Listener, DataSelectionListener, HighlightUpdateListener {
     158public class OsmDataLayer extends AbstractOsmDataLayer
     159        implements Listener, DataSelectionListener, HighlightUpdateListener, PrimitiveHoverListener {
    148160    private static final int HATCHED_SIZE = 15;
    149161    // U+2205 EMPTY SET
    150162    private static final String IS_EMPTY_SYMBOL = "\u2205";
     
    155167    private boolean requiresUploadToServer;
    156168    /** Flag used to know if the layer is being uploaded */
    157169    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;
    158180
    159181    /**
    160182     * List of validation errors in this layer.
     
    497519     * Draw nodes last to overlap the ways they belong to.
    498520     */
    499521    @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        }
    500526        boolean active = mv.getLayerManager().getActiveLayer() == this;
    501527        boolean inactive = !active && Config.getPref().getBoolean("draw.data.inactive_color", true);
    502528        boolean virtual = !inactive && mv.isVirtualNodesEnabled();
     
    537563            }
    538564        }
    539565
    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        }
    544650        MainApplication.getMap().conflictDialog.paintConflicts(g, mv);
    545651    }
    546652
     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
    547704    @Override public String getToolTipText() {
    548705        DataCountVisitor counter = new DataCountVisitor();
    549706        for (final OsmPrimitive osm : data.allPrimitives()) {
     
    11471304        validationErrors.clear();
    11481305        removeClipboardDataFor(this);
    11491306        recentRelations.clear();
     1307        if (hoverListenerAdded) {
     1308            hoverListenerAdded = false;
     1309            MainApplication.getMap().mapView.removePrimitiveHoverListener(this);
     1310        }
    11501311    }
    11511312
    11521313    protected static void removeClipboardDataFor(OsmDataLayer osm) {
     
    11651326
    11661327    @Override
    11671328    public void processDatasetEvent(AbstractDatasetChangedEvent event) {
     1329        resetTiles(event.getPrimitives());
    11681330        invalidate();
    11691331        setRequiresSaveToFile(true);
    11701332        setRequiresUploadToServer(event.getDataset().requiresUploadToServer());
     
    11721334
    11731335    @Override
    11741336    public void selectionChanged(SelectionChangeEvent event) {
     1337        Set<IPrimitive> primitives = new HashSet<>(event.getAdded());
     1338        primitives.addAll(event.getRemoved());
     1339        resetTiles(primitives);
    11751340        invalidate();
    11761341    }
    11771342
     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
    11781352    @Override
    11791353    public void projectionChanged(Projection oldValue, Projection newValue) {
    11801354         // No reprojection required. The dataset itself is registered as projection
     
    13071481        invalidate();
    13081482    }
    13091483
     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
    13101503    @Override
    13111504    public void setName(String name) {
    13121505        if (data != null) {