/*******************************************************************************
* Copyright (c) 2014 Open Door Logistics (www.opendoorlogistics.com)
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the GNU Lesser Public License 3.0
* which accompanies this distribution, and is available at
* http://www.gnu.org/licenses/lgpl.html
*
******************************************************************************/
package com.opendoorlogistics.core.geometry.rog;
import java.awt.geom.Point2D;
import java.io.BufferedInputStream;
import java.io.Closeable;
import java.io.DataInputStream;
import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.channels.Channels;
import java.nio.channels.FileChannel;
import java.util.List;
import org.geotools.geometry.jts.JTS;
import org.opengis.referencing.operation.MathTransform;
import org.opengis.referencing.operation.TransformException;
import com.fasterxml.jackson.core.JsonParseException;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.opendoorlogistics.codefromweb.jxmapviewer2.fork.swingx.OSMTileFactoryInfo;
import com.opendoorlogistics.codefromweb.jxmapviewer2.fork.swingx.mapviewer.TileFactoryInfo;
import com.opendoorlogistics.core.cache.ApplicationCache;
import com.opendoorlogistics.core.cache.RecentlyUsedCache;
import com.opendoorlogistics.core.utils.LargeList;
import com.vividsolutions.jts.geom.Geometry;
import com.vividsolutions.jts.io.WKBReader;
import de.undercouch.bson4jackson.BsonFactory;
public class QuadLoader implements Closeable, RogFileInformation{
private final LargeList<Long> quadPositions = new LargeList<>();
private final RandomAccessFile rf;
private final File file;
private final FileChannel channel;
private final TileFactoryInfo info = new OSMTileFactoryInfo();
private final Point2D [] mapCentresAtZoom ;
private boolean isNOLP;
public QuadLoader(File file ) {
this(file, null);
}
public QuadLoader(File file , List<ODLRenderOptimisedGeom> readObjects) {
this.file = file;
// pre-fetch info for zooms to prevent unneccessary object allocation
int nz = info.getMaximumZoomLevel()+1;
mapCentresAtZoom = new Point2D[nz];
for(int zoom = info.getMinimumZoomLevel() ; zoom <= info.getMaximumZoomLevel() ; zoom++){
mapCentresAtZoom[zoom] = info.getMapCenterInPixelsAtZoom(zoom);
}
try {
rf = new RandomAccessFile(file, "r");
channel = rf.getChannel();
DataInputStream dis = createDIS();
readObjectsFromFileStart(dis, readObjects);
// read quad positions
long n = dis.readLong();
for(long l= 0 ; l < n ; l++){
quadPositions.add(dis.readLong());
}
} catch (Exception e) {
throw new RuntimeException(e);
}
}
private DataInputStream createDIS() {
DataInputStream dis = new DataInputStream(new BufferedInputStream(Channels.newInputStream(channel)));
return dis;
}
public List<ODLRenderOptimisedGeom> readObjects(){
LargeList<ODLRenderOptimisedGeom> ret = new LargeList<>();
readObjects( ret);
return ret;
}
private void readObjects( List<ODLRenderOptimisedGeom> list) {
try {
channel.position(0);
DataInputStream dis = createDIS();
readObjectsFromFileStart(dis, list);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
/**
* Read the objects assuming the input DataInputStream is positioned at the start of the file
* @param dis
* @param list
*/
private void readObjectsFromFileStart(DataInputStream dis, List<ODLRenderOptimisedGeom> list) {
try {
// Read header
readFileHeader(dis);
// read number of objects
int nbObjs = dis.readInt();
for(int i =0 ; i<nbObjs ; i++){
ODLRenderOptimisedGeom geom = new ODLRenderOptimisedGeom(dis, this);
if(list!=null){
list.add(geom);
}
}
} catch (Exception e) {
throw new RuntimeException(e);
}
}
/**
* @param dis
* @throws IOException
* @throws JsonParseException
* @throws JsonMappingException
*/
private void readFileHeader(DataInputStream dis) throws IOException, JsonParseException, JsonMappingException {
BsonFactory factory = new BsonFactory();
ObjectMapper mapper = new ObjectMapper(factory);
JsonNode rootNode = mapper.readValue(RogReaderUtils.readBytesArray(dis), JsonNode.class);
JsonNode version = rootNode.findValue(RogReaderUtils.VERSION_KEY);
int rogversion =version.asInt();
if(rogversion > RogReaderUtils.RENDER_GEOMETRY_FILE_VERSION){
throw new RuntimeException(RogReaderUtils.RENDER_GEOMETRY_FILE_EXT + " cannot be read as it is made for a newer version of ODL Studio.");
}
JsonNode isNOLP = rootNode.findValue(RogReaderUtils.IS_NOPL_KEY);
if(isNOLP!=null){
this.isNOLP = isNOLP.asBoolean();
}else{
this.isNOLP = false;
}
}
private static class CacheKey{
final File file;
final int blockNb;
CacheKey(File file, int blockNb) {
this.file = file;
this.blockNb = blockNb;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + blockNb;
result = prime * result + ((file == null) ? 0 : file.hashCode());
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
CacheKey other = (CacheKey) obj;
if (blockNb != other.blockNb)
return false;
if (file == null) {
if (other.file != null)
return false;
} else if (!file.equals(other.file))
return false;
return true;
}
}
private static class CachedQuadBlock{
private final byte[] quadBinaryData;
private final long [] geomIds;
private final byte[][] bjsonBytes;
private final byte[][] geomBytes;
private long sizeBytes=0;
public CachedQuadBlock(DataInputStream dis, int blockNb) {
try {
// read block nb
int readBlockNb = dis.readInt();
if(readBlockNb!=blockNb){
throw new RuntimeException("Corrupt quadtree file.");
}
// read bjson
quadBinaryData = RogReaderUtils.readBytesArray(dis);
// read number of leaves
int n =dis.readInt();
// allocate arrays to hold the data, incrementing the object size
geomIds = new long[n];
sizeBytes += n*8;
sizeBytes += n*8 + 8;
bjsonBytes = new byte[n][];
sizeBytes += n*8 + 8;
geomBytes = new byte[n][];
// skip past the position of all leaves relative to block start...
for(int i =0 ; i<n ; i++){
dis.readInt();
}
// read the leaves themselves
for(int i =0 ; i<n ; i++){
// read geom id
geomIds[i] = dis.readLong();
// read bjson array
bjsonBytes[i] = RogReaderUtils.readBytesArray(dis);
addToSize(bjsonBytes[i]);
// read geom array
geomBytes[i] = RogReaderUtils.readBytesArray(dis);
addToSize(geomBytes[i]);
}
} catch (Exception e) {
throw new RuntimeException(e);
}
}
private void addToSize(byte[]bytes){
if(bytes!=null){
sizeBytes += bytes.length;
}
}
public long getSizeInBytes(){
return sizeBytes;
}
}
public synchronized Geometry loadGeometry(long geomId, int blockNb, int geomNbInBlock){
// try fetching the block from the cache
RecentlyUsedCache cache = ApplicationCache.singleton().get(ApplicationCache.ROG_QUADTREE_BLOCKS);
CacheKey cacheKey = new CacheKey(file, blockNb);
CachedQuadBlock block = (CachedQuadBlock)cache.get(cacheKey);
// load block and cache it if we didn't find it
if(block==null){
try {
channel.position(quadPositions.get(blockNb));
block = new CachedQuadBlock(createDIS(),blockNb);
cache.put(cacheKey, block, block.getSizeInBytes());
} catch (Exception e) {
throw new RuntimeException(e);
}
}
// read the geometry
if(block.geomIds[geomNbInBlock]!=geomId){
throw new RuntimeException("Invalid quadtree file; read incorrect geometry id");
}
try {
WKBReader reader = new WKBReader();
Geometry g = reader.read(block.geomBytes[geomNbInBlock]);
return g;
} catch (Exception e) {
throw new RuntimeException(e);
}
}
public Geometry loadTransformedGeometry(long geomId,int blockNb, int geomNbInBlock,final int sourceZoom,final int targetZoom) {
Geometry g = loadGeometry(geomId,blockNb, geomNbInBlock);
if(g==null){
return null;
}
MathTransform transform = new Abstract2dMathTransform(){
@Override
public void transform(double[] srcPts, int srcOff, double[] dstPts, int dstOff, int numPts) throws TransformException {
for(int i =0 ; i< numPts ;i++){
int srcIndex = srcOff + i*2;
// The transform from lat long to x and y works as:
// x = mcx_z + lng * pixelsPerLngDegree_z
// y = mcy_z + fn(lat) * pixelsPerLngRadian_z
// get longitude and fn(lat) which is zoom-independent
double lng = (srcPts[srcIndex] - mapCentresAtZoom[sourceZoom].getX()) / info.getLongitudeDegreeWidthInPixels(sourceZoom);
double fnLat = (srcPts[srcIndex+1] - mapCentresAtZoom[sourceZoom].getY()) / info.getLongitudeRadianWidthInPixels(sourceZoom);
// now get x and y in the target zoom
double x = mapCentresAtZoom[targetZoom].getX() + lng * info.getLongitudeDegreeWidthInPixels(targetZoom);
double y = mapCentresAtZoom[targetZoom].getY() + fnLat * info.getLongitudeRadianWidthInPixels(targetZoom);
int destIndex = dstOff + i*2;
dstPts[destIndex] = x;
dstPts[destIndex+1] = y;
}
}
};
try {
Geometry gTrans = JTS.transform(g, transform);
return gTrans;
} catch (Exception e) {
throw new RuntimeException(e);
}
}
@Override
public void close() throws IOException {
rf.close();
}
@Override
public boolean getIsNOLPL(){
return isNOLP;
}
@Override
public File getFile(){
return file;
}
}