/**************************************************************************
OSMemory library for OSM data processing.
Copyright (C) 2014 Aleś Bułojčyk <alex73mail@gmail.com>
This is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This software is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
**************************************************************************/
package org.alex73.osmemory.geometry;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import org.alex73.osmemory.IOsmNode;
import org.alex73.osmemory.IOsmObject;
import org.alex73.osmemory.IOsmRelation;
import org.alex73.osmemory.IOsmWay;
import org.alex73.osmemory.MemoryStorage;
import com.vividsolutions.jts.algorithm.LineIntersector;
import com.vividsolutions.jts.algorithm.RobustLineIntersector;
import com.vividsolutions.jts.geom.Coordinate;
import com.vividsolutions.jts.geom.Envelope;
import com.vividsolutions.jts.geom.Geometry;
import com.vividsolutions.jts.geom.LineString;
import com.vividsolutions.jts.geom.Polygon;
import com.vividsolutions.jts.geomgraph.GeometryGraph;
import com.vividsolutions.jts.geomgraph.index.SegmentIntersector;
/**
* Class for cache some extended information about relation, like boundary, geometry, etc.
*/
public class ExtendedRelation implements IExtendedObject {
private final IOsmRelation relation;
private final MemoryStorage storage;
private BoundingBox boundingBox;
private boolean allPointsDefined;
private Geometry area;
// Area's border points for check 'isInBorder'
private final Set<Long> borderNodes = new HashSet<>();
public ExtendedRelation(IOsmRelation relation, MemoryStorage storage) {
this.relation = relation;
this.storage = storage;
}
@Override
public IOsmObject getObject() {
return relation;
}
public BoundingBox getBoundingBox() {
checkProcessed();
return boundingBox;
}
public boolean isAllPointsDefined() {
checkProcessed();
return allPointsDefined;
}
public Set<Long> getBorderNodes() {
return borderNodes;
}
public synchronized Geometry getArea() {
if (area == null) {
List<Line> lines = new ArrayList<>();
for (int i = 0; i < relation.getMembersCount(); i++) {
IOsmObject m = relation.getMemberObject(storage, i);
switch (relation.getMemberRole(storage, i)) {
case "outer":
case "":
if (m == null) {
throw new RuntimeException("Object " + relation.getMemberCode(i)
+ " not exist for relation #" + relation.getId());
}
if (m.getType() != IOsmObject.TYPE_WAY) {
throw new RuntimeException("Not a way outer object " + m.getObjectCode()
+ " for relation #" + relation.getId());
}
lines.add(new Line(false, way2line(storage, (IOsmWay) m)));
break;
case "inner":
if (m == null) {
throw new RuntimeException("Object " + relation.getMemberCode(i)
+ " not exist for relation #" + relation.getId());
}
if (m.getType() != IOsmObject.TYPE_WAY) {
throw new RuntimeException("Not a way inner object " + m.getObjectCode()
+ " for relation #" + relation.getId());
}
lines.add(new Line(true, way2line(storage, (IOsmWay) m)));
break;
case "border":
if (m == null) {
throw new RuntimeException("Object " + relation.getMemberCode(i)
+ " not exist for relation #" + relation.getId());
}
throw new RuntimeException();
}
}
Geometry g = null;
while (!lines.isEmpty()) {
List<LineString> outer = getAs(false, lines);
List<LineString> inner = getAs(true, lines);
Geometry go = polygonize(outer);
if (!go.isValid()) {
throw new RuntimeException("Impossible to create polygon from relation #"
+ relation.getId() + ": outer part is not valid");
}
Geometry gi = polygonize(inner);
if (!gi.isValid()) {
throw new RuntimeException("Impossible to create polygon from relation #"
+ relation.getId() + ": inner part is not valid");
}
if (g == null) {
g = GeometryHelper.substract(go, gi);
} else {
g = g.union(GeometryHelper.substract(go, gi));
}
}
if (!g.isValid()) {
throw new RuntimeException("Impossible to create polygon from relation #" + relation.getId()
+ ": it is not valid");
}
area = g;
}
return area;
}
LineString way2line(MemoryStorage storage, IOsmWay way) {
long[] nodeIds = way.getNodeIds();
Coordinate[] points = new Coordinate[nodeIds.length];
for (int i = 0; i < points.length; i++) {
IOsmNode node = storage.getNodeById(nodeIds[i]);
if (node == null) {
throw new RuntimeException("Node #" + nodeIds[i] + " not exist for way #" + way.getId());
}
borderNodes.add(nodeIds[i]);
points[i] = GeometryHelper.coord(node.getLongitude(), node.getLatitude());
}
return GeometryHelper.createLine(points);
}
Geometry polygonize(List<LineString> lines) {
if (lines.isEmpty()) {
return GeometryHelper.emptyCollection();
}
List<Polygon> polygons = new ArrayList<>();
while (!lines.isEmpty()) {
LineString ring = fullClosedLine(lines);
polygons.add(GeometryHelper.createPolygon(ring.getCoordinates()));
}
if (polygons.size() > 1) {
return GeometryHelper.multipolygon(polygons);
} else {
return polygons.get(0);
}
}
LineString fullClosedLine(List<LineString> lines) {
List<Coordinate> tail = new ArrayList<>();
boolean found;
do {
found = false;
for (int i = 0; i < lines.size(); i++) {
if (addToClosed(tail, lines.get(i))) {
lines.remove(i);
i--;
found = true;
}
}
} while (found);
LineString s = GeometryHelper.createLine(tail);
if (!s.isClosed()) {
throw new RuntimeException("Non-closed line starts from " + tail.get(0) + " ends to "
+ tail.get(tail.size() - 1));
}
if (!s.isSimple()) {
GeometryGraph graph = new GeometryGraph(0, s);
LineIntersector li = new RobustLineIntersector();
SegmentIntersector si = graph.computeSelfNodes(li, true);
if (si.hasProperInteriorIntersection()) {
throw new RuntimeException("Self-intersection for " + relation.getObjectCode()
+ " near point " + si.getProperIntersectionPoint());
}else {
throw new RuntimeException("Self-intersected line: " + s);
}
}
return s;
}
boolean addToClosed(List<Coordinate> tail, LineString line) {
boolean revert = false;
int place = -1;
if (tail.isEmpty()) {
place = 0;
} else {
Coordinate tailFirst = tail.get(0);
Coordinate tailLast = tail.get(tail.size() - 1);
Coordinate lineFirst = line.getCoordinateN(0);
Coordinate lineLast = line.getCoordinateN(line.getNumPoints() - 1);
if (lineFirst.equals(tailLast)) {
revert = false;
place = tail.size();
} else if (lineFirst.equals(tailFirst)) {
revert = true;
place = 0;
} else if (lineLast.equals(tailFirst)) {
revert = false;
place = 0;
} else if (lineLast.equals(tailLast)) {
revert = true;
place = tail.size();
}
}
if (place >= 0) {
List<Coordinate> cs = Arrays.asList(line.getCoordinates());
if (revert) {
Collections.reverse(cs);
}
tail.addAll(place, cs);
}
return place >= 0;
}
protected synchronized void checkProcessed() {
if (boundingBox != null) {
return;
}
boundingBox = new BoundingBox();
allPointsDefined = true;
iterateNodes(new NodesIterator() {
@Override
public Boolean processNode(IOsmNode n) {
if (n != null) {
boundingBox.expandToInclude(n.getLat(), n.getLon());
} else {
allPointsDefined = false;
}
return null;
}
});
}
public Boolean iterateNodes(NodesIterator iterator) {
return iterateRelation(iterator, relation, new HashSet<>());
}
protected Boolean iterateWay(NodesIterator iterator, IOsmWay way) {
for (int i = 0; i < way.getNodeIds().length; i++) {
long nid = way.getNodeIds()[i];
IOsmNode n = storage.getNodeById(nid);
Boolean r = iterator.processNode(n);
if (r != null) {
return r;
}
}
return null;
}
protected Boolean iterateRelation(NodesIterator iterator, IOsmRelation relation,
Set<Long> processedRelations) {
for (int i = 0; i < relation.getMembersCount(); i++) {
IOsmObject m = relation.getMemberObject(storage, i);
if (m == null) {
continue;
}
Boolean r;
switch (relation.getMemberType(i)) {
case IOsmObject.TYPE_NODE:
r = iterator.processNode((IOsmNode) m);
break;
case IOsmObject.TYPE_WAY:
r = iterateWay(iterator, (IOsmWay) m);
break;
case IOsmObject.TYPE_RELATION:
if (processedRelations.contains(m.getId())) {
continue;
}
processedRelations.add(relation.getId());
r = iterateRelation(iterator, (IOsmRelation) m, processedRelations);
break;
default:
throw new RuntimeException();
}
if (r != null) {
return r;
}
}
return null;
}
List<LineString> getAs(boolean inner, List<Line> lines) {
List<LineString> result = new ArrayList<>();
while (!lines.isEmpty()) {
if (lines.get(0).inner == inner) {
result.add(lines.remove(0).line);
} else {
break;
}
}
return result;
}
static class Line {
boolean inner;
LineString line;
public Line(boolean inner, LineString line) {
this.inner = inner;
this.line = line;
}
}
}