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