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                        pd.wayNodesChanged((WayNodesChangedEvent) event);
215                    }
216                }
217            }
218        }
219    }
220
221    private static void removeMultipolygonFrom(Relation r, Collection<Map<Relation, Multipolygon>> maps) {
222        for (Map<Relation, Multipolygon> map : maps) {
223            map.remove(r);
224        }
225        // Erase style cache for polygon members
226        for (OsmPrimitive member : r.getMemberPrimitives()) {
227            member.clearCachedStyle();
228        }
229    }
230
231    @Override
232    public void primitivesAdded(PrimitivesAddedEvent event) {
233        // Do nothing
234    }
235
236    @Override
237    public void primitivesRemoved(PrimitivesRemovedEvent event) {
238        updateMultipolygonsReferringTo(event);
239    }
240
241    @Override
242    public void tagsChanged(TagsChangedEvent event) {
243        updateMultipolygonsReferringTo(event);
244    }
245
246    @Override
247    public void nodeMoved(NodeMovedEvent event) {
248        updateMultipolygonsReferringTo(event);
249    }
250
251    @Override
252    public void wayNodesChanged(WayNodesChangedEvent event) {
253        updateMultipolygonsReferringTo(event);
254    }
255
256    @Override
257    public void relationMembersChanged(RelationMembersChangedEvent event) {
258        updateMultipolygonsReferringTo(event);
259    }
260
261    @Override
262    public void otherDatasetChange(AbstractDatasetChangedEvent event) {
263        // Do nothing
264    }
265
266    @Override
267    public void dataChanged(DataChangedEvent event) {
268        // Do not call updateMultipolygonsReferringTo as getPrimitives()
269        // can return all the data set primitives for this event
270        Collection<Map<Relation, Multipolygon>> maps = null;
271        for (OsmPrimitive p : event.getPrimitives()) {
272            if (isMultipolygon(p)) {
273                if (maps == null) {
274                    maps = getMapsFor(event.getDataset());
275                }
276                for (Map<Relation, Multipolygon> map : maps) {
277                    // DataChangedEvent is sent after downloading incomplete members (see #7131),
278                    // without having received RelationMembersChangedEvent or PrimitivesAddedEvent
279                    // OR when undoing a move of a large number of nodes (see #7195),
280                    // without having received NodeMovedEvent
281                    // This ensures concerned multipolygons will be correctly redrawn
282                    map.remove(p);
283                }
284            }
285        }
286    }
287
288    @Override
289    public void layerAdded(LayerAddEvent e) {
290        // Do nothing
291    }
292
293    @Override
294    public void layerOrderChanged(LayerOrderChangeEvent e) {
295        // Do nothing
296    }
297
298    @Override
299    public void layerRemoving(LayerRemoveEvent e) {
300        if (e.getRemovedLayer() instanceof OsmDataLayer) {
301            clear(((OsmDataLayer) e.getRemovedLayer()).data);
302        }
303    }
304
305    @Override
306    public void projectionChanged(Projection oldValue, Projection newValue) {
307        clear();
308    }
309
310    @Override
311    public void selectionChanged(Collection<? extends OsmPrimitive> newSelection) {
312
313        for (Iterator<PolyData> it = selectedPolyData.iterator(); it.hasNext();) {
314            it.next().setSelected(false);
315            it.remove();
316        }
317
318        DataSet ds = null;
319        Collection<Map<Relation, Multipolygon>> maps = null;
320        for (OsmPrimitive p : newSelection) {
321            if (p instanceof Way && p.getDataSet() != null) {
322                if (ds == null) {
323                    ds = p.getDataSet();
324                }
325                for (OsmPrimitive ref : p.getReferrers()) {
326                    if (isMultipolygon(ref)) {
327                        if (maps == null) {
328                            maps = getMapsFor(ds);
329                        }
330                        for (Map<Relation, Multipolygon> map : maps) {
331                            Multipolygon multipolygon = map.get(ref);
332                            if (multipolygon != null) {
333                                for (PolyData pd : multipolygon.getCombinedPolygons()) {
334                                    if (pd.getWayIds().contains(p.getUniqueId())) {
335                                        pd.setSelected(true);
336                                        selectedPolyData.add(pd);
337                                    }
338                                }
339                            }
340                        }
341                    }
342                }
343            }
344        }
345    }
346}