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