// License: GPL. For details, see LICENSE file. package org.openstreetmap.josm.data.osm.visitor.paint.relations; import java.util.ArrayList; import java.util.Collection; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import org.openstreetmap.josm.Main; import org.openstreetmap.josm.data.SelectionChangedListener; import org.openstreetmap.josm.data.osm.DataSet; import org.openstreetmap.josm.data.osm.Node; import org.openstreetmap.josm.data.osm.OsmPrimitive; import org.openstreetmap.josm.data.osm.Relation; import org.openstreetmap.josm.data.osm.Way; import org.openstreetmap.josm.data.osm.event.AbstractDatasetChangedEvent; import org.openstreetmap.josm.data.osm.event.DataChangedEvent; import org.openstreetmap.josm.data.osm.event.DataSetListener; import org.openstreetmap.josm.data.osm.event.NodeMovedEvent; import org.openstreetmap.josm.data.osm.event.PrimitivesAddedEvent; import org.openstreetmap.josm.data.osm.event.PrimitivesRemovedEvent; import org.openstreetmap.josm.data.osm.event.RelationMembersChangedEvent; import org.openstreetmap.josm.data.osm.event.TagsChangedEvent; import org.openstreetmap.josm.data.osm.event.WayNodesChangedEvent; import org.openstreetmap.josm.data.osm.visitor.paint.relations.Multipolygon.PolyData; import org.openstreetmap.josm.data.projection.Projection; import org.openstreetmap.josm.data.projection.ProjectionChangeListener; import org.openstreetmap.josm.gui.layer.LayerManager.LayerAddEvent; import org.openstreetmap.josm.gui.layer.LayerManager.LayerChangeListener; import org.openstreetmap.josm.gui.layer.LayerManager.LayerOrderChangeEvent; import org.openstreetmap.josm.gui.layer.LayerManager.LayerRemoveEvent; import org.openstreetmap.josm.gui.layer.OsmDataLayer; /** * A memory cache for {@link Multipolygon} objects. * @since 4623 */ public final class MultipolygonCache implements DataSetListener, LayerChangeListener, ProjectionChangeListener, SelectionChangedListener { private static final MultipolygonCache INSTANCE = new MultipolygonCache(); private final Map<DataSet, Map<Relation, Multipolygon>> cache; private final Collection<PolyData> selectedPolyData; private MultipolygonCache() { this.cache = new ConcurrentHashMap<>(); // see ticket 11833 this.selectedPolyData = new ArrayList<>(); Main.addProjectionChangeListener(this); DataSet.addSelectionListener(this); Main.getLayerManager().addLayerChangeListener(this); } /** * Replies the unique instance. * @return the unique instance */ public static MultipolygonCache getInstance() { return INSTANCE; } /** * Gets a multipolygon from cache. * @param r The multipolygon relation * @return A multipolygon object for the given relation, or {@code null} * @since 11779 */ public Multipolygon get(Relation r) { return get(r, false); } /** * Gets a multipolygon from cache. * @param r The multipolygon relation * @param forceRefresh if {@code true}, a new object will be created even of present in cache * @return A multipolygon object for the given relation, or {@code null} * @since 11779 */ public Multipolygon get(Relation r, boolean forceRefresh) { Multipolygon multipolygon = null; if (r != null) { Map<Relation, Multipolygon> map2 = cache.get(r.getDataSet()); if (map2 == null) { map2 = new ConcurrentHashMap<>(); cache.put(r.getDataSet(), map2); } multipolygon = map2.get(r); if (multipolygon == null || forceRefresh) { multipolygon = new Multipolygon(r); map2.put(r, multipolygon); for (PolyData pd : multipolygon.getCombinedPolygons()) { if (pd.isSelected()) { selectedPolyData.add(pd); } } } } return multipolygon; } /** * Clears the cache for the given dataset. * @param ds the data set */ public void clear(DataSet ds) { Map<Relation, Multipolygon> map2 = cache.remove(ds); if (map2 != null) { map2.clear(); } } /** * Clears the whole cache. */ public void clear() { cache.clear(); } private Collection<Map<Relation, Multipolygon>> getMapsFor(DataSet ds) { List<Map<Relation, Multipolygon>> result = new ArrayList<>(); Map<Relation, Multipolygon> map2 = cache.get(ds); if (map2 != null) { result.add(map2); } return result; } private static boolean isMultipolygon(OsmPrimitive p) { return p instanceof Relation && ((Relation) p).isMultipolygon(); } private void updateMultipolygonsReferringTo(AbstractDatasetChangedEvent event) { updateMultipolygonsReferringTo(event, event.getPrimitives(), event.getDataset()); } private void updateMultipolygonsReferringTo( final AbstractDatasetChangedEvent event, Collection<? extends OsmPrimitive> primitives, DataSet ds) { updateMultipolygonsReferringTo(event, primitives, ds, null); } private Collection<Map<Relation, Multipolygon>> updateMultipolygonsReferringTo( AbstractDatasetChangedEvent event, Collection<? extends OsmPrimitive> primitives, DataSet ds, Collection<Map<Relation, Multipolygon>> initialMaps) { Collection<Map<Relation, Multipolygon>> maps = initialMaps; if (primitives != null) { for (OsmPrimitive p : primitives) { if (isMultipolygon(p)) { if (maps == null) { maps = getMapsFor(ds); } processEvent(event, (Relation) p, maps); } else if (p instanceof Way && p.getDataSet() != null) { for (OsmPrimitive ref : p.getReferrers()) { if (isMultipolygon(ref)) { if (maps == null) { maps = getMapsFor(ds); } processEvent(event, (Relation) ref, maps); } } } else if (p instanceof Node && p.getDataSet() != null) { maps = updateMultipolygonsReferringTo(event, p.getReferrers(), ds, maps); } } } return maps; } private static void processEvent(AbstractDatasetChangedEvent event, Relation r, Collection<Map<Relation, Multipolygon>> maps) { if (event instanceof NodeMovedEvent || event instanceof WayNodesChangedEvent) { dispatchEvent(event, r, maps); } else if (event instanceof PrimitivesRemovedEvent) { if (event.getPrimitives().contains(r)) { removeMultipolygonFrom(r, maps); } } else { // Default (non-optimal) action: remove multipolygon from cache removeMultipolygonFrom(r, maps); } } private static void dispatchEvent(AbstractDatasetChangedEvent event, Relation r, Collection<Map<Relation, Multipolygon>> maps) { for (Map<Relation, Multipolygon> map : maps) { Multipolygon m = map.get(r); if (m != null) { for (PolyData pd : m.getCombinedPolygons()) { if (event instanceof NodeMovedEvent) { pd.nodeMoved((NodeMovedEvent) event); } else if (event instanceof WayNodesChangedEvent) { final boolean oldClosedStatus = pd.isClosed(); pd.wayNodesChanged((WayNodesChangedEvent) event); if (pd.isClosed() != oldClosedStatus) { removeMultipolygonFrom(r, maps); // see ticket #13591 return; } } } } } } private static void removeMultipolygonFrom(Relation r, Collection<Map<Relation, Multipolygon>> maps) { for (Map<Relation, Multipolygon> map : maps) { map.remove(r); } // Erase style cache for polygon members for (OsmPrimitive member : r.getMemberPrimitivesList()) { member.clearCachedStyle(); } } @Override public void primitivesAdded(PrimitivesAddedEvent event) { // Do nothing } @Override public void primitivesRemoved(PrimitivesRemovedEvent event) { updateMultipolygonsReferringTo(event); } @Override public void tagsChanged(TagsChangedEvent event) { updateMultipolygonsReferringTo(event); } @Override public void nodeMoved(NodeMovedEvent event) { updateMultipolygonsReferringTo(event); } @Override public void wayNodesChanged(WayNodesChangedEvent event) { updateMultipolygonsReferringTo(event); } @Override public void relationMembersChanged(RelationMembersChangedEvent event) { updateMultipolygonsReferringTo(event); } @Override public void otherDatasetChange(AbstractDatasetChangedEvent event) { // Do nothing } @Override public void dataChanged(DataChangedEvent event) { // Do not call updateMultipolygonsReferringTo as getPrimitives() // can return all the data set primitives for this event Collection<Map<Relation, Multipolygon>> maps = null; for (OsmPrimitive p : event.getPrimitives()) { if (isMultipolygon(p)) { if (maps == null) { maps = getMapsFor(event.getDataset()); } for (Map<Relation, Multipolygon> map : maps) { // DataChangedEvent is sent after downloading incomplete members (see #7131), // without having received RelationMembersChangedEvent or PrimitivesAddedEvent // OR when undoing a move of a large number of nodes (see #7195), // without having received NodeMovedEvent // This ensures concerned multipolygons will be correctly redrawn map.remove(p); } } } } @Override public void layerAdded(LayerAddEvent e) { // Do nothing } @Override public void layerOrderChanged(LayerOrderChangeEvent e) { // Do nothing } @Override public void layerRemoving(LayerRemoveEvent e) { if (e.getRemovedLayer() instanceof OsmDataLayer) { clear(((OsmDataLayer) e.getRemovedLayer()).data); } } @Override public void projectionChanged(Projection oldValue, Projection newValue) { clear(); } @Override public void selectionChanged(Collection<? extends OsmPrimitive> newSelection) { for (Iterator<PolyData> it = selectedPolyData.iterator(); it.hasNext();) { it.next().setSelected(false); it.remove(); } DataSet ds = null; Collection<Map<Relation, Multipolygon>> maps = null; for (OsmPrimitive p : newSelection) { if (p instanceof Way && p.getDataSet() != null) { if (ds == null) { ds = p.getDataSet(); } for (OsmPrimitive ref : p.getReferrers()) { if (isMultipolygon(ref)) { if (maps == null) { maps = getMapsFor(ds); } for (Map<Relation, Multipolygon> map : maps) { Multipolygon multipolygon = map.get(ref); if (multipolygon != null) { for (PolyData pd : multipolygon.getCombinedPolygons()) { if (pd.getWayIds().contains(p.getUniqueId())) { pd.setSelected(true); selectedPolyData.add(pd); } } } } } } } } } }