org.openstreetmap.atlas.utilities.collections.ShardBucketCollection Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of atlas Show documentation
Show all versions of atlas Show documentation
"Library to load OSM data into an Atlas format"
The newest version!
package org.openstreetmap.atlas.utilities.collections;
import java.io.Serializable;
import java.lang.reflect.Array;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.apache.commons.lang3.ArrayUtils;
import org.openstreetmap.atlas.geography.Located;
import org.openstreetmap.atlas.geography.Rectangle;
import org.openstreetmap.atlas.geography.index.RTree;
import org.openstreetmap.atlas.geography.sharding.Shard;
import org.openstreetmap.atlas.geography.sharding.Sharding;
import org.openstreetmap.atlas.geography.sharding.SlippyTile;
import org.openstreetmap.atlas.utilities.scalars.Counter;
/**
* A collection wrapper for a set of collections associated with shards containing located items,
* such as CheckFlags or AtlasEntities. Supports concurrent add, contains, remove. The term bucket
* is used because of the usability of the collections mostly as storage for various location based
* sorting tasks. Does not support safe iteration while modifying.
*
* @param
* Located item type
* @param
* Type of the Collection of LocatedType associated with shards
* @author jklamer
*/
public abstract class ShardBucketCollection & Serializable>
implements Collection, Serializable
{
/**
* A helper class that associates a shard with the index that its collection is at in the
* collectionBuckets array.
*/
private static class ShardToCollectionIndex implements Located, Serializable
{
private static final long serialVersionUID = 4050100671815503794L;
private final int index;
private final Shard shard;
ShardToCollectionIndex(final int index, final Shard shard)
{
this.index = index;
this.shard = shard;
}
@Override
public Rectangle bounds()
{
return this.getShard().bounds();
}
public int getIndex()
{
return this.index;
}
public Shard getShard()
{
return this.shard;
}
}
private static final long serialVersionUID = -7892704554302160820L;
private final CollectionType[] collectionBuckets;
private final RTree collectionIndex;
private final HashMap initializedShards = new HashMap<>();
private final Rectangle maximumBounds;
public ShardBucketCollection(final Rectangle maximumBounds, final Integer zoomLevel)
{
this(maximumBounds, SlippyTile.allTiles(zoomLevel, maximumBounds));
}
public ShardBucketCollection(final Rectangle maximumBounds, final Sharding sharding)
{
this(maximumBounds, sharding.shards(maximumBounds));
}
/**
* Construct the collection by allocating space for a collection for each of the shards and
* assigning the index back.
*
* @param maximumBounds
* maximum bound of the collection
* @param shards
* shards to use
*/
@SuppressWarnings("unchecked")
private ShardBucketCollection(final Rectangle maximumBounds,
final Iterable extends Shard> shards)
{
this.maximumBounds = maximumBounds;
this.collectionIndex = new RTree<>();
final Counter counter = new Counter();
shards.forEach(shardBucket ->
{
final ShardToCollectionIndex shardToCollectionIndex = new ShardToCollectionIndex(
(int) counter.getValueAndIncrement(), shardBucket);
this.collectionIndex.add(shardToCollectionIndex.bounds(), shardToCollectionIndex);
});
this.collectionBuckets = (CollectionType[]) Array.newInstance(
this.initializeBucketCollection().getClass(), (int) counter.getValue());
}
@Override
public final boolean add(final LocatedType item)
{
if (Objects.nonNull(item) && item.bounds().overlaps(this.maximumBounds))
{
final List indexes = this.collectionIndex.get(item.bounds());
if (indexes.size() == 1)
{
return this.addFunction(item, this.getOrCreateBucketCollectionAt(indexes.get(0)),
indexes.get(0).getShard());
}
else if (this.allowMultipleBucketInsertion())
{
final long addedAmount = indexes.stream()
.filter(index -> this.addFunction(item,
this.getOrCreateBucketCollectionAt(index), index.getShard()))
.count();
return addedAmount > 0;
}
else
{
final Shard toInsertAt = this.resolveShard(item, indexes.stream()
.map(ShardToCollectionIndex::getShard).collect(Collectors.toList()));
final Optional toAddTo = indexes.stream()
.filter(index -> toInsertAt.equals(index.getShard())).findFirst();
return toAddTo
.map(index -> this.addFunction(item,
this.getOrCreateBucketCollectionAt(index), index.getShard()))
.orElse(false);
}
}
return false;
}
@Override
public boolean addAll(final Collection extends LocatedType> collection)
{
if (Objects.nonNull(collection))
{
final long addCount = collection.stream().filter(this::add).count();
return addCount > 0;
}
return false;
}
@Override
public void clear()
{
synchronized (this.collectionBuckets)
{
for (int i = 0; i < this.collectionBuckets.length; i++)
{
this.collectionBuckets[i] = null;
}
this.initializedShards.clear();
}
}
@Override
@SuppressWarnings("unchecked")
public boolean contains(final Object object)
{
final Optional typedItem = this.castToLocatedType(object);
if (typedItem.isPresent())
{
final LocatedType item = typedItem.get();
return this.getBucketCollectionsForBounds(item.bounds())
.anyMatch(collection -> collection.contains(item));
}
return false;
}
@Override
public boolean containsAll(final Collection> collection)
{
if (Objects.nonNull(collection))
{
return collection.stream().allMatch(this::contains);
}
return false;
}
/**
* A stream of only distinct elements in the collection. Useful if allowing multiple bucket
* insertion
*
* @return A disticnt stream of the collection
*/
public Stream distinctStream()
{
return this.stream().distinct();
}
/**
* @return a stream of all initialized bucket collections
*/
public Stream getAllBucketCollections()
{
return Arrays.stream(this.collectionBuckets).filter(Objects::nonNull);
}
/**
* Get a map of all Shards to their corresponding collection. This will only return shard
* collection entries with initialized collections.
*
* @return Map of shard to collection
*/
public Map getAllShardBucketCollectionPairs()
{
return this.initializedShards.values().stream()
.collect(Collectors.toMap(ShardToCollectionIndex::getShard, this::getCollectionAt));
}
/**
* Get the collection for a given shard. It will return optional empty if the shard's collection
* isn't initialized
*
* @param shard
* shard whose collection you want.
* @return Optional of the initialized Collection
*/
public Optional getBucketCollectionForShard(final Shard shard)
{
return Optional.ofNullable(this.initializedShards.get(shard)).map(this::getCollectionAt)
.filter(Objects::nonNull);
}
/**
* A stream of all the bucket collects whose associated shard overlaps with the bounds. Note it
* will return more than one collection if given a shard bound because the edges of shards
* overlap
*
* @param bounds
* to use
* @return stream of a subset of bucket collections
*/
public Stream getBucketCollectionsForBounds(final Rectangle bounds)
{
return this.collectionIndex.get(bounds).stream().map(this::getCollectionAt)
.filter(Objects::nonNull);
}
/**
* Get the max bounds of what located can be in the collection.
*
* @return rectangle bounds
*/
public Rectangle getMaximumBounds()
{
return this.maximumBounds;
}
@Override
public boolean isEmpty()
{
return this.size() == 0;
}
@Override
public Iterator iterator()
{
return this.stream().iterator();
}
@Override
@SuppressWarnings("unchecked")
public boolean remove(final Object object)
{
final Optional typedItem = this.castToLocatedType(object);
if (typedItem.isPresent())
{
final LocatedType item = typedItem.get();
if (item.bounds().overlaps(this.maximumBounds))
{
final long removeCount = this.getBucketCollectionsForBounds(item.bounds())
.filter(collection -> collection.remove(item)).count();
return removeCount > 0;
}
}
return false;
}
@Override
public boolean removeAll(final Collection> collection)
{
if (Objects.nonNull(collection))
{
final long removeCount = collection.stream().filter(this::remove).count();
return removeCount > 0;
}
return false;
}
@Override
public boolean retainAll(final Collection> collection)
{
final List toRemove = this.stream().filter(item -> !collection.contains(item))
.collect(Collectors.toList());
return this.removeAll(toRemove);
}
@Override
public int size()
{
synchronized (this.collectionBuckets)
{
return this.getAllBucketCollections().mapToInt(CollectionType::size).sum();
}
}
@Override
public Stream stream()
{
return this.getAllBucketCollections().flatMap(CollectionType::stream);
}
@Override
public Object[] toArray()
{
Object[] toReturn = new Object[0];
synchronized (this.collectionBuckets)
{
final Iterator bucketIterator = this.getAllBucketCollections()
.iterator();
while (bucketIterator.hasNext())
{
toReturn = ArrayUtils.addAll(toReturn, bucketIterator.next().toArray());
}
}
return toReturn;
}
@Override
@SuppressWarnings("unchecked")
public T1[] toArray(final T1[] otherArray)
{
return ArrayUtils.addAll(otherArray, (T1[]) this.toArray());
}
/**
* To add the item into the collection in a special way or dependent on the shard, this function
* can be overridden. The return contract is the same of {@link Collection}'s add. This function
* must be deterministic.
*
* @param item
* to add
* @param collection
* to add to
* @param shard
* shard associated with the collection
* @return true if the collection has been added to successfully and changed as a result, false
* otherwise
*/
protected boolean addFunction(final LocatedType item, final CollectionType collection,
final Shard shard)
{
return collection.add(item);
}
/**
* @return true if items are allowed to be in multiple buckets. False otherwise
*/
protected abstract boolean allowMultipleBucketInsertion();
/**
* The collection should be agnostic to the shard. Deciding the collection to insert into and
* how to insert into based on the shard should be handled by resolveShard and addFunction
* respectively.
*
* @return an intialized empty bucket collection.
*/
protected abstract CollectionType initializeBucketCollection();
/**
* Resolve which of multiple overlapping shards. Note these shards overlap due to a bounds call,
* agnostic of the items geometry.
*
* @param item
* located item to insert
* @param possibleBuckets
* possible buckets for the item to work into
* @return Shard whose collection you should add to.
*/
protected Shard resolveShard(final LocatedType item,
final List extends Shard> possibleBuckets)
{
throw new UnsupportedOperationException(
"Implement this method when not allowing multiple bucket insertion");
}
@SuppressWarnings("unchecked")
private Optional castToLocatedType(final Object object)
{
try
{
return Optional.ofNullable(object).map(cast -> (LocatedType) cast);
}
catch (final ClassCastException e)
{
return Optional.empty();
}
}
private void createBucketCollectionAt(final ShardToCollectionIndex index)
{
synchronized (this.collectionBuckets)
{
if (Objects.isNull(this.collectionBuckets[index.getIndex()]))
{
this.collectionBuckets[index.getIndex()] = this.initializeBucketCollection();
this.initializedShards.put(index.getShard(), index);
}
}
}
private CollectionType getCollectionAt(final ShardToCollectionIndex index)
{
synchronized (this.collectionBuckets)
{
return this.collectionBuckets[index.getIndex()];
}
}
private CollectionType getOrCreateBucketCollectionAt(final ShardToCollectionIndex index)
{
final CollectionType collection = this.getCollectionAt(index);
if (Objects.isNull(collection))
{
this.createBucketCollectionAt(index);
return this.getCollectionAt(index);
}
else
{
return collection;
}
}
}