001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.data.osm.visitor.paint.relations; 003 004import java.util.ArrayList; 005import java.util.Collection; 006import java.util.Iterator; 007import java.util.List; 008import java.util.Map; 009import java.util.concurrent.ConcurrentHashMap; 010 011import org.openstreetmap.josm.Main; 012import org.openstreetmap.josm.data.osm.DataSelectionListener; 013import org.openstreetmap.josm.data.osm.DataSet; 014import org.openstreetmap.josm.data.osm.Node; 015import org.openstreetmap.josm.data.osm.OsmPrimitive; 016import org.openstreetmap.josm.data.osm.Relation; 017import org.openstreetmap.josm.data.osm.Way; 018import org.openstreetmap.josm.data.osm.event.AbstractDatasetChangedEvent; 019import org.openstreetmap.josm.data.osm.event.DataChangedEvent; 020import org.openstreetmap.josm.data.osm.event.DataSetListener; 021import org.openstreetmap.josm.data.osm.event.NodeMovedEvent; 022import org.openstreetmap.josm.data.osm.event.PrimitivesAddedEvent; 023import org.openstreetmap.josm.data.osm.event.PrimitivesRemovedEvent; 024import org.openstreetmap.josm.data.osm.event.RelationMembersChangedEvent; 025import org.openstreetmap.josm.data.osm.event.SelectionEventManager; 026import org.openstreetmap.josm.data.osm.event.TagsChangedEvent; 027import org.openstreetmap.josm.data.osm.event.WayNodesChangedEvent; 028import org.openstreetmap.josm.data.osm.visitor.paint.relations.Multipolygon.PolyData; 029import org.openstreetmap.josm.data.projection.Projection; 030import org.openstreetmap.josm.data.projection.ProjectionChangeListener; 031import org.openstreetmap.josm.gui.MainApplication; 032import org.openstreetmap.josm.gui.layer.LayerManager.LayerAddEvent; 033import org.openstreetmap.josm.gui.layer.LayerManager.LayerChangeListener; 034import org.openstreetmap.josm.gui.layer.LayerManager.LayerOrderChangeEvent; 035import org.openstreetmap.josm.gui.layer.LayerManager.LayerRemoveEvent; 036import org.openstreetmap.josm.gui.layer.OsmDataLayer; 037 038/** 039 * A memory cache for {@link Multipolygon} objects. 040 * @since 4623 041 */ 042public final class MultipolygonCache implements DataSetListener, LayerChangeListener, ProjectionChangeListener, DataSelectionListener { 043 044 private static final MultipolygonCache INSTANCE = new MultipolygonCache(); 045 046 private final Map<DataSet, Map<Relation, Multipolygon>> cache = new ConcurrentHashMap<>(); // see ticket 11833 047 048 private final Collection<PolyData> selectedPolyData = new ArrayList<>(); 049 050 private MultipolygonCache() { 051 Main.addProjectionChangeListener(this); 052 SelectionEventManager.getInstance().addSelectionListener(this); 053 MainApplication.getLayerManager().addLayerChangeListener(this); 054 } 055 056 /** 057 * Replies the unique instance. 058 * @return the unique instance 059 */ 060 public static MultipolygonCache getInstance() { 061 return INSTANCE; 062 } 063 064 /** 065 * Gets a multipolygon from cache. 066 * @param r The multipolygon relation 067 * @return A multipolygon object for the given relation, or {@code null} 068 * @since 11779 069 */ 070 public Multipolygon get(Relation r) { 071 return get(r, false); 072 } 073 074 /** 075 * Gets a multipolygon from cache. 076 * @param r The multipolygon relation 077 * @param forceRefresh if {@code true}, a new object will be created even of present in cache 078 * @return A multipolygon object for the given relation, or {@code null} 079 * @since 11779 080 */ 081 public Multipolygon get(Relation r, boolean forceRefresh) { 082 Multipolygon multipolygon = null; 083 if (r != null && r.getDataSet() != null) { 084 Map<Relation, Multipolygon> map2 = cache.get(r.getDataSet()); 085 if (map2 == null) { 086 map2 = new ConcurrentHashMap<>(); 087 cache.put(r.getDataSet(), map2); 088 } 089 multipolygon = map2.get(r); 090 if (multipolygon == null || forceRefresh) { 091 multipolygon = new Multipolygon(r); 092 map2.put(r, multipolygon); 093 synchronized (this) { 094 for (PolyData pd : multipolygon.getCombinedPolygons()) { 095 if (pd.isSelected()) { 096 selectedPolyData.add(pd); 097 } 098 } 099 } 100 } 101 } 102 return multipolygon; 103 } 104 105 /** 106 * Clears the cache for the given dataset. 107 * @param ds the data set 108 */ 109 public void clear(DataSet ds) { 110 Map<Relation, Multipolygon> map2 = cache.remove(ds); 111 if (map2 != null) { 112 map2.clear(); 113 } 114 } 115 116 /** 117 * Clears the whole cache. 118 */ 119 public void clear() { 120 cache.clear(); 121 } 122 123 private Collection<Map<Relation, Multipolygon>> getMapsFor(DataSet ds) { 124 List<Map<Relation, Multipolygon>> result = new ArrayList<>(); 125 Map<Relation, Multipolygon> map2 = cache.get(ds); 126 if (map2 != null) { 127 result.add(map2); 128 } 129 return result; 130 } 131 132 private static boolean isMultipolygon(OsmPrimitive p) { 133 return p instanceof Relation && ((Relation) p).isMultipolygon(); 134 } 135 136 private void updateMultipolygonsReferringTo(AbstractDatasetChangedEvent event) { 137 updateMultipolygonsReferringTo(event, event.getPrimitives(), event.getDataset()); 138 } 139 140 private void updateMultipolygonsReferringTo( 141 final AbstractDatasetChangedEvent event, Collection<? extends OsmPrimitive> primitives, DataSet ds) { 142 updateMultipolygonsReferringTo(event, primitives, ds, null); 143 } 144 145 private Collection<Map<Relation, Multipolygon>> updateMultipolygonsReferringTo( 146 AbstractDatasetChangedEvent event, Collection<? extends OsmPrimitive> primitives, 147 DataSet ds, Collection<Map<Relation, Multipolygon>> initialMaps) { 148 Collection<Map<Relation, Multipolygon>> maps = initialMaps; 149 if (primitives != null) { 150 for (OsmPrimitive p : primitives) { 151 if (isMultipolygon(p)) { 152 if (maps == null) { 153 maps = getMapsFor(ds); 154 } 155 processEvent(event, (Relation) p, maps); 156 157 } else if (p instanceof Way && p.getDataSet() != null) { 158 for (OsmPrimitive ref : p.getReferrers()) { 159 if (isMultipolygon(ref)) { 160 if (maps == null) { 161 maps = getMapsFor(ds); 162 } 163 processEvent(event, (Relation) ref, maps); 164 } 165 } 166 } else if (p instanceof Node && p.getDataSet() != null) { 167 maps = updateMultipolygonsReferringTo(event, p.getReferrers(), ds, maps); 168 } 169 } 170 } 171 return maps; 172 } 173 174 private static void processEvent(AbstractDatasetChangedEvent event, Relation r, Collection<Map<Relation, Multipolygon>> maps) { 175 if (event instanceof NodeMovedEvent || event instanceof WayNodesChangedEvent) { 176 dispatchEvent(event, r, maps); 177 } else if (event instanceof PrimitivesRemovedEvent) { 178 if (event.getPrimitives().contains(r)) { 179 removeMultipolygonFrom(r, maps); 180 } 181 } else { 182 // Default (non-optimal) action: remove multipolygon from cache 183 removeMultipolygonFrom(r, maps); 184 } 185 } 186 187 private static void dispatchEvent(AbstractDatasetChangedEvent event, Relation r, Collection<Map<Relation, Multipolygon>> maps) { 188 for (Map<Relation, Multipolygon> map : maps) { 189 Multipolygon m = map.get(r); 190 if (m != null) { 191 for (PolyData pd : m.getCombinedPolygons()) { 192 if (event instanceof NodeMovedEvent) { 193 pd.nodeMoved((NodeMovedEvent) event); 194 } else if (event instanceof WayNodesChangedEvent) { 195 final boolean oldClosedStatus = pd.isClosed(); 196 pd.wayNodesChanged((WayNodesChangedEvent) event); 197 if (pd.isClosed() != oldClosedStatus) { 198 removeMultipolygonFrom(r, maps); // see ticket #13591 199 return; 200 } 201 } 202 } 203 } 204 } 205 } 206 207 private static void removeMultipolygonFrom(Relation r, Collection<Map<Relation, Multipolygon>> maps) { 208 for (Map<Relation, Multipolygon> map : maps) { 209 map.remove(r); 210 } 211 // Erase style cache for polygon members 212 for (OsmPrimitive member : r.getMemberPrimitivesList()) { 213 member.clearCachedStyle(); 214 } 215 } 216 217 @Override 218 public void primitivesAdded(PrimitivesAddedEvent event) { 219 // Do nothing 220 } 221 222 @Override 223 public void primitivesRemoved(PrimitivesRemovedEvent event) { 224 updateMultipolygonsReferringTo(event); 225 } 226 227 @Override 228 public void tagsChanged(TagsChangedEvent event) { 229 updateMultipolygonsReferringTo(event); 230 } 231 232 @Override 233 public void nodeMoved(NodeMovedEvent event) { 234 updateMultipolygonsReferringTo(event); 235 } 236 237 @Override 238 public void wayNodesChanged(WayNodesChangedEvent event) { 239 updateMultipolygonsReferringTo(event); 240 } 241 242 @Override 243 public void relationMembersChanged(RelationMembersChangedEvent event) { 244 updateMultipolygonsReferringTo(event); 245 } 246 247 @Override 248 public void otherDatasetChange(AbstractDatasetChangedEvent event) { 249 // Do nothing 250 } 251 252 @Override 253 public void dataChanged(DataChangedEvent event) { 254 // Do not call updateMultipolygonsReferringTo as getPrimitives() 255 // can return all the data set primitives for this event 256 Collection<Map<Relation, Multipolygon>> maps = null; 257 for (OsmPrimitive p : event.getPrimitives()) { 258 if (isMultipolygon(p)) { 259 if (maps == null) { 260 maps = getMapsFor(event.getDataset()); 261 } 262 for (Map<Relation, Multipolygon> map : maps) { 263 // DataChangedEvent is sent after downloading incomplete members (see #7131), 264 // without having received RelationMembersChangedEvent or PrimitivesAddedEvent 265 // OR when undoing a move of a large number of nodes (see #7195), 266 // without having received NodeMovedEvent 267 // This ensures concerned multipolygons will be correctly redrawn 268 map.remove(p); 269 } 270 } 271 } 272 } 273 274 @Override 275 public void layerAdded(LayerAddEvent e) { 276 // Do nothing 277 } 278 279 @Override 280 public void layerOrderChanged(LayerOrderChangeEvent e) { 281 // Do nothing 282 } 283 284 @Override 285 public void layerRemoving(LayerRemoveEvent e) { 286 if (e.getRemovedLayer() instanceof OsmDataLayer) { 287 clear(((OsmDataLayer) e.getRemovedLayer()).data); 288 } 289 } 290 291 @Override 292 public void projectionChanged(Projection oldValue, Projection newValue) { 293 clear(); 294 } 295 296 @Override 297 public synchronized void selectionChanged(SelectionChangeEvent event) { 298 299 for (Iterator<PolyData> it = selectedPolyData.iterator(); it.hasNext();) { 300 it.next().setSelected(false); 301 it.remove(); 302 } 303 304 DataSet ds = null; 305 Collection<Map<Relation, Multipolygon>> maps = null; 306 for (OsmPrimitive p : event.getSelection()) { 307 if (p instanceof Way && p.getDataSet() != null) { 308 if (ds == null) { 309 ds = p.getDataSet(); 310 } 311 for (OsmPrimitive ref : p.getReferrers()) { 312 if (isMultipolygon(ref)) { 313 if (maps == null) { 314 maps = getMapsFor(ds); 315 } 316 for (Map<Relation, Multipolygon> map : maps) { 317 Multipolygon multipolygon = map.get(ref); 318 if (multipolygon != null) { 319 for (PolyData pd : multipolygon.getCombinedPolygons()) { 320 if (pd.getWayIds().contains(p.getUniqueId())) { 321 pd.setSelected(true); 322 selectedPolyData.add(pd); 323 } 324 } 325 } 326 } 327 } 328 } 329 } 330 } 331 } 332}