Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
164 changes: 164 additions & 0 deletions src/main/java/il/org/osm/israelhiking/LineMerger.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
package il.org.osm.israelhiking;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.stream.Collectors;

import org.locationtech.jts.geom.Coordinate;
import org.locationtech.jts.geom.Geometry;

public class LineMerger {

List<Geometry> lines = new ArrayList<>();

public void add(Geometry line) {
lines.add(line);
}

public Coordinate getFirstPoint() {
List<List<Coordinate>> nodesGroups = new ArrayList<>();
List<Geometry> waysToGroup = lines.stream()
.filter(w -> !w.isEmpty())
.collect(Collectors.toList());

while (!waysToGroup.isEmpty()) {
Geometry wayToGroup = waysToGroup.stream()
.filter(w -> nodesGroups.stream()
.anyMatch(g -> canBeMerged(Arrays.asList(w.getCoordinates()), g)))
.findFirst()
.orElse(null);

if (wayToGroup == null) {
nodesGroups.add(new LinkedList<>(Arrays.asList((waysToGroup.getFirst().getCoordinates()))));
waysToGroup.removeFirst();
continue;
}

List<Coordinate> currentNodes = new LinkedList<>(Arrays.asList(wayToGroup.getCoordinates()));
waysToGroup.remove(wayToGroup);
List<Coordinate> group = nodesGroups.stream()
.filter(g -> canBeMerged(currentNodes, g))
.findFirst()
.get();

if (canBeReverseMerged(group, currentNodes)) {
Collections.reverse(currentNodes);
}

if (currentNodes.getFirst().equals2D(group.getLast())) {
currentNodes.removeFirst();
group.addAll(currentNodes);
continue;
}

currentNodes.removeLast();
group.addAll(0, currentNodes);
}

List<List<Coordinate>> nodes = rearrangeInCaseOfCircleAndLine(nodesGroups);

return nodes.getFirst().getFirst();
}

private boolean canBeMerged(List<Coordinate> nodes1, List<Coordinate> nodes2)
{
return nodes1.getLast().equals2D(nodes2.getFirst()) ||
nodes1.getFirst().equals2D(nodes2.getLast()) ||
canBeReverseMerged(nodes1, nodes2);
}

private boolean canBeReverseMerged(List<Coordinate> nodes1, List<Coordinate> nodes2)
{
return nodes1.getFirst().equals2D(nodes2.getFirst()) ||
nodes1.getLast().equals2D(nodes2.getLast());
}

/**
* The purpose of this method is to take grouped results that grouped into "O" shape and lines that
* touches this "O" shape and turn them into a "Q" shape.
* This should only be applied to multiline strings
* It does so by going over all the circles, finding lines that are not circles that touches those
* and reorder the points, adding a new line and removes the circle and the line from the original list
* @param coordinateGroups original list of list of nodes to alter
* @returns A new list of list of nodes after the changes
*/
private List<List<Coordinate>> rearrangeInCaseOfCircleAndLine(List<List<Coordinate>> coordinateGroups) {
if (coordinateGroups.size() == 1) {
return coordinateGroups;
}

// Find circles (groups where first and last coordinates are the same)
List<List<Coordinate>> circles = coordinateGroups.stream()
.filter(g -> g.getFirst().equals2D(g.getLast()))
.collect(Collectors.toList());

if (circles.isEmpty()) {
return coordinateGroups;
}

for (List<Coordinate> circle : circles) {
// Find a line that touches the circle
List<Coordinate> lineThatTouchesTheCircle = coordinateGroups.stream()
.filter(g -> !circles.contains(g))
.filter(g -> circle.stream().anyMatch(n ->
n.equals2D(g.getFirst()) || n.equals2D(g.getLast())))
.findFirst()
.orElse(null);

if (lineThatTouchesTheCircle == null) {
continue;
}

coordinateGroups.remove(circle);
coordinateGroups.remove(lineThatTouchesTheCircle);

// Find the coordinate in circle that touches the end of the line
Coordinate nodeInCircleThatTouches = circle.stream()
.filter(n -> n.equals2D(lineThatTouchesTheCircle.getLast()))
.findFirst()
.orElse(null);

if (nodeInCircleThatTouches != null) {
int indexInCircle = circle.indexOf(nodeInCircleThatTouches);
List<Coordinate> newList = new ArrayList<>(lineThatTouchesTheCircle);

// Add remaining coordinates after the touch point
newList.addAll(circle.subList(indexInCircle + 1, circle.size()));

// Add coordinates from start up to touch point
newList.addAll(circle.subList(1, indexInCircle));

coordinateGroups.add(newList);
continue;
}

// Find the coordinate in circle that touches the start of the line
nodeInCircleThatTouches = circle.stream()
.filter(n -> n.equals2D(lineThatTouchesTheCircle.getFirst()))
.findFirst()
.orElse(null);

if (nodeInCircleThatTouches != null) {
int indexInCircle = circle.indexOf(nodeInCircleThatTouches);
List<Coordinate> newList = new ArrayList<>();

// Add coordinates from circle between start and touch point
newList.addAll(circle.subList(1, indexInCircle));

// Add the line
newList.addAll(lineThatTouchesTheCircle);

// Add remaining coordinates from circle
newList.addAll(0, circle.subList(indexInCircle, circle.size()));

coordinateGroups.add(newList);
}
}

return coordinateGroups;
}

}
2 changes: 0 additions & 2 deletions src/main/java/il/org/osm/israelhiking/MergedLinesHelper.java
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
package il.org.osm.israelhiking;

import org.locationtech.jts.operation.linemerge.LineMerger;

import com.onthegomap.planetiler.reader.SourceFeature;

class MergedLinesHelper {
Expand Down
35 changes: 3 additions & 32 deletions src/main/java/il/org/osm/israelhiking/PlanetSearchProfile.java
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,6 @@
import java.util.stream.Collectors;

import org.locationtech.jts.geom.Envelope;
import org.locationtech.jts.geom.Geometry;
import org.locationtech.jts.geom.LineString;
import org.locationtech.jts.geom.Point;

import com.onthegomap.planetiler.FeatureCollector;
Expand Down Expand Up @@ -225,7 +223,7 @@ private void processOsmRelationFeature(SourceFeature feature, FeatureCollector f
continue;
}
// All relation members were reached. Add a POI element for line relation
var point = getFirstPointOfLineRelation(mergedLines);
var point = GeoUtils.point(mergedLines.lineMerger.getFirstPoint());
var lngLatPoint = GeoUtils.worldToLatLonCoords(point).getCoordinate();
relation.pointDocument.location = new double[]{lngLatPoint.getX(), lngLatPoint.getY()};

Expand Down Expand Up @@ -263,7 +261,7 @@ private boolean processMtbNameFeature(SourceFeature feature, FeatureCollector fe
return true;
}
var minIdFeature = mergedLines.feature;
var point = GeoUtils.point(((Geometry)mergedLines.lineMerger.getMergedLineStrings().iterator().next()).getCoordinate());
var point = GeoUtils.point(mergedLines.lineMerger.getFirstPoint());

var pointDocument = new PointDocument();
for (String language : supportedLanguages) {
Expand Down Expand Up @@ -325,7 +323,7 @@ private boolean processWaterwayFeature(SourceFeature feature, FeatureCollector f
return true;
}
var minIdFeature = mergedLines.feature;
var point = GeoUtils.point(((Geometry)mergedLines.lineMerger.getMergedLineStrings().iterator().next()).getCoordinate());
var point = GeoUtils.point(mergedLines.lineMerger.getFirstPoint());

var pointDocument = new PointDocument();
for (String language : supportedLanguages) {
Expand Down Expand Up @@ -517,33 +515,6 @@ private void insertBboxToElasticsearch(SourceFeature feature, String[] supported
}
}

/**
* Get the first point of the trail relation by checking some heuristics related to the relation's first member
* @param mergedLines - the merged lines helper
* @return the first point of the trail relation
* @throws GeometryException
*/
private Point getFirstPointOfLineRelation(MergedLinesHelper mergedLines) throws GeometryException {
var firstMergedLineString = (LineString) mergedLines.lineMerger.getMergedLineStrings().iterator().next();
var firstMergedLineCoordinate = firstMergedLineString.getCoordinate();
var lastMergedLineCoordinate = firstMergedLineString.getCoordinateN(firstMergedLineString.getNumPoints() - 1);

var firstMemberGeometry = (LineString) mergedLines.feature.line();
var firstMemberStartCoordinate = firstMemberGeometry.getCoordinate();
var firstMemberEndCoordinate = firstMemberGeometry.getCoordinateN(firstMemberGeometry.getNumPoints() - 1);

if (firstMergedLineCoordinate.equals(firstMemberStartCoordinate)) {
// The direction of the related's first memeber and the merged lines is the same
return GeoUtils.point(firstMergedLineCoordinate);
}

if (lastMergedLineCoordinate.equals(firstMemberStartCoordinate) || lastMergedLineCoordinate.equals(firstMemberEndCoordinate)) {
// The direction of the related's first memeber and the merged lines is the opposite
return GeoUtils.point(lastMergedLineCoordinate);
}
// Otherwise, return the first point of the merged line
return GeoUtils.point(firstMergedLineCoordinate);
}

private boolean isInterestingPoint(PointDocument pointDocument) {
return !pointDocument.description.isEmpty() ||
Expand Down
30 changes: 30 additions & 0 deletions src/test/java/il/org/osm/israelhiking/LineMergerTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package il.org.osm.israelhiking;

import static org.junit.jupiter.api.Assertions.assertTrue;

import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
import org.locationtech.jts.geom.Coordinate;
import org.locationtech.jts.geom.GeometryFactory;


@Tag("unit")
public class LineMergerTest {

@Test
public void simpleLine_ShouldReturnFirstPoint() {
var merger = new LineMerger();
var factory = new GeometryFactory();
merger.add(factory.createLineString(new Coordinate[] {new Coordinate(0, 0), new Coordinate(1, 1)}));
assertTrue(merger.getFirstPoint().equals2D(new Coordinate(0, 0)));
}

@Test
public void lineWithLoop_ShouldReturnFirstPoint() {
var merger = new LineMerger();
var factory = new GeometryFactory();
merger.add(factory.createLineString(new Coordinate[] {new Coordinate(0, 0), new Coordinate(1, 1)}));
merger.add(factory.createLineString(new Coordinate[] {new Coordinate(1, 1), new Coordinate(2, 2), new Coordinate(1, 1)}));
assertTrue(merger.getFirstPoint().equals2D(new Coordinate(0, 0)));
}
}