/* Copyright 2015 The jeo project. All rights reserved.
*
* 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.jeo.geobuf;
import com.vividsolutions.jts.geom.Coordinate;
import com.vividsolutions.jts.geom.CoordinateSequence;
import com.vividsolutions.jts.geom.LineString;
import com.vividsolutions.jts.geom.LinearRing;
import com.vividsolutions.jts.geom.MultiLineString;
import com.vividsolutions.jts.geom.MultiPoint;
import com.vividsolutions.jts.geom.MultiPolygon;
import com.vividsolutions.jts.geom.Point;
import com.vividsolutions.jts.geom.Polygon;
import com.vividsolutions.jts.geom.impl.CoordinateArraySequence;
import io.jeo.data.Disposable;
import io.jeo.geobuf.Geobuf.Data;
import io.jeo.geobuf.Geobuf.Data.Feature;
import io.jeo.geobuf.Geobuf.Data.FeatureCollection;
import io.jeo.geobuf.Geobuf.Data.Geometry;
import io.jeo.geobuf.Geobuf.Data.Geometry.Type;
import io.jeo.geobuf.Geobuf.Data.Value;
import io.jeo.geom.Geom;
import io.jeo.geom.GeometryAdapter;
import io.jeo.proj.Proj;
import io.jeo.util.Function;
import io.jeo.vector.FeatureCursor;
import io.jeo.vector.Features;
import io.jeo.vector.Field;
import org.osgeo.proj4j.CoordinateReferenceSystem;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.io.OutputStream;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Map;
import static io.jeo.geobuf.CustomKeys.CRS;
/**
* Writes a geobuf protocol buffer stream.
*/
public class GeobufWriter implements Disposable {
static Logger LOG = LoggerFactory.getLogger(GeobufWriter.class);
OutputStream out;
Data.Builder data;
int dim = 2;
double e = 1;
double maxPrecision = 1E6;
CoordinateReferenceSystem crs;
Map<String,Integer> keys = new LinkedHashMap<>();
int keyIndex;
FeatureCollection.Builder fcb;
Feature.Builder fb;
Value.Builder vb;
public GeobufWriter(OutputStream out) {
this.out = out;
data = Data.newBuilder();
fcb = FeatureCollection.newBuilder();
fb = Feature.newBuilder();
vb = Value.newBuilder();
}
public GeobufWriter dimension(int dim) {
this.dim = dim;
return this;
}
public GeobufWriter maxPrecision(double maxPrecision) {
this.maxPrecision = maxPrecision;
return this;
}
public GeobufWriter write(Object obj) throws IOException {
obj = analyze(obj);
// add custom property keys
keys.put(CRS, keyIndex++);
// encode keys
data.addAllKeys(keys.keySet());
if (obj instanceof com.vividsolutions.jts.geom.Geometry) {
encode((com.vividsolutions.jts.geom.Geometry)obj);
}
else if (obj instanceof io.jeo.vector.Feature) {
encode((io.jeo.vector.Feature)obj);
}
else if (obj instanceof FeatureCursor) {
encode((FeatureCursor)obj);
}
else {
throw new IllegalArgumentException("Unable to encode object as geobuf: " + obj);
}
return write();
}
public GeobufWriter append(io.jeo.vector.Feature f) throws IOException {
if (data.getFeatureCollection() == null) {
data.setFeatureCollection(fcb);
}
fcb.addFeatures(doEncode(f));
return this;
}
public GeobufWriter write() throws IOException {
data.build().writeTo(out);
return this;
}
<T> T analyze(T obj) throws IOException {
if (obj instanceof com.vividsolutions.jts.geom.Geometry) {
com.vividsolutions.jts.geom.Geometry g = (com.vividsolutions.jts.geom.Geometry) obj;
Geom.visit(g, new GeometryAdapter(true) {
@Override
public void visit(Point point) {
upPrecision(point);
//dim = Math.max(dim, point.getCoordinateSequence().getDimension());
}
@Override
public void visit(LineString line) {
upPrecision(line.getStartPoint()).upPrecision(line.getEndPoint());
//dim = Math.max(dim, line.getCoordinateSequence().getDimension());
}
});
// adjust dimension and precision
if (dim != 2) {
data.setDimensions(dim);
}
int p = (int) Math.ceil(Math.log(e) / Math.log(10));
if (p != 6) {
data.setPrecision(p);
}
}
else if (obj instanceof io.jeo.vector.Feature){
io.jeo.vector.Feature f = (io.jeo.vector.Feature) obj;
analyze(f.geometry());
// calculate key / value index
for (Map.Entry<String,Object> kv : f.map().entrySet()) {
if (kv.getValue() instanceof com.vividsolutions.jts.geom.Geometry) {
continue;
}
String key = kv.getKey();
if (!keys.containsKey(key)) {
keys.put(key, keyIndex++);
}
}
// crs
if (crs == null) {
crs = Features.crs(f);
}
}
else if (obj instanceof FeatureCursor) {
// rather than scan through entire cursor, do the first n features
// TODO: make n configurable
int n = 10;
FeatureCursor cursor = ((FeatureCursor) obj).buffer(n);
for (int i = 0; i < n && cursor.hasNext(); i++) {
analyze(cursor.next());
}
cursor.rewind();
return (T) cursor;
}
return obj;
}
GeobufWriter upPrecision(Point point) {
upPrecision(point.getX()).upPrecision(point.getY());
if (dim > 2) {
upPrecision(point.getCoordinate().z);
}
return this;
}
GeobufWriter upPrecision(double val) {
while (Math.round(val * e) / e != val && e < maxPrecision) e *= 10;
return this;
}
GeobufWriter encode(com.vividsolutions.jts.geom.Geometry g) throws IOException {
data.setGeometry(doEncode(g));
return this;
}
Geometry doEncode(com.vividsolutions.jts.geom.Geometry g) {
if (g == null) {
return null;
}
Geometry.Builder b = Geometry.newBuilder();
switch(Geom.Type.from(g)) {
case POINT:
encode((Point) g, b);
break;
case LINESTRING:
encode((LineString) g, b);
break;
case POLYGON:
encode((Polygon) g, b);
break;
case MULTIPOINT:
encode((MultiPoint) g, b);
break;
case MULTILINESTRING:
encode((MultiLineString) g, b);
break;
case MULTIPOLYGON:
encode((MultiPolygon) g, b);
break;
default:
throw new IllegalArgumentException("Unable to encode geometry " + g);
}
return b.build();
}
GeobufWriter encode(Point p, Geometry.Builder b) {
b.setType(Type.POINT);
return coord(p.getCoordinate(), b);
}
GeobufWriter encode(LineString l, Geometry.Builder b) {
b.setType(Type.LINESTRING);
return coords(l, b);
}
GeobufWriter encode(Polygon p, Geometry.Builder b) {
b.setType(Type.POLYGON);
return coords(p, b, false);
}
GeobufWriter encode(final MultiPoint mp, Geometry.Builder b) {
b.setType(Type.MULTIPOINT);
b.addAllCoords(new Iterable<Long>() {
@Override
public Iterator<Long> iterator() {
return new CoordIterator(new CoordinateArraySequence(mp.getCoordinates()), false);
}
});
return this;
}
GeobufWriter encode(final MultiLineString ml, Geometry.Builder b) {
b.setType(Type.MULTILINESTRING);
if (ml.getNumGeometries() > 1) {
for (LineString line : Geom.iterate(ml)) {
b.addLengths(line.getNumPoints());
coords(line, b);
}
}
else {
coords((LineString) ml.getGeometryN(0), b);
}
return this;
}
GeobufWriter encode(final MultiPolygon mp, Geometry.Builder b) {
b.setType(Type.MULTIPOLYGON);
if (mp.getNumGeometries() != 1 || Geom.first(mp).getNumInteriorRing() > 0) {
// encode with lengths
b.addLengths(mp.getNumGeometries());
for (Polygon p : Geom.iterate(mp)) {
b.addLengths(1+p.getNumInteriorRing());
coords(p, b, true);
}
}
else {
coords(Geom.first(mp).getExteriorRing(), b);
}
return this;
}
GeobufWriter encode(io.jeo.vector.Feature f) {
data.setFeature(doEncode(f));
return this;
}
Feature doEncode(io.jeo.vector.Feature f) {
fb.clear();
// geometry
fb.setGeometry(doEncode(f.geometry()));
// values
int i = 0;
for (Map.Entry<String,Object> kv : f.map().entrySet()) {
Object val = kv.getValue();
if (val == null || val instanceof com.vividsolutions.jts.geom.Geometry) {
continue;
}
fb.addValues(encodeValue(val));
fb.addProperties(keys.get(kv.getKey()));
fb.addProperties(i++);
}
return fb.build();
}
GeobufWriter encode(FeatureCursor cursor) throws IOException {
FeatureCollection.Builder b = FeatureCollection.newBuilder();
if (crs != null) {
b.addValues(encodeValue(Proj.toString(crs)));
b.addCustomProperties(keys.get(CRS));
b.addCustomProperties(b.getValuesCount()-1);
}
b.addAllFeatures(cursor.map(new Function<io.jeo.vector.Feature, Feature>() {
@Override
public Feature apply(io.jeo.vector.Feature f) {
return doEncode(f);
}
}));
data.setFeatureCollection(b.build());
return this;
}
Value encodeValue(Object obj) {
vb.clear();
if (obj instanceof Boolean) {
vb.setBoolValue(((Boolean) obj).booleanValue());
}
else if (obj instanceof Number) {
Number n = (Number) obj;
if (n.doubleValue() % 1 != 0) {
vb.setDoubleValue(n.doubleValue());
}
else {
long l = n.longValue();
if (l < 0) {
vb.setNegIntValue(-l);
}
else {
vb.setPosIntValue(l);
}
}
}
else {
vb.setStringValue(obj.toString());
}
return vb.build();
}
GeobufWriter coord(double val, Geometry.Builder b) {
b.addCoords(Math.round(val * e));
return this;
}
GeobufWriter coord(Coordinate coord, Geometry.Builder b) {
coord(coord.x, b).coord(coord.y, b);
if (dim > 2) {
coord(coord.z, b);
}
return this;
}
GeobufWriter coords(final LineString line, Geometry.Builder b) {
b.addAllCoords(new Iterable<Long>() {
@Override
public Iterator<Long> iterator() {
return new CoordIterator(line.getCoordinateSequence(), line instanceof LinearRing);
}
});
return this;
}
GeobufWriter coords(Polygon p, Geometry.Builder b, boolean lengths) {
if (lengths || p.getNumInteriorRing() > 0) {
b.addLengths(p.getExteriorRing().getNumPoints()-1);
for (LineString hole : Geom.holes(p)) {
b.addLengths(hole.getNumPoints()-1);
}
}
coords(p.getExteriorRing(), b);
if (p.getNumInteriorRing() > 0) {
for (LineString hole : Geom.holes(p)) {
coords(hole, b);
}
}
return this;
}
class CoordIterator implements Iterator<Long> {
CoordinateSequence seq;
int n;
int c = 0; // coordinate index
int o = 0; // ordinate index
double[] last = new double[dim];
public CoordIterator(CoordinateSequence seq, boolean close) {
this.seq = seq;
n = seq.size();
if (close) {
n--;
}
}
@Override
public boolean hasNext() {
return c < n;
}
@Override
public Long next() {
double val = seq.getOrdinate(c, o);
long l = Math.round((val-last[o]) * e);
last[o] = val;
if (++o >= dim) {
c++;
o = 0;
}
return l;
}
@Override
public void remove() {
throw new UnsupportedOperationException();
}
}
public void close() {
try {
out.flush();
out.close();
}
catch(IOException e) {
LOG.debug("Error closing geobuf writer", e);
}
}
}