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