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