提交 0fd0134f authored 作者: Thomas Mueller's avatar Thomas Mueller

Spatial index (work in progress)

上级 d23ee071
......@@ -18,7 +18,8 @@ Change Log
<h1>Change Log</h1>
<h2>Next Version (unreleased)</h2>
<ul><li>Issue 467: OSGi Class Loader (ability to create reference to class
<ul><li>Improved spatial index and data type.
</li><li>Issue 467: OSGi Class Loader (ability to create reference to class
in other ClassLoader, for example in another OSGi bundle).
</li></ul>
......
......@@ -4,8 +4,8 @@
* (http://h2database.com/html/license.html).
* Initial Developer: H2 Group
*
* N. Fortin, Atelier SIG - IRSTV CNRS 2488:
* Support for the operator "&&" as an alias for SPATIAL_INTERSECTS.
* Nicolas Fortin, Atelier SIG, IRSTV FR CNRS 24888
* Support for the operator "&&" as an alias for SPATIAL_INTERSECTS
*/
package org.h2.command;
......@@ -149,6 +149,10 @@ import org.h2.value.ValueTimestamp;
/**
* The parser is used to convert a SQL statement string to an command object.
*
* @author Thomas Mueller
* @author Noel Grandin
* @author Nicolas Fortin, Atelier SIG, IRSTV FR CNRS 24888
*/
public class Parser {
......
......@@ -22,6 +22,10 @@ import org.h2.value.ValueNull;
/**
* Example comparison expressions are ID=1, NAME=NAME, NAME IS NULL.
*
* @author Thomas Mueller
* @author Noel Grandin
* @author Nicolas Fortin, Atelier SIG, IRSTV FR CNRS 24888
*/
public class Comparison extends Condition {
......@@ -102,7 +106,7 @@ public class Comparison extends Condition {
/**
* This is a comparison type that is only used for spatial index
* conditions.
* conditions (operator "&&").
*/
public static final int SPATIAL_INTERSECTS = 11;
......@@ -161,6 +165,8 @@ public class Comparison extends Condition {
return "<>";
case NOT_EQUAL_NULL_SAFE:
return "IS NOT";
case SPATIAL_INTERSECTS:
return "&&";
default:
throw DbException.throwInternalError("compareType=" + compareType);
}
......@@ -285,7 +291,7 @@ public class Comparison extends Condition {
case SPATIAL_INTERSECTS: {
ValueGeometry lg = (ValueGeometry) l.convertTo(Value.GEOMETRY);
ValueGeometry rg = (ValueGeometry) r.convertTo(Value.GEOMETRY);
result = lg.intersects(rg);
result = lg.intersectsBoundingBox(rg);
break;
}
default:
......@@ -300,6 +306,7 @@ public class Comparison extends Condition {
case EQUAL_NULL_SAFE:
case NOT_EQUAL:
case NOT_EQUAL_NULL_SAFE:
case SPATIAL_INTERSECTS:
return type;
case BIGGER_EQUAL:
return SMALLER_EQUAL;
......@@ -314,6 +321,15 @@ public class Comparison extends Condition {
}
}
@Override
public Expression getNotIfPossible(Session session) {
if (compareType == SPATIAL_INTERSECTS) {
return null;
}
int type = getNotCompareType();
return new Comparison(session, type, left, right);
}
private int getNotCompareType() {
switch (compareType) {
case EQUAL:
......@@ -341,12 +357,6 @@ public class Comparison extends Condition {
}
}
@Override
public Expression getNotIfPossible(Session session) {
int type = getNotCompareType();
return new Comparison(session, type, left, right);
}
@Override
public void createIndexConditions(Session session, TableFilter filter) {
ExpressionColumn l = null;
......
......@@ -29,6 +29,10 @@ import org.h2.value.Value;
* A index condition object is made for each condition that can potentially use
* an index. This class does not extend expression, but in general there is one
* expression that maps to each index condition.
*
* @author Thomas Mueller
* @author Noel Grandin
* @author Nicolas Fortin, Atelier SIG, IRSTV FR CNRS 24888
*/
public class IndexCondition {
......@@ -210,6 +214,9 @@ public class IndexCondition {
buff.append(expressionQuery.getPlanSQL());
buff.append(')');
break;
case Comparison.SPATIAL_INTERSECTS:
buff.append(" && ");
break;
default:
DbException.throwInternalError("type="+compareType);
}
......
......@@ -26,6 +26,10 @@ import org.h2.value.ValueNull;
/**
* The filter used to walk through an index. This class supports IN(..)
* and IN(SELECT ...) optimizations.
*
* @author Thomas Mueller
* @author Noel Grandin
* @author Nicolas Fortin, Atelier SIG, IRSTV FR CNRS 24888
*/
public class IndexCursor implements Cursor {
......@@ -125,7 +129,7 @@ public class IndexCursor implements Cursor {
end = getSearchRow(end, columnId, v, false);
}
if (isIntersects) {
intersects = getSpatialSearchRow(intersects, columnId, v, true);
intersects = getSpatialSearchRow(intersects, columnId, v);
}
if (isStart || isEnd) {
// an X=? condition will produce less rows than
......@@ -148,8 +152,8 @@ public class IndexCursor implements Cursor {
return;
}
if (!alwaysFalse) {
if (intersects != null && index instanceof SpatialTreeIndex) {
cursor = ((SpatialTreeIndex) index).findByGeometry(tableFilter,
if (intersects != null && index instanceof SpatialIndex) {
cursor = ((SpatialIndex) index).findByGeometry(tableFilter,
intersects);
} else {
cursor = index.find(tableFilter, start, end);
......@@ -174,16 +178,13 @@ public class IndexCursor implements Cursor {
return idxCol == null || idxCol.column == column;
}
private SearchRow getSpatialSearchRow(SearchRow row, int columnId, Value v, boolean isIntersects) {
private SearchRow getSpatialSearchRow(SearchRow row, int columnId, Value v) {
if (row == null) {
row = table.getTemplateRow();
} else {
ValueGeometry vg = (ValueGeometry) row.getValue(columnId);
if (isIntersects) {
v = ((ValueGeometry) v).intersection(vg);
} else {
v = ((ValueGeometry) v).union(vg);
}
} else if (row.getValue(columnId) != null) {
// the intersection of the two envelopes
ValueGeometry vg = (ValueGeometry) row.getValue(columnId).convertTo(Value.GEOMETRY);
v = ((ValueGeometry) v.convertTo(Value.GEOMETRY)).getEnvelopeIntersection(vg);
}
if (columnId < 0) {
row.setKey(v.getLong());
......
......@@ -5,36 +5,65 @@
*/
package org.h2.index;
import java.util.List;
import java.util.Iterator;
import org.h2.engine.Constants;
import org.h2.engine.Session;
import org.h2.message.DbException;
import org.h2.mvstore.MVStore;
import org.h2.mvstore.db.MVTableEngine;
import org.h2.mvstore.rtree.MVRTreeMap;
import org.h2.mvstore.rtree.SpatialKey;
import org.h2.result.Row;
import org.h2.result.SearchRow;
import org.h2.result.SortOrder;
import org.h2.table.Column;
import org.h2.table.IndexColumn;
import org.h2.table.RegularTable;
import org.h2.table.TableFilter;
import org.h2.value.Value;
import org.h2.value.ValueGeometry;
import com.vividsolutions.jts.geom.Envelope;
import com.vividsolutions.jts.geom.Geometry;
import com.vividsolutions.jts.index.quadtree.Quadtree;
/**
* This is an in-memory index based on a R-Tree.
* This is an index based on a MVR-TreeMap.
*
* @author Thomas Mueller
* @author Noel Grandin
* @author Nicolas Fortin, Atelier SIG, IRSTV FR CNRS 24888
*/
public class SpatialTreeIndex extends BaseIndex implements SpatialIndex {
private static final String MAP_PREFIX = "RTREE_";
private Quadtree root;
private final MVRTreeMap<Long> treeMap;
private final MVStore store;
private final RegularTable tableData;
private long rowCount;
private boolean closed;
public SpatialTreeIndex(RegularTable table, int id, String indexName, IndexColumn[] columns, IndexType indexType) {
private boolean needRebuild;
private boolean persistent;
/**
* Constructor.
* @param table Table instance
* @param id Index Id
* @param indexName Index name
* @param columns Indexed columns (only one geometry column allowed)
* @param indexType Index type (only spatial index)
* @param persistent Persistent, can be used in-memory or stored in a file.
*/
public SpatialTreeIndex(RegularTable table, int id, String indexName,
IndexColumn[] columns, IndexType indexType, boolean persistent,
boolean create, Session session) {
if (indexType.isUnique()) {
throw DbException.getUnsupportedException("not unique");
}
if (!persistent && !create) {
throw DbException.getUnsupportedException("Non persistent index called with create==false");
}
if (columns.length > 1) {
throw DbException.getUnsupportedException("can only do one column");
}
......@@ -47,8 +76,9 @@ public class SpatialTreeIndex extends BaseIndex implements SpatialIndex {
if ((columns[0].sortType & SortOrder.NULLS_LAST) != 0) {
throw DbException.getUnsupportedException("cannot do nulls last");
}
initBaseIndex(table, id, indexName, columns, indexType);
this.needRebuild = create;
this.persistent = persistent;
tableData = table;
if (!database.isStarting()) {
if (columns[0].column.getType() != Value.GEOMETRY) {
......@@ -56,13 +86,34 @@ public class SpatialTreeIndex extends BaseIndex implements SpatialIndex {
+ columns[0].column.getCreateSQL());
}
}
root = new Quadtree();
if (!persistent) {
// Index in memory
store = MVStore.open(null);
treeMap = store.openMap("spatialIndex",
new MVRTreeMap.Builder<Long>());
} else {
if (id < 0) {
throw DbException.getUnsupportedException("Persistent index with id<0");
}
MVTableEngine.init(session.getDatabase());
store = session.getDatabase().getMvStore().getStore();
// Called after CREATE SPATIAL INDEX or
// by PageStore.addMeta
treeMap = store.openMap(MAP_PREFIX + getId(),
new MVRTreeMap.Builder<Long>());
if (treeMap.isEmpty()) {
needRebuild = true;
}
}
}
@Override
public void close(Session session) {
root = null;
if (persistent) {
store.store();
} else {
store.close();
}
closed = true;
}
......@@ -71,14 +122,16 @@ public class SpatialTreeIndex extends BaseIndex implements SpatialIndex {
if (closed) {
throw DbException.throwInternalError();
}
root.insert(getEnvelope(row), row);
rowCount++;
treeMap.add(getEnvelope(row), row.getKey());
}
private Envelope getEnvelope(SearchRow row) {
private SpatialKey getEnvelope(SearchRow row) {
Value v = row.getValue(columnIds[0]);
Geometry g = ((ValueGeometry) v).getGeometry();
return g.getEnvelopeInternal();
Geometry g = ((ValueGeometry) v.convertTo(Value.GEOMETRY)).getGeometry();
Envelope env = g.getEnvelopeInternal();
return new SpatialKey(row.getKey(),
(float) env.getMinX(), (float) env.getMaxX(),
(float) env.getMinY(), (float) env.getMaxY());
}
@Override
......@@ -86,45 +139,49 @@ public class SpatialTreeIndex extends BaseIndex implements SpatialIndex {
if (closed) {
throw DbException.throwInternalError();
}
if (!root.remove(getEnvelope(row), row)) {
if (!treeMap.remove(getEnvelope(row), row.getKey())) {
throw DbException.throwInternalError("row not found");
}
rowCount--;
}
@Override
public Cursor find(TableFilter filter, SearchRow first, SearchRow last) {
return find();
return find(filter.getSession());
}
@Override
public Cursor find(Session session, SearchRow first, SearchRow last) {
return find();
return find(session);
}
@SuppressWarnings("unchecked")
private Cursor find() {
// TODO use an external iterator,
// but let's see if we can get it working first
// TODO in the context of a spatial index,
// a query that uses ">" or "<" has no real meaning, so for now just ignore
// it and return all rows
List<Row> list = root.queryAll();
return new ListCursor(list, true /*first*/);
private Cursor find(Session session) {
return new SpatialCursor(treeMap.keySet().iterator(), tableData, session);
}
@SuppressWarnings("unchecked")
@Override
public Cursor findByGeometry(TableFilter filter, SearchRow intersection) {
// TODO use an external iterator,
// but let's see if we can get it working first
List<Row> list;
if (intersection != null) {
list = root.query(getEnvelope(intersection));
} else {
list = root.queryAll();
if (intersection == null) {
return find(filter.getSession());
}
return new SpatialCursor(treeMap.findIntersectingKeys(getEnvelope(intersection)), tableData, filter.getSession());
}
@Override
protected long getCostRangeIndex(int[] masks, long rowCount, SortOrder sortOrder) {
rowCount += Constants.COST_ROW_OFFSET;
long cost = rowCount;
long rows = rowCount;
if (masks == null) {
return cost;
}
for (Column column : columns) {
int index = column.getColumnId();
int mask = masks[index];
if ((mask & IndexCondition.SPATIAL_INTERSECTS) != 0) {
cost = 3 + rows / 4;
}
}
return new ListCursor(list, true/*first*/);
return cost;
}
@Override
......@@ -134,13 +191,14 @@ public class SpatialTreeIndex extends BaseIndex implements SpatialIndex {
@Override
public void remove(Session session) {
truncate(session);
if (!treeMap.isClosed()) {
treeMap.removeMap();
}
}
@Override
public void truncate(Session session) {
root = null;
rowCount = 0;
treeMap.clear();
}
@Override
......@@ -150,7 +208,7 @@ public class SpatialTreeIndex extends BaseIndex implements SpatialIndex {
@Override
public boolean needRebuild() {
return true;
return needRebuild;
}
@Override
......@@ -163,65 +221,69 @@ public class SpatialTreeIndex extends BaseIndex implements SpatialIndex {
if (closed) {
throw DbException.throwInternalError();
}
// TODO use an external iterator,
// but let's see if we can get it working first
@SuppressWarnings("unchecked")
List<Row> list = root.queryAll();
return new ListCursor(list, first);
if (!first) {
throw DbException.throwInternalError("Spatial Index can only be fetch by ascending order");
}
return find(session);
}
@Override
public long getRowCount(Session session) {
return rowCount;
return treeMap.getSize();
}
@Override
public long getRowCountApproximation() {
return rowCount;
return treeMap.getSize();
}
@Override
public long getDiskSpaceUsed() {
// TODO estimate disk space usage
return 0;
}
/**
* A cursor of a fixed list of rows.
* A cursor to iterate over spatial keys.
*/
private static final class ListCursor implements Cursor {
private final List<Row> rows;
private int index;
private Row current;
public ListCursor(List<Row> rows, boolean first) {
this.rows = rows;
this.index = first ? 0 : rows.size();
private static final class SpatialCursor implements Cursor {
private final Iterator<SpatialKey> it;
private SpatialKey current;
private final RegularTable tableData;
private Session session;
public SpatialCursor(Iterator<SpatialKey> it, RegularTable tableData, Session session) {
this.it = it;
this.tableData = tableData;
this.session = session;
}
@Override
public Row get() {
return current;
return tableData.getRow(session, current.getId());
}
@Override
public SearchRow getSearchRow() {
return current;
return get();
}
@Override
public boolean next() {
current = index >= rows.size() ? null : rows.get(index++);
return current != null;
if (!it.hasNext()) {
return false;
}
current = it.next();
return true;
}
@Override
public boolean previous() {
current = index < 0 ? null : rows.get(index--);
return current != null;
return false;
}
}
}
......@@ -56,6 +56,10 @@ import org.h2.value.ValueUuid;
/**
* This class represents a byte buffer that contains persistent data of a page.
*
* @author Thomas Mueller
* @author Noel Grandin
* @author Nicolas Fortin, Atelier SIG, IRSTV FR CNRS 24888
*/
public class Data {
......@@ -520,11 +524,13 @@ public class Data {
}
break;
}
case Value.GEOMETRY:
case Value.JAVA_OBJECT: {
writeByte((byte) type);
byte[] b = v.getBytesNoCopy();
writeVarInt(b.length);
write(b, 0, b.length);
int len = b.length;
writeVarInt(len);
write(b, 0, len);
break;
}
case Value.BYTES: {
......@@ -532,11 +538,11 @@ public class Data {
int len = b.length;
if (len < 32) {
writeByte((byte) (BYTES_0_31 + len));
write(b, 0, b.length);
write(b, 0, len);
} else {
writeByte((byte) type);
writeVarInt(b.length);
write(b, 0, b.length);
writeVarInt(len);
write(b, 0, len);
}
break;
}
......@@ -671,14 +677,6 @@ public class Data {
}
break;
}
case Value.GEOMETRY: {
writeByte((byte) type);
byte[] b = v.getBytes();
int len = b.length;
writeVarInt(len);
write(b, 0, len);
break;
}
default:
DbException.throwInternalError("type=" + v.getType());
}
......@@ -764,6 +762,12 @@ public class Data {
read(b, 0, len);
return ValueBytes.getNoCopy(b);
}
case Value.GEOMETRY: {
int len = readVarInt();
byte[] b = DataUtils.newBytes(len);
read(b, 0, len);
return ValueGeometry.get(b);
}
case Value.JAVA_OBJECT: {
int len = readVarInt();
byte[] b = DataUtils.newBytes(len);
......@@ -848,12 +852,6 @@ public class Data {
}
return ValueResultSet.get(rs);
}
case Value.GEOMETRY: {
int len = readVarInt();
byte[] b = DataUtils.newBytes(len);
read(b, 0, len);
return ValueGeometry.get(b);
}
default:
if (type >= INT_0_15 && type < INT_0_15 + 16) {
return ValueInt.get(type - INT_0_15);
......@@ -999,6 +997,7 @@ public class Data {
Timestamp ts = v.getTimestamp();
return 1 + getVarLongLen(DateTimeUtils.getTimeLocalWithoutDst(ts)) + getVarIntLen(ts.getNanos());
}
case Value.GEOMETRY:
case Value.JAVA_OBJECT: {
byte[] b = v.getBytesNoCopy();
return 1 + getVarIntLen(b.length) + b.length;
......@@ -1089,11 +1088,6 @@ public class Data {
}
return len;
}
case Value.GEOMETRY: {
byte[] b = v.getBytesNoCopy();
int len = b.length;
return 1 + getVarIntLen(len) + len;
}
default:
throw DbException.throwInternalError("type=" + v.getType());
}
......
......@@ -228,10 +228,10 @@ public class RegularTable extends TableBase {
if (mainIndexColumn != -1) {
mainIndex.setMainIndexColumn(mainIndexColumn);
index = new PageDelegateIndex(this, indexId, indexName, indexType, mainIndex, create, session);
} else if (!indexType.isSpatial()) {
index = new PageBtreeIndex(this, indexId, indexName, cols, indexType, create, session);
} else if (indexType.isSpatial()) {
index = new SpatialTreeIndex(this, indexId, indexName, cols, indexType, true, create, session);
} else {
throw new UnsupportedOperationException("Spatial index only supported with the MVStore");
index = new PageBtreeIndex(this, indexId, indexName, cols, indexType, create, session);
}
} else {
if (indexType.isHash() && cols.length <= 1) {
......@@ -243,7 +243,7 @@ public class RegularTable extends TableBase {
} else if (!indexType.isSpatial()) {
index = new TreeIndex(this, indexId, indexName, cols, indexType);
} else {
index = new SpatialTreeIndex(this, indexId, indexName, cols, indexType);
index = new SpatialTreeIndex(this, indexId, indexName, cols, indexType, false, true, session);
}
}
if (database.isMultiVersion()) {
......
......@@ -961,7 +961,13 @@ public class DataType {
}
}
private static boolean isGeometry(Object x) {
/**
* Check whether a given object is a Geometry object.
*
* @param x the the object
* @return true if it is a Geometry object
*/
public static boolean isGeometry(Object x) {
if (x == null || GEOMETRY_CLASS == null) {
return false;
}
......
......@@ -34,6 +34,10 @@ import org.h2.util.Utils;
/**
* This is the base class for all value classes.
* It provides conversion and comparison methods.
*
* @author Thomas Mueller
* @author Noel Grandin
* @author Nicolas Fortin, Atelier SIG, IRSTV FR CNRS 24888
*/
public abstract class Value {
......@@ -793,6 +797,11 @@ public abstract class Value {
switch(getType()) {
case BYTES:
return ValueGeometry.get(getBytesNoCopy());
case JAVA_OBJECT:
Object object = Utils.deserialize(getBytesNoCopy());
if (DataType.isGeometry(object)) {
return ValueGeometry.getFromGeometry(object);
}
}
}
// conversion by parsing the string value
......
......@@ -11,7 +11,9 @@ import java.sql.SQLException;
import org.h2.message.DbException;
import org.h2.util.StringUtils;
import com.vividsolutions.jts.geom.Envelope;
import com.vividsolutions.jts.geom.Geometry;
import com.vividsolutions.jts.geom.GeometryFactory;
import com.vividsolutions.jts.io.ParseException;
import com.vividsolutions.jts.io.WKBReader;
import com.vividsolutions.jts.io.WKBWriter;
......@@ -20,6 +22,10 @@ import com.vividsolutions.jts.io.WKTWriter;
/**
* Implementation of the GEOMETRY data type.
*
* @author Thomas Mueller
* @author Noel Grandin
* @author Nicolas Fortin, Atelier SIG, IRSTV FR CNRS 24888
*/
public class ValueGeometry extends Value {
......@@ -71,35 +77,37 @@ public class ValueGeometry extends Value {
}
/**
* Check whether two values intersect.
*
* @param r the second value
* @return true if they intersect
* Test if this geometry envelope intersects with the other geometry
* envelope.
*
* @param r the other geometry
* @return true if the two envelopes overlaps
*/
public boolean intersects(ValueGeometry r) {
return geometry.intersects(r.getGeometry());
public boolean intersectsBoundingBox(ValueGeometry r) {
// it is useless to cache the envelope as the Geometry object do this already
return geometry.getEnvelopeInternal().intersects(r.getGeometry().getEnvelopeInternal());
}
/**
* Get the intersection of two values.
*
* @param r the second value
* @return the intersection
*/
public Value intersection(ValueGeometry r) {
return get(geometry.intersection(r.geometry));
}
/**
* Get the union of two values.
*
* @param r the second value
* @return the union
* Get the intersection.
*
* @param r the other geometry
* @return the intersection of this geometry envelope and another geometry envelope
*/
public Value union(ValueGeometry r) {
return get(geometry.union(r.geometry));
public ValueGeometry getEnvelopeIntersection(ValueGeometry r) {
Envelope e1 = geometry.getEnvelopeInternal();
Envelope e2 = r.getGeometry().getEnvelopeInternal();
Envelope e3 = e1.intersection(e2);
// try to re-use the object
if (e3 == e1) {
return this;
} else if (e3 == e2) {
return r;
}
GeometryFactory gf = new GeometryFactory();
return get(gf.toGeometry(e3));
}
@Override
public int getType() {
return Value.GEOMETRY;
......@@ -107,7 +115,7 @@ public class ValueGeometry extends Value {
@Override
public String getSQL() {
return StringUtils.quoteStringSQL(toWKT());
return StringUtils.quoteStringSQL(toWKT()) + "'::Geometry";
}
@Override
......@@ -141,6 +149,11 @@ public class ValueGeometry extends Value {
return toWKB();
}
@Override
public byte[] getBytesNoCopy() {
return toWKB();
}
@Override
public void set(PreparedStatement prep, int parameterIndex) throws SQLException {
prep.setObject(parameterIndex, geometry);
......@@ -167,18 +180,16 @@ public class ValueGeometry extends Value {
* @return the well-known-text
*/
public String toWKT() {
WKTWriter w = new WKTWriter();
return w.write(geometry);
return new WKTWriter().write(geometry);
}
/**
* Convert to value to the Well-Known-Binary format.
* Convert to Well-Known-Binary format.
*
* @return the well-known-binary
*/
public byte[] toWKB() {
WKBWriter w = new WKBWriter();
return w.write(geometry);
return new WKBWriter().write(geometry);
}
/**
......@@ -188,9 +199,8 @@ public class ValueGeometry extends Value {
* @return the Geometry object
*/
private static Geometry fromWKT(String s) {
WKTReader r = new WKTReader();
try {
return r.read(s);
return new WKTReader().read(s);
} catch (ParseException ex) {
throw DbException.convert(ex);
}
......@@ -203,9 +213,8 @@ public class ValueGeometry extends Value {
* @return the Geometry object
*/
private static Geometry fromWKB(byte[] bytes) {
WKBReader r = new WKBReader();
try {
return r.read(bytes);
return new WKBReader().read(bytes);
} catch (ParseException ex) {
throw DbException.convert(ex);
}
......
......@@ -6,18 +6,40 @@
package org.h2.test.db;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Savepoint;
import java.sql.Statement;
import java.sql.Types;
import java.util.HashSet;
import java.util.Random;
import java.util.Set;
import org.h2.engine.Database;
import org.h2.engine.Session;
import org.h2.jdbc.JdbcConnection;
import org.h2.mvstore.MVStore;
import org.h2.test.TestBase;
import org.h2.tools.SimpleResultSet;
import org.h2.tools.SimpleRowSource;
import org.h2.value.DataType;
import org.h2.value.ValueGeometry;
import com.vividsolutions.jts.geom.Coordinate;
import com.vividsolutions.jts.geom.Envelope;
import com.vividsolutions.jts.geom.Geometry;
import com.vividsolutions.jts.geom.GeometryFactory;
import com.vividsolutions.jts.geom.Polygon;
import com.vividsolutions.jts.io.ParseException;
import com.vividsolutions.jts.io.WKTReader;
/**
* Spatial datatype and index tests.
*
* @author Thomas Mueller
* @author Noel Grandin
* @author Nicolas Fortin, Atelier SIG, IRSTV FR CNRS 24888
*/
public class TestSpatial extends TestBase {
......@@ -34,8 +56,17 @@ public class TestSpatial extends TestBase {
public void test() throws SQLException {
if (DataType.GEOMETRY_CLASS != null) {
deleteDb("spatial");
testSpatialValues();
// testSpatialValues();
// testOverlap();
// testNotOverlap();
// testPersistentSpatialIndex();
// testSpatialIndexQueryMultipleTable();
// testIndexTransaction();
// testJavaAlias();
// testJavaAliasTableFunction();
// testPersistentSpatialIndex2();
testMemorySpatialIndex();
testRandom();
deleteDb("spatial");
}
}
......@@ -70,7 +101,270 @@ public class TestSpatial extends TestBase {
deleteDb("spatial");
}
/** test in the in-memory spatial index */
/**
* Generate a random linestring under the given bounding box
* @param minX Bounding box min x
* @param maxX Bounding box max x
* @param minY Bounding box min y
* @param maxY Bounding box max y
* @param maxLength LineString maximum length
* @return A segment within this bounding box
*/
public static Geometry getRandomGeometry(Random geometryRand,double minX,double maxX,double minY, double maxY, double maxLength) {
GeometryFactory factory = new GeometryFactory();
// Create the start point
Coordinate start = new Coordinate(geometryRand.nextDouble()*(maxX-minX)+minX,
geometryRand.nextDouble()*(maxY-minY)+minY);
// Compute an angle
double angle = geometryRand.nextDouble() * Math.PI * 2;
// Compute length
double length = geometryRand.nextDouble() * maxLength;
// Compute end point
Coordinate end = new Coordinate(start.x + Math.cos(angle) * length, start.y + Math.sin(angle) * length);
return factory.createLineString(new Coordinate[]{start,end});
}
private void testRandom() throws SQLException {
deleteDb("spatial");
Connection conn = getConnection("spatial");
testRandom(conn, 69, 3500);
testRandom(conn, 44, 3500);
conn.close();
}
private void testRandom(Connection conn, long seed,long size) throws SQLException {
Statement stat = conn.createStatement();
stat.execute("drop table if exists test");
Random geometryRand = new Random(seed);
// Generate a set of geometry
// It is marked as random, but it generate always the same geometry set, given the same seed
stat.execute("create memory table test(id long primary key auto_increment, poly geometry)");
// Create segment generation bounding box
Envelope bbox = ValueGeometry.get("POLYGON ((301804.1049793153 2251719.1222191923," +
" 301804.1049793153 2254747.2888244865, 304646.87362918374 2254747.2888244865," +
" 304646.87362918374 2251719.1222191923, 301804.1049793153 2251719.1222191923))")
.getGeometry().getEnvelopeInternal();
// Create overlap test bounding box
String testBBoxString = "POLYGON ((302215.44416332216 2252748, 302215.44416332216 2253851.781225762," +
" 303582.85796541866 2253851.781225762, 303582.85796541866 2252748.526908161," +
" 302215.44416332216 2252748))";
Envelope testBBox = ValueGeometry.get(testBBoxString).getGeometry().getEnvelopeInternal();
PreparedStatement ps = conn.prepareStatement("insert into test(poly) values (?)");
long overlapCount = 0;
Set<Integer> overlaps = new HashSet<Integer>(680);
for(int i=1;i<=size;i++) {
Geometry geometry = getRandomGeometry(geometryRand,bbox.getMinX(),bbox.getMaxX(),bbox.getMinY(),bbox.getMaxY(),200);
ps.setObject(1,geometry);
ps.execute();
ResultSet keys = ps.getGeneratedKeys();
keys.next();
if(geometry.getEnvelopeInternal().intersects(testBBox)) {
overlapCount++;
overlaps.add(keys.getInt(1));
}
}
ps.close();
// Create index
stat.execute("create spatial index idx_test_poly on test(poly)");
// Must find the same overlap count with index
ps = conn.prepareStatement("select id from test where poly && ?::Geometry");
ps.setString(1,testBBoxString);
ResultSet rs = ps.executeQuery();
long found = 0;
while(rs.next()) {
overlaps.remove(rs.getInt(1));
found++;
}
// Index count must be the same as sequential count
assertEquals(overlapCount,found);
// Missing id still in overlaps map
assertTrue(overlaps.isEmpty());
stat.execute("drop table if exists test");
}
private void testOverlap() throws SQLException {
deleteDb("spatial");
Connection conn = getConnection("spatial");
try {
Statement stat = conn.createStatement();
stat.execute("create memory table test(id int primary key, poly geometry)");
stat.execute("insert into test values(1, 'POLYGON ((1 1, 1 2, 2 2, 1 1))')");
stat.execute("insert into test values(2, 'POLYGON ((3 1, 3 2, 4 2, 3 1))')");
stat.execute("insert into test values(3, 'POLYGON ((1 3, 1 4, 2 4, 1 3))')");
ResultSet rs = stat.executeQuery("select * from test where poly && 'POINT (1.5 1.5)'::Geometry");
assertTrue(rs.next());
assertEquals(1,rs.getInt("id"));
assertFalse(rs.next());
stat.execute("drop table test");
} finally {
conn.close();
}
}
private void testPersistentSpatialIndex() throws SQLException {
deleteDb("spatial_pers");
Connection conn = getConnection("spatial_pers");
try {
Statement stat = conn.createStatement();
stat.execute("create table test(id int primary key, poly geometry)");
stat.execute("insert into test values(1, 'POLYGON ((1 1, 1 2, 2 2, 1 1))')");
stat.execute("insert into test values(2, 'POLYGON ((3 1, 3 2, 4 2, 3 1))')");
stat.execute("insert into test values(3, 'POLYGON ((1 3, 1 4, 2 4, 1 3))')");
stat.execute("create spatial index on test(poly)");
ResultSet rs = stat.executeQuery("select * from test where poly && 'POINT (1.5 1.5)'::Geometry");
assertTrue(rs.next());
assertEquals(1, rs.getInt("id"));
assertFalse(rs.next());
rs.close();
// Test with multiple operator
rs = stat.executeQuery("select * from test where poly && 'POINT (1.5 1.5)'::Geometry AND poly && 'POINT (1.7 1.75)'::Geometry");
assertTrue(rs.next());
assertEquals(1, rs.getInt("id"));
assertFalse(rs.next());
rs.close();
} finally {
// Close the database
conn.close();
}
conn = getConnection("spatial_pers");
try {
Statement stat = conn.createStatement();
ResultSet rs = stat.executeQuery("select * from test where poly && 'POINT (1.5 1.5)'::Geometry");
assertTrue(rs.next());
assertEquals(1,rs.getInt("id"));
assertFalse(rs.next());
stat.execute("drop table test");
} finally {
conn.close();
}
}
private void testNotOverlap() throws SQLException {
deleteDb("spatial");
Connection conn = getConnection("spatial");
try {
Statement stat = conn.createStatement();
stat.execute("create memory table test(id int primary key, poly geometry)");
stat.execute("insert into test values(1, 'POLYGON ((1 1, 1 2, 2 2, 1 1))')");
stat.execute("insert into test values(2, 'POLYGON ((3 1, 3 2, 4 2, 3 1))')");
stat.execute("insert into test values(3, 'POLYGON ((1 3, 1 4, 2 4, 1 3))')");
ResultSet rs = stat.executeQuery("select * from test where NOT poly && 'POINT (1.5 1.5)'::Geometry");
assertTrue(rs.next());
assertEquals(2,rs.getInt("id"));
assertTrue(rs.next());
assertEquals(3,rs.getInt("id"));
assertFalse(rs.next());
stat.execute("drop table test");
} finally {
conn.close();
}
}
private static void createTestTable(Statement stat) throws SQLException {
stat.execute("create table area(idarea int primary key, the_geom geometry)");
stat.execute("create spatial index on area(the_geom)");
stat.execute("insert into area values(1, 'POLYGON ((-10 109, 90 109, 90 9, -10 9, -10 109))')");
stat.execute("insert into area values(2, 'POLYGON ((90 109, 190 109, 190 9, 90 9, 90 109))')");
stat.execute("insert into area values(3, 'POLYGON ((190 109, 290 109, 290 9, 190 9, 190 109))')");
stat.execute("insert into area values(4, 'POLYGON ((-10 9, 90 9, 90 -91, -10 -91, -10 9))')");
stat.execute("insert into area values(5, 'POLYGON ((90 9, 190 9, 190 -91, 90 -91, 90 9))')");
stat.execute("insert into area values(6, 'POLYGON ((190 9, 290 9, 290 -91, 190 -91, 190 9))')");
stat.execute("create table roads(idroad int primary key, the_geom geometry)");
stat.execute("create spatial index on roads(the_geom)");
stat.execute("insert into roads values(1, 'LINESTRING (27.65595463138 -16.728733459357244, 47.61814744801515 40.435727788279806)')");
stat.execute("insert into roads values(2, 'LINESTRING (17.674858223062415 55.861058601134246, 55.78449905482046 76.73062381852554)')");
stat.execute("insert into roads values(3, 'LINESTRING (68.48771266540646 67.65689981096412, 108.4120982986768 88.52646502835542)')");
stat.execute("insert into roads values(4, 'LINESTRING (177.3724007561437 18.65879017013235, 196.4272211720227 -16.728733459357244)')");
stat.execute("insert into roads values(5, 'LINESTRING (106.5973534971645 -12.191871455576518, 143.79962192816637 30.454631379962223)')");
stat.execute("insert into roads values(6, 'LINESTRING (144.70699432892252 55.861058601134246, 150.1512287334594 83.9896030245747)')");
stat.execute("insert into roads values(7, 'LINESTRING (60.321361058601155 -13.099243856332663, 149.24385633270325 5.955576559546344)')");
}
private void testSpatialIndexQueryMultipleTable() throws SQLException {
deleteDb("spatial");
Connection conn = getConnection("spatial");
try {
Statement stat = conn.createStatement();
createTestTable(stat);
testRoadAndArea(stat);
} finally {
// Close the database
conn.close();
}
deleteDb("spatial");
}
private void testRoadAndArea(Statement stat) throws SQLException {
ResultSet rs = stat.executeQuery("select idarea, COUNT(idroad) roadscount from area,roads where area.the_geom && roads.the_geom GROUP BY idarea ORDER BY idarea");
assertTrue(rs.next());
assertEquals(1,rs.getInt("idarea"));
assertEquals(3,rs.getInt("roadscount"));
assertTrue(rs.next());
assertEquals(2,rs.getInt("idarea"));
assertEquals(4,rs.getInt("roadscount"));
assertTrue(rs.next());
assertEquals(3,rs.getInt("idarea"));
assertEquals(1,rs.getInt("roadscount"));
assertTrue(rs.next());
assertEquals(4,rs.getInt("idarea"));
assertEquals(2,rs.getInt("roadscount"));
assertTrue(rs.next());
assertEquals(5,rs.getInt("idarea"));
assertEquals(3,rs.getInt("roadscount"));
assertTrue(rs.next());
assertEquals(6,rs.getInt("idarea"));
assertEquals(1,rs.getInt("roadscount"));
assertFalse(rs.next());
rs.close();
}
private void testIndexTransaction() throws SQLException {
// Check session management in index
deleteDb("spatialIndex");
Connection conn = getConnection("spatialIndex");
conn.setAutoCommit(false);
try {
Statement stat = conn.createStatement();
createTestTable(stat);
Savepoint sp = conn.setSavepoint();
// Remove a row but do not commit
stat.execute("delete from roads where idroad=7");
// Check if index is updated
ResultSet rs = stat.executeQuery("select idarea, COUNT(idroad) roadscount from area,roads where area.the_geom && roads.the_geom GROUP BY idarea ORDER BY idarea");
assertTrue(rs.next());
assertEquals(1,rs.getInt("idarea"));
assertEquals(3,rs.getInt("roadscount"));
assertTrue(rs.next());
assertEquals(2,rs.getInt("idarea"));
assertEquals(4,rs.getInt("roadscount"));
assertTrue(rs.next());
assertEquals(3,rs.getInt("idarea"));
assertEquals(1,rs.getInt("roadscount"));
assertTrue(rs.next());
assertEquals(4,rs.getInt("idarea"));
assertEquals(1,rs.getInt("roadscount"));
assertTrue(rs.next());
assertEquals(5,rs.getInt("idarea"));
assertEquals(2,rs.getInt("roadscount"));
assertTrue(rs.next());
assertEquals(6,rs.getInt("idarea"));
assertEquals(1,rs.getInt("roadscount"));
assertFalse(rs.next());
rs.close();
conn.rollback(sp);
// Check if the index is restored
testRoadAndArea(stat);
} finally {
conn.close();
}
}
/**
* Test the in the in-memory spatial index
*/
private void testMemorySpatialIndex() throws SQLException {
deleteDb("spatialIndex");
Connection conn = getConnection("spatialIndex");
......@@ -79,16 +373,28 @@ public class TestSpatial extends TestBase {
stat.execute("create memory table test(id int primary key, polygon geometry)");
stat.execute("create spatial index idx_test_polygon on test(polygon)");
stat.execute("insert into test values(1, 'POLYGON ((1 1, 1 2, 2 2, 1 1))')");
ResultSet rs = stat.executeQuery("explain select * from test where polygon = 'POLYGON ((1 1, 1 2, 2 2, 1 1))'");
ResultSet rs;
// an query that can not possibly return a result
rs = stat.executeQuery("select * from test " +
"where polygon && 'POLYGON ((1 1, 1 2, 2 2, 1 1))'::Geometry " +
"and polygon && 'POLYGON ((10 10, 10 20, 20 20, 10 10))'::Geometry");
assertFalse(rs.next());
rs = stat.executeQuery("explain select * from test where polygon && 'POLYGON ((1 1, 1 2, 2 2, 1 1))'::Geometry");
rs.next();
assertContains(rs.getString(1), "/* PUBLIC.IDX_TEST_POLYGON: POLYGON =");
assertContains(rs.getString(1), "/* PUBLIC.IDX_TEST_POLYGON: POLYGON &&");
// these queries actually have no meaning in the context of a spatial index,
// just check that the query works
stat.executeQuery("select * from test where polygon = 'POLYGON ((1 1, 1 2, 2 2, 1 1))'");
stat.executeQuery("select * from test where polygon > 'POLYGON ((1 1, 1 2, 2 2, 1 1))'");
stat.executeQuery("select * from test where polygon < 'POLYGON ((1 1, 1 2, 2 2, 1 1))'");
int todo;
// TODO equality should probably also use the spatial index
// rs = stat.executeQuery("explain select * from test where polygon = 'POLYGON ((1 1, 1 2, 2 2, 1 1))'");
// rs.next();
// assertContains(rs.getString(1), "/* PUBLIC.IDX_TEST_POLYGON: POLYGON =");
// these queries actually have no meaning in the context of a spatial index, but
// check them anyhow
stat.executeQuery("select * from test where polygon > 'POLYGON ((1 1, 1 2, 2 2, 1 1))'::Geometry");
stat.executeQuery("select * from test where polygon < 'POLYGON ((1 1, 1 2, 2 2, 1 1))'::Geometry");
rs = stat.executeQuery("select * from test where intersects(polygon, 'POLYGON ((1 1, 1 2, 2 2, 1 1))')");
assertTrue(rs.next());
......@@ -104,4 +410,124 @@ public class TestSpatial extends TestBase {
deleteDb("spatialIndex");
}
/**
* Test java alias with Geometry type.
*/
private void testJavaAlias() throws SQLException {
deleteDb("spatialIndex");
Connection conn = getConnection("spatialIndex");
try {
Statement stat = conn.createStatement();
stat.execute("CREATE ALIAS T_GEOMFROMTEXT FOR \"" + TestSpatial.class.getName() + ".geomFromText\"");
stat.execute("create table test(id int primary key auto_increment, the_geom geometry)");
stat.execute("insert into test(the_geom) values(T_GEOMFROMTEXT('POLYGON ((62 48, 84 48, 84 42, 56 34, 62 48))',1488))");
stat.execute("DROP ALIAS T_GEOMFROMTEXT");
ResultSet rs = stat.executeQuery("select the_geom from test");
assertTrue(rs.next());
assertEquals("POLYGON ((62 48, 84 48, 84 42, 56 34, 62 48))", rs.getObject(1).toString());
} finally {
conn.close();
}
deleteDb("spatialIndex");
}
/**
* Test java alias with Geometry type.
*/
private void testJavaAliasTableFunction() throws SQLException {
deleteDb("spatialIndex");
Connection conn = getConnection("spatialIndex");
try {
Statement stat = conn.createStatement();
stat.execute("CREATE ALIAS T_RANDOM_GEOM_TABLE FOR \"" + TestSpatial.class.getName() + ".getRandomGeometryTable\"");
stat.execute("create table test as select * from T_RANDOM_GEOM_TABLE(42,20,-100,100,-100,100,4)");
stat.execute("DROP ALIAS T_RANDOM_GEOM_TABLE");
ResultSet rs = stat.executeQuery("select count(*) cpt from test");
assertTrue(rs.next());
assertEquals(20, rs.getInt(1));
} finally {
conn.close();
}
deleteDb("spatialIndex");
}
public static ResultSet getRandomGeometryTable(final long seed,final long rowCount, final double minX,final double maxX,final double minY, final double maxY, final double maxLength) {
SimpleResultSet rs = new SimpleResultSet(new SimpleRowSource() {
private final Random rnd = new Random(seed);
private int cpt = 0;
@Override
public Object[] readRow() throws SQLException {
if(cpt++<rowCount) {
return new Object[]{getRandomGeometry(rnd,minX,maxX,minY,maxY,maxLength)}; //To change body of implemented methods use File | Settings | File Templates.
} else {
return null;
}
}
@Override
public void close() {
}
@Override
public void reset() throws SQLException {
rnd.setSeed(seed);
}
});
rs.addColumn("the_geom", Types.OTHER,Integer.MAX_VALUE,0);
return rs;
}
/**
*
* @param text Geometry in Well Known Text
* @param srid Projection ID
* @return Geometry object
*/
public static Geometry geomFromText(String text, int srid) throws SQLException {
WKTReader wktReader = new WKTReader();
try {
Geometry geom = wktReader.read(text);
geom.setSRID(srid);
return geom;
} catch (ParseException ex) {
throw new SQLException(ex);
}
}
/**
* Not really a test case but show that something go crazy (in mvstore) after some seconds.
* @throws SQLException
*/
private void testPersistentSpatialIndex2() throws SQLException {
deleteDb("spatial_pers");
final long count = 150000;
Connection conn = getConnection("spatial_pers");
try {
Statement stat = conn.createStatement();
stat.execute("create table test(id int primary key auto_increment, the_geom geometry)");
PreparedStatement ps = conn.prepareStatement("insert into test(the_geom) values(?)");
Random rnd = new Random(44);
for(int i=0;i<count;i++) {
ps.setObject(1,getRandomGeometry(rnd,0,100,-50,50,3));
ps.execute();
}
stat.execute("create spatial index on test(the_geom)");
Database db = ((Session)((JdbcConnection) conn).getSession()).getDatabase();
MVStore store = db.getMvStore().getStore();
int cpt=0;
while(cpt<46) {
try {
// First it shows 610, then 5 until cpt==44, finally at cpt==45 it shows an unsaved 688 with a trace in spatial_pers.trace.db
System.out.println((cpt++)+" store.getUnsavedPageCount()=="+store.getUnsavedPageCount());
Thread.sleep(1000);
} catch (InterruptedException ex) {
throw new SQLException(ex);
}
}
} finally {
// Close the database
conn.close();
}
}
}
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论