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