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