001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.data.vector;
003
004import java.util.ArrayList;
005import java.util.Arrays;
006import java.util.Collection;
007import java.util.Collections;
008import java.util.HashSet;
009import java.util.LinkedHashSet;
010import java.util.List;
011import java.util.Map;
012import java.util.Objects;
013import java.util.Optional;
014import java.util.Set;
015import java.util.concurrent.ConcurrentHashMap;
016import java.util.concurrent.locks.Lock;
017import java.util.concurrent.locks.ReentrantReadWriteLock;
018import java.util.function.Function;
019import java.util.function.Predicate;
020import java.util.function.Supplier;
021import java.util.stream.Collectors;
022import java.util.stream.Stream;
023
024import org.openstreetmap.josm.data.DataSource;
025import org.openstreetmap.josm.data.imagery.vectortile.mapbox.MVTTile;
026import org.openstreetmap.josm.data.osm.BBox;
027import org.openstreetmap.josm.data.osm.DataSelectionListener;
028import org.openstreetmap.josm.data.osm.DownloadPolicy;
029import org.openstreetmap.josm.data.osm.HighlightUpdateListener;
030import org.openstreetmap.josm.data.osm.IPrimitive;
031import org.openstreetmap.josm.data.osm.OsmData;
032import org.openstreetmap.josm.data.osm.PrimitiveId;
033import org.openstreetmap.josm.data.osm.Storage;
034import org.openstreetmap.josm.data.osm.UploadPolicy;
035import org.openstreetmap.josm.data.osm.WaySegment;
036import org.openstreetmap.josm.data.osm.event.IDataSelectionEventSource;
037import org.openstreetmap.josm.data.osm.event.IDataSelectionListener;
038import org.openstreetmap.josm.data.osm.event.IDataSelectionListener.SelectionAddEvent;
039import org.openstreetmap.josm.data.osm.event.IDataSelectionListener.SelectionChangeEvent;
040import org.openstreetmap.josm.data.osm.event.IDataSelectionListener.SelectionRemoveEvent;
041import org.openstreetmap.josm.data.osm.event.IDataSelectionListener.SelectionReplaceEvent;
042import org.openstreetmap.josm.data.osm.event.IDataSelectionListener.SelectionToggleEvent;
043import org.openstreetmap.josm.gui.mappaint.ElemStyles;
044import org.openstreetmap.josm.tools.ListenerList;
045import org.openstreetmap.josm.tools.Logging;
046import org.openstreetmap.josm.tools.SubclassFilteredCollection;
047
048/**
049 * A data class for Vector Data
050 *
051 * @author Taylor Smock
052 * @since 17862
053 */
054public class VectorDataSet implements OsmData<VectorPrimitive, VectorNode, VectorWay, VectorRelation>,
055       IDataSelectionEventSource<VectorPrimitive, VectorNode, VectorWay, VectorRelation, VectorDataSet> {
056    // Note: In Java 8, computeIfAbsent is blocking for both pre-existing and new values. In Java 9, it is only blocking
057    // for new values (perf increase). See JDK-8161372 for more info.
058    private final Map<Integer, Storage<MVTTile>> dataStoreMap = new ConcurrentHashMap<>();
059    // This is for "custom" data
060    private final VectorDataStore customDataStore = new VectorDataStore();
061    // Both of these listener lists are useless, since they expect OsmPrimitives at this time
062    private final ListenerList<HighlightUpdateListener> highlightUpdateListenerListenerList = ListenerList.create();
063    private final ListenerList<DataSelectionListener> dataSelectionListenerListenerList = ListenerList.create();
064    private boolean lock = true;
065    private String name;
066    private short mappaintCacheIdx = 1;
067
068    private final Object selectionLock = new Object();
069    /**
070     * The current selected primitives. This is always a unmodifiable set.
071     *
072     * The set should be ordered in the order in which the primitives have been added to the selection.
073     */
074    private Set<PrimitiveId> currentSelectedPrimitives = Collections.emptySet();
075
076    private final ListenerList<IDataSelectionListener<VectorPrimitive, VectorNode, VectorWay, VectorRelation, VectorDataSet>> listeners =
077            ListenerList.create();
078
079    private final ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
080
081    /**
082     * The distance to consider nodes duplicates -- mostly a memory saving measure.
083     * 0.000_000_1 ~1.2 cm (+- 5.57 mm)
084     * Descriptions from <a href="https://xkcd.com/2170/">https://xkcd.com/2170/</a>
085     * Notes on <a href="https://wiki.openstreetmap.org/wiki/Node">https://wiki.openstreetmap.org/wiki/Node</a> indicate
086     * that IEEE 32-bit floats should not be used at high longitude (0.000_01 precision)
087     */
088    protected static final float DUPE_NODE_DISTANCE = 0.000_000_1f;
089
090    /**
091     * The current zoom we are getting/adding to
092     */
093    private int zoom;
094    /**
095     * Default to normal download policy
096     */
097    private DownloadPolicy downloadPolicy = DownloadPolicy.NORMAL;
098    /**
099     * Default to a blocked upload policy
100     */
101    private UploadPolicy uploadPolicy = UploadPolicy.BLOCKED;
102    /**
103     * The paint style for this layer
104     */
105    private ElemStyles styles;
106    private final Collection<PrimitiveId> highlighted = new HashSet<>();
107
108    @Override
109    public Collection<DataSource> getDataSources() {
110        // TODO
111        return Collections.emptyList();
112    }
113
114    @Override
115    public void lock() {
116        this.lock = true;
117    }
118
119    @Override
120    public void unlock() {
121        this.lock = false;
122    }
123
124    @Override
125    public boolean isLocked() {
126        return this.lock;
127    }
128
129    @Override
130    public String getVersion() {
131        return "8"; // TODO get this dynamically. Not critical, as this is currently the _only_ version.
132    }
133
134    @Override
135    public String getName() {
136        return this.name;
137    }
138
139    @Override
140    public void setName(String name) {
141        this.name = name;
142    }
143
144    /**
145     * Add a primitive to the custom data store
146     * @param primitive the primitive to add
147     */
148    @Override
149    public void addPrimitive(VectorPrimitive primitive) {
150        tryWrite(this.readWriteLock, () -> {
151            this.customDataStore.addPrimitive(primitive);
152            primitive.setDataSet(this);
153        });
154    }
155
156    /**
157     * Remove a primitive from the custom data store
158     * @param primitive The primitive to add to the custom data store
159     */
160    public void removePrimitive(VectorPrimitive primitive) {
161        this.customDataStore.removePrimitive(primitive);
162        primitive.setDataSet(null);
163    }
164
165    @Override
166    public void clear() {
167        synchronized (this.dataStoreMap) {
168            this.dataStoreMap.clear();
169        }
170    }
171
172    @Override
173    public List<VectorNode> searchNodes(BBox bbox) {
174        return tryRead(this.readWriteLock, () -> {
175            final Storage<MVTTile> dataStore = this.getBestZoomDataStore().orElse(null);
176            final Stream<VectorDataStore> dataStoreStream = dataStore != null ? dataStore.stream().map(MVTTile::getData) : Stream.empty();
177            return Stream.concat(dataStoreStream, Stream.of(this.customDataStore)).map(VectorDataStore::getStore)
178                    .flatMap(store -> store.searchNodes(bbox).stream()).collect(Collectors.toList());
179        }).orElseGet(Collections::emptyList);
180    }
181
182    @Override
183    public boolean containsNode(VectorNode vectorNode) {
184        return tryRead(this.readWriteLock, () -> {
185            final Storage<MVTTile> dataStore = this.getBestZoomDataStore().orElse(null);
186            final Stream<VectorDataStore> dataStoreStream = dataStore != null ? dataStore.stream().map(MVTTile::getData) : Stream.empty();
187            return Stream.concat(dataStoreStream, Stream.of(this.customDataStore)).map(VectorDataStore::getStore)
188                    .anyMatch(store -> store.containsNode(vectorNode));
189        }).orElse(Boolean.FALSE);
190    }
191
192    @Override
193    public List<VectorWay> searchWays(BBox bbox) {
194        return tryRead(this.readWriteLock, () -> {
195            final Storage<MVTTile> dataStore = this.getBestZoomDataStore().orElse(null);
196            final Stream<VectorDataStore> dataStoreStream = dataStore != null ? dataStore.stream().map(MVTTile::getData) : Stream.empty();
197            return Stream.concat(dataStoreStream, Stream.of(this.customDataStore)).map(VectorDataStore::getStore)
198                    .flatMap(store -> store.searchWays(bbox).stream()).collect(Collectors.toList());
199        }).orElseGet(Collections::emptyList);
200    }
201
202    @Override
203    public boolean containsWay(VectorWay vectorWay) {
204        return tryRead(this.readWriteLock, () -> {
205            final Storage<MVTTile> dataStore = this.getBestZoomDataStore().orElse(null);
206            final Stream<VectorDataStore> dataStoreStream = dataStore != null ? dataStore.stream().map(MVTTile::getData) : Stream.empty();
207            return Stream.concat(dataStoreStream, Stream.of(this.customDataStore)).map(VectorDataStore::getStore)
208                    .anyMatch(store -> store.containsWay(vectorWay));
209        }).orElse(Boolean.FALSE);
210    }
211
212    @Override
213    public List<VectorRelation> searchRelations(BBox bbox) {
214        return tryRead(this.readWriteLock, () -> {
215            final Storage<MVTTile> dataStore = this.getBestZoomDataStore().orElse(null);
216            final Stream<VectorDataStore> dataStoreStream = dataStore != null ? dataStore.stream().map(MVTTile::getData) : Stream.empty();
217            return Stream.concat(dataStoreStream, Stream.of(this.customDataStore)).map(VectorDataStore::getStore)
218                    .flatMap(store -> store.searchRelations(bbox).stream()).collect(Collectors.toList());
219        }).orElseGet(Collections::emptyList);
220    }
221
222    @Override
223    public boolean containsRelation(VectorRelation vectorRelation) {
224        return tryRead(this.readWriteLock, () -> {
225            final Storage<MVTTile> dataStore = this.getBestZoomDataStore().orElse(null);
226            final Stream<VectorDataStore> dataStoreStream = dataStore != null ? dataStore.stream().map(MVTTile::getData) : Stream.empty();
227            return Stream.concat(dataStoreStream, Stream.of(this.customDataStore)).map(VectorDataStore::getStore)
228                    .anyMatch(store -> store.containsRelation(vectorRelation));
229        }).orElse(Boolean.FALSE);
230    }
231
232    /**
233     * Get a primitive for an id
234     * @param primitiveId type and uniqueId of the primitive. Might be &lt; 0 for newly created primitives
235     * @return The primitive for the id. Please note that since this is vector data, there may be more primitives with this id.
236     * Please use {@link #getPrimitivesById(PrimitiveId...)} to get all primitives for that {@link PrimitiveId}.
237     */
238    @Override
239    public VectorPrimitive getPrimitiveById(PrimitiveId primitiveId) {
240        return this.getPrimitivesById(primitiveId).findFirst().orElse(null);
241    }
242
243    /**
244     * Get all primitives for ids
245     * @param primitiveIds The ids to search for
246     * @return The primitives for the ids (note: as this is vector data, a {@link PrimitiveId} may have multiple associated primitives)
247     */
248    public Stream<VectorPrimitive> getPrimitivesById(PrimitiveId... primitiveIds) {
249        final Storage<MVTTile> dataStore = this.getBestZoomDataStore().orElse(null);
250        return Stream.concat(dataStore != null ? dataStore.stream().map(MVTTile::getData) : Stream.empty(),
251                Stream.of(this.customDataStore)).map(VectorDataStore::getPrimitivesMap)
252                .flatMap(m -> Stream.of(primitiveIds).map(m::get)).filter(Objects::nonNull);
253    }
254
255    @Override
256    public <T extends VectorPrimitive> Collection<T> getPrimitives(Predicate<? super VectorPrimitive> predicate) {
257        Collection<VectorPrimitive> primitives = tryRead(this.readWriteLock, () -> {
258            final Storage<MVTTile> dataStore = this.getBestZoomDataStore().orElse(null);
259            final Stream<VectorDataStore> dataStoreStream = dataStore != null ? dataStore.stream().map(MVTTile::getData) : Stream.empty();
260            return Stream.concat(dataStoreStream, Stream.of(this.customDataStore))
261                    .map(VectorDataStore::getAllPrimitives).flatMap(Collection::stream).distinct().collect(Collectors.toList());
262
263        }).orElseGet(Collections::emptyList);
264        return new SubclassFilteredCollection<>(primitives, predicate);
265    }
266
267    @Override
268    public Collection<VectorNode> getNodes() {
269        return this.getPrimitives(VectorNode.class::isInstance);
270    }
271
272    @Override
273    public Collection<VectorWay> getWays() {
274        return this.getPrimitives(VectorWay.class::isInstance);
275    }
276
277    @Override
278    public Collection<VectorRelation> getRelations() {
279        return this.getPrimitives(VectorRelation.class::isInstance);
280    }
281
282    @Override
283    public DownloadPolicy getDownloadPolicy() {
284        return this.downloadPolicy;
285    }
286
287    @Override
288    public void setDownloadPolicy(DownloadPolicy downloadPolicy) {
289        this.downloadPolicy = downloadPolicy;
290    }
291
292    @Override
293    public UploadPolicy getUploadPolicy() {
294        return this.uploadPolicy;
295    }
296
297    @Override
298    public void setUploadPolicy(UploadPolicy uploadPolicy) {
299        this.uploadPolicy = uploadPolicy;
300    }
301
302    /**
303     * Get the current Read/Write lock.
304     * This changes based off of zoom level. Please do not use this in a finally block
305     * @return The current read/write lock
306     */
307    @Override
308    public Lock getReadLock() {
309        return this.readWriteLock.readLock();
310    }
311
312    @Override
313    public Collection<WaySegment> getHighlightedVirtualNodes() {
314        // TODO? This requires a change to WaySegment so that it isn't Way/Node specific
315        return Collections.emptyList();
316    }
317
318    @Override
319    public void setHighlightedVirtualNodes(Collection<WaySegment> waySegments) {
320        // TODO? This requires a change to WaySegment so that it isn't Way/Node specific
321    }
322
323    @Override
324    public Collection<WaySegment> getHighlightedWaySegments() {
325        // TODO? This requires a change to WaySegment so that it isn't Way/Node specific
326        return Collections.emptyList();
327    }
328
329    @Override
330    public void setHighlightedWaySegments(Collection<WaySegment> waySegments) {
331        // TODO? This requires a change to WaySegment so that it isn't Way/Node specific
332    }
333
334    /**
335     * Mark some primitives as highlighted.
336     * API is *highly likely* to change, as the inherited methods are modified to accept primitives other than OSM primitives.
337     * @param primitives The primitives to highlight
338     */
339    public void setHighlighted(Collection<PrimitiveId> primitives) {
340        this.highlighted.clear();
341        this.highlighted.addAll(primitives);
342        // The highlight event updates are very OSM specific, and require a DataSet.
343        this.highlightUpdateListenerListenerList.fireEvent(event -> event.highlightUpdated(null));
344    }
345
346    /**
347     * Get the highlighted objects
348     * @return The highlighted objects
349     */
350    public Collection<PrimitiveId> getHighlighted() {
351        return Collections.unmodifiableCollection(this.highlighted);
352    }
353
354    @Override
355    public void addHighlightUpdateListener(HighlightUpdateListener listener) {
356        this.highlightUpdateListenerListenerList.addListener(listener);
357    }
358
359    @Override
360    public void removeHighlightUpdateListener(HighlightUpdateListener listener) {
361        this.highlightUpdateListenerListenerList.removeListener(listener);
362    }
363
364    @Override
365    public Collection<VectorPrimitive> getAllSelected() {
366        return tryRead(this.readWriteLock, () -> {
367            final Storage<MVTTile> dataStore = this.getBestZoomDataStore().orElse(null);
368            Stream<VectorDataStore> dataStoreStream = dataStore != null ? dataStore.stream().map(MVTTile::getData) : Stream.empty();
369                return Stream.concat(dataStoreStream, Stream.of(this.customDataStore)).map(VectorDataStore::getPrimitivesMap)
370                  .flatMap(dataMap -> {
371                    // Synchronize on dataMap to avoid concurrent modification errors
372                    synchronized (dataMap) {
373                        return this.currentSelectedPrimitives.stream().map(dataMap::get).filter(Objects::nonNull);
374                    }
375                }).collect(Collectors.toList());
376        }).orElseGet(Collections::emptyList);
377    }
378
379    /**
380     * Get the best zoom datastore
381     * @return A datastore with data, or {@code null} if no good datastore exists.
382     */
383    private Optional<Storage<MVTTile>> getBestZoomDataStore() {
384        final int currentZoom = this.zoom;
385        if (this.dataStoreMap.containsKey(currentZoom)) {
386            return Optional.of(this.dataStoreMap.get(currentZoom));
387        }
388        // Check up to two zooms higher (may cause perf hit)
389        for (int tZoom = currentZoom + 1; tZoom < currentZoom + 3; tZoom++) {
390            if (this.dataStoreMap.containsKey(tZoom)) {
391                return Optional.of(this.dataStoreMap.get(tZoom));
392            }
393        }
394        // Return *any* lower zoom data (shouldn't cause a perf hit...)
395        for (int tZoom = currentZoom - 1; tZoom >= 0; tZoom--) {
396            if (this.dataStoreMap.containsKey(tZoom)) {
397                return Optional.of(this.dataStoreMap.get(tZoom));
398            }
399        }
400        // Check higher level zooms. May cause perf issues if selected datastore has a lot of data.
401        for (int tZoom = currentZoom + 3; tZoom < 34; tZoom++) {
402            if (this.dataStoreMap.containsKey(tZoom)) {
403                return Optional.of(this.dataStoreMap.get(tZoom));
404            }
405        }
406        return Optional.empty();
407    }
408
409    @Override
410    public boolean selectionEmpty() {
411        return this.currentSelectedPrimitives.isEmpty();
412    }
413
414    @Override
415    public boolean isSelected(VectorPrimitive osm) {
416        return this.currentSelectedPrimitives.contains(osm.getPrimitiveId());
417    }
418
419    @Override
420    public void toggleSelected(Collection<? extends PrimitiveId> osm) {
421        this.toggleSelectedImpl(osm.stream());
422    }
423
424    @Override
425    public void toggleSelected(PrimitiveId... osm) {
426        this.toggleSelectedImpl(Stream.of(osm));
427    }
428
429    private void toggleSelectedImpl(Stream<? extends PrimitiveId> osm) {
430        this.doSelectionChange(old -> new SelectionToggleEvent<>(this, old,
431                osm.flatMap(this::getPrimitivesById).filter(Objects::nonNull)));
432    }
433
434    @Override
435    public void setSelected(Collection<? extends PrimitiveId> selection) {
436        this.setSelectedImpl(selection.stream());
437    }
438
439    @Override
440    public void setSelected(PrimitiveId... osm) {
441        this.setSelectedImpl(Stream.of(osm));
442    }
443
444    private void setSelectedImpl(Stream<? extends PrimitiveId> osm) {
445        this.doSelectionChange(old -> new SelectionReplaceEvent<>(this, old,
446                osm.filter(Objects::nonNull).flatMap(this::getPrimitivesById).filter(Objects::nonNull)));
447    }
448
449    @Override
450    public void addSelected(Collection<? extends PrimitiveId> selection) {
451        this.addSelectedImpl(selection.stream());
452    }
453
454    @Override
455    public void addSelected(PrimitiveId... osm) {
456        this.addSelectedImpl(Stream.of(osm));
457    }
458
459    private void addSelectedImpl(Stream<? extends PrimitiveId> osm) {
460        this.doSelectionChange(old -> new SelectionAddEvent<>(this, old,
461                osm.flatMap(this::getPrimitivesById).filter(Objects::nonNull)));
462    }
463
464    @Override
465    public void clearSelection(PrimitiveId... osm) {
466        this.clearSelectionImpl(Stream.of(osm));
467    }
468
469    @Override
470    public void clearSelection(Collection<? extends PrimitiveId> list) {
471        this.clearSelectionImpl(list.stream());
472    }
473
474    @Override
475    public void clearSelection() {
476        this.clearSelectionImpl(new ArrayList<>(this.currentSelectedPrimitives).stream());
477    }
478
479    private void clearSelectionImpl(Stream<? extends PrimitiveId> osm) {
480        this.doSelectionChange(old -> new SelectionRemoveEvent<>(this, old,
481                osm.flatMap(this::getPrimitivesById).filter(Objects::nonNull)));
482    }
483
484    /**
485     * Do a selection change.
486     * <p>
487     * This is the only method that changes the current selection state.
488     * @param command A generator that generates the {@link SelectionChangeEvent}
489     *                for the given base set of currently selected primitives.
490     * @return true iff the command did change the selection.
491     */
492    private boolean doSelectionChange(final Function<Set<VectorPrimitive>,
493            SelectionChangeEvent<VectorPrimitive, VectorNode, VectorWay, VectorRelation, VectorDataSet>> command) {
494        synchronized (this.selectionLock) {
495            SelectionChangeEvent<VectorPrimitive, VectorNode, VectorWay, VectorRelation, VectorDataSet> event =
496                    command.apply(currentSelectedPrimitives.stream().map(this::getPrimitiveById).collect(Collectors.toSet()));
497            if (event.isNop()) {
498                return false;
499            }
500            this.currentSelectedPrimitives = event.getSelection().stream().map(IPrimitive::getPrimitiveId)
501                    .collect(Collectors.toCollection(LinkedHashSet::new));
502            this.listeners.fireEvent(l -> l.selectionChanged(event));
503            return true;
504        }
505    }
506
507    @Override
508    public void addSelectionListener(DataSelectionListener listener) {
509        this.dataSelectionListenerListenerList.addListener(listener);
510    }
511
512    @Override
513    public void removeSelectionListener(DataSelectionListener listener) {
514        this.dataSelectionListenerListenerList.removeListener(listener);
515    }
516
517    public short getMappaintCacheIndex() {
518        return this.mappaintCacheIdx;
519    }
520
521    @Override
522    public void clearMappaintCache() {
523        this.mappaintCacheIdx++;
524    }
525
526    public void setZoom(int zoom) {
527        if (zoom == this.zoom) {
528            return; // Do nothing -- zoom isn't actually changing
529        }
530        this.zoom = zoom;
531        this.clearMappaintCache();
532        final int[] nearestZoom = {-1, -1, -1, -1};
533        nearestZoom[0] = zoom;
534        // Create a new list to avoid concurrent modification issues
535        synchronized (this.dataStoreMap) {
536            final int[] keys = new ArrayList<>(this.dataStoreMap.keySet()).stream().filter(Objects::nonNull)
537              .mapToInt(Integer::intValue).sorted().toArray();
538            final int index;
539            if (this.dataStoreMap.containsKey(zoom)) {
540                index = Arrays.binarySearch(keys, zoom);
541            } else {
542                // (-(insertion point) - 1) = return -> insertion point = -(return + 1)
543                index = -(Arrays.binarySearch(keys, zoom) + 1);
544            }
545            if (index > 0) {
546                nearestZoom[1] = keys[index - 1];
547            }
548            if (index < keys.length - 2) {
549                nearestZoom[2] = keys[index + 1];
550            }
551
552            // TODO cleanup zooms for memory
553        }
554    }
555
556    public int getZoom() {
557        return this.zoom;
558    }
559
560    /**
561     * Add tile data to this dataset
562     * @param tile The tile to add
563     */
564    public void addTileData(MVTTile tile) {
565        tryWrite(this.readWriteLock, () -> {
566            final int currentZoom = tile.getZoom();
567            // computeIfAbsent should be thread safe (ConcurrentHashMap indicates it is, anyway)
568            final Storage<MVTTile> dataStore = this.dataStoreMap.computeIfAbsent(currentZoom, tZoom -> new Storage<>());
569            tile.getData().getAllPrimitives().forEach(primitive -> primitive.setDataSet(this));
570            dataStore.add(tile);
571        });
572    }
573
574    /**
575     * Try to read something (here to avoid boilerplate)
576     *
577     * @param lock     The lock
578     * @param supplier The reading function
579     * @param <T>      The return type
580     * @return The optional return
581     */
582    private static <T> Optional<T> tryRead(ReentrantReadWriteLock lock, Supplier<T> supplier) {
583        try {
584            lock.readLock().lockInterruptibly();
585            return Optional.ofNullable(supplier.get());
586        } catch (InterruptedException e) {
587            Logging.error(e);
588            Thread.currentThread().interrupt();
589        } finally {
590            lock.readLock().unlock();
591        }
592        return Optional.empty();
593    }
594
595    /**
596     * Try to write something (here to avoid boilerplate)
597     * @param lock lock
598     * @param runnable The writing function
599     */
600    private static void tryWrite(ReentrantReadWriteLock lock, Runnable runnable) {
601        try {
602            lock.writeLock().lockInterruptibly();
603            runnable.run();
604        } catch (InterruptedException e) {
605            Logging.error(e);
606            Thread.currentThread().interrupt();
607        } finally {
608            if (lock.isWriteLockedByCurrentThread()) {
609                lock.writeLock().unlock();
610            }
611        }
612    }
613
614    /**
615     * Get the styles for this layer
616     *
617     * @return The styles
618     */
619    public ElemStyles getStyles() {
620        return this.styles;
621    }
622
623    /**
624     * Set the styles for this layer
625     * @param styles The styles to set for this layer
626     */
627    public void setStyles(Collection<ElemStyles> styles) {
628        if (styles.size() == 1) {
629            this.styles = styles.iterator().next();
630        } else if (!styles.isEmpty()) {
631            this.styles = new ElemStyles(styles.stream().flatMap(style -> style.getStyleSources().stream()).collect(Collectors.toList()));
632        } else {
633            this.styles = null;
634        }
635    }
636
637    /**
638     * Mark some layers as invisible
639     * @param invisibleLayers The layer to not show
640     */
641    public void setInvisibleLayers(Collection<String> invisibleLayers) {
642        String[] currentInvisibleLayers = invisibleLayers.stream().filter(Objects::nonNull).toArray(String[]::new);
643        List<String> temporaryList = Arrays.asList(currentInvisibleLayers);
644        this.dataStoreMap.values().stream().flatMap(Collection::stream).map(MVTTile::getData)
645          .forEach(dataStore -> dataStore.getAllPrimitives().parallelStream()
646            .forEach(primitive -> primitive.setVisible(!temporaryList.contains(primitive.getLayer()))));
647    }
648
649    @Override
650    public boolean addSelectionListener(IDataSelectionListener<VectorPrimitive, VectorNode, VectorWay, VectorRelation, VectorDataSet> listener) {
651        if (!this.listeners.containsListener(listener)) {
652            this.listeners.addListener(listener);
653        }
654        return this.listeners.containsListener(listener);
655    }
656
657    @Override
658    public boolean removeSelectionListener(
659            IDataSelectionListener<VectorPrimitive, VectorNode, VectorWay, VectorRelation, VectorDataSet> listener) {
660        if (this.listeners.containsListener(listener)) {
661            this.listeners.removeListener(listener);
662        }
663        return this.listeners.containsListener(listener);
664    }
665}