package me.osm.gazetter.striper.builders;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import me.osm.gazetter.Options;
import me.osm.gazetter.striper.BoundariesFallbacker;
import me.osm.gazetter.striper.FeatureTypes;
import me.osm.gazetter.striper.GeoJsonWriter;
import me.osm.gazetter.striper.builders.handlers.BoundariesHandler;
import me.osm.gazetter.striper.readers.PointsReader.Node;
import me.osm.gazetter.striper.readers.RelationsReader.Relation;
import me.osm.gazetter.striper.readers.RelationsReader.Relation.RelationMember;
import me.osm.gazetter.striper.readers.RelationsReader.Relation.RelationMember.ReferenceType;
import me.osm.gazetter.striper.readers.WaysReader.Way;
import me.osm.gazetter.utils.binary.Accessor;
import me.osm.gazetter.utils.binary.Accessors;
import me.osm.gazetter.utils.binary.BinaryBuffer;
import me.osm.gazetter.utils.binary.ByteBufferList;
import org.json.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.vividsolutions.jts.geom.Coordinate;
import com.vividsolutions.jts.geom.GeometryFactory;
import com.vividsolutions.jts.geom.LineString;
import com.vividsolutions.jts.geom.LinearRing;
import com.vividsolutions.jts.geom.MultiPolygon;
import com.vividsolutions.jts.geom.Point;
import com.vividsolutions.jts.geom.Polygon;
public class BoundariesBuilder extends ABuilder {
private static final Logger log = LoggerFactory.getLogger(BoundariesBuilder.class.getName());
private static final GeometryFactory geometryFactory = new GeometryFactory();
protected BoundariesHandler handler;
private BoundariesFallbacker fallback = null;
public BoundariesBuilder(BoundariesHandler handler, BoundariesFallbacker fallback) {
this.fallback = fallback;
this.handler = handler;
}
private static Set<String> ADMIN_LEVELS = new HashSet<>();
static {
ADMIN_LEVELS.addAll(Arrays.asList(new String[]{"1", "2", "3", "4", "5", "6", "7", "8", "9", "10"}));
}
//way rel role
private BinaryBuffer way2relation = new ByteBufferList(8);
//node way index
private BinaryBuffer node2way = new ByteBufferList(8 + 8 + 4 + 8 + 8);
private boolean byMemberOrdered = false;
private boolean byNodeOrdered = false;
private boolean byWayOrdered = false;
private boolean indexFilled = false;
private final ExecutorService executorService =
Executors.newFixedThreadPool(Options.get().getNumberOfThreads());
private static final Accessor n2wWayAccessor = Accessors.longAccessor(8);
private static final Accessor n2wNodeAccessor = Accessors.longAccessor(0);
private static final Accessor w2rWayAccessor = Accessors.longAccessor(0);
private static final class Task implements Runnable {
private final Relation relation;
private final BoundariesBuilder bb;
public Task(Relation relation, BoundariesBuilder bb) {
this.relation = relation;
this.bb = bb;
}
@Override
public void run() {
bb.handleAdminBoundaryRelation(relation);
}
}
@Override
public void handle(Relation rel) {
if(filterByTags(rel.tags)) {
if(!isIndexFilled()) {
addRelIndex(rel);
}
else {
executorService.execute(new Task(rel, this));
}
}
}
private void handleAdminBoundaryRelation(Relation rel) {
MultiPolygon geometry = buildRelationGeometry(rel);
if(geometry != null) {
JSONObject meta = getRelMeta(rel);
doneRelation(rel, geometry, meta);
}
else if (fallback != null) {
geometry = fallback.getGeometry("r" + rel.id);
if(geometry != null) {
log.warn("Load geometry for relation {}.", rel.id);
JSONObject meta = getRelMeta(rel);
doneRelation(rel, geometry, meta);
}
else {
log.warn("Failed to build geometry for relation {}.", rel.id);
}
}
}
protected void doneRelation(Relation rel, MultiPolygon geometry,
JSONObject meta) {
String fType = FeatureTypes.ADMIN_BOUNDARY_FTYPE;
Point originalCentroid = geometry.getEnvelope().getCentroid();
String id = GeoJsonWriter.getId(fType, originalCentroid, meta);
JSONObject featureWithoutGeometry = GeoJsonWriter.createFeature(id, fType, rel.tags, null, meta);
assert GeoJsonWriter.getId(featureWithoutGeometry.toString()).equals(id)
: "Failed getId for " + featureWithoutGeometry.toString();
assert GeoJsonWriter.getFtype(featureWithoutGeometry.toString()).equals(FeatureTypes.ADMIN_BOUNDARY_FTYPE)
: "Failed getFtype for " + featureWithoutGeometry.toString();
saveBoundary(featureWithoutGeometry, geometry);
handler.handleBoundary(featureWithoutGeometry, geometry);
}
protected static JSONObject getRelMeta(Relation rel) {
JSONObject meta = new JSONObject();
meta.put("id", rel.id);
meta.put("type", "relation");
return meta;
}
private MultiPolygon buildRelationGeometry(final Relation rel) {
orderByWay();
List<LineString> outers = new ArrayList<>();
List<LineString> inners = new ArrayList<>();
for(final RelationMember m : rel.members) {
if(m.type == ReferenceType.WAY) {
int wi = node2way.find(m.ref, n2wWayAccessor);
List<ByteBuffer> points = node2way.findAll(wi, m.ref, n2wWayAccessor);
Collections.sort(points, new Comparator<ByteBuffer>() {
@Override
public int compare(ByteBuffer bb1, ByteBuffer bb2) {
return Integer.compare(bb1.getInt(16), bb2.getInt(16));
}
});
if(!points.isEmpty() && points.size() >= 2) {
List<Coordinate> coords = new ArrayList<Coordinate>();
for(ByteBuffer bb : points) {
double lon = bb.getDouble(20);
double lat = bb.getDouble(28);
if(lon != 0.0 && lat != 0.0) {
coords.add(new Coordinate(lon, lat));
}
}
if(coords.size() >= 2) {
LineString ls = geometryFactory.createLineString(coords.toArray(new Coordinate[coords.size()]));
if("inner".equals(m.role)) {
inners.add(ls);
}
else {
outers.add(ls);
}
}
}
}
}
return BuildUtils.buildMultyPolygon(log, rel, outers, inners);
}
private Coordinate[] buildWayGeometry(Way line) {
orderByWay();
int wayIndex = node2way.find(line.id, n2wWayAccessor);
List<ByteBuffer> nodes = node2way.findAll(wayIndex, line.id, n2wWayAccessor);
return BuildUtils.buildWayGeometry(line, nodes, 0, 20, 28);
}
private boolean isIndexFilled() {
return indexFilled;
}
private void addRelIndex(Relation rel) {
for(RelationMember m : rel.members){
if(m.type == ReferenceType.WAY) {
Boolean outer = isOuter(m);
if(outer != null) {
ByteBuffer bb = ByteBuffer.allocate(8);
bb.putLong(m.ref);
way2relation.add(bb);
}
}
}
}
protected static Boolean isOuter(RelationMember m) {
Boolean outer = null;
if("outer".equals(m.role) || "".equals(m.role) || m.role == null || "exclave".equals(m.role)) {
outer = true;
}
else if("inner".equals(m.role) || "enclave".equals(m.role)) {
outer = false;
}
return outer;
}
protected boolean filterByTags(Map<String, String> tags) {
return ("administrative".equals(tags.get("boundary"))
&& ADMIN_LEVELS.contains(tags.get("admin_level"))
&& (tags.get("maritime") == null || !tags.get("maritime").equals("yes")));
}
@Override
public void handle(Way line) {
if(!isIndexFilled()) {
int i = way2relation.find(line.id, w2rWayAccessor);
if (i >= 0 || (line.isClosed() && filterByTags(line.tags))) {
addWayToIndex(line);
}
}
else if (line.isClosed() && filterByTags(line.tags)) {
Coordinate[] wayGeometry = buildWayGeometry(line);
if(wayGeometry != null && wayGeometry.length > 3) {
LinearRing shell = geometryFactory.createLinearRing(wayGeometry);
Polygon poly = geometryFactory.createPolygon(shell);
MultiPolygon multiPolygon = geometryFactory.createMultiPolygon(new Polygon[]{poly});
doneWay(line, multiPolygon);
}
else {
MultiPolygon geometry = null;
if(fallback != null) {
geometry = fallback.getGeometry("w" + line.id);
}
if(geometry != null) {
log.warn("Load fallback geometry for way {}." + line.id);
doneWay(line, geometry);
}
else {
log.warn("Failed to build geometry for way {}." + line.id);
}
}
}
}
protected void doneWay(Way line, MultiPolygon multiPolygon) {
String fType = FeatureTypes.ADMIN_BOUNDARY_FTYPE;
Point originalCentroid = multiPolygon.getEnvelope().getCentroid();
JSONObject meta = getWayMeta(line);
String id = GeoJsonWriter.getId(fType, originalCentroid, meta);
JSONObject featureWithoutGeometry = GeoJsonWriter.createFeature(id, fType, line.tags, null, meta);
assert GeoJsonWriter.getId(featureWithoutGeometry.toString()).equals(id)
: "Failed getId for " + featureWithoutGeometry.toString();
assert GeoJsonWriter.getFtype(featureWithoutGeometry.toString()).equals(FeatureTypes.ADMIN_BOUNDARY_FTYPE)
: "Failed getFtype for " + featureWithoutGeometry.toString();
saveBoundary(featureWithoutGeometry, multiPolygon);
handler.handleBoundary(featureWithoutGeometry, multiPolygon);
}
protected JSONObject getWayMeta(Way line) {
JSONObject result = new JSONObject();
result.put("id", line.id);
result.put("type", "way");
return result;
}
private void addWayToIndex(Way line) {
int i = 0;
for(long node : line.nodes) {
ByteBuffer bb = ByteBuffer.allocate(8 + 8 + 4 + 8 + 8);
bb.putLong(0, node);
bb.putLong(8, line.id);
bb.putInt(16, i++);
node2way.add(bb);
}
}
@Override
public void firstRunDoneRelations() {
handler.newThreadpoolUser(getThreadPoolUser());
if(!byMemberOrdered) {
way2relation.sort(Builder.FIRST_LONG_FIELD_COMPARATOR);
byMemberOrdered = true;
}
log.info("Done read relations. {} ways addes to index.", way2relation.size());
}
@Override
public void firstRunDoneWays() {
if(!byNodeOrdered) {
node2way.sort(Builder.FIRST_LONG_FIELD_COMPARATOR);
byNodeOrdered = true;
byWayOrdered = false;
}
log.info("Done read ways. {} nodes addes to index.", node2way.size());
}
public void orderByWay() {
if(!byWayOrdered) {
node2way.sort(Builder.SECOND_LONG_FIELD_COMPARATOR);
byWayOrdered = true;
byNodeOrdered = false;
}
}
@Override
public void handle(Node node) {
int i = node2way.find(node.id, n2wNodeAccessor);
List<ByteBuffer> lines = node2way.findAll(i, node.id, n2wNodeAccessor);
for(ByteBuffer bb : lines) {
bb.putDouble(20, node.lon).putDouble(28, node.lat);
}
indexFilled = true;
}
@Override
public void secondRunDoneRelations() {
executorService.shutdown();
try {
while(!executorService.awaitTermination(1, TimeUnit.MINUTES)) {
//wait untill done
}
} catch (InterruptedException e) {
log.error("Awaiting for thread pull was terminated.", e);
throw new RuntimeException(e);
}
finally {
if(fallback != null) {
fallback.close();
}
handler.freeThreadPool(getThreadPoolUser());
}
}
private void saveBoundary(JSONObject feature,
MultiPolygon geometry) {
if(fallback != null) {
this.fallback.saveBoundary(feature, geometry);
}
}
}