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}