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