/* Copyright (c) 2014 Boundless and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Distribution License v1.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/org/documents/edl-v10.html
*
* Contributors:
* Gabriel Roldan (Boundless) - initial implementation
*/
package org.locationtech.geogig.osm.internal.coordcache;
import static com.google.common.base.Preconditions.checkState;
import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.BufferOverflowException;
import java.nio.ByteBuffer;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.channels.FileChannel.MapMode;
import java.util.AbstractList;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.RandomAccess;
import java.util.TreeMap;
import javax.annotation.Nullable;
import org.locationtech.geogig.osm.internal.OSMCoordinateSequence;
import org.locationtech.geogig.osm.internal.OSMCoordinateSequenceFactory;
import com.google.common.base.Throwables;
import com.google.common.collect.Maps;
import com.google.common.io.Closeables;
import com.google.common.primitives.Longs;
class MappedIndex {
private static class Entry implements Comparable<MappedIndex.Entry> {
public static final int RECSIZE = 16;// sizeOf(long) + 2 * sizeOf(int)
public final long nodeId;
public final int x;
public final int y;
public Entry(long nodeId, int[] coordinate) {
this(nodeId, coordinate[0], coordinate[1]);
}
public Entry(long nodeId, int x, int y) {
this.nodeId = nodeId;
this.x = x;
this.y = y;
}
@Override
public int compareTo(MappedIndex.Entry o) {
return Longs.compare(nodeId, o.nodeId);
}
@Override
public boolean equals(Object o) {
if (o == this)
return true;
if (!(o instanceof MappedIndex.Entry)) {
return false;
}
MappedIndex.Entry e = (MappedIndex.Entry) o;
return nodeId == e.nodeId && x == e.x && y == e.y;
}
@Override
public String toString() {
return new StringBuilder("Entry[node: ").append(nodeId).append(", x: ").append(x)
.append(", y: ").append(y).append(']').toString();
}
public static void write(ByteBuffer buffer, MappedIndex.Entry entry) {
if (buffer.remaining() < Entry.RECSIZE) {
throw new BufferOverflowException();
}
buffer.putLong(entry.nodeId);
buffer.putInt(entry.x);
buffer.putInt(entry.y);
}
public static MappedIndex.Entry read(ByteBuffer buffer) {
long nodeId = buffer.getLong();
int x = buffer.getInt();
int y = buffer.getInt();
return new Entry(nodeId, x, y);
}
}
private static final int MAX_ENTRIES_PER_BUFFER = 250 * 1000;
private static final long MAX_BUFF_SIZE = MAX_ENTRIES_PER_BUFFER * MappedIndex.Entry.RECSIZE;
private static final OSMCoordinateSequenceFactory CSFAC = new OSMCoordinateSequenceFactory();
private File indexFile;
private RandomAccessFile randomAccessFile;
private FileChannel indexChannel;
private BufferRange currBuffer;
private List<BufferRange> ranges = new ArrayList<BufferRange>();
public MappedIndex(final File parentDir) throws IOException {
this.indexFile = new File(parentDir, "coordinates.idx");
this.indexFile.deleteOnExit();
checkState(this.indexFile.createNewFile(), "unable to create index file");
randomAccessFile = new RandomAccessFile(indexFile, "rw");
this.indexChannel = randomAccessFile.getChannel();
this.ranges = new ArrayList<MappedIndex.BufferRange>(2);
newBuffer();
}
private void newBuffer() throws IOException {
long position = MAX_BUFF_SIZE * ranges.size();
long size = MAX_BUFF_SIZE;
MappedByteBuffer buff = indexChannel.map(MapMode.READ_WRITE, position, size);
BufferRange range = new BufferRange(buff);
ranges.add(range);
this.currBuffer = range;
}
public void close() {
try {
Closeables.close(indexChannel, true);
Closeables.close(randomAccessFile, true);
} catch (IOException e) {
//
}
currBuffer = null;
indexChannel = null;
indexFile.delete();
}
public void putCoordinate(final long nodeId, int[] coordinate) {
Entry entry = new Entry(nodeId, coordinate);
try {
currBuffer.put(entry);
} catch (BufferOverflowException needsNewBuffer) {
try {
newBuffer();
} catch (IOException e) {
throw Throwables.propagate(e);
}
currBuffer.put(entry);
}
}
private static class BufferRange {
private int size = 0;
private long minId = Long.MAX_VALUE, maxId = Long.MIN_VALUE;
private TreeMap<Long, Entry> unsavedEntries = Maps.newTreeMap();
public final ByteBuffer buffer;
public BufferRange(ByteBuffer viewBuff) {
this.buffer = viewBuff;
}
private synchronized void put(MappedIndex.Entry entry) {
if (unsavedEntries.size() == MAX_ENTRIES_PER_BUFFER) {
save();
throw new BufferOverflowException();
}
long nodeId = entry.nodeId;
unsavedEntries.put(Long.valueOf(nodeId), entry);
minId = Math.min(minId, nodeId);
maxId = Math.max(maxId, nodeId);
size++;
}
private void save() {
for (Entry e : unsavedEntries.values()) {
Entry.write(buffer, e);
}
unsavedEntries.clear();
}
public boolean mayContain(Long nodeId) {
return nodeId >= minId && nodeId <= maxId;
}
@Override
public String toString() {
return new StringBuilder("Nodes (").append(size).append(")[").append(minId)
.append("..").append(maxId).append(']').toString();
}
@Nullable
public MappedIndex.Entry search(final Long nodeId) {
if (unsavedEntries.isEmpty() && size > 0) {
ByteBuffer view = buffer.duplicate();
view.flip();
List<MappedIndex.Entry> list = new EntryList(view);
final long id = nodeId.longValue();
final MappedIndex.Entry key = new Entry(id, 0, 0);
final int index = Collections.binarySearch(list, key);
if (index > -1) {
MappedIndex.Entry found = list.get(index);
return found;
}
return null;
}
Entry entry = unsavedEntries.get(nodeId);
return entry;
}
}
private static final class EntryList extends AbstractList<MappedIndex.Entry> implements
RandomAccess {
private ByteBuffer buffer;
public EntryList(ByteBuffer buffer) {
this.buffer = buffer;
}
@Override
public MappedIndex.Entry get(int index) {
int offset = index * Entry.RECSIZE;
buffer.position(offset);
MappedIndex.Entry entry = Entry.read(buffer);
return entry;
}
@Override
public MappedIndex.Entry set(int index, MappedIndex.Entry element) {
ByteBuffer buffer = this.buffer;
final int offset = index * Entry.RECSIZE;
buffer.position(offset);
// MappedIndex.Entry prev = serializer.read(buffer);
// buffer.position(offset);
Entry.write(buffer, element);
// return prev;
return null;
}
@Override
public int size() {
int limit = buffer.limit();
int size = limit / MappedIndex.Entry.RECSIZE;
return size;
}
}
public OSMCoordinateSequence build(List<Long> ids) {
// sort();
OSMCoordinateSequence sequence = CSFAC.create(ids.size());
int[] coordinateBuff = new int[2];
for (int index = 0; index < ids.size(); index++) {
Long nodeId = ids.get(index);
getCoordinate(nodeId, coordinateBuff);
sequence.setOrdinate(index, 0, coordinateBuff[0]);
sequence.setOrdinate(index, 1, coordinateBuff[1]);
}
return sequence;
}
private void getCoordinate(Long nodeId, int[] coordinateBuff) throws IllegalArgumentException {
// Search ranges backwards to catch the latest version of a coordinate in case the same one
// was added to more than one range
for (int i = ranges.size() - 1; i >= 0; i--) {
MappedIndex.BufferRange br = ranges.get(i);
if (br.mayContain(nodeId)) {
MappedIndex.Entry entry = br.search(nodeId);
if (entry != null) {
coordinateBuff[0] = entry.x;
coordinateBuff[1] = entry.y;
return;
}
}
}
throw new IllegalArgumentException("Node #" + nodeId + " not found");
}
}