package me.osm.gazetter.striper.builders;
import gnu.trove.list.TLongList;
import gnu.trove.list.array.TLongArrayList;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import me.osm.gazetter.striper.GeoJsonWriter;
import me.osm.gazetter.striper.builders.handlers.AddrPointHandler;
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.LocatePoint;
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.apache.commons.lang3.StringUtils;
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 AddrPointsBuilder extends ABuilder {
private static final Logger log = LoggerFactory.getLogger(AddrPointsBuilder.class.getName());
private static final String ADDR_STREET = "addr:street";
private static final String ADDR_INTERPOLATION = "addr:interpolation";
private static final String ADDR_HOUSENUMBER = "addr:housenumber";
private BinaryBuffer way2relation = new ByteBufferList(8 + 8);
private BinaryBuffer node2way = new ByteBufferList(8 + 8 + 2 + 8 + 8);
private BinaryBuffer nodeInterpolation = new ByteBufferList(8 + 2 + 8);
private Map<Long, String> interpolation2Street = new HashMap<>();
//Trying to save some memory
private TLongList writedAddrNodes = new TLongArrayList();
private boolean indexFilled = false;
private boolean orderedByway = false;
private AddrPointHandler handler;
private GeometryFactory factory = new GeometryFactory();
private boolean byRealtionOrdered = false;
private boolean skipInterpolation = false;
public AddrPointsBuilder (AddrPointHandler handler, boolean skipInterpolation) {
this.handler = handler;
this.skipInterpolation = skipInterpolation;
}
private static final boolean fullGeometry = true;
private static final long MASK_16_BITS = 0xFFFFL;
private static final Accessor w2rRelAccessor = Accessors.longAccessor(0);
private static final Accessor niNodeAccessor = Accessors.longAccessor(0);
private static final Accessor n2wNodeAccessor = Accessors.longAccessor(0);
private static final Accessor way2RelRelIdAccessor = Accessors.longAccessor(8);
private static final Accessor n2wWayAccessor = Accessors.longAccessor(8);
private static final Accessor inplnNodeAccessor = Accessors.longAccessor(0);
private static final Accessor n2wLineAccessor = Accessors.longAccessor(8);
@Override
public void handle(final Relation rel) {
if(!indexFilled) {
if(hasAddr(rel.tags)) {
indexRelation(rel);
}
}
else {
orderByRelation();
orderByWay();
buildAddrPoint4Relation(rel);
}
}
private void buildAddrPoint4Relation(final Relation rel) {
int i = way2relation.find(rel.id, way2RelRelIdAccessor);
if(i < 0) {
return;
}
Point centroid = null;
List<LineString> lines = new ArrayList<>();
for(ByteBuffer bb : way2relation.findAll(i, rel.id, way2RelRelIdAccessor)) {
final long way = bb.getLong(0);
int p = node2way.find(way, n2wWayAccessor);
if(fullGeometry) {
List<ByteBuffer> wayPoints = getWayPoints(way);
Collections.sort(wayPoints, new Comparator<ByteBuffer>() {
@Override
public int compare(ByteBuffer o1, ByteBuffer o2) {
return Short.compare(o1.getShort(8 + 8), o2.getShort(8 + 8));
}
});
List<Coordinate> coords = new ArrayList<>();
for(ByteBuffer pbb : wayPoints) {
double lon = pbb.getDouble(8 + 8 + 2);
double lat = pbb.getDouble(8 + 8 + 2 + 8);
coords.add(new Coordinate(lon, lat));
}
if(coords.isEmpty()) {
log.error("Failed to build geometry for relation {}. No points found.", rel.id);
return;
}
LineString l = factory.createLineString(coords.toArray(new Coordinate[coords.size()]));
lines.add(l);
}
else {
for(ByteBuffer bb2 : node2way.findAll(p, way, n2wWayAccessor)) {
double lon = bb2.getDouble(8 + 8 + 2);
double lat = bb2.getDouble(8 + 8 + 2 + 8);
centroid = factory.createPoint(new Coordinate(lon, lat));
}
break;
}
}
JSONObject meta = new JSONObject();
meta.put("id", rel.id);
meta.put("type", "relation");
if(fullGeometry) {
if(lines.isEmpty()) {
return;
}
MultiPolygon mp = BuildUtils.buildMultyPolygon(log, rel, lines, null);
if(mp != null && !mp.isEmpty() && mp.isValid()) {
centroid = mp.getCentroid();
Polygon polygon = (Polygon) mp.getGeometryN(0);
meta.put(GeoJsonWriter.FULL_GEOMETRY, GeoJsonWriter.geometryToJSON(polygon));
}
else {
centroid = lines.get(0).getCentroid();
}
}
handler.handleAddrPoint(rel.tags, centroid, meta);
}
private void orderByRelation() {
if(!this.byRealtionOrdered) {
way2relation.sort(Builder.SECOND_LONG_FIELD_COMPARATOR);
this.byRealtionOrdered = true;
}
}
private void indexRelation(Relation rel) {
for (RelationMember rm : rel.members) {
if(rm.type == ReferenceType.WAY && (StringUtils.isEmpty(rm.role) || "outer".equals(rm.role))) {
ByteBuffer bb = ByteBuffer.allocate(8 + 8);
bb.putLong(rm.ref).putLong(rel.id);
way2relation.add(bb);
if(!fullGeometry) {
//one way (one point) will be enough
break;
}
}
}
}
@Override
public void firstRunDoneRelations() {
handler.newThreadpoolUser(getThreadPoolUser());
way2relation.sort(Builder.FIRST_LONG_FIELD_COMPARATOR);
}
@Override
public void handle(final Way line) {
if (!indexFilled) {
indexWay(line);
} else {
orderByWay();
if (line.isClosed() && hasAddr(line.tags)) {
buildAddrPointForWay(line);
}
else if (isInterpolation(line.tags)) {
buildAddrPoints4Interpolation(line);
}
if (line.isClosed() && line.tags.containsKey("building")) {
for(int i = 0; i < line.nodes.size() - 1; i++) {
long nid = line.nodes.get(i);
int nodeIndex = binarySearchWithMask(writedAddrNodes, nid);
if (nodeIndex >= 0) {
long addrNodeIdWithN = writedAddrNodes.get(nodeIndex);
long n = addrNodeIdWithN & MASK_16_BITS;
long addrNodeId = addrNodeIdWithN >> 16;
this.handler.handleAddrPoint2Building(
String.format("%04d", n), addrNodeId, line.id,
line.tags);
}
}
}
}
}
private void buildAddrPoints4Interpolation(final Way line) {
String interpolation = line.tags.get(ADDR_INTERPOLATION);
int step = getInterpolationStep(interpolation);
if(step > 0) {
List<ByteBuffer> points = getWayPoints(line.id);
long prevPID = -1;
short prevHN = -1;
int counter = 0;
List<double[]> coords = new ArrayList<>();
if(points.size() > 1) {
for(ByteBuffer bb : points) {
long pid = bb.getLong(0);
double lon = bb.getDouble(8 + 8 + 2);
double lat = bb.getDouble(8 + 8 + 2 + 8);
if(pid != prevPID) {
short hn = getInterpolationPointHN(pid);
coords.add(new double[]{lon, lat});
if(hn > 0 && coords.size() > 1) {
counter = interpolateSegment(step, prevHN, hn, coords, line, pid, prevPID, counter);
coords.clear();
coords.add(new double[]{lon, lat});
}
if (hn <= 0 && line.nodes.get(0).equals(pid)) {
log.warn("Broken interpolation at point {}. First point has no recognizeable addr:housenumber", pid);
}
prevPID = pid;
prevHN = hn > 0 ? hn : prevHN;
}
}
if(coords.size() > 1) {
log.warn("Broken interpolation at point {}. Last point has no recognizeable addr:housenumber", prevPID);
}
}
}
else {
log.warn("Unsupported interpolation type: {} for way {}", interpolation, line.id);
}
}
private int interpolateSegment(int s, short prevHN, short hn,
List<double[]> coords, Way way, long pid, long prevPID, int counter) {
int from = Math.min(prevHN, hn);
int to = Math.max(prevHN, hn);
int step = Math.abs(s);
int a = 0;
Coordinate[] coordinates = new Coordinate[coords.size()];
for(double[] d : coords) {
coordinates[a++] = new Coordinate(d[0], d[1]);
}
LineString ls = factory.createLineString(coordinates);
double length = ls.getLength();
int steps = (to - from) / step;
double dl = length / steps;
for(int i = from, stepN = 0; i <= to; i += step, stepN++) {
double l = stepN * dl;
Coordinate c = new LocatePoint(ls, l).getPoint();
JSONObject meta = new JSONObject();
meta.put("id", way.id);
meta.put("type", "interpolation");
//such points will be duplicated by simple nodes, so mark it.
if(i == from) {
meta.put("firstInInterpolation", true);
meta.put("basePointid", pid);
}
if(i == to) {
meta.put("lastInInterpolation", true);
meta.put("basePointid", prevPID);
}
if(way.tags.get(ADDR_STREET) == null && interpolation2Street.get(way.id) != null){
way.tags.put(ADDR_STREET, interpolation2Street.get(way.id));
}
way.tags.put(ADDR_HOUSENUMBER, String.valueOf(i));
meta.put("counter", counter++);
handler.handleAddrPoint(way.tags, factory.createPoint(c), meta);
}
return counter;
}
private short getInterpolationPointHN(final long id) {
int i = nodeInterpolation.find(id, inplnNodeAccessor);
if(i >= 0) {
return nodeInterpolation.get(i).getShort(8);
}
return -1;
}
private int getInterpolationStep(String interpolation) {
if("all".equalsIgnoreCase(interpolation)) {
return 1;
}
if("even".equalsIgnoreCase(interpolation) || "odd".equalsIgnoreCase(interpolation)) {
return 2;
}
try {
return Integer.parseInt(interpolation);
}
catch (Exception e) {
}
return -1;
}
private List<ByteBuffer> getWayPoints(final long lineId) {
int i = node2way.find(lineId, n2wLineAccessor);
List<ByteBuffer> points = node2way.findAll(i, lineId, n2wLineAccessor);
Collections.sort(points, new Comparator<ByteBuffer>() {
@Override
public int compare(ByteBuffer o1, ByteBuffer o2) {
return Short.compare(o1.getShort(8 + 8), o2.getShort(8 + 8));
}
});
return points;
}
private void buildAddrPointForWay(final Way line) {
int i = node2way.find(line.id, n2wLineAccessor);
if(i >= 0) {
JSONObject meta = new JSONObject();
meta.put("id", line.id);
meta.put("type", "way");
Point centroid = null;
if(fullGeometry) {
List<ByteBuffer> wayPoints = getWayPoints(line.id);
Collections.sort(wayPoints, new Comparator<ByteBuffer>() {
@Override
public int compare(ByteBuffer o1, ByteBuffer o2) {
return Short.compare(o1.getShort(8 + 8), o2.getShort(8 + 8));
}
});
List<Coordinate> coords = new ArrayList<>();
for(ByteBuffer bb : wayPoints) {
double lon = bb.getDouble(8 + 8 + 2);
double lat = bb.getDouble(8 + 8 + 2 + 8);
coords.add(new Coordinate(lon, lat));
}
if(coords.isEmpty()) {
log.error("Failed to build geometry for way {}. No points found.", line.id);
return;
}
if(coords.size() != line.nodes.size()) {
log.warn("Failed to build geometry for way {}. Some points wasn't found.", line.id);
centroid = factory.createPoint(coords.get(0));
}
else if(coords.size() < 4) {
log.warn("Wrong number of points for {}", line.id);
centroid = factory.createPoint(coords.get(0));
}
else {
LinearRing geom = factory.createLinearRing(coords.toArray(new Coordinate[coords.size()]));
centroid = geom.getCentroid();
Polygon p = factory.createPolygon(geom);
if(p.isValid()) {
meta.put("fullGeometry", GeoJsonWriter.geometryToJSON(p));
}
}
}
else {
ByteBuffer bb = node2way.get(i);
double lon = bb.getDouble(8 + 8 + 2);
double lat = bb.getDouble(8 + 8 + 2 + 8);
centroid = factory.createPoint(new Coordinate(lon, lat));
}
handler.handleAddrPoint(line.tags, centroid, meta);
}
}
private void orderByWay() {
if(!this.orderedByway) {
node2way.sort(Builder.SECOND_LONG_FIELD_COMPARATOR);
this.orderedByway = true;
}
}
private void indexWay(Way line) {
if(line.isClosed() && hasAddr(line.tags)) {
indexLine(line);
}
else if (!skipInterpolation && isInterpolation(line.tags)) {
short i = 1;
for(long p : line.nodes) {
ByteBuffer bb = ByteBuffer.allocate(8 + 8 + 2 + 8 + 8);
bb.putLong(p).putLong(line.id).putShort(i++);
node2way.add(bb);
bb = ByteBuffer.allocate(8 + 2 + 8);
bb.putLong(0, p);
bb.putLong(8 + 2, line.id);
nodeInterpolation.add(bb);
}
}
else if (way2relation.find(line.id, w2rRelAccessor) >= 0) {
indexLine(line);
}
}
private void indexLine(Way line) {
short i = 0;
for(long ln :line.nodes) {
ByteBuffer bb = ByteBuffer.allocate(8 + 8 + 2 + 8 + 8);
bb.putLong(ln).putLong(line.id).putShort(i++);
node2way.add(bb);
if(!fullGeometry) {
break;
}
}
}
@Override
public void firstRunDoneWays() {
node2way.sort(Builder.FIRST_LONG_FIELD_COMPARATOR);
nodeInterpolation.sort(Builder.FIRST_LONG_FIELD_COMPARATOR);
log.info("Done read ways. {} nodes added to index.", node2way.size());
this.indexFilled = true;
}
@Override
public void handle(final Node node) {
if(hasAddr(node.tags)) {
JSONObject meta = new JSONObject();
meta.put("id", node.id);
meta.put("type", "node");
Point point = factory.createPoint(new Coordinate(node.lon, node.lat));
handler.handleAddrPoint(node.tags, point, meta);
short n = new Double((point.getX() + 180.0) * 10.0).shortValue();
long nodeWithN = node.id;
nodeWithN <<= 16;
nodeWithN |= n;
writedAddrNodes.add(nodeWithN);
}
indexNode2Way(node);
indexNodeInterpolation(node);
}
@Override
public void firstRunDoneNodes() {
writedAddrNodes.sort();
}
private void indexNodeInterpolation(final Node node) {
if(hasAddr(node.tags)) {
int ni = nodeInterpolation.find(node.id, niNodeAccessor);
for(ByteBuffer bb : nodeInterpolation.findAll(ni, node.id, niNodeAccessor)) {
bb.putShort(8, getHN(node.tags));
String street = node.tags.get(ADDR_STREET);
if(street != null) {
long intWayId = bb.getLong(8 + 2);
if(interpolation2Street.get(intWayId) == null) {
interpolation2Street.put(intWayId, street);
}
else if(!interpolation2Street.get(intWayId).equals(street)) {
log.warn("Different streets on addr interpolated nodes. "
+ "Interpolation way id: {} street: {} ({}) Node: {}",
new Object[]{intWayId, street, interpolation2Street.get(intWayId), node.id});
}
}
}
}
}
private short getHN(Map<String, String> tags) {
try {
String hn = tags.get(ADDR_HOUSENUMBER);
if(StringUtils.isNumericSpace(hn)) {
return Short.valueOf(StringUtils.trim(hn));
}
else if(StringUtils.isNumericSpace(hn.substring(0, hn.length() - 2))) {
return Short.valueOf(hn.substring(0, hn.length() - 2));
}
}
catch (Exception e) {
}
return -1;
}
private void indexNode2Way(final Node node) {
int ni = node2way.find(node.id, n2wNodeAccessor);
for(ByteBuffer bb : node2way.findAll(ni, node.id, n2wNodeAccessor)) {
bb.putDouble(8 + 8 + 2, node.lon);
bb.putDouble(8 + 8 + 2 + 8, node.lat);
}
}
private static boolean isInterpolation(Map<String, String> tags) {
return tags.containsKey(ADDR_INTERPOLATION);
}
private static boolean hasAddr(Map<String, String> tags) {
return tags.containsKey(ADDR_HOUSENUMBER);
}
@Override
public void secondRunDoneRelations() {
handler.freeThreadPool(getThreadPoolUser());
}
}