Changeset 19176 in josm for trunk/src


Ignore:
Timestamp:
2024-08-08T23:04:30+02:00 (3 months ago)
Author:
taylor.smock
Message:

Fix #11487: Have josm render data to tiles

This adds a new rendering method that renders async. This avoids blocking the UI.

Where this is useful:

  • Large datasets (think county or country level)

Where this is not useful:

  • Micromapping -- the tiles aren't being rendered exactly where they should be and there are some minor rendering artifacts.

Known issues:

  • Some tiles aren't exactly where they should be (off by a pixel or two -- by default, we use the old render method at z16+)
  • Rendering of tiles is slow -- there is some prework done to render tiles in batches. The primary reason rendering is slow is we are effectively rendering 25 total tiles (to avoid movement of text, we render 2 tiles in each directory and only keep the middle one)
  • Due to the above speed issue, hovering over an object will cause the highlight to render in slowly.

New advanced preferences:

  • mappaint.fast_render.tile_size -- controls the number of pixels in a tile
  • mappaint.fast_render.zlevel -- controls the maximum z level at which tiles are generated
Location:
trunk/src/org/openstreetmap/josm
Files:
4 added
4 edited

Legend:

Unmodified
Added
Removed
  • trunk/src/org/openstreetmap/josm/data/osm/visitor/paint/MapRendererFactory.java

    r17333 r19176  
    192192                tr("Renders the map using style rules in a set of style sheets.")
    193193        );
     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        );
    194199    }
    195200
     
    319324     */
    320325    public boolean isWireframeMapRendererActive() {
    321         return WireframeMapRenderer.class.equals(activeRenderer);
     326        return isMapRendererActive(WireframeMapRenderer.class);
     327    }
     328
     329    /**
     330     * <p>Replies true, if currently the specified map renderer is active. Otherwise, false.</p>
     331     *
     332     * @param clazz The class that we are checking to see if it is the current renderer
     333     * @return true, if currently the wireframe map renderer is active. Otherwise, false
     334     * @since 19176
     335     */
     336    public boolean isMapRendererActive(Class<? extends AbstractMapRenderer> clazz) {
     337        return clazz.equals(activeRenderer);
    322338    }
    323339}
  • trunk/src/org/openstreetmap/josm/gui/MainMenu.java

    r18814 r19176  
    104104import org.openstreetmap.josm.actions.SplitWayAction;
    105105import org.openstreetmap.josm.actions.TaggingPresetSearchAction;
     106import org.openstreetmap.josm.actions.TiledRenderToggleAction;
    106107import org.openstreetmap.josm.actions.UnGlueAction;
    107108import org.openstreetmap.josm.actions.UnJoinNodeWayAction;
     
    249250    /** View / Wireframe View */
    250251    public final WireframeToggleAction wireFrameToggleAction = new WireframeToggleAction();
     252    /** View / Tiled Rendering */
     253    public final TiledRenderToggleAction tiledRenderToggleAction = new TiledRenderToggleAction();
    251254    /** View / Hatch area outside download */
    252255    public final DrawBoundariesOfDownloadedDataAction drawBoundariesOfDownloadedDataAction = new DrawBoundariesOfDownloadedDataAction();
     
    800803        wireframe.setAccelerator(wireFrameToggleAction.getShortcut().getKeyStroke());
    801804        wireFrameToggleAction.addButtonModel(wireframe.getModel());
     805        // -- tiled render toggle action -- not intended to be permanently an "Expert" mode option
     806        final JCheckBoxMenuItem tiledRender = new JCheckBoxMenuItem(tiledRenderToggleAction);
     807        viewMenu.add(tiledRender);
     808        tiledRenderToggleAction.addButtonModel(tiledRender.getModel());
     809        ExpertToggleAction.addVisibilitySwitcher(tiledRender);
     810        // -- hatch toggle action
    802811        final JCheckBoxMenuItem hatchAreaOutsideDownloadMenuItem = drawBoundariesOfDownloadedDataAction.getCheckbox();
    803812        viewMenu.add(hatchAreaOutsideDownloadMenuItem);
  • trunk/src/org/openstreetmap/josm/gui/MapView.java

    r19108 r19176  
    77import java.awt.BasicStroke;
    88import java.awt.Color;
     9import java.awt.Component;
    910import java.awt.Dimension;
    1011import java.awt.Graphics;
    1112import java.awt.Graphics2D;
     13import java.awt.GraphicsEnvironment;
    1214import java.awt.Point;
    1315import java.awt.Rectangle;
    1416import java.awt.Shape;
    1517import java.awt.Stroke;
     18import java.awt.Transparency;
    1619import java.awt.event.ComponentAdapter;
    1720import java.awt.event.ComponentEvent;
     
    331334    }
    332335
     336    private static BufferedImage getAcceleratedImage(Component mv, int width, int height) {
     337        if (GraphicsEnvironment.isHeadless()) {
     338            return new BufferedImage(width, height, BufferedImage.TYPE_3BYTE_BGR);
     339        }
     340        return mv.getGraphicsConfiguration().createCompatibleImage(width, height, Transparency.OPAQUE);
     341    }
     342
    333343    // remebered geometry of the component
    334344    private Dimension oldSize;
     
    549559
    550560        if (null == offscreenBuffer || offscreenBuffer.getWidth() != width || offscreenBuffer.getHeight() != height) {
    551             offscreenBuffer = new BufferedImage(width, height, BufferedImage.TYPE_3BYTE_BGR);
     561            offscreenBuffer = getAcceleratedImage(this, width, height);
    552562        }
    553563
     
    555565            if (null == nonChangedLayersBuffer
    556566                    || nonChangedLayersBuffer.getWidth() != width || nonChangedLayersBuffer.getHeight() != height) {
    557                 nonChangedLayersBuffer = new BufferedImage(width, height, BufferedImage.TYPE_3BYTE_BGR);
     567                nonChangedLayersBuffer = getAcceleratedImage(this, width, height);
    558568            }
    559569            Graphics2D g2 = nonChangedLayersBuffer.createGraphics();
  • trunk/src/org/openstreetmap/josm/gui/layer/OsmDataLayer.java

    r19116 r19176  
    5050import javax.swing.JScrollPane;
    5151
     52import org.apache.commons.jcs3.access.CacheAccess;
     53import org.openstreetmap.gui.jmapviewer.OsmMercator;
    5254import org.openstreetmap.josm.actions.AutoScaleAction;
    5355import org.openstreetmap.josm.actions.ExpertToggleAction;
     
    5759import org.openstreetmap.josm.data.Bounds;
    5860import org.openstreetmap.josm.data.Data;
     61import org.openstreetmap.josm.data.IBounds;
    5962import org.openstreetmap.josm.data.ProjectionBounds;
    6063import org.openstreetmap.josm.data.UndoRedoHandler;
     64import org.openstreetmap.josm.data.cache.JCSCacheManager;
    6165import org.openstreetmap.josm.data.conflict.Conflict;
    6266import org.openstreetmap.josm.data.conflict.ConflictCollection;
     
    7175import org.openstreetmap.josm.data.gpx.IGpxTrackSegment;
    7276import org.openstreetmap.josm.data.gpx.WayPoint;
     77import org.openstreetmap.josm.data.osm.BBox;
    7378import org.openstreetmap.josm.data.osm.DataIntegrityProblemException;
    7479import org.openstreetmap.josm.data.osm.DataSelectionListener;
     
    7883import org.openstreetmap.josm.data.osm.DownloadPolicy;
    7984import org.openstreetmap.josm.data.osm.HighlightUpdateListener;
     85import org.openstreetmap.josm.data.osm.INode;
    8086import org.openstreetmap.josm.data.osm.IPrimitive;
     87import org.openstreetmap.josm.data.osm.IRelation;
     88import org.openstreetmap.josm.data.osm.IWay;
    8189import org.openstreetmap.josm.data.osm.Node;
    8290import org.openstreetmap.josm.data.osm.OsmPrimitive;
     
    92100import org.openstreetmap.josm.data.osm.visitor.OsmPrimitiveVisitor;
    93101import org.openstreetmap.josm.data.osm.visitor.paint.AbstractMapRenderer;
     102import org.openstreetmap.josm.data.osm.visitor.paint.ImageCache;
    94103import org.openstreetmap.josm.data.osm.visitor.paint.MapRendererFactory;
     104import org.openstreetmap.josm.data.osm.visitor.paint.StyledTiledMapRenderer;
     105import org.openstreetmap.josm.data.osm.visitor.paint.TileZXY;
    95106import org.openstreetmap.josm.data.osm.visitor.paint.relations.MultipolygonCache;
    96107import org.openstreetmap.josm.data.preferences.BooleanProperty;
     
    105116import org.openstreetmap.josm.gui.MapView;
    106117import org.openstreetmap.josm.gui.MapViewState.MapViewPoint;
     118import org.openstreetmap.josm.gui.NavigatableComponent;
     119import org.openstreetmap.josm.gui.PrimitiveHoverListener;
    107120import org.openstreetmap.josm.gui.datatransfer.ClipboardUtils;
    108121import org.openstreetmap.josm.gui.datatransfer.data.OsmLayerTransferData;
     
    145158 * @since 17
    146159 */
    147 public class OsmDataLayer extends AbstractOsmDataLayer implements Listener, DataSelectionListener, HighlightUpdateListener {
     160public class OsmDataLayer extends AbstractOsmDataLayer
     161        implements Listener, DataSelectionListener, HighlightUpdateListener, PrimitiveHoverListener {
     162    private static final int MAX_ZOOM = 30;
     163    private static final int OVER_ZOOM = 2;
    148164    private static final int HATCHED_SIZE = 15;
    149165    // U+2205 EMPTY SET
     
    156172    /** Flag used to know if the layer is being uploaded */
    157173    private final AtomicBoolean isUploadInProgress = new AtomicBoolean(false);
     174    /**
     175     * A cache used for painting
     176     */
     177    private final CacheAccess<TileZXY, ImageCache> cache = JCSCacheManager.getCache("osmDataLayer:" + System.identityHashCode(this));
     178    /** The map paint index that was painted (used to invalidate {@link #cache}) */
     179    private int lastDataIdx;
     180    /** The last zoom level (we invalidate all tiles when switching layers) */
     181    private int lastZoom;
     182    private boolean hoverListenerAdded;
    158183
    159184    /**
     
    498523     */
    499524    @Override public void paint(final Graphics2D g, final MapView mv, Bounds box) {
     525        if (!hoverListenerAdded) {
     526            MainApplication.getMap().mapView.addPrimitiveHoverListener(this);
     527            hoverListenerAdded = true;
     528        }
    500529        boolean active = mv.getLayerManager().getActiveLayer() == this;
    501530        boolean inactive = !active && Config.getPref().getBoolean("draw.data.inactive_color", true);
    502531        boolean virtual = !inactive && mv.isVirtualNodesEnabled();
    503 
     532        paintHatch(g, mv, active);
     533        paintData(g, mv, box, inactive, virtual);
     534    }
     535
     536    private void paintHatch(final Graphics2D g, final MapView mv, boolean active) {
    504537        // draw the hatched area for non-downloaded region. only draw if we're the active
    505538        // and bounds are defined; don't draw for inactive layers or loaded GPX files etc
    506         if (active && DrawingPreference.SOURCE_BOUNDS_PROP.get() && !data.getDataSources().isEmpty()) {
     539        if (active && Boolean.TRUE.equals(DrawingPreference.SOURCE_BOUNDS_PROP.get()) && !data.getDataSources().isEmpty()) {
    507540            // initialize area with current viewport
    508541            Rectangle b = mv.getBounds();
     
    537570            }
    538571        }
    539 
     572    }
     573
     574    private void paintData(final Graphics2D g, final MapView mv, Bounds box, boolean inactive, boolean virtual) {
     575        // Used to invalidate cache
     576        int zoom = getZoom(mv);
     577        if (zoom != lastZoom) {
     578            // We just mark the previous zoom as dirty before moving in.
     579            // It means we don't have to traverse up/down z-levels marking tiles as dirty (this can get *very* expensive).
     580            this.cache.getMatching("TileZXY\\{" + lastZoom + "/.*")
     581                    .forEach((tile, imageCache) -> this.cache.put(tile, imageCache.becomeDirty()));
     582        }
     583        lastZoom = zoom;
    540584        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);
     585        if (!(painter instanceof StyledTiledMapRenderer) || zoom - OVER_ZOOM > Config.getPref().getInt("mappaint.fast_render.zlevel", 16)) {
     586            painter.enableSlowOperations(mv.getMapMover() == null || !mv.getMapMover().movementInProgress()
     587                    || !PROPERTY_HIDE_LABELS_WHILE_DRAGGING.get());
     588        } else {
     589            StyledTiledMapRenderer renderer = (StyledTiledMapRenderer) painter;
     590            renderer.setCache(box, this.cache, zoom, (tile) -> {
     591                /* This causes "bouncing". I'm not certain why.
     592                if (oldState.equalsInWindow(mv.getState())) { (oldstate = mv.getState())
     593                    final Point upperLeft = mv.getPoint(tile);
     594                    final Point lowerRight = mv.getPoint(new TileZXY(tile.zoom(), tile.x() + 1, tile.y() + 1));
     595                    GuiHelper.runInEDT(() -> mv.repaint(0, upperLeft.x, upperLeft.y, lowerRight.x - upperLeft.x, lowerRight.y - upperLeft.y));
     596                }
     597                 */
     598                // Invalidate doesn't trigger an instant repaint, but putting this off lets us batch the repaints needed for multiple tiles
     599                MainApplication.worker.submit(this::invalidate);
     600            });
     601
     602            if (this.data.getMappaintCacheIndex() != this.lastDataIdx) {
     603                this.cache.clear();
     604                this.lastDataIdx = this.data.getMappaintCacheIndex();
     605                Logging.trace("OsmDataLayer {0} paint cache cleared", this.getName());
     606            }
     607        }
     608        painter.render(this.data, virtual, box);
    544609        MainApplication.getMap().conflictDialog.paintConflicts(g, mv);
    545610    }
     
    11481213        removeClipboardDataFor(this);
    11491214        recentRelations.clear();
     1215        if (hoverListenerAdded) {
     1216            hoverListenerAdded = false;
     1217            MainApplication.getMap().mapView.removePrimitiveHoverListener(this);
     1218        }
    11501219    }
    11511220
     
    11661235    @Override
    11671236    public void processDatasetEvent(AbstractDatasetChangedEvent event) {
     1237        resetTiles(event.getPrimitives());
    11681238        invalidate();
    11691239        setRequiresSaveToFile(true);
     
    11731243    @Override
    11741244    public void selectionChanged(SelectionChangeEvent event) {
     1245        Set<IPrimitive> primitives = new HashSet<>(event.getAdded());
     1246        primitives.addAll(event.getRemoved());
     1247        resetTiles(primitives);
    11751248        invalidate();
     1249    }
     1250
     1251    private void resetTiles(Collection<? extends IPrimitive> primitives) {
     1252        if (primitives.size() >= this.data.allNonDeletedCompletePrimitives().size() || primitives.size() > 100) {
     1253            dirtyAll();
     1254            return;
     1255        }
     1256        if (primitives.size() < 5) {
     1257            for (IPrimitive p : primitives) {
     1258                resetTiles(p);
     1259            }
     1260            return;
     1261        }
     1262        // Most of the time, a selection is going to be a big box.
     1263        // So we want to optimize for that case.
     1264        BBox box = null;
     1265        for (IPrimitive primitive : primitives) {
     1266            if (primitive == null || primitive.getDataSet() != this.getDataSet()) continue;
     1267            final Collection<? extends IPrimitive> referrers = primitive.getReferrers();
     1268            if (box == null) {
     1269                box = new BBox(primitive.getBBox());
     1270            } else {
     1271                box.addPrimitive(primitive, 0);
     1272            }
     1273            for (IPrimitive referrer : referrers) {
     1274                box.addPrimitive(referrer, 0);
     1275            }
     1276        }
     1277        if (box != null) {
     1278            resetBounds(box.getMinLat(), box.getMinLon(), box.getMaxLat(), box.getMaxLon());
     1279        }
     1280    }
     1281
     1282    private void resetTiles(IPrimitive p) {
     1283        if (p instanceof INode) {
     1284            resetBounds(getInvalidatedBBox((INode) p, null));
     1285        } else if (p instanceof IWay) {
     1286            IWay<?> way = (IWay<?>) p;
     1287            for (int i = 0; i < way.getNodesCount() - 1; i++) {
     1288                resetBounds(getInvalidatedBBox(way.getNode(i), way.getNode(i + 1)));
     1289            }
     1290        } else if (p instanceof IRelation<?>) {
     1291            for (IPrimitive member : ((IRelation<?>) p).getMemberPrimitivesList()) {
     1292                resetTiles(member);
     1293            }
     1294        } else {
     1295            throw new IllegalArgumentException("Unsupported primitive type: " + p.getClass().getName());
     1296        }
     1297    }
     1298
     1299    private BBox getInvalidatedBBox(INode first, INode second) {
     1300        final BBox bbox = new BBox(first);
     1301        if (second != null) {
     1302            bbox.add(second);
     1303        }
     1304        return bbox;
     1305    }
     1306
     1307    private void resetBounds(IBounds bbox) {
     1308        resetBounds(bbox.getMinLat(), bbox.getMinLon(), bbox.getMaxLat(), bbox.getMaxLon());
     1309    }
     1310
     1311    private void resetBounds(double minLat, double minLon, double maxLat, double maxLon) {
     1312        // Get the current zoom. Hopefully we aren't painting with a different navigatable component
     1313        final int currentZoom = lastZoom;
     1314        final AtomicInteger counter = new AtomicInteger();
     1315        TileZXY.boundsToTiles(minLat, minLon, maxLat, maxLon, currentZoom, 1).limit(100).forEach(tile -> {
     1316            final ImageCache imageCache = this.cache.get(tile);
     1317            if (imageCache != null && !imageCache.isDirty()) {
     1318                this.cache.put(tile, imageCache.becomeDirty());
     1319            }
     1320            counter.incrementAndGet();
     1321        });
     1322        if (counter.get() > 100) {
     1323            dirtyAll();
     1324        }
     1325    }
     1326
     1327    private void dirtyAll() {
     1328        this.cache.getMatching(".*").forEach((key, value) -> {
     1329            this.cache.remove(key);
     1330            this.cache.put(key, value.becomeDirty());
     1331        });
     1332    }
     1333
     1334    /**
     1335     * Get the zoom for a {@link NavigatableComponent}
     1336     * @param navigatableComponent The component to get the zoom from
     1337     * @return The zoom for the navigatable component
     1338     */
     1339    private static int getZoom(NavigatableComponent navigatableComponent) {
     1340        final double scale = navigatableComponent.getScale();
     1341        // We might have to fall back to the old method if user is reprojecting
     1342        // 256 is the "target" size, (TODO check HiDPI!)
     1343        final int targetSize = Config.getPref().getInt("mappaint.fast_render.tile_size", 256);
     1344        final double topResolution = 2 * Math.PI * OsmMercator.EARTH_RADIUS / targetSize;
     1345        int zoom;
     1346        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)
     1347            if (scale > topResolution / Math.pow(2, zoom)) {
     1348                zoom = zoom > 0 ? zoom - 1 : zoom;
     1349                break;
     1350            }
     1351        }
     1352        // We paint at a few levels higher, note that the tiles are appropriately sized (if 256 is the "target" size, the tiles should be
     1353        // 64px square).
     1354        zoom += OVER_ZOOM;
     1355        return zoom;
    11761356    }
    11771357
     
    13101490
    13111491    @Override
     1492    public void primitiveHovered(PrimitiveHoverEvent e) {
     1493        List<IPrimitive> primitives = new ArrayList<>(2);
     1494        primitives.add(e.getHoveredPrimitive());
     1495        primitives.add(e.getPreviousPrimitive());
     1496        primitives.removeIf(Objects::isNull);
     1497        resetTiles(primitives);
     1498        this.invalidate();
     1499    }
     1500
     1501    @Override
    13121502    public void setName(String name) {
    13131503        if (data != null) {
Note: See TracChangeset for help on using the changeset viewer.