Skip to content

Commit

Permalink
Refactor SimpleFeatureFactory so it has no external dependencies (#75232
Browse files Browse the repository at this point in the history
)

SimpleFeatureFactory has no external dependencies now.
  • Loading branch information
iverase authored Jul 14, 2021
1 parent c8c7f0e commit e5eb047
Show file tree
Hide file tree
Showing 8 changed files with 602 additions and 163 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
import org.elasticsearch.geometry.Point;
import org.elasticsearch.geometry.Polygon;
import org.elasticsearch.geometry.Rectangle;
import org.elasticsearch.search.aggregations.bucket.geogrid.GeoTileUtils;
import org.locationtech.jts.geom.Coordinate;
import org.locationtech.jts.geom.Envelope;
import org.locationtech.jts.geom.GeometryFactory;
Expand All @@ -49,8 +50,9 @@ public class FeatureFactory {
private final Envelope clipEnvelope;

public FeatureFactory(int z, int x, int y, int extent) {
this.tileEnvelope = FeatureFactoryUtils.getJTSTileBounds(z, x, y);
this.clipEnvelope = FeatureFactoryUtils.getJTSTileBounds(z, x, y);
final Rectangle r = SphericalMercatorUtils.recToSphericalMercator(GeoTileUtils.toBoundingBox(x, y, z));
this.tileEnvelope = new Envelope(r.getMinX(), r.getMaxX(), r.getMinY(), r.getMaxY());
this.clipEnvelope = new Envelope(tileEnvelope);
this.clipEnvelope.expandBy(tileEnvelope.getWidth() * 0.1d, tileEnvelope.getHeight() * 0.1d);
this.builder = new JTSGeometryBuilder(geomFactory);
// TODO: Not sure what is the difference between extent and tile size?
Expand Down Expand Up @@ -89,7 +91,8 @@ public org.locationtech.jts.geom.Geometry visit(Circle circle) {

@Override
public org.locationtech.jts.geom.Geometry visit(GeometryCollection<?> collection) {
throw new IllegalArgumentException("Circle is not supported");
// TODO: Geometry collections are not supported by the vector tile specification.
throw new IllegalArgumentException("GeometryCollection is not supported");
}

@Override
Expand All @@ -112,8 +115,8 @@ public org.locationtech.jts.geom.Geometry visit(MultiPoint multiPoint) throws Ru
}

private org.locationtech.jts.geom.Point buildPoint(Point point) {
final double x = FeatureFactoryUtils.lonToSphericalMercator(point.getX());
final double y = FeatureFactoryUtils.latToSphericalMercator(point.getY());
final double x = SphericalMercatorUtils.lonToSphericalMercator(point.getX());
final double y = SphericalMercatorUtils.latToSphericalMercator(point.getY());
return geomFactory.createPoint(new Coordinate(x, y));
}

Expand All @@ -134,8 +137,8 @@ public org.locationtech.jts.geom.Geometry visit(MultiLine multiLine) throws Runt
private LineString buildLine(Line line) {
final Coordinate[] coordinates = new Coordinate[line.length()];
for (int i = 0; i < line.length(); i++) {
final double x = FeatureFactoryUtils.lonToSphericalMercator(line.getX(i));
final double y = FeatureFactoryUtils.latToSphericalMercator(line.getY(i));
final double x = SphericalMercatorUtils.lonToSphericalMercator(line.getX(i));
final double y = SphericalMercatorUtils.latToSphericalMercator(line.getY(i));
coordinates[i] = new Coordinate(x, y);
}
return geomFactory.createLineString(coordinates);
Expand Down Expand Up @@ -170,8 +173,8 @@ private org.locationtech.jts.geom.Polygon buildPolygon(Polygon polygon) {
private org.locationtech.jts.geom.LinearRing buildLinearRing(LinearRing ring) throws RuntimeException {
final Coordinate[] coordinates = new Coordinate[ring.length()];
for (int i = 0; i < ring.length(); i++) {
final double x = FeatureFactoryUtils.lonToSphericalMercator(ring.getX(i));
final double y = FeatureFactoryUtils.latToSphericalMercator(ring.getY(i));
final double x = SphericalMercatorUtils.lonToSphericalMercator(ring.getX(i));
final double y = SphericalMercatorUtils.latToSphericalMercator(ring.getY(i));
coordinates[i] = new Coordinate(x, y);
}
return geomFactory.createLinearRing(coordinates);
Expand All @@ -180,10 +183,10 @@ private org.locationtech.jts.geom.LinearRing buildLinearRing(LinearRing ring) th
@Override
public org.locationtech.jts.geom.Geometry visit(Rectangle rectangle) throws RuntimeException {
// TODO: handle degenerated rectangles?
final double xMin = FeatureFactoryUtils.lonToSphericalMercator(rectangle.getMinX());
final double yMin = FeatureFactoryUtils.latToSphericalMercator(rectangle.getMinY());
final double xMax = FeatureFactoryUtils.lonToSphericalMercator(rectangle.getMaxX());
final double yMax = FeatureFactoryUtils.latToSphericalMercator(rectangle.getMaxY());
final double xMin = SphericalMercatorUtils.lonToSphericalMercator(rectangle.getMinX());
final double yMin = SphericalMercatorUtils.latToSphericalMercator(rectangle.getMinY());
final double xMax = SphericalMercatorUtils.lonToSphericalMercator(rectangle.getMaxX());
final double yMax = SphericalMercatorUtils.latToSphericalMercator(rectangle.getMaxY());
final Coordinate[] coordinates = new Coordinate[5];
coordinates[0] = new Coordinate(xMin, yMin);
coordinates[1] = new Coordinate(xMax, yMin);
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -7,66 +7,157 @@

package org.elasticsearch.xpack.vectortile.feature;

import com.wdtinc.mapbox_vector_tile.VectorTile;
import com.wdtinc.mapbox_vector_tile.encoding.GeomCmd;
import com.wdtinc.mapbox_vector_tile.encoding.GeomCmdHdr;

import org.apache.lucene.util.BitUtil;
import org.elasticsearch.common.io.stream.BytesStreamOutput;
import org.elasticsearch.geometry.Point;
import org.elasticsearch.geometry.Rectangle;
import org.elasticsearch.search.aggregations.bucket.geogrid.GeoTileUtils;

import java.io.IOException;
import java.util.Comparator;
import java.util.List;

/**
* Similar to {@link FeatureFactory} but only supports points and rectangles. It is just
* much more efficient for those shapes.
* more efficient for those shapes and it does not use external dependencies.
*/
public class SimpleFeatureFactory {

private final int extent;
private final double pointXScale, pointYScale, pointXTranslate, pointYTranslate;

private static final int MOVETO = 1;
private static final int LINETO = 2;
private static final int CLOSEPATH = 7;

private static final byte[] EMPTY = new byte[0];

public SimpleFeatureFactory(int z, int x, int y, int extent) {
this.extent = extent;
final Rectangle rectangle = FeatureFactoryUtils.getTileBounds(z, x, y);
final Rectangle rectangle = SphericalMercatorUtils.recToSphericalMercator(GeoTileUtils.toBoundingBox(x, y, z));
pointXScale = (double) extent / (rectangle.getMaxLon() - rectangle.getMinLon());
pointYScale = (double) -extent / (rectangle.getMaxLat() - rectangle.getMinLat());
pointXTranslate = -pointXScale * rectangle.getMinX();
pointYTranslate = -pointYScale * rectangle.getMinY();
}

public void point(VectorTile.Tile.Feature.Builder featureBuilder, double lon, double lat) {
featureBuilder.setType(VectorTile.Tile.GeomType.POINT);
featureBuilder.addGeometry(GeomCmdHdr.cmdHdr(GeomCmd.MoveTo, 1));
featureBuilder.addGeometry(BitUtil.zigZagEncode(lon(lon)));
featureBuilder.addGeometry(BitUtil.zigZagEncode(lat(lat)));
/**
* Returns a {@code byte[]} containing the mvt representation of the provided point
*/
public byte[] point(double lon, double lat) throws IOException {
final int posLon = lon(lon);
if (posLon > extent || posLon < 0) {
return EMPTY;
}
final int posLat = lat(lat);
if (posLat > extent || posLat < 0) {
return EMPTY;
}
final int[] commands = new int[3];
commands[0] = encodeCommand(MOVETO, 1);
commands[1] = BitUtil.zigZagEncode(posLon);
commands[2] = BitUtil.zigZagEncode(posLat);
return writeCommands(commands, 1, 3);
}

/**
* Returns a {@code byte[]} containing the mvt representation of the provided points
*/
public byte[] points(List<Point> multiPoint) throws IOException {
multiPoint.sort(Comparator.comparingDouble(Point::getLon).thenComparingDouble(Point::getLat));
final int[] commands = new int[2 * multiPoint.size() + 1];
int pos = 1, prevLon = 0, prevLat = 0, numPoints = 0;
for (int i = 0; i < multiPoint.size(); i++) {
final Point point = multiPoint.get(i);
final int posLon = lon(point.getLon());
if (posLon > extent || posLon < 0) {
continue;
}
final int posLat = lat(point.getLat());
if (posLat > extent || posLat < 0) {
continue;
}
if (i == 0 || posLon != prevLon || posLat != prevLat) {
commands[pos++] = BitUtil.zigZagEncode(posLon - prevLon);
commands[pos++] = BitUtil.zigZagEncode(posLat - prevLat);
prevLon = posLon;
prevLat = posLat;
numPoints++;
}
}
if (numPoints == 0) {
return EMPTY;
}
commands[0] = encodeCommand(MOVETO, numPoints);
return writeCommands(commands, 1, pos);
}

public void box(VectorTile.Tile.Feature.Builder featureBuilder, double minLon, double maxLon, double minLat, double maxLat) {
featureBuilder.setType(VectorTile.Tile.GeomType.POLYGON);
final int minX = lon(minLon);
final int minY = lat(minLat);
final int maxX = lon(maxLon);
final int maxY = lat(maxLat);
featureBuilder.addGeometry(GeomCmdHdr.cmdHdr(GeomCmd.MoveTo, 1));
featureBuilder.addGeometry(BitUtil.zigZagEncode(minX));
featureBuilder.addGeometry(BitUtil.zigZagEncode(minY));
featureBuilder.addGeometry(GeomCmdHdr.cmdHdr(GeomCmd.LineTo, 3));
/**
* Returns a {@code byte[]} containing the mvt representation of the provided rectangle
*/
public byte[] box(double minLon, double maxLon, double minLat, double maxLat) throws IOException {
int[] commands = new int[11];
final int minX = Math.max(0, lon(minLon));
if (minX > extent) {
return EMPTY;
}
final int minY = Math.min(extent, lat(minLat));
if (minY > extent) {
return EMPTY;
}
final int maxX = Math.min(extent, lon(maxLon));
if (maxX < 0 || minX == maxX) {
return EMPTY;
}
final int maxY = Math.max(0, lat(maxLat));
if (maxY < 0 || minY == maxY) {
return EMPTY;
}
commands[0] = encodeCommand(MOVETO, 1);
commands[1] = BitUtil.zigZagEncode(minX);
commands[2] = BitUtil.zigZagEncode(minY);
commands[3] = encodeCommand(LINETO, 3);
// 1
featureBuilder.addGeometry(BitUtil.zigZagEncode(maxX - minX));
featureBuilder.addGeometry(BitUtil.zigZagEncode(0));
commands[4] = BitUtil.zigZagEncode(maxX - minX);
commands[5] = BitUtil.zigZagEncode(0);
// 2
featureBuilder.addGeometry(BitUtil.zigZagEncode(0));
featureBuilder.addGeometry(BitUtil.zigZagEncode(maxY - minY));
commands[6] = BitUtil.zigZagEncode(0);
commands[7] = BitUtil.zigZagEncode(maxY - minY);
// 3
featureBuilder.addGeometry(BitUtil.zigZagEncode(minX - maxX));
featureBuilder.addGeometry(BitUtil.zigZagEncode(0));
commands[8] = BitUtil.zigZagEncode(minX - maxX);
commands[9] = BitUtil.zigZagEncode(0);
// close
featureBuilder.addGeometry(GeomCmdHdr.cmdHdr(GeomCmd.ClosePath, 1));
commands[10] = encodeCommand(CLOSEPATH, 1);
return writeCommands(commands, 3, 11);
}

private int lat(double lat) {
return (int) Math.round(pointYScale * FeatureFactoryUtils.latToSphericalMercator(lat) + pointYTranslate) + extent;
return (int) Math.round(pointYScale * SphericalMercatorUtils.latToSphericalMercator(lat) + pointYTranslate) + extent;
}

private int lon(double lon) {
return (int) Math.round(pointXScale * FeatureFactoryUtils.lonToSphericalMercator(lon) + pointXTranslate);
return (int) Math.round(pointXScale * SphericalMercatorUtils.lonToSphericalMercator(lon) + pointXTranslate);
}

private static int encodeCommand(int id, int length) {
return (id & 0x7) | (length << 3);
}

private static byte[] writeCommands(final int[] commands, final int type, final int length) throws IOException {
try (BytesStreamOutput output = new BytesStreamOutput()) {
for (int i = 0; i < length; i++) {
output.writeVInt(commands[i]);
}
final int dataSize = output.size();
output.reset();
output.writeVInt(24);
output.writeVInt(type);
output.writeVInt(34);
output.writeVInt(dataSize);
for (int i = 0; i < length; i++) {
output.writeVInt(commands[i]);
}
return output.copyBytes().array();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

package org.elasticsearch.xpack.vectortile.feature;

import org.elasticsearch.geometry.Rectangle;

/**
* Utility functions to transforms WGS84 coordinates into spherical mercator.
*/
class SphericalMercatorUtils {

private static double MERCATOR_FACTOR = 20037508.34 / 180.0;

/**
* Transforms WGS84 longitude to a Spherical mercator longitude
*/
public static double lonToSphericalMercator(double lon) {
return lon * MERCATOR_FACTOR;
}

/**
* Transforms WGS84 latitude to a Spherical mercator latitude
*/
public static double latToSphericalMercator(double lat) {
double y = Math.log(Math.tan((90 + lat) * Math.PI / 360)) / (Math.PI / 180);
return y * MERCATOR_FACTOR;
}

/**
* Transforms WGS84 rectangle to a Spherical mercator rectangle
*/
public static Rectangle recToSphericalMercator(Rectangle r) {
return new Rectangle(
lonToSphericalMercator(r.getMinLon()),
lonToSphericalMercator(r.getMaxLon()),
latToSphericalMercator(r.getMaxLat()),
latToSphericalMercator(r.getMinLat())
);

}

private SphericalMercatorUtils() {
// no instances
}
}
Loading

0 comments on commit e5eb047

Please sign in to comment.