com.graphhopper.reader.osm.WaySegmentParser Maven / Gradle / Ivy
Show all versions of graphhopper-core Show documentation
/*
* Licensed to GraphHopper GmbH under one or more contributor
* license agreements. See the NOTICE file distributed with this work for
* additional information regarding copyright ownership.
*
* GraphHopper GmbH licenses this file to you under the Apache License,
* Version 2.0 (the "License"); you may not use this file except in
* compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.graphhopper.reader.osm;
import com.carrotsearch.hppc.cursors.LongCursor;
import com.graphhopper.reader.ReaderElement;
import com.graphhopper.reader.ReaderNode;
import com.graphhopper.reader.ReaderRelation;
import com.graphhopper.reader.ReaderWay;
import com.graphhopper.reader.dem.ElevationProvider;
import com.graphhopper.storage.Directory;
import com.graphhopper.util.Helper;
import com.graphhopper.util.PointAccess;
import com.graphhopper.util.PointList;
import com.graphhopper.util.StopWatch;
import com.graphhopper.util.shapes.GHPoint3D;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.xml.stream.XMLStreamException;
import java.io.File;
import java.io.IOException;
import java.text.ParseException;
import java.util.*;
import java.util.function.Consumer;
import java.util.function.LongToIntFunction;
import java.util.function.Predicate;
import static com.graphhopper.reader.osm.OSMNodeData.*;
import static com.graphhopper.util.Helper.nf;
/**
* This class parses a given OSM file and splits OSM ways into 'segments' at all intersections (or 'junctions').
* Intersections can be either crossings of different OSM ways or duplicate appearances of the same node within one
* way (when the way contains a loop). Furthermore, this class creates artificial segments at certain nodes. It
* also provides several hooks/callbacks to customize the processing of nodes, ways and relations.
*
* The OSM file is read twice. The first time we ignore OSM nodes and only determine the OSM node IDs at which accepted
* ways are intersecting. During the second pass we split the OSM ways at intersections, introduce the artificial
* segments and pass the way information along with the corresponding nodes to a given callback.
*
* We assume a strict order of the OSM file: nodes, ways, then relations.
*
* The main difficulty is that the OSM ID range is very large (64bit integers) and to be able to provide the full
* node information for each segment we have to efficiently store the node data temporarily. This is addressed by
* {@link OSMNodeData}.
*/
public class WaySegmentParser {
private static final Logger LOGGER = LoggerFactory.getLogger(WaySegmentParser.class);
private static final Set INCLUDE_IF_NODE_TAGS = new HashSet<>(Arrays.asList("barrier", "highway", "railway", "crossing", "ford"));
private ElevationProvider elevationProvider = ElevationProvider.NOOP;
private Predicate wayFilter = way -> true;
private Predicate splitNodeFilter = node -> false;
private WayPreprocessor wayPreprocessor = (way, coordinateSupplier, nodeTagSupplier) -> {
};
private Consumer relationPreprocessor = relation -> {
};
private RelationProcessor relationProcessor = (relation, map) -> {
};
private EdgeHandler edgeHandler = (from, to, pointList, way, nodeTags) ->
System.out.println("edge " + from + "->" + to + " (" + pointList.size() + " points)");
private int workerThreads = 2;
private final OSMNodeData nodeData;
private Date timestamp;
private WaySegmentParser(OSMNodeData nodeData) {
this.nodeData = nodeData;
}
/**
* @param osmFile the OSM file to parse, supported formats include .osm.xml, .osm.gz and .xml.pbf
*/
public void readOSM(File osmFile) {
if (nodeData.getNodeCount() > 0)
throw new IllegalStateException("You can only run way segment parser once");
LOGGER.info("Start reading OSM file: '" + osmFile + "'");
LOGGER.info("pass1 - start");
StopWatch sw1 = StopWatch.started();
readOSM(osmFile, new Pass1Handler(), new SkipOptions(true, false, false));
LOGGER.info("pass1 - finished, took: {}", sw1.stop().getTimeString());
long nodes = nodeData.getNodeCount();
LOGGER.info("Creating graph. Node count (pillar+tower): " + nodes + ", " + Helper.getMemInfo());
LOGGER.info("pass2 - start");
StopWatch sw2 = new StopWatch().start();
readOSM(osmFile, new Pass2Handler(), SkipOptions.none());
LOGGER.info("pass2 - finished, took: {}", sw2.stop().getTimeString());
nodeData.release();
LOGGER.info("Finished reading OSM file." +
" pass1: " + (int) sw1.getSeconds() + "s, " +
" pass2: " + (int) sw2.getSeconds() + "s, " +
" total: " + (int) (sw1.getSeconds() + sw2.getSeconds()) + "s");
}
/**
* @return the timestamp read from the OSM file, or null if nothing was read yet
*/
public Date getTimeStamp() {
return timestamp;
}
private class Pass1Handler implements ReaderElementHandler {
private boolean handledWays;
private boolean handledRelations;
private long wayCounter = 0;
private long acceptedWays = 0;
private long relationsCounter = 0;
@Override
public void handleWay(ReaderWay way) {
if (!handledWays) {
LOGGER.info("pass1 - start reading OSM ways");
handledWays = true;
}
if (handledRelations)
throw new IllegalStateException("OSM way elements must be located before relation elements in OSM file");
if (++wayCounter % 10_000_000 == 0)
LOGGER.info("pass1 - processed ways: " + nf(wayCounter) + ", accepted ways: " + nf(acceptedWays) +
", way nodes: " + nf(nodeData.getNodeCount()) + ", " + Helper.getMemInfo());
if (!wayFilter.test(way))
return;
acceptedWays++;
for (LongCursor node : way.getNodes()) {
final boolean isEnd = node.index == 0 || node.index == way.getNodes().size() - 1;
final long osmId = node.value;
nodeData.setOrUpdateNodeType(osmId,
isEnd ? END_NODE : INTERMEDIATE_NODE,
// connection nodes are those where (only) two OSM ways are connected at their ends
prev -> prev == END_NODE && isEnd ? CONNECTION_NODE : JUNCTION_NODE);
}
}
@Override
public void handleRelation(ReaderRelation relation) {
if (!handledRelations) {
LOGGER.info("pass1 - start reading OSM relations");
handledRelations = true;
}
if (++relationsCounter % 1_000_000 == 0)
LOGGER.info("pass1 - processed relations: " + nf(relationsCounter) + ", " + Helper.getMemInfo());
relationPreprocessor.accept(relation);
}
@Override
public void handleFileHeader(OSMFileHeader fileHeader) throws ParseException {
timestamp = Helper.createFormatter().parse(fileHeader.getTag("timestamp"));
}
@Override
public void onFinish() {
LOGGER.info("pass1 - finished, processed ways: " + nf(wayCounter) + ", accepted ways: " +
nf(acceptedWays) + ", way nodes: " + nf(nodeData.getNodeCount()) + ", relations: " +
nf(relationsCounter) + ", " + Helper.getMemInfo());
}
}
private class Pass2Handler implements ReaderElementHandler {
private boolean handledNodes;
private boolean handledWays;
private boolean handledRelations;
private long nodeCounter = 0;
private long acceptedNodes = 0;
private long ignoredSplitNodes = 0;
private long wayCounter = 0;
@Override
public void handleNode(ReaderNode node) {
if (!handledNodes) {
LOGGER.info("pass2 - start reading OSM nodes");
handledNodes = true;
}
if (handledWays)
throw new IllegalStateException("OSM node elements must be located before way elements in OSM file");
if (handledRelations)
throw new IllegalStateException("OSM node elements must be located before relation elements in OSM file");
if (++nodeCounter % 10_000_000 == 0)
LOGGER.info("pass2 - processed nodes: " + nf(nodeCounter) + ", accepted nodes: " + nf(acceptedNodes) +
", " + Helper.getMemInfo());
long nodeType = nodeData.addCoordinatesIfMapped(node.getId(), node.getLat(), node.getLon(), () -> elevationProvider.getEle(node));
if (nodeType == EMPTY_NODE)
return;
acceptedNodes++;
// remember which nodes we want to split
if (splitNodeFilter.test(node)) {
if (nodeType == JUNCTION_NODE) {
LOGGER.debug("OSM node {} at {},{} is a barrier node at a junction. The barrier will be ignored",
node.getId(), Helper.round(node.getLat(), 7), Helper.round(node.getLon(), 7));
ignoredSplitNodes++;
} else
nodeData.setSplitNode(node.getId());
}
// store node tags if at least one important tag is included and make this available for the edge handler
for (Map.Entry e : node.getTags().entrySet()) {
if (INCLUDE_IF_NODE_TAGS.contains(e.getKey())) {
node.removeTag("created_by");
node.removeTag("source");
node.removeTag("note");
node.removeTag("fixme");
nodeData.setTags(node);
break;
}
}
}
@Override
public void handleWay(ReaderWay way) {
if (!handledWays) {
LOGGER.info("pass2 - start reading OSM ways");
handledWays = true;
}
if (handledRelations)
throw new IllegalStateException("OSM way elements must be located before relation elements in OSM file");
if (++wayCounter % 10_000_000 == 0)
LOGGER.info("pass2 - processed ways: " + nf(wayCounter) + ", " + Helper.getMemInfo());
if (!wayFilter.test(way))
return;
List segment = new ArrayList<>(way.getNodes().size());
for (LongCursor node : way.getNodes())
segment.add(new SegmentNode(node.value, nodeData.getId(node.value), nodeData.getTags(node.value)));
wayPreprocessor.preprocessWay(way, osmNodeId -> nodeData.getCoordinates(nodeData.getId(osmNodeId)), osmNodeId -> nodeData.getTags(osmNodeId));
splitWayAtJunctionsAndEmptySections(segment, way);
}
private void splitWayAtJunctionsAndEmptySections(List fullSegment, ReaderWay way) {
List segment = new ArrayList<>();
for (SegmentNode node : fullSegment) {
if (!isNodeId(node.id)) {
// this node exists in ways, but not in nodes. we ignore it, but we split the way when we encounter
// such a missing node. for example an OSM way might lead out of an area where nodes are available and
// back into it. we do not want to connect the exit/entry points using a straight line. this usually
// should only happen for OSM extracts
if (segment.size() > 1) {
splitLoopSegments(segment, way);
segment = new ArrayList<>();
}
} else if (isTowerNode(node.id)) {
if (!segment.isEmpty()) {
segment.add(node);
splitLoopSegments(segment, way);
segment = new ArrayList<>();
}
segment.add(node);
} else {
segment.add(node);
}
}
// the last segment might end at the end of the way
if (segment.size() > 1)
splitLoopSegments(segment, way);
}
private void splitLoopSegments(List segment, ReaderWay way) {
if (segment.size() < 2)
throw new IllegalStateException("Segment size must be >= 2, but was: " + segment.size());
boolean isLoop = segment.get(0).osmNodeId == segment.get(segment.size() - 1).osmNodeId;
if (segment.size() == 2 && isLoop) {
LOGGER.warn("Loop in OSM way: {}, will be ignored, duplicate node: {}", way.getId(), segment.get(0).osmNodeId);
} else if (isLoop) {
// split into two segments
splitSegmentAtSplitNodes(segment.subList(0, segment.size() - 1), way);
splitSegmentAtSplitNodes(segment.subList(segment.size() - 2, segment.size()), way);
} else {
splitSegmentAtSplitNodes(segment, way);
}
}
private void splitSegmentAtSplitNodes(List parentSegment, ReaderWay way) {
List segment = new ArrayList<>();
for (int i = 0; i < parentSegment.size(); i++) {
SegmentNode node = parentSegment.get(i);
if (nodeData.isSplitNode(node.osmNodeId)) {
// do not split this node again. for example a barrier can be connecting two ways (appear in both
// ways) and we only want to add a barrier edge once (but we want to add one).
nodeData.unsetSplitNode(node.osmNodeId);
// this node is a barrier. we will copy it and add an extra edge
SegmentNode barrierFrom = node;
SegmentNode barrierTo = nodeData.addCopyOfNode(node);
if (i == parentSegment.size() - 1) {
// make sure the barrier node is always on the inside of the segment
SegmentNode tmp = barrierFrom;
barrierFrom = barrierTo;
barrierTo = tmp;
}
if (!segment.isEmpty()) {
segment.add(barrierFrom);
handleSegment(segment, way);
segment = new ArrayList<>();
}
// mark barrier edge
way.setTag("gh:barrier_edge", true);
segment.add(barrierFrom);
segment.add(barrierTo);
handleSegment(segment, way);
way.removeTag("gh:barrier_edge");
segment = new ArrayList<>();
segment.add(barrierTo);
} else {
segment.add(node);
}
}
if (segment.size() > 1)
handleSegment(segment, way);
}
void handleSegment(List segment, ReaderWay way) {
final PointList pointList = new PointList(segment.size(), nodeData.is3D());
final List