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