org.openstreetmap.atlas.geography.converters.MultiplePolyLineToPolygonsConverter Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of atlas Show documentation
Show all versions of atlas Show documentation
"Library to load OSM data into an Atlas format"
The newest version!
package org.openstreetmap.atlas.geography.converters;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.stream.Collectors;
import org.locationtech.jts.geom.LineString;
import org.locationtech.jts.operation.polygonize.Polygonizer;
import org.openstreetmap.atlas.exception.CoreException;
import org.openstreetmap.atlas.geography.Location;
import org.openstreetmap.atlas.geography.PolyLine;
import org.openstreetmap.atlas.geography.Polygon;
import org.openstreetmap.atlas.geography.converters.jts.JtsPolyLineConverter;
import org.openstreetmap.atlas.geography.converters.jts.JtsPolygonConverter;
import org.openstreetmap.atlas.utilities.collections.Iterables;
import org.openstreetmap.atlas.utilities.collections.MultiIterable;
import org.openstreetmap.atlas.utilities.collections.StringList;
import org.openstreetmap.atlas.utilities.conversion.Converter;
/**
* From a set of {@link PolyLine}s, try to stitch all the {@link PolyLine}s together to form
* {@link Polygon}s.
*
* @author matthieun
* @author Sid
*/
public class MultiplePolyLineToPolygonsConverter
implements Converter, Iterable>
{
/**
* @author Sid
*/
public static class OpenPolygonException extends CoreException
{
private static final long serialVersionUID = -278028096455310936L;
public static final String OPEN_LOCATIONS_ARE = " Open Locations are: ";
private final List openLocations;
public OpenPolygonException(final String message, final List openLocations)
{
super(message + OPEN_LOCATIONS_ARE + openLocations.toString());
this.openLocations = openLocations;
}
public OpenPolygonException(final String message, final List openLocations,
final Object... arguments)
{
super(message + OPEN_LOCATIONS_ARE + openLocations.toString(), arguments);
this.openLocations = openLocations;
}
public OpenPolygonException(final String message, final List openLocations,
final Throwable cause, final Object... arguments)
{
super(message + OPEN_LOCATIONS_ARE + openLocations.toString(), cause, arguments);
this.openLocations = openLocations;
}
public List getOpenLocations()
{
return this.openLocations;
}
}
/**
* Simple object containing connectivity information about two {@link PolyLine}s. "connected"
* set to "true" means the two {@link PolyLine}s connect at some end. "reversed" set to true
* means that one of the two {@link PolyLine}s had to be reversed to connect.
*
* @author matthieun
*/
private static class ConnectResult
{
private final boolean connected;
private final boolean reversed;
ConnectResult(final boolean connected, final boolean reversed)
{
this.connected = connected;
this.reversed = reversed;
}
public boolean isConnected()
{
return this.connected;
}
public boolean isReversed()
{
return this.reversed;
}
@Override
public String toString()
{
final String isReversed = this.reversed ? " and reversed" : "";
return this.connected ? "Connected" + isReversed : "Not connected";
}
}
/**
* A {@link Polygon} in construction, with many other {@link PolyLine}s
*
* @author matthieun
*/
private static class PossiblePolygon
{
private boolean completed;
// An ordered list of polylines, based on connectivity
private final List polyLines = new ArrayList<>();
PossiblePolygon(final PolyLine first)
{
this.completed = first instanceof Polygon || first.first().equals(first.last());
this.polyLines.add(first);
}
/**
* @param candidate
* A polyLine to attach to that possible polygon
* @return True if the polyLine was successfully attached.
*/
public boolean attach(final PolyLine candidate)
{
boolean result = false;
final ConnectResult canAppendCandidateToLine = canAppendSecondToFirst(lastPolyLine(),
candidate);
final ConnectResult canPrependCandidateToLine = canPrependFirstToSecond(candidate,
firstPolyLine());
PolyLine toAdd = candidate;
if (canAppendCandidateToLine.isConnected())
{
if (canAppendCandidateToLine.isReversed())
{
toAdd = toAdd.reversed();
}
if (toAdd.size() > 1)
{
toAdd = trimFirst(toAdd);
}
else
{
if (canPrependCandidateToLine.isConnected())
{
this.completed = true;
}
return true;
}
}
if (canPrependCandidateToLine.isConnected())
{
if (canPrependCandidateToLine.isReversed())
{
if (canAppendCandidateToLine.isConnected())
{
// Already reversed previously
}
else
{
toAdd = toAdd.reversed();
}
}
if (toAdd.size() > 1)
{
toAdd = trimLast(toAdd);
}
else
{
if (canAppendCandidateToLine.isConnected())
{
this.completed = true;
}
return true;
}
}
if (canAppendCandidateToLine.isConnected())
{
this.polyLines.add(toAdd);
result = true;
}
else if (canPrependCandidateToLine.isConnected())
{
this.polyLines.add(0, toAdd);
result = true;
}
if (canPrependCandidateToLine.isConnected() && canAppendCandidateToLine.isConnected())
{
this.completed = true;
}
return result;
}
public Location firstLocation()
{
return this.polyLines.get(0).first();
}
public boolean isCompleted()
{
return this.completed;
}
public Location lastLocation()
{
return this.polyLines.get(this.polyLines.size() - 1).last();
}
public int size()
{
return this.polyLines.size();
}
public Polygon toPolygon()
{
if (!this.isCompleted() && this.size() >= 1)
{
// If that method is called and the PossiblePolygon is not closed (i.e. completed)
// we gather the first and end point of the partially completed polyline and throw
// an exception.
final List openLocations = new ArrayList<>();
final Location firstLocation = this.polyLines.get(0).first();
final Location lastLocation = this.polyLines.get(this.size() - 1).last();
if (firstLocation != null && lastLocation != null)
{
openLocations.add(firstLocation);
openLocations.add(lastLocation);
throw new OpenPolygonException(
"Cannot build polygon with multiple polylines. Loop is not closed.",
openLocations);
}
}
return new Polygon(new MultiIterable<>(this.polyLines));
}
@Override
public String toString()
{
final StringList list = new StringList();
this.polyLines.forEach(polyLine -> list.add(polyLine.first() + " -> "));
list.add(this.lastLocation());
return list.join("");
}
/**
* Test if two {@link PolyLine}s connect by appending the second {@link PolyLine} (straight
* or reversed) to the first one (unchanged).
*
* @param one
* The {@link PolyLine} from which the end will be considered
* @param two
* The {@link PolyLine} from which the start will be considered
* @return ConnectResult: connected = true if the end of one is the same as the start of two
* and reversed = true if the {@link PolyLine} two had to be reversed to be able to
* connect the end of one to the beginning of two.
*/
private ConnectResult canAppendSecondToFirst(final PolyLine one, final PolyLine two)
{
if (one.last().equals(two.first()))
{
return new ConnectResult(true, false);
}
else if (one.last().equals(two.last()))
{
return new ConnectResult(true, true);
}
else
{
return new ConnectResult(false, false);
}
}
/**
* Test if two {@link PolyLine}s connect by prepending the first {@link PolyLine} (straight
* or reversed) to the second one (unchanged).
*
* @param one
* The {@link PolyLine} from which the end will be considered
* @param two
* The {@link PolyLine} from which the start will be considered
* @return ConnectResult: connected = true if the end of one is the same as the start of two
* and reversed = true if the {@link PolyLine} one had to be reversed to be able to
* connect the end of one to the beginning of two.
*/
private ConnectResult canPrependFirstToSecond(final PolyLine one, final PolyLine two)
{
if (one.last().equals(two.first()))
{
return new ConnectResult(true, false);
}
else if (one.first().equals(two.first()))
{
return new ConnectResult(true, true);
}
else
{
return new ConnectResult(false, false);
}
}
private PolyLine firstPolyLine()
{
return this.polyLines.get(0);
}
private PolyLine lastPolyLine()
{
return this.polyLines.get(this.polyLines.size() - 1);
}
/**
* Remove the first point of this {@link PolyLine} to append it to another {@link PolyLine}
*
* @param current
* The {@link PolyLine} to trim
* @return The {@link PolyLine} trimmed of its first point.
*/
private PolyLine trimFirst(final PolyLine current)
{
final List result = new ArrayList<>();
for (final Location location : current)
{
result.add(location);
}
result.remove(0);
return new PolyLine(result);
}
/**
* Remove the last point of this {@link PolyLine} to prepend it to another {@link PolyLine}
*
* @param current
* The {@link PolyLine} to trim
* @return The {@link PolyLine} trimmed of its last point.
*/
private PolyLine trimLast(final PolyLine current)
{
final List result = new ArrayList<>();
for (final Location location : current)
{
result.add(location);
}
result.remove(result.size() - 1);
return new PolyLine(result);
}
}
private static final JtsPolyLineConverter JTS_POLY_LINE_CONVERTER = new JtsPolyLineConverter();
private static final JtsPolygonConverter JTS_POLYGON_CONVERTER = new JtsPolygonConverter();
private final boolean usePolygonizer;
public MultiplePolyLineToPolygonsConverter()
{
this.usePolygonizer = false;
}
public MultiplePolyLineToPolygonsConverter(final boolean usePolygonizer)
{
this.usePolygonizer = usePolygonizer;
}
@Override
public Iterable convert(final Iterable candidates)
{
if (this.usePolygonizer)
{
return convertAttemptPolygonizer(candidates);
}
else
{
return convertLegacy(candidates);
}
}
public Iterable convertAttemptPolygonizer(final Iterable candidates)
{
final Polygonizer polygonizer = new Polygonizer();
candidates.forEach(polyLine -> polygonizer.add(JTS_POLY_LINE_CONVERTER.convert(polyLine)));
// Check for missing parts
final List errors = new ArrayList<>();
Exception potentialException = null;
try
{
errors.addAll(polygonizer.getDangles());
errors.addAll(polygonizer.getCutEdges());
errors.addAll(polygonizer.getInvalidRingLines());
}
catch (final Exception e)
{
potentialException = e;
}
if (errors.isEmpty() && potentialException == null)
{
// Get results
final List result = (List) polygonizer
.getPolygons();
return result.stream().map(polygon ->
{
polygon.normalize();
return JTS_POLYGON_CONVERTER.backwardConvert(polygon);
}).collect(Collectors.toList());
}
else
{
final List locations = errors.stream()
.map(JTS_POLY_LINE_CONVERTER::backwardConvert)
.flatMap(dangle -> Iterables
.asList(Iterables.from(dangle.first(), dangle.last())).stream())
.collect(Collectors.toList());
final OpenPolygonException jtsException;
final String errorMessage = "Unable to close all the polygons!";
if (potentialException != null)
{
jtsException = new OpenPolygonException(errorMessage, locations,
potentialException);
}
else
{
jtsException = new OpenPolygonException(errorMessage, locations);
}
// Try the legacy convert
try
{
return convertLegacy(candidates);
}
catch (final Exception e)
{
throw new OpenPolygonException(
"Failed second legacy attempt. JTS Exception was: \"{}\"",
jtsException.getOpenLocations(), jtsException.getMessage(), e);
}
}
}
public Iterable convertLegacy(final Iterable candidates) // NOSONAR
{
// The complete polygons
final List completes = new ArrayList<>();
// The polygons that have been started, but that are incomplete.
final List incompletes = new ArrayList<>();
// The polyLines that have not found a match yet
final LinkedList remainingPolyLines = new LinkedList<>();
candidates.forEach(remainingPolyLines::add);
int iterationsSinceLastPolyLineTaken = 0;
while (!remainingPolyLines.isEmpty()
&& iterationsSinceLastPolyLineTaken <= remainingPolyLines.size())
{
final PolyLine candidate = remainingPolyLines.removeFirst();
boolean added = false;
if (!incompletes.isEmpty())
{
// There are some incompletes. Always try to fill the incompletes to the end until
// they are complete before creating new incomplete polygons.
boolean completed = false;
int index = -1;
// Try the candidate polyline with all the incomplete polygons
for (final PossiblePolygon incomplete : incompletes)
{
index++;
if (incomplete.attach(candidate))
{
added = true;
completed = incomplete.isCompleted();
break;
}
}
if (completed)
{
final PossiblePolygon increased = incompletes.get(index);
incompletes.remove(index);
completes.add(increased);
}
}
else
{
// There are no incomplete polygons, just create one.
final PossiblePolygon incompleteCandidate = new PossiblePolygon(candidate);
if (incompleteCandidate.isCompleted())
{
completes.add(incompleteCandidate);
}
else
{
incompletes.add(incompleteCandidate);
}
added = true;
}
if (!added)
{
// Could not add the polyline to any incomplete polygon, so adding it back to the
// end of the list. It might get better luck once those incomplete polygons have
// grown a bit more.
remainingPolyLines.addLast(candidate);
iterationsSinceLastPolyLineTaken++;
}
else
{
iterationsSinceLastPolyLineTaken = 0;
}
}
if (!incompletes.isEmpty())
{
throw new OpenPolygonException("Unable to close all the polygons!",
Iterables
.stream(incompletes).flatMap(incomplete -> Iterables
.from(incomplete.firstLocation(), incomplete.lastLocation()))
.collectToList());
}
return completes.stream().map(PossiblePolygon::toPolygon).collect(Collectors.toList());
}
}