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