Skip to content

Commit

Permalink
Support storing and retrieving arrays of points to/from properties
Browse files Browse the repository at this point in the history
  • Loading branch information
craigtaverner authored and MishaDemianenko committed Nov 27, 2017
1 parent e151c21 commit 152cf36
Show file tree
Hide file tree
Showing 11 changed files with 267 additions and 27 deletions.
Expand Up @@ -20,6 +20,7 @@
package org.neo4j.cypher.internal.runtime.interpreted

import org.neo4j.cypher.internal.util.v3_4.CypherTypeException
import org.neo4j.graphdb.spatial.Point
import org.neo4j.values.storable.{ArrayValue, _}
import org.neo4j.values.virtual._
import org.neo4j.values.{AnyValue, AnyValueWriter}
Expand Down Expand Up @@ -90,6 +91,8 @@ object CastSupport {
case (_: FloatValue, _: NumberValue) => a
case (_: DoubleValue, _: NumberValue) => a

case (_: PointValue, _: PointValue) => a

case (a, b) if a == Values.NO_VALUE || b == Values.NO_VALUE => throw new CypherTypeException(
"Collections containing null values can not be stored in properties.")

Expand Down Expand Up @@ -123,6 +126,10 @@ object CastSupport {
transform(new ArrayConverterWriter(classOf[Float], a => Values.floatArray(a.asInstanceOf[Array[Float]]))))
case _: DoubleValue => Converter(
transform(new ArrayConverterWriter(classOf[Double], a => Values.doubleArray(a.asInstanceOf[Array[Double]]))))
case _: PointValue => Converter(
transform(new ArrayConverterWriter(classOf[PointValue], a => Values.pointArray(a.asInstanceOf[Array[PointValue]]))))
case _: Point => Converter(
transform(new ArrayConverterWriter(classOf[Point], a => Values.pointArray(a.asInstanceOf[Array[Point]]))))
case _ => throw new CypherTypeException("Property values can only be of primitive types or arrays thereof")
}

Expand Down Expand Up @@ -199,9 +206,8 @@ object CastSupport {
_array = value
}

override def writePoint(crs: CoordinateReferenceSystem, coordinate: Array[Double]): Unit = {
throw new UnsupportedOperationException("Arrays of points are not yet supported")
}
override def writePoint(crs: CoordinateReferenceSystem, coordinate: Array[Double]): Unit =
write(Values.pointValue(crs, coordinate: _*))
}

}
Expand Up @@ -36,6 +36,24 @@ abstract class ScalaPoint extends Point {

@BeanProperty
val geometryType: String = "Point"

// Different versions of Neo4j return different implementations of Point, so we need
// This to ensure compatibility tests pass when comparing between versions.
override def equals(other: Any): Boolean = {
if (other == null) {
return false
}
other match {
case otherPoint: Point =>
if (!otherPoint.getGeometryType.equals(this.getGeometryType)) return false
if (!otherPoint.getCRS.getHref.equals(this.getCRS.getHref)) return false
val otherCoord = otherPoint.getCoordinate.getCoordinate
val thisCoord = this.getCoordinate.getCoordinate
otherCoord == thisCoord
case _ =>
false
}
}
}

case class CartesianPoint(@BeanProperty x: Double,
Expand Down
Expand Up @@ -22,6 +22,7 @@
import java.nio.ByteBuffer;
import java.nio.ByteOrder;

import org.neo4j.kernel.impl.store.GeometryType;
import org.neo4j.kernel.impl.store.PropertyType;
import org.neo4j.kernel.impl.store.ShortArray;
import org.neo4j.kernel.impl.util.Bits;
Expand Down Expand Up @@ -55,6 +56,14 @@ public static ArrayValue readArrayFromBuffer( ByteBuffer buffer )
}
return Values.stringArray( result );
}

else if ( typeId == PropertyType.GEOMETRY.intValue() )
{
GeometryType.GeometryHeader header = GeometryType.GeometryHeader.fromArrayHeaderByteBuffer( buffer );
byte[] byteArray = new byte[buffer.limit() - buffer.position()];
buffer.get( byteArray );
return GeometryType.decodePointArray( header, byteArray );
}
else
{
ShortArray type = ShortArray.typeOf( typeId );
Expand Down
Expand Up @@ -34,6 +34,7 @@
import org.neo4j.kernel.impl.store.record.DynamicRecord;
import org.neo4j.kernel.impl.util.Bits;
import org.neo4j.logging.LogProvider;
import org.neo4j.values.storable.PointValue;
import org.neo4j.values.storable.Value;
import org.neo4j.values.storable.Values;

Expand All @@ -46,6 +47,7 @@ public class DynamicArrayStore extends AbstractDynamicStore
{
public static final int NUMBER_HEADER_SIZE = 3;
public static final int STRING_HEADER_SIZE = 5;
public static final int GEOMETRY_HEADER_SIZE = 6; // This should match contents of GeometryType.GeometryHeader

// store version, each store ends with this string (byte encoded)
public static final String TYPE_DESCRIPTOR = "ArrayPropertyStore";
Expand Down Expand Up @@ -73,8 +75,7 @@ public <FAILURE extends Exception> void accept( RecordStore.Processor<FAILURE> p
processor.processArray( this, record );
}

public static void allocateFromNumbers( Collection<DynamicRecord> target, Object array,
DynamicRecordAllocator recordAllocator )
public static byte[] encodeFromNumbers( Object array, int offsetBytes )
{
Class<?> componentType = array.getClass().getComponentType();
boolean isPrimitiveByteArray = componentType.equals( Byte.TYPE );
Expand All @@ -95,20 +96,20 @@ public static void allocateFromNumbers( Collection<DynamicRecord> target, Object
byte[] bytes;
if ( isByteArray )
{
bytes = new byte[NUMBER_HEADER_SIZE + arrayLength];
bytes[0] = (byte) type.intValue();
bytes[1] = (byte) bitsUsedInLastByte;
bytes[2] = (byte) requiredBits;
bytes = new byte[NUMBER_HEADER_SIZE + arrayLength + offsetBytes];
bytes[offsetBytes + 0] = (byte) type.intValue();
bytes[offsetBytes + 1] = (byte) bitsUsedInLastByte;
bytes[offsetBytes + 2] = (byte) requiredBits;
if ( isPrimitiveByteArray )
{
arraycopy( array, 0, bytes, NUMBER_HEADER_SIZE, arrayLength );
arraycopy( array, 0, bytes, NUMBER_HEADER_SIZE + offsetBytes, arrayLength );
}
else
{
Byte[] source = (Byte[]) array;
for ( int i = 0; i < source.length; i++ )
{
bytes[NUMBER_HEADER_SIZE + i] = source[i];
bytes[NUMBER_HEADER_SIZE + offsetBytes + i] = source[i];
}
}
}
Expand All @@ -119,8 +120,22 @@ public static void allocateFromNumbers( Collection<DynamicRecord> target, Object
bits.put( (byte) bitsUsedInLastByte );
bits.put( (byte) requiredBits );
type.writeAll( array, arrayLength, requiredBits, bits );
bytes = bits.asBytes();
bytes = bits.asBytes( offsetBytes );
}
return bytes;
}

public static void allocateFromNumbers( Collection<DynamicRecord> target, Object array,
DynamicRecordAllocator recordAllocator )
{
byte[] bytes = encodeFromNumbers( array, 0 );
allocateRecordsFromBytes( target, bytes, recordAllocator );
}

public static void allocateFromPoints( Collection<DynamicRecord> target, PointValue[] array,
DynamicRecordAllocator recordAllocator )
{
byte[] bytes = GeometryType.encodePointArray( array );
allocateRecordsFromBytes( target, bytes, recordAllocator );
}

Expand Down Expand Up @@ -166,6 +181,10 @@ public static void allocateRecords( Collection<DynamicRecord> target, Object arr
{
allocateFromString( target, (String[]) array, recordAllocator );
}
else if ( type.equals( PointValue.class ) )
{
allocateFromPoints( target, (PointValue[]) array, recordAllocator );
}
else
{
allocateFromNumbers( target, array, recordAllocator );
Expand Down Expand Up @@ -193,6 +212,19 @@ public static Value getRightArray( Pair<byte[],byte[]> data )
}
return Values.stringArray( result );
}
else if ( typeId == PropertyType.GEOMETRY.intValue() )
{
GeometryType.GeometryHeader geometryHeader = GeometryType.GeometryHeader.fromArrayHeaderBytes(header);
if ( geometryHeader.geometryType == GeometryType.GEOMETRY_POINT.gtype )
{
return GeometryType.decodePointArray( geometryHeader, bArray );
}
else
{
//TODO: Perhaps should throw an exception
return Values.NO_VALUE;
}
}
else
{
ShortArray type = ShortArray.typeOf( typeId );
Expand Down
Expand Up @@ -19,9 +19,15 @@
*/
package org.neo4j.kernel.impl.store;

import java.nio.ByteBuffer;

import org.neo4j.helpers.collection.Pair;
import org.neo4j.kernel.impl.store.format.standard.StandardFormatSettings;
import org.neo4j.kernel.impl.store.record.PropertyBlock;
import org.neo4j.values.storable.ArrayValue;
import org.neo4j.values.storable.CoordinateReferenceSystem;
import org.neo4j.values.storable.FloatingPointArray;
import org.neo4j.values.storable.PointValue;
import org.neo4j.values.storable.Value;
import org.neo4j.values.storable.Values;

Expand Down Expand Up @@ -52,7 +58,7 @@ public int calculateNumberOfBlocksUsedForGeometry( long firstBlock )
}
};

private final int gtype;
public final int gtype;

GeometryType( int gtype )
{
Expand Down Expand Up @@ -133,13 +139,13 @@ public static Value decode( long[] valueBlocks, int offset )
}
if ( dimension > 3 )
{
throw new UnsupportedOperationException( "Points with more than 3 dimensions are not supported in the PropertyStore" );
throw new UnsupportedOperationException( "Points with more than 3 dimensions are not supported" );
}
CoordinateReferenceSystem crs = CoordinateReferenceSystem.get( getCRSTable( firstBlock ), getCRSCode( firstBlock ) );
return find( gtype ).decode( crs, dimension, valueBlocks, offset );
}

public static long[] encodePoint( int keyId, CoordinateReferenceSystem crs, double[] coordinate ) throws IllegalArgumentException
public static long[] encodePoint( int keyId, CoordinateReferenceSystem crs, double[] coordinate )
{
if ( coordinate.length > 3 )
{
Expand All @@ -161,4 +167,98 @@ public static long[] encodePoint( int keyId, CoordinateReferenceSystem crs, doub
}
return data;
}

public static byte[] encodePointArray( PointValue[] points )
{
int dimension = points[0].coordinate().length;
double[] data = new double[points.length * dimension];
for ( int i = 0; i < data.length; i++ )
{
data[i] = points[i / dimension].coordinate()[i % dimension];
}
int code = points[0].getCoordinateReferenceSystem().code;
GeometryHeader geometryHeader = new GeometryHeader( GeometryType.GEOMETRY_POINT.gtype, dimension,
points[0].getCoordinateReferenceSystem() );
byte[] bytes = DynamicArrayStore.encodeFromNumbers( data, DynamicArrayStore.GEOMETRY_HEADER_SIZE );
geometryHeader.writeArrayHeaderTo(bytes);
return bytes;
}

/**
* Handler for header information for Geometry objects and arrays of Geometry objects
*/
public static class GeometryHeader
{
public final int geometryType;
private final int dimension;
private final CoordinateReferenceSystem crs;

private GeometryHeader( int geometryType, int dimension, CoordinateReferenceSystem crs )
{
this.geometryType = geometryType;
this.dimension = dimension;
this.crs = crs;
}

private GeometryHeader( int geometryType, int dimension, int crsTableId, int crsCode )
{
this( geometryType, dimension, CoordinateReferenceSystem.get( crsTableId, crsCode ) );
}

private void writeArrayHeaderTo(byte[] bytes)
{
bytes[0] = (byte) PropertyType.GEOMETRY.intValue();
bytes[1] = (byte) geometryType;
bytes[2] = (byte) dimension;
bytes[3] = (byte) crs.table.tableId;
bytes[4] = (byte) (crs.code & 0xFFL);
bytes[5] = (byte) (crs.code >> 8 & 0xFFL);
}

static GeometryHeader fromArrayHeaderBytes( byte[] header )
{
int geometryType = header[1];
int dimension = header[2];
int crsTableId = header[3];
int crsCode = header[4] + (header[5] << 8);
return new GeometryHeader( geometryType, dimension, crsTableId, crsCode );
}

public static GeometryHeader fromArrayHeaderByteBuffer( ByteBuffer buffer )
{
int geometryType = buffer.get();
int dimension = buffer.get();
int crsTableId = buffer.get();
int crsCode = buffer.get() + (buffer.get() << 8);
return new GeometryHeader( geometryType, dimension, crsTableId, crsCode );
}
}

public static ArrayValue decodePointArray( GeometryHeader header, byte[] data )
{
byte[] dataHeader = PropertyType.ARRAY.readDynamicRecordHeader( data );
byte[] dataBody = new byte[data.length - dataHeader.length];
System.arraycopy( data, dataHeader.length, dataBody, 0, dataBody.length );
Value dataValue = DynamicArrayStore.getRightArray( Pair.of( dataHeader, dataBody ) );
if ( dataValue instanceof FloatingPointArray )
{
FloatingPointArray numbers = (FloatingPointArray) dataValue;
PointValue[] points = new PointValue[((int) numbers.length() / header.dimension)];
for ( int i = 0; i < points.length; i++ )
{
double[] coords = new double[header.dimension];
for ( int d = 0; d < header.dimension; d++ )
{
coords[d] = numbers.doubleValue( i * header.dimension + d );
}
points[i] = Values.pointValue( header.crs, coords );
}
return Values.pointArray( points );
}
else
{
//TODO: Perhaps throw an exception
return Values.EMPTY_POINT_ARRAY;
}
}
}
Expand Up @@ -155,6 +155,10 @@ else if ( itemType <= DOUBLE.byteValue() )
{
return headOf( recordBytes, DynamicArrayStore.NUMBER_HEADER_SIZE );
}
else if ( itemType <= GEOMETRY.byteValue() )
{
return headOf( recordBytes, DynamicArrayStore.GEOMETRY_HEADER_SIZE );
}
throw new IllegalArgumentException( "Unknown array type " + itemType );
}

Expand Down
Expand Up @@ -132,16 +132,20 @@ public long[] getLongs()
}

public byte[] asBytes()
{
return asBytes(0);
}

public byte[] asBytes( int offsetBytes )
{
int readPositionBefore = readPosition;
readPosition = 0;
try
{
byte[] result = new byte[numberOfBytes];
final int count = result.length;
for ( int i = 0; i < count; i++ )
byte[] result = new byte[numberOfBytes + offsetBytes];
for ( int i = 0; i < numberOfBytes; i++ )
{
result[i] = getByte();
result[i + offsetBytes] = getByte();
}
return result;
}
Expand Down
Expand Up @@ -19,9 +19,9 @@
*/
package org.neo4j.values.storable;

abstract class FloatingPointArray extends NumberArray
public abstract class FloatingPointArray extends NumberArray
{
abstract double doubleValue( int offset );
public abstract double doubleValue( int offset );

@Override
public int compareTo( IntegralArray other )
Expand Down

0 comments on commit 152cf36

Please sign in to comment.