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