
com.graphhopper.GraphHopper Maven / Gradle / Ivy
/*
* 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;
import com.graphhopper.config.CHProfile;
import com.graphhopper.config.LMProfile;
import com.graphhopper.config.Profile;
import com.graphhopper.reader.dem.*;
import com.graphhopper.reader.osm.OSMReader;
import com.graphhopper.reader.osm.conditional.DateRangeParser;
import com.graphhopper.routing.DefaultWeightingFactory;
import com.graphhopper.routing.Router;
import com.graphhopper.routing.RouterConfig;
import com.graphhopper.routing.WeightingFactory;
import com.graphhopper.routing.ch.CHPreparationHandler;
import com.graphhopper.routing.ev.*;
import com.graphhopper.routing.lm.LMConfig;
import com.graphhopper.routing.lm.LMPreparationHandler;
import com.graphhopper.routing.lm.LandmarkStorage;
import com.graphhopper.routing.lm.PrepareLandmarks;
import com.graphhopper.routing.subnetwork.PrepareRoutingSubnetworks;
import com.graphhopper.routing.subnetwork.PrepareRoutingSubnetworks.PrepareJob;
import com.graphhopper.routing.util.DefaultFlagEncoderFactory;
import com.graphhopper.routing.util.EncodingManager;
import com.graphhopper.routing.util.FlagEncoder;
import com.graphhopper.routing.util.FlagEncoderFactory;
import com.graphhopper.routing.util.parsers.DefaultTagParserFactory;
import com.graphhopper.routing.util.parsers.TagParserFactory;
import com.graphhopper.routing.util.spatialrules.AbstractSpatialRule;
import com.graphhopper.routing.util.spatialrules.SpatialRuleLookup;
import com.graphhopper.routing.util.spatialrules.SpatialRuleLookupBuilder;
import com.graphhopper.routing.weighting.Weighting;
import com.graphhopper.routing.weighting.custom.CustomProfile;
import com.graphhopper.routing.weighting.custom.CustomWeighting;
import com.graphhopper.storage.*;
import com.graphhopper.storage.index.LocationIndex;
import com.graphhopper.storage.index.LocationIndexTree;
import com.graphhopper.util.*;
import com.graphhopper.util.Parameters.CH;
import com.graphhopper.util.Parameters.Landmark;
import com.graphhopper.util.Parameters.Routing;
import com.graphhopper.util.details.PathDetailsBuilderFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.io.IOException;
import java.text.DateFormat;
import java.util.*;
import static com.graphhopper.util.Helper.*;
import static com.graphhopper.util.Parameters.Algorithms.RoundTrip;
/**
* Easy to use access point to configure import and (offline) routing.
*
* @author Peter Karich
* @see GraphHopperAPI
*/
public class GraphHopper implements GraphHopperAPI {
private final Logger logger = LoggerFactory.getLogger(getClass());
private final Map profilesByName = new LinkedHashMap<>();
private final String fileLockName = "gh.lock";
// utils
private final TranslationMap trMap = new TranslationMap().doImport();
boolean removeZipped = true;
// for graph:
private GraphHopperStorage ghStorage;
private final EncodingManager.Builder emBuilder = new EncodingManager.Builder();
private EncodingManager encodingManager;
private int defaultSegmentSize = -1;
private String ghLocation = "";
private DAType dataAccessType = DAType.RAM_STORE;
private boolean sortGraph = false;
private boolean elevation = false;
private LockFactory lockFactory = new NativeFSLockFactory();
private boolean allowWrites = true;
private boolean fullyLoaded = false;
private boolean smoothElevation = false;
private double longEdgeSamplingDistance = Double.MAX_VALUE;
// for routing
private final RouterConfig routerConfig = new RouterConfig();
// for index
private LocationIndex locationIndex;
private int preciseIndexResolution = 300;
private int maxRegionSearch = 4;
// for prepare
private int minNetworkSize = 200;
// for LM
private final JsonFeatureCollection landmarkSplittingFeatureCollection;
// preparation handlers
private final LMPreparationHandler lmPreparationHandler = new LMPreparationHandler();
private final CHPreparationHandler chPreparationHandler = new CHPreparationHandler();
// for data reader
private String osmFile;
private double dataReaderWayPointMaxDistance = 1;
private int dataReaderWorkerThreads = 2;
private ElevationProvider eleProvider = ElevationProvider.NOOP;
private FlagEncoderFactory flagEncoderFactory = new DefaultFlagEncoderFactory();
private EncodedValueFactory encodedValueFactory = new DefaultEncodedValueFactory();
private TagParserFactory tagParserFactory = new DefaultTagParserFactory();
private PathDetailsBuilderFactory pathBuilderFactory = new PathDetailsBuilderFactory();
public GraphHopper() {
this(null);
}
public GraphHopper(JsonFeatureCollection landmarkSplittingFeatureCollection) {
this.landmarkSplittingFeatureCollection = landmarkSplittingFeatureCollection;
}
public EncodingManager.Builder getEncodingManagerBuilder() {
return emBuilder;
}
public EncodingManager getEncodingManager() {
if (encodingManager == null)
throw new IllegalStateException("EncodingManager not yet build");
return encodingManager;
}
public ElevationProvider getElevationProvider() {
return eleProvider;
}
public GraphHopper setElevationProvider(ElevationProvider eleProvider) {
if (eleProvider == null || eleProvider == ElevationProvider.NOOP)
setElevation(false);
else
setElevation(true);
this.eleProvider = eleProvider;
return this;
}
/**
* Threads for data reading.
*/
protected int getWorkerThreads() {
return dataReaderWorkerThreads;
}
/**
* Return maximum distance (in meter) to reduce points via douglas peucker while OSM import.
*/
protected double getWayPointMaxDistance() {
return dataReaderWayPointMaxDistance;
}
/**
* This parameter specifies how to reduce points via douglas peucker while OSM import. Higher
* value means more details, unit is meter. Default is 1. Disable via 0.
*/
public GraphHopper setWayPointMaxDistance(double wayPointMaxDistance) {
this.dataReaderWayPointMaxDistance = wayPointMaxDistance;
return this;
}
public GraphHopper setPathDetailsBuilderFactory(PathDetailsBuilderFactory pathBuilderFactory) {
this.pathBuilderFactory = pathBuilderFactory;
return this;
}
public PathDetailsBuilderFactory getPathDetailsBuilderFactory() {
return pathBuilderFactory;
}
/**
* Precise location resolution index means also more space (disc/RAM) could be consumed and
* probably slower query times, which would be e.g. not suitable for Android. The resolution
* specifies the tile width (in meter).
*/
public GraphHopper setPreciseIndexResolution(int precision) {
ensureNotLoaded();
preciseIndexResolution = precision;
return this;
}
public GraphHopper setMinNetworkSize(int minNetworkSize) {
ensureNotLoaded();
this.minNetworkSize = minNetworkSize;
return this;
}
/**
* Only valid option for in-memory graph and if you e.g. want to disable store on flush for unit
* tests. Specify storeOnFlush to true if you want that existing data will be loaded FROM disc
* and all in-memory data will be flushed TO disc after flush is called e.g. while OSM import.
*
* @param storeOnFlush true by default
*/
public GraphHopper setStoreOnFlush(boolean storeOnFlush) {
ensureNotLoaded();
if (storeOnFlush)
dataAccessType = DAType.RAM_STORE;
else
dataAccessType = DAType.RAM;
return this;
}
/**
* Sets the routing profiles that shall be supported by this GraphHopper instance. The (and only the) given profiles
* can be used for routing without preparation and for CH/LM preparation.
*
* Here is an example how to setup two CH profiles and one LM profile (via the Java API)
*
*
* {@code
* hopper.setProfiles(
* new Profile("my_car").setVehicle("car").setWeighting("shortest"),
* new Profile("your_bike").setVehicle("bike").setWeighting("fastest")
* );
* hopper.getCHPreparationHandler().setCHProfiles(
* new CHProfile("my_car"),
* new CHProfile("your_bike")
* );
* hopper.getLMPreparationHandler().setLMProfiles(
* new LMProfile("your_bike")
* );
* }
*
*
* See also https://github.com/graphhopper/graphhopper/pull/1922.
*
* @see CHPreparationHandler#setCHProfiles
* @see LMPreparationHandler#setLMProfiles
*/
public GraphHopper setProfiles(Profile... profiles) {
return setProfiles(Arrays.asList(profiles));
}
public GraphHopper setProfiles(List profiles) {
if (!profilesByName.isEmpty())
throw new IllegalArgumentException("Cannot initialize profiles multiple times");
if (encodingManager != null)
throw new IllegalArgumentException("Cannot set profiles after EncodingManager was built");
for (Profile profile : profiles) {
Profile previous = this.profilesByName.put(profile.getName(), profile);
if (previous != null)
throw new IllegalArgumentException("Profile names must be unique. Duplicate name: '" + profile.getName() + "'");
}
return this;
}
public List getProfiles() {
return new ArrayList<>(profilesByName.values());
}
/**
* Returns the profile for the given profile name, or null if it does not exist
*/
public Profile getProfile(String profileName) {
return profilesByName.get(profileName);
}
/**
* @return true if storing and fetching elevation data is enabled. Default is false
*/
public boolean hasElevation() {
return elevation;
}
/**
* Enable storing and fetching elevation data. Default is false
*/
public GraphHopper setElevation(boolean includeElevation) {
this.elevation = includeElevation;
return this;
}
/**
* Sets the distance distance between elevation samples on long edges
*/
public GraphHopper setLongEdgeSamplingDistance(double longEdgeSamplingDistance) {
this.longEdgeSamplingDistance = longEdgeSamplingDistance;
return this;
}
/**
* Sets the max elevation discrepancy between way points and the simplified polyline in meters
*/
public GraphHopper setElevationWayPointMaxDistance(double elevationWayPointMaxDistance) {
this.routerConfig.setElevationWayPointMaxDistance(elevationWayPointMaxDistance);
return this;
}
public String getGraphHopperLocation() {
return ghLocation;
}
/**
* Sets the graphhopper folder.
*/
public GraphHopper setGraphHopperLocation(String ghLocation) {
ensureNotLoaded();
if (ghLocation == null)
throw new IllegalArgumentException("graphhopper location cannot be null");
this.ghLocation = ghLocation;
return this;
}
public String getOSMFile() {
return osmFile;
}
/**
* This file can be an osm xml (.osm), a compressed xml (.osm.zip or .osm.gz) or a protobuf file
* (.pbf).
*/
public GraphHopper setOSMFile(String osmFile) {
ensureNotLoaded();
if (isEmpty(osmFile))
throw new IllegalArgumentException("OSM file cannot be empty.");
this.osmFile = osmFile;
return this;
}
/**
* The underlying graph used in algorithms.
*
* @throws IllegalStateException if graph is not instantiated.
*/
public GraphHopperStorage getGraphHopperStorage() {
if (ghStorage == null)
throw new IllegalStateException("GraphHopper storage not initialized");
return ghStorage;
}
public void setGraphHopperStorage(GraphHopperStorage ghStorage) {
this.ghStorage = ghStorage;
setFullyLoaded();
}
/**
* The location index created from the graph.
*
* @throws IllegalStateException if index is not initialized
*/
public LocationIndex getLocationIndex() {
if (locationIndex == null)
throw new IllegalStateException("LocationIndex not initialized");
return locationIndex;
}
protected void setLocationIndex(LocationIndex locationIndex) {
this.locationIndex = locationIndex;
}
/**
* Sorts the graph which requires more RAM while import. See #12
*/
public GraphHopper setSortGraph(boolean sortGraph) {
ensureNotLoaded();
this.sortGraph = sortGraph;
return this;
}
public boolean isAllowWrites() {
return allowWrites;
}
/**
* Specifies if it is allowed for GraphHopper to write. E.g. for read only filesystems it is not
* possible to create a lock file and so we can avoid write locks.
*/
public GraphHopper setAllowWrites(boolean allowWrites) {
this.allowWrites = allowWrites;
return this;
}
public TranslationMap getTranslationMap() {
return trMap;
}
public GraphHopper setFlagEncoderFactory(FlagEncoderFactory factory) {
this.flagEncoderFactory = factory;
return this;
}
public EncodedValueFactory getEncodedValueFactory() {
return this.encodedValueFactory;
}
public GraphHopper setEncodedValueFactory(EncodedValueFactory factory) {
this.encodedValueFactory = factory;
return this;
}
public TagParserFactory getTagParserFactory() {
return this.tagParserFactory;
}
public GraphHopper setTagParserFactory(TagParserFactory factory) {
this.tagParserFactory = factory;
return this;
}
/**
* Reads the configuration from a {@link GraphHopperConfig} object which can be manually filled, or more typically
* is read from `config.yml`.
*/
public GraphHopper init(GraphHopperConfig ghConfig) {
// disabling_allowed config options were removed for GH 3.0
if (ghConfig.has("routing.ch.disabling_allowed"))
throw new IllegalArgumentException("The 'routing.ch.disabling_allowed' configuration option is no longer supported");
if (ghConfig.has("routing.lm.disabling_allowed"))
throw new IllegalArgumentException("The 'routing.lm.disabling_allowed' configuration option is no longer supported");
if (ghConfig.has("osmreader.osm"))
throw new IllegalArgumentException("Instead osmreader.osm use datareader.file, for other changes see CHANGELOG.md");
String tmpOsmFile = ghConfig.getString("datareader.file", "");
if (!isEmpty(tmpOsmFile))
osmFile = tmpOsmFile;
String graphHopperFolder = ghConfig.getString("graph.location", "");
if (isEmpty(graphHopperFolder) && isEmpty(ghLocation)) {
if (isEmpty(osmFile))
throw new IllegalArgumentException("If no graph.location is provided you need to specify an OSM file.");
graphHopperFolder = pruneFileEnd(osmFile) + "-gh";
}
// graph
setGraphHopperLocation(graphHopperFolder);
defaultSegmentSize = ghConfig.getInt("graph.dataaccess.segment_size", defaultSegmentSize);
String graphDATypeStr = ghConfig.getString("graph.dataaccess", "RAM_STORE");
dataAccessType = DAType.fromString(graphDATypeStr);
sortGraph = ghConfig.getBool("graph.do_sort", sortGraph);
removeZipped = ghConfig.getBool("graph.remove_zipped", removeZipped);
if (encodingManager != null)
throw new IllegalStateException("Cannot call init twice. EncodingManager was already initialized.");
emBuilder.setEnableInstructions(ghConfig.getBool("datareader.instructions", true));
emBuilder.setPreferredLanguage(ghConfig.getString("datareader.preferred_language", ""));
emBuilder.setDateRangeParser(DateRangeParser.createInstance(ghConfig.getString("datareader.date_range_parser_day", "")));
setProfiles(ghConfig.getProfiles());
// currently we cannot require profiles at this point as GTFS module does not use them
encodingManager = buildEncodingManager(ghConfig, false);
if (ghConfig.getString("graph.locktype", "native").equals("simple"))
lockFactory = new SimpleFSLockFactory();
else
lockFactory = new NativeFSLockFactory();
// elevation
this.smoothElevation = ghConfig.getBool("graph.elevation.smoothing", false);
this.longEdgeSamplingDistance = ghConfig.getDouble("graph.elevation.long_edge_sampling_distance", Double.MAX_VALUE);
setElevationWayPointMaxDistance(ghConfig.getDouble("graph.elevation.way_point_max_distance", Double.MAX_VALUE));
ElevationProvider elevationProvider = createElevationProvider(ghConfig);
setElevationProvider(elevationProvider);
if (longEdgeSamplingDistance < Double.MAX_VALUE && !elevationProvider.getInterpolate())
logger.warn("Long edge sampling enabled, but bilinear interpolation disabled. See #1953");
// optimizable prepare
minNetworkSize = ghConfig.getInt("prepare.min_network_size", minNetworkSize);
// prepare CH&LM
chPreparationHandler.init(ghConfig);
lmPreparationHandler.init(ghConfig);
// osm import
dataReaderWayPointMaxDistance = ghConfig.getDouble(Routing.INIT_WAY_POINT_MAX_DISTANCE, dataReaderWayPointMaxDistance);
dataReaderWorkerThreads = ghConfig.getInt("datareader.worker_threads", dataReaderWorkerThreads);
// index
preciseIndexResolution = ghConfig.getInt("index.high_resolution", preciseIndexResolution);
maxRegionSearch = ghConfig.getInt("index.max_region_search", maxRegionSearch);
// routing
routerConfig.setMaxVisitedNodes(ghConfig.getInt(Routing.INIT_MAX_VISITED_NODES, routerConfig.getMaxVisitedNodes()));
routerConfig.setMaxRoundTripRetries(ghConfig.getInt(RoundTrip.INIT_MAX_RETRIES, routerConfig.getMaxRoundTripRetries()));
routerConfig.setNonChMaxWaypointDistance(ghConfig.getInt(Parameters.NON_CH.MAX_NON_CH_POINT_DISTANCE, routerConfig.getNonChMaxWaypointDistance()));
int activeLandmarkCount = ghConfig.getInt(Landmark.ACTIVE_COUNT_DEFAULT, Math.min(8, lmPreparationHandler.getLandmarks()));
if (activeLandmarkCount > lmPreparationHandler.getLandmarks())
throw new IllegalArgumentException("Default value for active landmarks " + activeLandmarkCount
+ " should be less or equal to landmark count of " + lmPreparationHandler.getLandmarks());
routerConfig.setActiveLandmarkCount(activeLandmarkCount);
return this;
}
private EncodingManager buildEncodingManager(GraphHopperConfig ghConfig, boolean requireProfilesByName) {
String flagEncodersStr = ghConfig.getString("graph.flag_encoders", "");
String encodedValueStr = ghConfig.getString("graph.encoded_values", "");
Map flagEncoderMap = new LinkedHashMap<>(), implicitFlagEncoderMap = new HashMap<>();
for (String encoderStr : Arrays.asList(flagEncodersStr.split(","))) {
String key = encoderStr.split("\\|")[0];
if (!key.isEmpty()) {
if (flagEncoderMap.containsKey(key))
throw new IllegalArgumentException("FlagEncoder " + key + " needs to be unique");
flagEncoderMap.put(key, encoderStr);
}
}
if (requireProfilesByName && profilesByName.isEmpty())
throw new IllegalStateException("no profiles exist but assumed to create EncodingManager. E.g. provide them in GraphHopperConfig when calling GraphHopper.init");
for (Profile profile : profilesByName.values()) {
emBuilder.add(Subnetwork.create(profile.getName()));
if (!flagEncoderMap.containsKey(profile.getVehicle())
// overwrite key in implicit map if turn cost support required
&& (!implicitFlagEncoderMap.containsKey(profile.getVehicle()) || profile.isTurnCosts()))
implicitFlagEncoderMap.put(profile.getVehicle(), profile.getVehicle() + (profile.isTurnCosts() ? "|turn_costs=true" : ""));
}
flagEncoderMap.putAll(implicitFlagEncoderMap);
flagEncoderMap.values().stream().forEach(s -> emBuilder.addIfAbsent(flagEncoderFactory, s));
for (String tpStr : encodedValueStr.split(",")) {
if (!tpStr.isEmpty()) emBuilder.addIfAbsent(tagParserFactory, tpStr);
}
return emBuilder.build();
}
private static ElevationProvider createElevationProvider(GraphHopperConfig ghConfig) {
String eleProviderStr = toLowerCase(ghConfig.getString("graph.elevation.provider", "noop"));
if (ghConfig.has("graph.elevation.calcmean"))
throw new IllegalArgumentException("graph.elevation.calcmean is deprecated, use graph.elevation.interpolate");
boolean interpolate = ghConfig.has("graph.elevation.interpolate")
? "bilinear".equals(ghConfig.getString("graph.elevation.interpolate", "none"))
: ghConfig.getBool("graph.elevation.calc_mean", false);
String cacheDirStr = ghConfig.getString("graph.elevation.cache_dir", "");
if (cacheDirStr.isEmpty() && ghConfig.has("graph.elevation.cachedir"))
throw new IllegalArgumentException("use graph.elevation.cache_dir not cachedir in configuration");
String baseURL = ghConfig.getString("graph.elevation.base_url", "");
if (baseURL.isEmpty() && ghConfig.has("graph.elevation.baseurl"))
throw new IllegalArgumentException("use graph.elevation.base_url not baseurl in configuration");
boolean removeTempElevationFiles = ghConfig.getBool("graph.elevation.cgiar.clear", true);
removeTempElevationFiles = ghConfig.getBool("graph.elevation.clear", removeTempElevationFiles);
DAType elevationDAType = DAType.fromString(ghConfig.getString("graph.elevation.dataaccess", "MMAP"));
ElevationProvider elevationProvider = ElevationProvider.NOOP;
if (eleProviderStr.equalsIgnoreCase("srtm")) {
elevationProvider = new SRTMProvider(cacheDirStr);
} else if (eleProviderStr.equalsIgnoreCase("cgiar")) {
elevationProvider = new CGIARProvider(cacheDirStr);
} else if (eleProviderStr.equalsIgnoreCase("gmted")) {
elevationProvider = new GMTEDProvider(cacheDirStr);
} else if (eleProviderStr.equalsIgnoreCase("srtmgl1")) {
elevationProvider = new SRTMGL1Provider(cacheDirStr);
} else if (eleProviderStr.equalsIgnoreCase("multi")) {
elevationProvider = new MultiSourceElevationProvider(cacheDirStr);
} else if (eleProviderStr.equalsIgnoreCase("skadi")) {
elevationProvider = new SkadiProvider(cacheDirStr);
}
elevationProvider.setAutoRemoveTemporaryFiles(removeTempElevationFiles);
elevationProvider.setInterpolate(interpolate);
if (!baseURL.isEmpty())
elevationProvider.setBaseURL(baseURL);
elevationProvider.setDAType(elevationDAType);
return elevationProvider;
}
private void printInfo() {
logger.info("version " + Constants.VERSION + "|" + Constants.BUILD_DATE + " (" + Constants.getVersions() + ")");
if (ghStorage != null)
logger.info("graph " + ghStorage.toString() + ", details:" + ghStorage.toDetailsString());
}
/**
* Imports provided data from disc and creates graph. Depending on the settings the resulting
* graph will be stored to disc so on a second call this method will only load the graph from
* disc which is usually a lot faster.
*/
public GraphHopper importOrLoad() {
if (!load(ghLocation)) {
printInfo();
process(ghLocation, false);
} else {
printInfo();
}
return this;
}
/**
* Imports and processes data, storing it to disk when complete.
*/
public void importAndClose() {
if (!load(ghLocation)) {
printInfo();
process(ghLocation, true);
} else {
printInfo();
logger.info("Graph already imported into " + ghLocation);
}
close();
}
/**
* Creates the graph from OSM data.
*/
private void process(String graphHopperLocation, boolean closeEarly) {
setGraphHopperLocation(graphHopperLocation);
GHLock lock = null;
try {
if (ghStorage == null)
throw new IllegalStateException("GraphHopperStorage must be initialized before starting the import");
if (ghStorage.getDirectory().getDefaultType().isStoring()) {
lockFactory.setLockDir(new File(graphHopperLocation));
lock = lockFactory.create(fileLockName, true);
if (!lock.tryLock())
throw new RuntimeException("To avoid multiple writers we need to obtain a write lock but it failed. In " + graphHopperLocation, lock.getObtainFailedReason());
}
ensureWriteAccess();
importOSM();
cleanUp();
postProcessing(closeEarly);
flush();
} finally {
if (lock != null)
lock.release();
}
}
protected void importOSM() {
if (osmFile == null)
throw new IllegalStateException("Couldn't load from existing folder: " + ghLocation
+ " but also cannot use file for DataReader as it wasn't specified!");
logger.info("start creating graph from " + osmFile);
OSMReader reader = new OSMReader(ghStorage).setFile(_getOSMFile()).
setElevationProvider(eleProvider).
setWorkerThreads(dataReaderWorkerThreads).
setWayPointMaxDistance(dataReaderWayPointMaxDistance).
setWayPointElevationMaxDistance(routerConfig.getElevationWayPointMaxDistance()).
setSmoothElevation(smoothElevation).
setLongEdgeSamplingDistance(longEdgeSamplingDistance);
logger.info("using " + ghStorage.toString() + ", memory:" + getMemInfo());
try {
reader.readGraph();
} catch (IOException ex) {
throw new RuntimeException("Cannot read file " + getOSMFile(), ex);
}
DateFormat f = createFormatter();
ghStorage.getProperties().put("datareader.import.date", f.format(new Date()));
if (reader.getDataDate() != null)
ghStorage.getProperties().put("datareader.data.date", f.format(reader.getDataDate()));
}
/**
* Currently we use this for a few tests where the dataReaderFile is loaded from the classpath
*/
protected File _getOSMFile() {
return new File(osmFile);
}
/**
* Opens existing graph folder.
*
* @param graphHopperFolder is the folder containing graphhopper files. Can be a compressed file
* too ala folder-content.ghz.
*/
public boolean load(String graphHopperFolder) {
if (isEmpty(graphHopperFolder))
throw new IllegalStateException("GraphHopperLocation is not specified. Call setGraphHopperLocation or init before");
if (fullyLoaded)
throw new IllegalStateException("graph is already successfully loaded");
File tmpFileOrFolder = new File(graphHopperFolder);
if (!tmpFileOrFolder.isDirectory() && tmpFileOrFolder.exists()) {
throw new IllegalArgumentException("GraphHopperLocation cannot be an existing file. Has to be either non-existing or a folder.");
} else {
File compressed = new File(graphHopperFolder + ".ghz");
if (compressed.exists() && !compressed.isDirectory()) {
try {
new Unzipper().unzip(compressed.getAbsolutePath(), graphHopperFolder, removeZipped);
} catch (IOException ex) {
throw new RuntimeException("Couldn't extract file " + compressed.getAbsolutePath()
+ " to " + graphHopperFolder, ex);
}
}
}
setGraphHopperLocation(graphHopperFolder);
if (!allowWrites && dataAccessType.isMMap())
dataAccessType = DAType.MMAP_RO;
if (encodingManager == null) {
StorableProperties properties = new StorableProperties(new GHDirectory(ghLocation, dataAccessType));
encodingManager = properties.loadExisting()
? EncodingManager.create(emBuilder, encodedValueFactory, flagEncoderFactory, properties)
: buildEncodingManager(new GraphHopperConfig(), true);
}
GHDirectory dir = new GHDirectory(ghLocation, dataAccessType);
ghStorage = new GraphHopperStorage(dir, encodingManager, hasElevation(), encodingManager.needsTurnCostsSupport(), defaultSegmentSize);
checkProfilesConsistency();
if (lmPreparationHandler.isEnabled())
initLMPreparationHandler();
List chConfigs;
if (chPreparationHandler.isEnabled()) {
initCHPreparationHandler();
chConfigs = chPreparationHandler.getCHConfigs();
} else {
chConfigs = Collections.emptyList();
}
ghStorage.addCHGraphs(chConfigs);
if (!new File(graphHopperFolder).exists())
return false;
GHLock lock = null;
try {
// create locks only if writes are allowed, if they are not allowed a lock cannot be created
// (e.g. on a read only filesystem locks would fail)
if (ghStorage.getDirectory().getDefaultType().isStoring() && isAllowWrites()) {
lockFactory.setLockDir(new File(ghLocation));
lock = lockFactory.create(fileLockName, false);
if (!lock.tryLock())
throw new RuntimeException("To avoid reading partial data we need to obtain the read lock but it failed. In " + ghLocation, lock.getObtainFailedReason());
}
if (!ghStorage.loadExisting())
return false;
postProcessing(false);
setFullyLoaded();
return true;
} finally {
if (lock != null)
lock.release();
}
}
private void checkProfilesConsistency() {
EncodingManager encodingManager = getEncodingManager();
for (Profile profile : profilesByName.values()) {
if (!encodingManager.hasEncoder(profile.getVehicle())) {
throw new IllegalArgumentException("Unknown vehicle '" + profile.getVehicle() + "' in profile: " + profile + ". Make sure all vehicles used in 'profiles' exist in 'graph.flag_encoders'");
}
FlagEncoder encoder = encodingManager.getEncoder(profile.getVehicle());
if (profile.isTurnCosts() && !encoder.supportsTurnCosts()) {
throw new IllegalArgumentException("The profile '" + profile.getName() + "' was configured with " +
"'turn_costs=true', but the corresponding vehicle '" + profile.getVehicle() + "' does not support turn costs." +
"\nYou need to add `|turn_costs=true` to the vehicle in `graph.flag_encoders`");
}
try {
createWeighting(profile, new PMap());
} catch (IllegalArgumentException e) {
throw new IllegalArgumentException("Could not create weighting for profile: '" + profile.getName() + "'.\n" +
"Profile: " + profile + "\n" +
"Error: " + e.getMessage());
}
if (profile instanceof CustomProfile) {
CustomModel customModel = ((CustomProfile) profile).getCustomModel();
if (customModel == null)
throw new IllegalArgumentException("custom model for profile '" + profile.getName() + "' was empty");
if (!CustomWeighting.NAME.equals(profile.getWeighting()))
throw new IllegalArgumentException("profile '" + profile.getName() + "' has a custom model but " +
"weighting=" + profile.getWeighting() + " was defined");
}
}
Set chProfileSet = new LinkedHashSet<>(chPreparationHandler.getCHProfiles().size());
for (CHProfile chProfile : chPreparationHandler.getCHProfiles()) {
boolean added = chProfileSet.add(chProfile.getProfile());
if (!added) {
throw new IllegalArgumentException("Duplicate CH reference to profile '" + chProfile.getProfile() + "'");
}
if (!profilesByName.containsKey(chProfile.getProfile())) {
throw new IllegalArgumentException("CH profile references unknown profile '" + chProfile.getProfile() + "'");
}
}
Map lmProfileMap = new LinkedHashMap<>(lmPreparationHandler.getLMProfiles().size());
for (LMProfile lmProfile : lmPreparationHandler.getLMProfiles()) {
LMProfile previous = lmProfileMap.put(lmProfile.getProfile(), lmProfile);
if (previous != null) {
throw new IllegalArgumentException("Multiple LM profiles are using the same profile '" + lmProfile.getProfile() + "'");
}
if (!profilesByName.containsKey(lmProfile.getProfile())) {
throw new IllegalArgumentException("LM profile references unknown profile '" + lmProfile.getProfile() + "'");
}
if (lmProfile.usesOtherPreparation() && !profilesByName.containsKey(lmProfile.getPreparationProfile())) {
throw new IllegalArgumentException("LM profile references unknown preparation profile '" + lmProfile.getPreparationProfile() + "'");
}
}
for (LMProfile lmProfile : lmPreparationHandler.getLMProfiles()) {
if (lmProfile.usesOtherPreparation() && !lmProfileMap.containsKey(lmProfile.getPreparationProfile())) {
throw new IllegalArgumentException("Unknown LM preparation profile '" + lmProfile.getPreparationProfile() + "' in LM profile '" + lmProfile.getProfile() + "' cannot be used as preparation_profile");
}
if (lmProfile.usesOtherPreparation() && lmProfileMap.get(lmProfile.getPreparationProfile()).usesOtherPreparation()) {
throw new IllegalArgumentException("Cannot use '" + lmProfile.getPreparationProfile() + "' as preparation_profile for LM profile '" + lmProfile.getProfile() + "', because it uses another profile for preparation itself.");
}
}
}
public final CHPreparationHandler getCHPreparationHandler() {
return chPreparationHandler;
}
private void initCHPreparationHandler() {
if (chPreparationHandler.hasCHConfigs()) {
return;
}
for (CHProfile chProfile : chPreparationHandler.getCHProfiles()) {
Profile profile = profilesByName.get(chProfile.getProfile());
if (profile.isTurnCosts()) {
chPreparationHandler.addCHConfig(CHConfig.edgeBased(profile.getName(), createWeighting(profile, new PMap())));
} else {
chPreparationHandler.addCHConfig(CHConfig.nodeBased(profile.getName(), createWeighting(profile, new PMap())));
}
}
}
public final LMPreparationHandler getLMPreparationHandler() {
return lmPreparationHandler;
}
private void initLMPreparationHandler() {
if (lmPreparationHandler.hasLMProfiles())
return;
for (LMProfile lmProfile : lmPreparationHandler.getLMProfiles()) {
if (lmProfile.usesOtherPreparation())
continue;
Profile profile = profilesByName.get(lmProfile.getProfile());
// Note that we have to make sure the weighting used for LM preparation does not include turn costs, because
// the LM preparation is running node-based and the landmark weights will be wrong if there are non-zero
// turn costs, see discussion in #1960
// Running the preparation without turn costs is also useful to allow e.g. changing the u_turn_costs per
// request (we have to use the minimum weight settings (= no turn costs) for the preparation)
Weighting weighting = createWeighting(profile, new PMap(), true);
lmPreparationHandler.addLMConfig(new LMConfig(profile.getName(), weighting));
}
}
/**
* Does the preparation and creates the location index
*/
public final void postProcessing() {
postProcessing(false);
}
/**
* Does the preparation and creates the location index
*
* @param closeEarly release resources as early as possible
*/
protected void postProcessing(boolean closeEarly) {
// Later: move this into the GraphStorage.optimize method
// Or: Doing it after preparation to optimize shortcuts too. But not possible yet #12
if (sortGraph) {
if (ghStorage.isCHPossible() && isCHPrepared())
throw new IllegalArgumentException("Sorting a prepared CHGraph is not possible yet. See #12");
GraphHopperStorage newGraph = GHUtility.newStorage(ghStorage);
GHUtility.sortDFS(ghStorage, newGraph);
logger.info("graph sorted (" + getMemInfo() + ")");
ghStorage = newGraph;
}
if (!hasInterpolated() && hasElevation()) {
interpolateBridgesTunnelsAndFerries();
}
initLocationIndex();
importPublicTransit();
if (lmPreparationHandler.isEnabled())
lmPreparationHandler.createPreparations(ghStorage, locationIndex);
loadOrPrepareLM(closeEarly);
if (chPreparationHandler.isEnabled())
chPreparationHandler.createPreparations(ghStorage);
if (isCHPrepared()) {
// check loaded profiles
for (CHProfile profile : chPreparationHandler.getCHProfiles()) {
if (!getProfileVersion(profile.getProfile()).equals("" + profilesByName.get(profile.getProfile()).getVersion()))
throw new IllegalArgumentException("CH preparation of " + profile.getProfile() + " already exists in storage and doesn't match configuration");
}
} else {
prepareCH(closeEarly);
}
}
protected void importPublicTransit() {
}
private static final String INTERPOLATION_KEY = "prepare.elevation_interpolation.done";
private boolean hasInterpolated() {
return "true".equals(ghStorage.getProperties().get(INTERPOLATION_KEY));
}
void interpolateBridgesTunnelsAndFerries() {
if (ghStorage.getEncodingManager().hasEncodedValue(RoadEnvironment.KEY)) {
EnumEncodedValue roadEnvEnc = ghStorage.getEncodingManager().getEnumEncodedValue(RoadEnvironment.KEY, RoadEnvironment.class);
StopWatch sw = new StopWatch().start();
new EdgeElevationInterpolator(ghStorage, roadEnvEnc, RoadEnvironment.TUNNEL).execute();
float tunnel = sw.stop().getSeconds();
sw = new StopWatch().start();
new EdgeElevationInterpolator(ghStorage, roadEnvEnc, RoadEnvironment.BRIDGE).execute();
float bridge = sw.stop().getSeconds();
// The SkadiProvider contains bathymetric data. For ferries this can result in bigger elevation changes
// See #2098 for mor information
sw = new StopWatch().start();
new EdgeElevationInterpolator(ghStorage, roadEnvEnc, RoadEnvironment.FERRY).execute();
ghStorage.getProperties().put(INTERPOLATION_KEY, true);
logger.info("Bridge interpolation " + (int) bridge + "s, " + "tunnel interpolation " + (int) tunnel + "s, ferry interpolation " + (int) sw.stop().getSeconds() + "s");
}
}
public final Weighting createWeighting(Profile profile, PMap hints) {
return createWeighting(profile, hints, false);
}
public final Weighting createWeighting(Profile profile, PMap hints, boolean disableTurnCosts) {
return createWeightingFactory().createWeighting(profile, hints, disableTurnCosts);
}
protected WeightingFactory createWeightingFactory() {
return new DefaultWeightingFactory(ghStorage, getEncodingManager());
}
@Override
public GHResponse route(GHRequest request) {
return createRouter().route(request);
}
private Router createRouter() {
if (ghStorage == null || !fullyLoaded)
throw new IllegalStateException("Do a successful call to load or importOrLoad before routing");
if (ghStorage.isClosed())
throw new IllegalStateException("You need to create a new GraphHopper instance as it is already closed");
if (locationIndex == null)
throw new IllegalStateException("Location index not initialized");
Map chGraphs = new LinkedHashMap<>();
for (CHProfile chProfile : chPreparationHandler.getCHProfiles()) {
String chGraphName = chPreparationHandler.getPreparation(chProfile.getProfile()).getCHConfig().getName();
chGraphs.put(chProfile.getProfile(), ghStorage.getCHGraph(chGraphName));
}
Map landmarks = new LinkedHashMap<>();
for (LMProfile lmp : lmPreparationHandler.getLMProfiles()) {
landmarks.put(lmp.getProfile(),
lmp.usesOtherPreparation()
// cross-querying
? lmPreparationHandler.getPreparation(lmp.getPreparationProfile()).getLandmarkStorage()
: lmPreparationHandler.getPreparation(lmp.getProfile()).getLandmarkStorage());
}
return doCreateRouter(ghStorage, locationIndex, profilesByName, pathBuilderFactory,
trMap, routerConfig, createWeightingFactory(), chGraphs, landmarks);
}
protected Router doCreateRouter(GraphHopperStorage ghStorage, LocationIndex locationIndex, Map profilesByName,
PathDetailsBuilderFactory pathBuilderFactory, TranslationMap trMap, RouterConfig routerConfig,
WeightingFactory weightingFactory, Map chGraphs, Map landmarks) {
return new Router(ghStorage, locationIndex, profilesByName, pathBuilderFactory,
trMap, routerConfig, weightingFactory, chGraphs, landmarks
);
}
protected LocationIndex createLocationIndex(Directory dir) {
LocationIndexTree tmpIndex = new LocationIndexTree(ghStorage, dir);
tmpIndex.setResolution(preciseIndexResolution);
tmpIndex.setMaxRegionSearch(maxRegionSearch);
if (!tmpIndex.loadExisting()) {
ensureWriteAccess();
tmpIndex.prepareIndex();
}
return tmpIndex;
}
/**
* Initializes the location index after the import is done.
*/
protected void initLocationIndex() {
if (locationIndex != null)
throw new IllegalStateException("Cannot initialize locationIndex twice!");
locationIndex = createLocationIndex(ghStorage.getDirectory());
}
private boolean isCHPrepared() {
return "true".equals(ghStorage.getProperties().get(CH.PREPARE + "done"));
}
private String getProfileVersion(String profile) {
return ghStorage.getProperties().get("graph.profiles." + profile + ".version");
}
private void setProfileVersion(String profile, int version) {
ghStorage.getProperties().put("graph.profiles." + profile + ".version", version);
}
protected void prepareCH(boolean closeEarly) {
for (CHProfile profile : chPreparationHandler.getCHProfiles()) {
if (!getProfileVersion(profile.getProfile()).isEmpty()
&& !getProfileVersion(profile.getProfile()).equals("" + profilesByName.get(profile.getProfile()).getVersion()))
throw new IllegalArgumentException("CH preparation of " + profile.getProfile() + " already exists in storage and doesn't match configuration");
}
boolean chEnabled = chPreparationHandler.isEnabled();
if (chEnabled) {
ensureWriteAccess();
if (closeEarly) {
locationIndex.close();
boolean includesCustomProfiles = getProfiles().stream().anyMatch(p -> p instanceof CustomProfile);
if (!includesCustomProfiles)
// when there are custom profiles we must not close way geometry or StringIndex, because
// they might be needed to evaluate the custom weighting during CH preparation
ghStorage.flushAndCloseEarly();
}
ghStorage.freeze();
chPreparationHandler.prepare(ghStorage.getProperties(), closeEarly);
ghStorage.getProperties().put(CH.PREPARE + "done", true);
for (CHProfile profile : chPreparationHandler.getCHProfiles()) {
// potentially overwrite existing keys from LM
setProfileVersion(profile.getProfile(), profilesByName.get(profile.getProfile()).getVersion());
}
}
}
/**
* For landmarks it is required to always call this method: either it creates the landmark data or it loads it.
*/
protected void loadOrPrepareLM(boolean closeEarly) {
if (!lmPreparationHandler.isEnabled() || lmPreparationHandler.getPreparations().isEmpty()) {
return;
}
if (landmarkSplittingFeatureCollection != null && !landmarkSplittingFeatureCollection.getFeatures().isEmpty()) {
SpatialRuleLookup ruleLookup = SpatialRuleLookupBuilder.buildIndex(
Collections.singletonList(landmarkSplittingFeatureCollection), "area",
(id, polygons) -> new AbstractSpatialRule(polygons) {
@Override
public String getId() {
return id;
}
});
for (PrepareLandmarks prep : getLMPreparationHandler().getPreparations()) {
// the ruleLookup splits certain areas from each other but avoids making this a permanent change so that other algorithms still can route through these regions.
if (ruleLookup != null && !ruleLookup.getRules().isEmpty()) {
prep.setSpatialRuleLookup(ruleLookup);
}
}
}
for (LMProfile profile : lmPreparationHandler.getLMProfiles()) {
if (!getProfileVersion(profile.getProfile()).isEmpty()
&& !getProfileVersion(profile.getProfile()).equals("" + profilesByName.get(profile.getProfile()).getVersion()))
throw new IllegalArgumentException("LM preparation of " + profile.getProfile() + " already exists in storage and doesn't match configuration");
}
ensureWriteAccess();
ghStorage.freeze();
if (lmPreparationHandler.loadOrDoWork(ghStorage.getProperties(), closeEarly)) {
ghStorage.getProperties().put(Landmark.PREPARE + "done", true);
for (LMProfile profile : lmPreparationHandler.getLMProfiles()) {
// potentially overwrite existing keys from CH
setProfileVersion(profile.getProfile(), profilesByName.get(profile.getProfile()).getVersion());
}
}
}
/**
* Internal method to clean up the graph.
*/
protected void cleanUp() {
PrepareRoutingSubnetworks preparation = new PrepareRoutingSubnetworks(ghStorage, buildSubnetworkRemovalJobs());
preparation.setMinNetworkSize(minNetworkSize);
preparation.doWork();
logger.info("nodes: " + Helper.nf(ghStorage.getNodes()) + ", edges: " + Helper.nf(ghStorage.getEdges()));
}
private List buildSubnetworkRemovalJobs() {
List jobs = new ArrayList<>();
for (Profile profile : profilesByName.values()) {
// if turn costs are enabled use u-turn costs of zero as we only want to make sure the graph is fully connected assuming finite u-turn costs
Weighting weighting = createWeighting(profile, new PMap().putObject(Parameters.Routing.U_TURN_COSTS, 0));
jobs.add(new PrepareJob(encodingManager.getBooleanEncodedValue(Subnetwork.key(profile.getName())), weighting));
}
return jobs;
}
protected void flush() {
logger.info("flushing graph " + ghStorage.toString() + ", details:" + ghStorage.toDetailsString() + ", "
+ getMemInfo() + ")");
ghStorage.flush();
logger.info("flushed graph " + getMemInfo() + ")");
setFullyLoaded();
}
/**
* Releases all associated resources like memory or files. But it does not remove them. To
* remove the files created in graphhopperLocation you have to call clean().
*/
public void close() {
if (ghStorage != null)
ghStorage.close();
if (locationIndex != null)
locationIndex.close();
try {
lockFactory.forceRemove(fileLockName, true);
} catch (Exception ex) {
// silently fail e.g. on Windows where we cannot remove an unreleased native lock
}
}
/**
* Removes the on-disc routing files. Call only after calling close or before importOrLoad or
* load
*/
public void clean() {
if (getGraphHopperLocation().isEmpty())
throw new IllegalStateException("Cannot clean GraphHopper without specified graphHopperLocation");
File folder = new File(getGraphHopperLocation());
removeDir(folder);
}
protected void ensureNotLoaded() {
if (fullyLoaded)
throw new IllegalStateException("No configuration changes are possible after loading the graph");
}
protected void ensureWriteAccess() {
if (!allowWrites)
throw new IllegalStateException("Writes are not allowed!");
}
private void setFullyLoaded() {
fullyLoaded = true;
}
public boolean getFullyLoaded() {
return fullyLoaded;
}
public RouterConfig getRouterConfig() {
return routerConfig;
}
}