/* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You 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 org.locationtech.spatial4j.io; import java.io.IOException; import java.io.StringWriter; import java.io.Writer; import java.util.Iterator; import org.locationtech.spatial4j.context.SpatialContext; import org.locationtech.spatial4j.context.SpatialContextFactory; import org.locationtech.spatial4j.shape.Circle; import org.locationtech.spatial4j.shape.Point; import org.locationtech.spatial4j.shape.Rectangle; import org.locationtech.spatial4j.shape.Shape; import org.locationtech.spatial4j.shape.ShapeCollection; import org.locationtech.spatial4j.shape.impl.BufferedLine; import org.locationtech.spatial4j.shape.impl.BufferedLineString; /** * Wrap the 'Encoded Polyline Algorithm Format' defined in: * <a href="https://developers.google.com/maps/documentation/utilities/polylinealgorithm">Google Maps API</a> * with flags to encode various Shapes: Point, Line, Polygon, etc * * For more details, see <a href="https://github.com/locationtech/spatial4j/blob/master/FORMATS.md#polyshape">FORMATS.md</a> * * This format works well for geographic shapes (-180...+180 / -90...+90), but is not appropriate for other coordinate systems * * @see <a href="https://developers.google.com/maps/documentation/utilities/polylinealgorithm">Google Maps API</a> * @see PolyshapeReader */ public class PolyshapeWriter implements ShapeWriter { public PolyshapeWriter(SpatialContext ctx, SpatialContextFactory factory) { } @Override public String getFormatName() { return ShapeIO.POLY; } @Override public void write(Writer output, Shape shape) throws IOException { if (shape == null) { throw new NullPointerException("Shape can not be null"); } write(new Encoder(output), shape); } public void write(Encoder enc, Shape shape) throws IOException { if (shape instanceof Point) { Point v = (Point) shape; enc.write(KEY_POINT); enc.write(v.getX(), v.getY()); return; } if (shape instanceof Rectangle) { Rectangle v = (Rectangle) shape; enc.write(KEY_BOX); enc.write(v.getMinX(), v.getMinY()); enc.write(v.getMaxX(), v.getMaxY()); return; } if (shape instanceof BufferedLine) { BufferedLine v = (BufferedLine) shape; enc.write(KEY_LINE); if(v.getBuf()>0) { enc.writeArg(v.getBuf()); } enc.write(v.getA().getX(), v.getA().getY()); enc.write(v.getB().getX(), v.getB().getY()); return; } if (shape instanceof BufferedLineString) { BufferedLineString v = (BufferedLineString) shape; enc.write(KEY_LINE); if(v.getBuf()>0) { enc.writeArg(v.getBuf()); } BufferedLine last = null; Iterator<BufferedLine> iter = v.getSegments().iterator(); while (iter.hasNext()) { BufferedLine seg = iter.next(); enc.write(seg.getA().getX(), seg.getA().getY()); last = seg; } if (last != null) { enc.write(last.getB().getX(), last.getB().getY()); } return; } if (shape instanceof Circle) { // See: https://github.com/geojson/geojson-spec/wiki/Proposal---Circles-and-Ellipses-Geoms Circle v = (Circle) shape; Point center = v.getCenter(); double radius = v.getRadius(); enc.write(KEY_CIRCLE); enc.writeArg(radius); enc.write(center.getX(), center.getY()); return; } if (shape instanceof ShapeCollection) { ShapeCollection v = (ShapeCollection) shape; Iterator<Shape> iter = v.iterator(); while(iter.hasNext()) { write(enc, iter.next()); if(iter.hasNext()) { enc.seperator(); } } return; } enc.writer.write("{unkwnwon " + LegacyShapeWriter.writeShape(shape) +"}"); } @Override public String toString(Shape shape) { try { StringWriter buffer = new StringWriter(); write(buffer, shape); return buffer.toString(); } catch (IOException ex) { throw new RuntimeException(ex); } } public static final char KEY_POINT = '0'; public static final char KEY_LINE = '1'; public static final char KEY_POLYGON = '2'; public static final char KEY_MULTIPOINT = '3'; public static final char KEY_CIRCLE = '4'; public static final char KEY_BOX = '5'; public static final char KEY_ARG_START = '('; public static final char KEY_ARG_END = ')'; public static final char KEY_SEPERATOR = ' '; /** * Encodes a sequence of LatLngs into an encoded path string. * * from Apache 2.0 licensed: * https://github.com/googlemaps/android-maps-utils/blob/master/library/src/com/google/maps/android/PolyUtil.java */ public static class Encoder { long lastLat = 0; long lastLng = 0; final Writer writer; public Encoder(Writer writer) { this.writer = writer; } public void seperator() throws IOException { writer.write(KEY_SEPERATOR); lastLat = lastLng = 0; } public void startRing() throws IOException { writer.write(KEY_ARG_START); lastLat = lastLng = 0; } public void write(char event) throws IOException { writer.write(event); lastLat = lastLng = 0; } public void writeArg(double value) throws IOException { writer.write(KEY_ARG_START); encode(Math.round(value * 1e5)); writer.write(KEY_ARG_END); } public void write(double latitude, double longitude) throws IOException { long lat = Math.round(latitude * 1e5); long lng = Math.round(longitude * 1e5); long dLat = lat - lastLat; long dLng = lng - lastLng; encode(dLat); encode(dLng); lastLat = lat; lastLng = lng; } private void encode(long v) throws IOException { v = v < 0 ? ~(v << 1) : v << 1; while (v >= 0x20) { writer.write(Character.toChars((int) ((0x20 | (v & 0x1f)) + 63))); v >>= 5; } writer.write(Character.toChars((int) (v + 63))); } } }