io.trino.plugin.geospatial.EncodedPolylineFunctions Maven / Gradle / Ivy
/*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.trino.plugin.geospatial;
import com.esri.core.geometry.MultiPath;
import com.esri.core.geometry.MultiVertexGeometry;
import com.esri.core.geometry.Point;
import com.esri.core.geometry.Polyline;
import com.esri.core.geometry.ogc.OGCGeometry;
import com.esri.core.geometry.ogc.OGCLineString;
import com.google.common.base.Joiner;
import io.airlift.slice.DynamicSliceOutput;
import io.airlift.slice.Slice;
import io.trino.geospatial.GeometryType;
import io.trino.spi.TrinoException;
import io.trino.spi.function.Description;
import io.trino.spi.function.ScalarFunction;
import io.trino.spi.function.SqlType;
import io.trino.spi.type.StandardTypes;
import java.util.EnumSet;
import java.util.Set;
import static io.trino.geospatial.GeometryType.LINE_STRING;
import static io.trino.geospatial.GeometryType.MULTI_POINT;
import static io.trino.geospatial.serde.GeometrySerde.deserialize;
import static io.trino.geospatial.serde.GeometrySerde.serialize;
import static io.trino.plugin.geospatial.GeometryType.GEOMETRY_TYPE_NAME;
import static io.trino.spi.StandardErrorCode.INVALID_FUNCTION_ARGUMENT;
import static java.lang.String.format;
/**
* A set of functions to convert between geometries and encoded polylines.
*
* @see
* https://developers.google.com/maps/documentation/utilities/polylinealgorithm for a description of encoded polylines.
*/
public final class EncodedPolylineFunctions
{
private EncodedPolylineFunctions() {}
@Description("Decodes a polyline to a linestring")
@ScalarFunction("from_encoded_polyline")
@SqlType(GEOMETRY_TYPE_NAME)
public static Slice fromEncodedPolyline(@SqlType(StandardTypes.VARCHAR) Slice input)
{
return serialize(decodePolyline(input.toStringUtf8()));
}
private static OGCLineString decodePolyline(String polyline)
{
MultiPath multipath = new Polyline();
boolean isFirstPoint = true;
int index = 0;
int latitude = 0;
int longitude = 0;
while (index < polyline.length()) {
int result = 1;
int shift = 0;
int bytes;
do {
bytes = polyline.charAt(index++) - 63 - 1;
result += bytes << shift;
shift += 5;
}
while (bytes >= 0x1f);
latitude += (result & 1) != 0 ? ~(result >> 1) : (result >> 1);
result = 1;
shift = 0;
do {
bytes = polyline.charAt(index++) - 63 - 1;
result += bytes << shift;
shift += 5;
}
while (bytes >= 0x1f);
longitude += (result & 1) != 0 ? ~(result >> 1) : (result >> 1);
if (isFirstPoint) {
multipath.startPath(longitude * 1e-5, latitude * 1e-5);
isFirstPoint = false;
}
else {
multipath.lineTo(longitude * 1e-5, latitude * 1e-5);
}
}
return new OGCLineString(multipath, 0, null);
}
@Description("Encodes a linestring or multipoint geometry to a polyline")
@ScalarFunction("to_encoded_polyline")
@SqlType(StandardTypes.VARCHAR)
public static Slice toEncodedPolyline(@SqlType(GEOMETRY_TYPE_NAME) Slice input)
{
OGCGeometry geometry = deserialize(input);
validateType("encode_polyline", geometry, EnumSet.of(LINE_STRING, MULTI_POINT));
GeometryType geometryType = GeometryType.getForEsriGeometryType(geometry.geometryType());
return switch (geometryType) {
case LINE_STRING, MULTI_POINT -> encodePolyline((MultiVertexGeometry) geometry.getEsriGeometry());
default -> throw new TrinoException(INVALID_FUNCTION_ARGUMENT, "Unexpected geometry type: " + geometryType);
};
}
private static Slice encodePolyline(MultiVertexGeometry multiVertexGeometry)
{
long lastLatitude = 0;
long lastLongitude = 0;
DynamicSliceOutput output = new DynamicSliceOutput(0);
for (int i = 0; i < multiVertexGeometry.getPointCount(); i++) {
Point point = multiVertexGeometry.getPoint(i);
long latitude = Math.round(point.getY() * 1e5);
long longitude = Math.round(point.getX() * 1e5);
long latitudeDelta = latitude - lastLatitude;
long longitudeDelta = longitude - lastLongitude;
encode(latitudeDelta, output);
encode(longitudeDelta, output);
lastLatitude = latitude;
lastLongitude = longitude;
}
return output.slice();
}
private static void encode(long value, DynamicSliceOutput output)
{
value = value < 0 ? ~(value << 1) : value << 1;
while (value >= 0x20) {
output.appendByte((byte) ((0x20 | (value & 0x1f)) + 63));
value >>= 5;
}
output.appendByte((byte) (value + 63));
}
private static void validateType(String function, OGCGeometry geometry, Set validTypes)
{
GeometryType type = GeometryType.getForEsriGeometryType(geometry.geometryType());
if (!validTypes.contains(type)) {
throw new TrinoException(INVALID_FUNCTION_ARGUMENT, format("%s only applies to %s. Input type is: %s", function, Joiner.on(" or ").join(validTypes), type));
}
}
}