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
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -36,4 +36,5 @@ docs/.vitepress/cache
/.integration-scenarios/debian-12-jar-mvn/graphs_volume/
/.integration-scenarios/debian-12-jar-mvn/tmp/

gatling-results/**
gatling-results/**
.worktrees/
8 changes: 3 additions & 5 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -88,11 +88,9 @@ COPY --chown=ors:ors --chmod=750 --from=build /tmp/ors/ors-api/target/ors.jar /o
# Switch to non-root user
USER ors

# Run Java jar directly as PID 1
# Configuration via environment variables:
# - JDK_JAVA_OPTIONS: additional JVM options
# - Server settings via Spring properties (e.g., server.port, server.servlet.context-path)
# - Logging via Spring properties (logging.level.*, logging.pattern.*)
# Override to set JVM flags. Example: -XX:+UseParallelGC -XX:InitialRAMPercentage=90.0 -XX:MaxRAMPercentage=90.0
# Default is empty — G1GC and JVM ergonomics apply automatically on Java 21.
ENV JDK_JAVA_OPTIONS=""
ENTRYPOINT ["java", "-jar", "/ors.jar"]

FROM base AS publish
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -108,16 +108,18 @@ public void merge(LMProperties other) {
@JsonInclude(JsonInclude.Include.NON_NULL)
public static class CoreProperties extends LMProperties {
private String lmsets;
private Double maximumLmWeight;

@JsonIgnore
@Override
public boolean isEmpty() {
return super.isEmpty() && lmsets == null;
return super.isEmpty() && lmsets == null && maximumLmWeight == null;
}

public void merge(CoreProperties other) {
super.merge(other);
lmsets = ofNullable(this.lmsets).orElse(other.lmsets);
maximumLmWeight = ofNullable(this.maximumLmWeight).orElse(other.maximumLmWeight);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,17 @@ public static boolean prepareGeneratedGraphForUpload(ProfileProperties profilePr
LOGGER.error("No graph files found to archive at %s, though we found a graph_build_info file before.".formatted(graphFilesPath.toString()));
return false;
}
long totalRawBytes = 0;
int fileCount = 0;
for (File f : graphFiles) {
if (!Files.isDirectory(f.toPath())) {
totalRawBytes += f.length();
fileCount++;
}
}
double totalRawMB = totalRawBytes / (1024.0 * 1024.0);
LOGGER.info("[ORS-PACKAGING-DIAG] Starting archive of %d files (%.1f MB raw) to %s".formatted(fileCount, totalRawMB, graphArchiveDst));
long packStart = System.currentTimeMillis();
for (File file : graphFiles) {
if (!Files.isDirectory(file.toPath())) {
try (FileInputStream fis = new FileInputStream(file)) {
Expand All @@ -222,6 +233,12 @@ public static boolean prepareGeneratedGraphForUpload(ProfileProperties profilePr
}
}
}
long packEnd = System.currentTimeMillis();
double packElapsedS = (packEnd - packStart) / 1000.0;
double archiveMB = graphArchiveDst.toFile().length() / (1024.0 * 1024.0);
double throughputMBs = packElapsedS > 0 ? totalRawMB / packElapsedS : 0;
LOGGER.info("[ORS-PACKAGING-DIAG] Compressed %d files (%.1f MB) into %.1f MB in %.1fs (%.1f MB/s) using sequential ZIP.".formatted(
fileCount, totalRawMB, archiveMB, packElapsedS, throughputMBs));
LOGGER.info("Created archive %s".formatted(graphArchiveDst.toString()));
} catch (IOException e) {
LOGGER.error("Failed to create archive: %s".formatted(e.toString()));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
import com.graphhopper.storage.RoutingCHGraph;
import com.graphhopper.storage.index.LocationIndex;
import com.graphhopper.util.PMap;
import com.graphhopper.util.StopWatch;
import com.graphhopper.util.TranslationMap;
import com.graphhopper.util.details.PathDetailsBuilderFactory;
import org.geotools.feature.SchemaException;
Expand Down Expand Up @@ -121,6 +122,10 @@ public ORSGraphHopper() {
// used to initialize tests more easily without the need to create GraphProcessContext etc. when they're anyway not used in the tested functions.
}

private String getProfileName() {
return profileProperties != null ? profileProperties.getEncoderName().toString() : "unknown";
}

@Override
public GraphHopper init(GraphHopperConfig ghConfig) {
GraphHopper ret = super.init(ghConfig);
Expand Down Expand Up @@ -169,6 +174,8 @@ public GraphHopper importOrLoad() {
if (isFullyLoaded()) {
throw new IllegalStateException("graph is already successfully loaded");
}
LOGGER.info("[ORS-BUILD-STAGE] profile={} stage=import_or_load action=start location={}", getProfileName(), getGraphHopperLocation());
StopWatch swBuild = new StopWatch().start();

ORSGraphHopper gh = (ORSGraphHopper) super.importOrLoad();
AppInfo.setGraphDate(gh.getGraphHopperStorage().getProperties().get("datareader.import.date"));
Expand Down Expand Up @@ -209,6 +216,7 @@ public GraphHopper importOrLoad() {
}
}

LOGGER.info("[ORS-BUILD-STAGE] profile={} stage=import_or_load action=end took={}s nodes={} edges={}", getProfileName(), swBuild.stop().getSeconds(), gh.getGraphHopperStorage().getNodes(), gh.getGraphHopperStorage().getEdges());
return gh;
}

Expand Down Expand Up @@ -278,25 +286,31 @@ protected void adjustLMWeighting(Weighting weighting) {
*/
@Override
protected void postProcessing(boolean closeEarly) {
String profile = getProfileName();
LOGGER.info("[ORS-BUILD-STAGE] profile={} stage=post_processing action=start", profile);
super.postProcessing(closeEarly);

//Create the core
GraphHopperStorage gs = getGraphHopperStorage();
if (corePreparationHandler.isEnabled())
corePreparationHandler.setProcessContext(processContext).createPreparations(gs);
if (isCorePrepared()) {
LOGGER.info("[ORS-BUILD-STAGE] profile={} stage=core_preparation action=skip reason=already_prepared", profile);
// check loaded profiles
for (CHProfile profile : corePreparationHandler.getCHProfiles()) {
if (!getProfileVersion(profile.getProfile()).isEmpty() && !getProfileVersion(profile.getProfile()).equals("" + profilesByName.get(profile.getProfile()).getVersion()))
throw new IllegalArgumentException("Core preparation of " + profile.getProfile() + " already exists in storage and doesn't match configuration");
for (CHProfile chProfile : corePreparationHandler.getCHProfiles()) {
if (!getProfileVersion(chProfile.getProfile()).isEmpty() && !getProfileVersion(chProfile.getProfile()).equals("" + profilesByName.get(chProfile.getProfile()).getVersion()))
throw new IllegalArgumentException("Core preparation of " + chProfile.getProfile() + " already exists in storage and doesn't match configuration");
}
} else {
prepareCore(closeEarly);
}

//Create the landmarks in the core
if (coreLMPreparationHandler.isEnabled()) {
LOGGER.info("[ORS-BUILD-STAGE] profile={} stage=core_lm_preparation action=start lm_config_count={}", profile, coreLMPreparationHandler.getLMProfiles().size());
StopWatch swCoreLM = new StopWatch().start();
loadOrPrepareCoreLM();
LOGGER.info("[ORS-BUILD-STAGE] profile={} stage=core_lm_preparation action=end took={}s", profile, swCoreLM.stop().getSeconds());
}

if (fastIsochroneFactory.isEnabled()) {
Expand All @@ -308,26 +322,38 @@ protected void postProcessing(boolean closeEarly) {
}
fastIsochroneFactory.createPreparation(gs, partitioningEdgeFilter);

if (!isPartitionPrepared())
if (!isPartitionPrepared()) {
LOGGER.info("[ORS-BUILD-STAGE] profile={} stage=fast_isochrone_partition action=start", profile);
StopWatch swPartition = new StopWatch().start();
preparePartition();
else {
LOGGER.info("[ORS-BUILD-STAGE] profile={} stage=fast_isochrone_partition action=end took={}s", profile, swPartition.stop().getSeconds());
} else {
LOGGER.info("[ORS-BUILD-STAGE] profile={} stage=fast_isochrone_partition action=skip reason=already_prepared", profile);
fastIsochroneFactory.setExistingStorages();
fastIsochroneFactory.getCellStorage().loadExisting();
fastIsochroneFactory.getIsochroneNodeStorage().loadExisting();
}
//No fast isochrones without partition
if (isPartitionPrepared()) {
// Initialize edge filter sequence for fast isochrones
LOGGER.info("[ORS-BUILD-STAGE] profile={} stage=fast_isochrone_contours action=start", profile);
StopWatch swContours = new StopWatch().start();
calculateContours();
LOGGER.info("[ORS-BUILD-STAGE] profile={} stage=fast_isochrone_contours action=end took={}s", profile, swContours.stop().getSeconds());

List<Profile> profiles = fastIsochroneFactory.getFastIsochroneProfiles();
for (Profile profile : profiles) {
Weighting weighting = ((ORSWeightingFactory) createWeightingFactory()).createIsochroneWeighting(profile);
LOGGER.info("[ORS-BUILD-STAGE] profile={} stage=fast_isochrone_eccentricity action=start isochrone_profile_count={}", profile, profiles.size());
StopWatch swEcc = new StopWatch().start();
for (Profile isoProfile : profiles) {
Weighting weighting = ((ORSWeightingFactory) createWeightingFactory()).createIsochroneWeighting(isoProfile);
for (FlagEncoder encoder : super.getEncodingManager().fetchEdgeEncoders()) {
calculateCellProperties(weighting, partitioningEdgeFilter, encoder, fastIsochroneFactory.getIsochroneNodeStorage(), fastIsochroneFactory.getCellStorage());
}
}
LOGGER.info("[ORS-BUILD-STAGE] profile={} stage=fast_isochrone_eccentricity action=end took={}s", profile, swEcc.stop().getSeconds());
}
}

LOGGER.info("[ORS-BUILD-STAGE] profile={} stage=post_processing action=end nodes={} edges={}", profile, gs.getNodes(), gs.getEdges());
}

@Override
Expand Down Expand Up @@ -437,21 +463,24 @@ private List<LMConfig> createCoreLMConfigs(List<LMProfile> lmProfiles) {


protected void prepareCore(boolean closeEarly) {
for (CHProfile profile : corePreparationHandler.getCHProfiles()) {
if (!getProfileVersion(profile.getProfile()).isEmpty()
&& !getProfileVersion(profile.getProfile()).equals("" + profilesByName.get(profile.getProfile()).getVersion()))
throw new IllegalArgumentException("Core preparation of " + profile.getProfile() + " already exists in storage and doesn't match configuration");
for (CHProfile chProfile : corePreparationHandler.getCHProfiles()) {
if (!getProfileVersion(chProfile.getProfile()).isEmpty()
&& !getProfileVersion(chProfile.getProfile()).equals("" + profilesByName.get(chProfile.getProfile()).getVersion()))
throw new IllegalArgumentException("Core preparation of " + chProfile.getProfile() + " already exists in storage and doesn't match configuration");
}
if (isCoreEnabled()) {
LOGGER.info("[ORS-BUILD-STAGE] profile={} stage=core_preparation action=start core_profile_count={}", getProfileName(), corePreparationHandler.getCHProfiles().size());
StopWatch sw = new StopWatch().start();
ensureWriteAccess();
GraphHopperStorage ghStorage = getGraphHopperStorage();
ghStorage.freeze();
corePreparationHandler.prepare(ghStorage.getProperties(), closeEarly);
ghStorage.getProperties().put(ORSParameters.Core.PREPARE + "done", true);
for (CHProfile profile : corePreparationHandler.getCHProfiles()) {
for (CHProfile chProfile : corePreparationHandler.getCHProfiles()) {
// potentially overwrite existing keys from CH/LM
setProfileVersion(profile.getProfile(), profilesByName.get(profile.getProfile()).getVersion());
setProfileVersion(chProfile.getProfile(), profilesByName.get(chProfile.getProfile()).getVersion());
}
LOGGER.info("[ORS-BUILD-STAGE] profile={} stage=core_preparation action=end took={}s", getProfileName(), sw.stop().getSeconds());
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,16 @@
import org.heigit.ors.config.ElevationProperties;
import org.heigit.ors.config.EngineProperties;
import org.heigit.ors.config.profile.*;
import org.heigit.ors.routing.RoutingProfileCategory;
import org.heigit.ors.routing.RoutingProfileType;
import org.heigit.ors.routing.graphhopper.extensions.util.ORSParameters;
import org.heigit.ors.util.ProfileTools;
import org.heigit.ors.util.StringUtility;
import com.graphhopper.routing.ev.*;

import java.nio.file.Path;
import java.util.*;
import java.util.stream.Collectors;

import org.apache.log4j.Logger;

Expand All @@ -42,7 +45,7 @@ public static ORSGraphHopperConfig createGHSettings(ProfileProperties profile, E

setElevationProperties(buildProperties, engineConfig, ghConfig);

setGraphLevelEncodedValues(buildProperties, ghConfig);
setGraphLevelEncodedValues(profile, ghConfig);

boolean prepareCH = false;
boolean prepareLM = false;
Expand Down Expand Up @@ -148,7 +151,10 @@ public static ORSGraphHopperConfig createGHSettings(ProfileProperties profile, E
String profileName = ProfileTools.makeProfileName(vehicle, weighting, considerTurnRestrictions);
profiles.put(profileName, new Profile(profileName).setVehicle(vehicle).setWeighting(weighting).setTurnCosts(considerTurnRestrictions));
coreProfiles.add(new CHProfile(profileName));
coreLMProfiles.add(new LMProfile(profileName));
LMProfile lmProfile = new LMProfile(profileName);
if (coreOpts.getMaximumLmWeight() != null)
lmProfile.setMaximumLMWeight(coreOpts.getMaximumLmWeight());
coreLMProfiles.add(lmProfile);
}
ghConfig.setCoreProfiles(coreProfiles);
ghConfig.setCoreLMProfiles(coreLMProfiles);
Expand Down Expand Up @@ -257,10 +263,101 @@ private static void setElevationProperties(BuildProperties buildProperties, Engi
}
}

private static void setGraphLevelEncodedValues(BuildProperties buildProperties, ORSGraphHopperConfig ghConfig) {
/**
* Determines which EncodedValues (EVs) are registered on the GraphHopper graph
* for the given profile
* and sets the {@code graph.encoded_values} configuration key accordingly.
*
* <p>
* <b>Current behaviour (one-graph-per-profile architecture):</b><br>
* Since each routing profile owns its own dedicated graph, EVs that are
* meaningless for a given
* profile category waste intsForFlags bits, disk space, and permanent heap. To
* prevent leakage
* from shared {@code profile_default} config blocks, this method filters the EV
* string by
* {@link RoutingProfileCategory}:
* <ul>
* <li>Hiking/MTB EVs ({@code sac_scale}, {@code mtb_scale},
* {@code mtb_scale_uphill},
* {@code hill_index}) are blocked for {@code DRIVING} and {@code WHEELCHAIR}
* profiles.</li>
* <li>Transit/freight EVs ({@code max_height}, {@code hazmat_access},
* {@code hgv_access}, etc.)
* are blocked for {@code WALKING} and {@code WHEELCHAIR} profiles.</li>
* </ul>
*
* <p>
* <b>TODO FUTURE — Unified Graph Architecture:</b><br>
* If ORS transitions to a <em>unified graph</em> (one shared graph serving all
* profiles), this
* entire filter block MUST be reverted. A unified graph requires
* {@code graph.encoded_values} to
* be the exact <em>union</em> of all EVs needed by every profile. Stripping EVs
* here would cause
* profile categories that rely on those EVs to fail at routing time.
* <br>
* In the unified-graph model, cross-profile leakage prevention must be moved
* <em>downstream</em>
* to the individual {@code FlagEncoder} / {@code VehicleTagParser}
* implementations, which should
* declare and enforce the EVs they read/write via their own {@code supports()}
* or equivalent
* contract — leaving unused EV slots allocated but unpopulated for irrelevant
* profiles.
*
* @param profile the ORS profile properties carrying build config and profile
* type information
* @param ghConfig the GraphHopper config object being assembled for this
* profile's graph
*/
private static void setGraphLevelEncodedValues(ProfileProperties profile, ORSGraphHopperConfig ghConfig) {
BuildProperties buildProperties = profile.getBuild();
List<String> encodedValues = new ArrayList<>();

encodedValues.add(buildProperties.getEncodedValuesString());
String evs = buildProperties.getEncodedValuesString();

Integer[] profilesTypes = profile.getProfilesTypes();
int category = RoutingProfileCategory.UNKNOWN;
if (profilesTypes != null && profilesTypes.length > 0) {
category = RoutingProfileCategory.getFromRouteProfile(profilesTypes[0]);
}

final int finalCategory = category;

// TODO FUTURE: If ORS moves to a Unified Graph architecture (one graph for all
// profiles),
// this entire filter block MUST be removed. A unified graph requires the union
// of all EVs.
// instead of a cross-profile EV leakage prevention
if (evs != null && !evs.isEmpty()) {
evs = Arrays.stream(evs.split(","))
.map(String::trim)
.filter(ev -> !ev.isEmpty())
.filter(ev -> {
boolean isHikingMtbEV = ev.equals(SacScale.KEY) || ev.equals(MtbScale.KEY)
|| ev.equals(MtbScaleUphill.KEY) || ev.equals(HillIndex.KEY);
boolean isDrivingTransitEV = ev.equals(MaxAxleLoad.KEY) || ev.equals(MaxHeight.KEY)
|| ev.equals(MaxLength.KEY) ||
ev.equals(MaxWeight.KEY) || ev.equals(MaxWidth.KEY) || ev.equals(HazmatAccess.KEY) ||
ev.equals(HgvAccess.KEY) || ev.equals(GoodsAccess.KEY) || ev.equals(DeliveryAccess.KEY)
||
ev.equals(BusAccess.KEY) || ev.equals(AgriculturalAccess.KEY)
|| ev.equals(ForestryAccess.KEY);

boolean blockHikingEV = isHikingMtbEV && (finalCategory == RoutingProfileCategory.DRIVING
|| finalCategory == RoutingProfileCategory.WHEELCHAIR);
boolean blockTransitEV = isDrivingTransitEV && (finalCategory == RoutingProfileCategory.WALKING
|| finalCategory == RoutingProfileCategory.WHEELCHAIR);

return !blockHikingEV && !blockTransitEV;
})
.collect(Collectors.joining(","));
}

if (evs != null && !evs.isEmpty()) {
encodedValues.add(evs);
}

if (Boolean.TRUE.equals(buildProperties.getInterpolateBridgesAndTunnels()))
encodedValues.add(RoadEnvironment.KEY);
Expand Down
Loading
Loading