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