From e1935302ddfab45b4a1dd2ddf6c98068861da128 Mon Sep 17 00:00:00 2001 From: Julian Psotta Date: Sat, 30 May 2026 14:54:58 +0200 Subject: [PATCH 01/13] fix: update springdoc-openapi and swagger dependencies to latest versions This fixes the test suite errors: Neither 'findJsonValueMethod' nor 'findJsonValueAccessor' found in jackson BeanDescription. --- pom.xml | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/pom.xml b/pom.xml index 3b5a8749c2..4d18a16bd9 100644 --- a/pom.xml +++ b/pom.xml @@ -50,9 +50,9 @@ 1.18.34 2.0.13 2.25.4 - 2.8.6 - 2.2.27 - 2.1.24 + 2.8.17 + 2.2.50 + 2.1.43 2.2 32.1 2.18.6 @@ -253,6 +253,12 @@ org.springdoc springdoc-openapi-starter-webmvc-ui ${springdoc-openapi-starter.version} + + + org.springframework.boot + spring-boot-starter-logging + + From 6283f2e336bb0dd894f1f0a385dfb48a8694c55c Mon Sep 17 00:00:00 2001 From: Julian Psotta Date: Sat, 30 May 2026 15:06:35 +0200 Subject: [PATCH 02/13] fix: prevent cross-profile Encoded Values leakage for the non-unified graph EncodedValues such as sac_scale and mtb_scale were leaking into driving and wheelchair graphs and ransit/freight EVs (max_height, hgv_access, etc.) into walking graphs. BuildProperties.merge() uses an orElse strategy that inherits any 'true' flag set in profile_default. This adds a categorical filter in setGraphLevelEncodedValues() using RoutingProfileCategory and GraphHopper's own EncodedValue.KEY constants. Hiking/MTB EVs are now blocked for DRIVING and WHEELCHAIR. Transit EVs are blocked for WALKING and WHEELCHAIR; CYCLING passes all EVs through. A Javadoc TODO marks this filter block for removal if ORS ever adopts a unified graph architecture, where graph.encoded_values must be the full union of all profile EVs and leakage prevention moves to the individual FlagEncoder/VehicleTagParser layer. --- .../extensions/ORSGraphHopperConfig.java | 100 +++++++++++++++++- .../extensions/ORSGraphHopperConfigTest.java | 42 +++++++- 2 files changed, 138 insertions(+), 4 deletions(-) diff --git a/ors-engine/src/main/java/org/heigit/ors/routing/graphhopper/extensions/ORSGraphHopperConfig.java b/ors-engine/src/main/java/org/heigit/ors/routing/graphhopper/extensions/ORSGraphHopperConfig.java index c8e3681209..b1c543e675 100644 --- a/ors-engine/src/main/java/org/heigit/ors/routing/graphhopper/extensions/ORSGraphHopperConfig.java +++ b/ors-engine/src/main/java/org/heigit/ors/routing/graphhopper/extensions/ORSGraphHopperConfig.java @@ -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; @@ -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; @@ -257,10 +260,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. + * + *

+ * Current behaviour (one-graph-per-profile architecture):
+ * 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}: + *

    + *
  • 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.
  • + *
  • Transit/freight EVs ({@code max_height}, {@code hazmat_access}, + * {@code hgv_access}, etc.) + * are blocked for {@code WALKING} and {@code WHEELCHAIR} profiles.
  • + *
+ * + *

+ * TODO FUTURE — Unified Graph Architecture:
+ * If ORS transitions to a unified graph (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 union of all EVs needed by every profile. Stripping EVs + * here would cause + * profile categories that rely on those EVs to fail at routing time. + *
+ * In the unified-graph model, cross-profile leakage prevention must be moved + * downstream + * 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 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); diff --git a/ors-engine/src/test/java/org/heigit/ors/routing/graphhopper/extensions/ORSGraphHopperConfigTest.java b/ors-engine/src/test/java/org/heigit/ors/routing/graphhopper/extensions/ORSGraphHopperConfigTest.java index 01d61e807e..6916fb2a13 100644 --- a/ors-engine/src/test/java/org/heigit/ors/routing/graphhopper/extensions/ORSGraphHopperConfigTest.java +++ b/ors-engine/src/test/java/org/heigit/ors/routing/graphhopper/extensions/ORSGraphHopperConfigTest.java @@ -8,6 +8,7 @@ import org.junit.jupiter.params.provider.CsvSource; import static org.junit.jupiter.api.Assertions.assertEquals; import java.nio.file.Path; +import java.util.Arrays; class ORSGraphHopperConfigTest { @@ -22,7 +23,9 @@ void createGHSettings(Boolean setElevation, String cachePath, String expectedPro EngineProperties engineConfig = new EngineProperties(); engineConfig.setGraphsDataAccess(DataAccessEnum.MMAP); engineConfig.getElevation().setCachePath(Path.of(cachePath)); - engineConfig.getElevation().setProvider(expectedProvider); + if (cachePath != null) { + engineConfig.getElevation().setProvider(expectedProvider); + } engineConfig.getElevation().setDataAccess(DataAccessEnum.MMAP); String graphLocation = ""; @@ -31,4 +34,41 @@ void createGHSettings(Boolean setElevation, String cachePath, String expectedPro assertEquals(setElevation, config.toString().contains("graph.elevation.provider")); } + + @ParameterizedTest + @CsvSource({ + "DRIVING_CAR, sac_scale, false", + "DRIVING_CAR, mtb_scale, false", + "DRIVING_CAR, hill_index, false", + "DRIVING_CAR, max_height, true", + "DRIVING_CAR, road_environment, true", + "FOOT_HIKING, sac_scale, true", + "FOOT_HIKING, max_height, false", + "FOOT_HIKING, max_width, false", + "CYCLING_MOUNTAIN, sac_scale, true", + "CYCLING_MOUNTAIN, max_height, true", + "WHEELCHAIR, sac_scale, false", + "WHEELCHAIR, mtb_scale, false", + "WHEELCHAIR, max_height, false" + }) + void testEncodedValuesLeakagePrevention(EncoderNameEnum encoderName, String ev, boolean shouldContain) { + ProfileProperties profile = new ProfileProperties(); + profile.setEncoderName(encoderName); + profile.getBuild().getEncodedValues().setSacScale(true); + profile.getBuild().getEncodedValues().setMtbScale(true); + profile.getBuild().getEncodedValues().setHillIndex(true); + profile.getBuild().getEncodedValues().setMaxHeight(true); + profile.getBuild().getEncodedValues().setMaxWidth(true); + profile.getBuild().getEncodedValues().setRoadEnvironment(true); + profile.setProfileName(encoderName.toString()); + + EngineProperties engineConfig = new EngineProperties(); + engineConfig.setGraphsDataAccess(DataAccessEnum.MMAP); + engineConfig.getElevation().setCachePath(Path.of("")); + + ORSGraphHopperConfig config = ORSGraphHopperConfig.createGHSettings(profile, engineConfig, ""); + + String encodedValues = config.getString("graph.encoded_values", ""); + assertEquals(shouldContain, Arrays.asList(encodedValues.split(",")).contains(ev)); + } } From 8a3e6b73d8ac954175f1fbbbdfa3379d0ef57b0a Mon Sep 17 00:00:00 2001 From: Julian Psotta Date: Sun, 31 May 2026 23:22:29 +0200 Subject: [PATCH 03/13] fix: enhance barrier node handling and logging in ORSOSMReader and CoreLandmarkStorage --- .../graphhopper/extensions/ORSOSMReader.java | 37 +++++++++++++++++++ .../extensions/core/CoreLandmarkStorage.java | 8 ++++ .../extensions/core/PrepareCore.java | 4 +- 3 files changed, 48 insertions(+), 1 deletion(-) diff --git a/ors-engine/src/main/java/org/heigit/ors/routing/graphhopper/extensions/ORSOSMReader.java b/ors-engine/src/main/java/org/heigit/ors/routing/graphhopper/extensions/ORSOSMReader.java index 9544dbfb1a..bb2b657df2 100644 --- a/ors-engine/src/main/java/org/heigit/ors/routing/graphhopper/extensions/ORSOSMReader.java +++ b/ors-engine/src/main/java/org/heigit/ors/routing/graphhopper/extensions/ORSOSMReader.java @@ -42,6 +42,7 @@ import java.io.IOException; import java.io.InvalidObjectException; import java.util.*; +import java.util.concurrent.atomic.AtomicInteger; import static com.graphhopper.reader.osm.OSMNodeData.isPillarNode; import static com.graphhopper.reader.osm.OSMNodeData.isTowerNode; @@ -67,6 +68,11 @@ public class ORSOSMReader extends OSMReader { private final Set nodeTagsToStore; private final GHLongObjectHashMap> osmNodeTagValues; + private final AtomicInteger barrierNodesTotal = new AtomicInteger( + 0); + private final AtomicInteger barrierNodesSkipped = new AtomicInteger( + 0); + public ORSOSMReader(GraphHopperStorage storage, OSMReaderConfig osmReaderConfig, GraphProcessContext procCntx) { super(storage, osmReaderConfig); @@ -430,7 +436,38 @@ private void calculateHillIndex(PointList pointList, ReaderWay way) { @Override public void readGraph() throws IOException { super.readGraph(); + LOGGER.info(String.format( + "[ORS-READER-DIAG] profile=%s barrier_nodes_total=%d barrier_edges_created=%d barrier_edges_skipped_passable=%d", + ghStorage.getBaseGraph().toString(), + barrierNodesTotal.get(), + barrierNodesTotal.get() - barrierNodesSkipped.get(), + barrierNodesSkipped.get())); procCntx.finish(); } + @Override + protected boolean isBarrierNode(ReaderNode node) { + if (!super.isBarrierNode(node)) + return false; + + barrierNodesTotal.incrementAndGet(); + + for (FlagEncoder encoder : encodingManager.fetchEdgeEncoders()) { + if (encoder instanceof AbstractFlagEncoder abstractEncoder + && !abstractEncoder.isBarrier(node)) { + barrierNodesSkipped.incrementAndGet(); + return false; // at least one encoder can pass → no topology split + } + } + return true; // all encoders blocked → split is correct + } + + public int getBarrierNodesTotal() { + return barrierNodesTotal.get(); + } + + public int getBarrierNodesSkipped() { + return barrierNodesSkipped.get(); + } + } diff --git a/ors-engine/src/main/java/org/heigit/ors/routing/graphhopper/extensions/core/CoreLandmarkStorage.java b/ors-engine/src/main/java/org/heigit/ors/routing/graphhopper/extensions/core/CoreLandmarkStorage.java index 00d3d3dc7f..b17ddc40cb 100644 --- a/ors-engine/src/main/java/org/heigit/ors/routing/graphhopper/extensions/core/CoreLandmarkStorage.java +++ b/ors-engine/src/main/java/org/heigit/ors/routing/graphhopper/extensions/core/CoreLandmarkStorage.java @@ -183,6 +183,13 @@ public void createLandmarks() { setMaximumWeight(maxWeight); additionalInfo = ", maxWeight:" + maxWeight + " from quick estimation"; } + + if (getFactor() <= 0) { + logger.warn(String.format("[ORS-LM-WARN] %s No component reached minimumNodes=%d in Core graph of %d nodes. Skipping landmark preparation — fallback to BeelineApproximator.", + configName(), minimumNodes, core.getCoreNodes())); + return; + } + double factor = getFactor(); if (logDetails) @@ -191,6 +198,7 @@ public void createLandmarks() { int nodes = 0; for (IntArrayList subnetworkIds : graphComponents) { nodes += subnetworkIds.size(); + logger.info(String.format("[ORS-LM-DIAG] %s eval subnetwork size=%d minimumNodes=%d", configName(), subnetworkIds.size(), minimumNodes)); if (subnetworkIds.size() < minimumNodes) continue; if (factor <= 0) diff --git a/ors-engine/src/main/java/org/heigit/ors/routing/graphhopper/extensions/core/PrepareCore.java b/ors-engine/src/main/java/org/heigit/ors/routing/graphhopper/extensions/core/PrepareCore.java index 62a531d2db..526e6ef063 100644 --- a/ors-engine/src/main/java/org/heigit/ors/routing/graphhopper/extensions/core/PrepareCore.java +++ b/ors-engine/src/main/java/org/heigit/ors/routing/graphhopper/extensions/core/PrepareCore.java @@ -175,7 +175,9 @@ protected IntContainer contractNode(int node, int level) { @Override public void finishContractionHook() { - chStore.setCoreNodes(sortedNodes.size() + restrictedNodesCount); + int coreNodes = sortedNodes.size() + restrictedNodesCount; + chStore.setCoreNodes(coreNodes); + logger.info("[ORS-CORE-DIAG] Core contraction finished. core_node_count={}", coreNodes); // insert shortcuts connected to core nodes CoreNodeContractor coreNodeContractor = (CoreNodeContractor) nodeContractor; From 0f31af1938a0281ce379c54244cdb1dfef19a9e5 Mon Sep 17 00:00:00 2001 From: Julian Psotta Date: Sun, 31 May 2026 23:22:51 +0200 Subject: [PATCH 04/13] fix: update ORSGraphHopperTest expected node count and add ORSOSMReaderBarrierTest for barrier node handling --- .../extensions/ORSGraphHopperTest.java | 2 +- .../extensions/ORSOSMReaderBarrierTest.java | 157 ++++++++++++++++++ .../corelm/CoreLandmarkStorageTest.java | 28 ++++ 3 files changed, 186 insertions(+), 1 deletion(-) create mode 100644 ors-engine/src/test/java/org/heigit/ors/routing/graphhopper/extensions/ORSOSMReaderBarrierTest.java diff --git a/ors-engine/src/test/java/org/heigit/ors/routing/graphhopper/extensions/ORSGraphHopperTest.java b/ors-engine/src/test/java/org/heigit/ors/routing/graphhopper/extensions/ORSGraphHopperTest.java index 9b2efba42a..0de8b9a092 100644 --- a/ors-engine/src/test/java/org/heigit/ors/routing/graphhopper/extensions/ORSGraphHopperTest.java +++ b/ors-engine/src/test/java/org/heigit/ors/routing/graphhopper/extensions/ORSGraphHopperTest.java @@ -55,7 +55,7 @@ void buildGraphWithPreprocessedData() throws Exception { gh.importOrLoad(); ORSGraphHopperStorage storage = (ORSGraphHopperStorage) gh.getGraphHopperStorage(); - assertEquals(440, storage.getNodes()); + assertEquals(419, storage.getNodes()); } private static EngineProperties createEngineProperties(Path localGraphsRootPath, diff --git a/ors-engine/src/test/java/org/heigit/ors/routing/graphhopper/extensions/ORSOSMReaderBarrierTest.java b/ors-engine/src/test/java/org/heigit/ors/routing/graphhopper/extensions/ORSOSMReaderBarrierTest.java new file mode 100644 index 0000000000..0b454cae8d --- /dev/null +++ b/ors-engine/src/test/java/org/heigit/ors/routing/graphhopper/extensions/ORSOSMReaderBarrierTest.java @@ -0,0 +1,157 @@ +/* This file is part of Openrouteservice. + * + * Openrouteservice is free software; you can redistribute it and/or modify it under the terms of the + * GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 + * of the License, or (at your option) any later version. + + * This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Lesser General Public License for more details. + + * You should have received a copy of the GNU Lesser General Public License along with this library; + * if not, see . + */ +package org.heigit.ors.routing.graphhopper.extensions; + +import com.graphhopper.reader.ReaderNode; +import com.graphhopper.routing.OSMReaderConfig; +import com.graphhopper.routing.util.CarFlagEncoder; +import com.graphhopper.routing.util.EncodingManager; +import com.graphhopper.storage.GraphBuilder; +import com.graphhopper.storage.GraphHopperStorage; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.mockito.Mockito; + +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * Unit tests for {@link ORSOSMReader#isBarrierNode(ReaderNode)}. + * + * Verifies that the barrier-edge fix restores pre-GH-4.13 semantics: + * a barrier edge is only created when ALL registered encoders block the node. + * If ANY encoder can pass it, no artificial barrier edge is created. + */ +class ORSOSMReaderBarrierTest { + + private ORSOSMReader reader; + + @BeforeEach + void setUp() { + CarFlagEncoder carEncoder = new CarFlagEncoder(); + EncodingManager encodingManager = new EncodingManager.Builder().add(carEncoder).build(); + GraphHopperStorage storage = new GraphBuilder(encodingManager).create(); + + GraphProcessContext mockCtx = Mockito.mock(GraphProcessContext.class); + Mockito.when(mockCtx.getStorageBuilders()).thenReturn(Collections.emptyList()); + Mockito.when(mockCtx.isUseSidewalks()).thenReturn(false); + + reader = new ORSOSMReader(storage, new OSMReaderConfig(), mockCtx); + } + + private static ReaderNode makeNode(long id, Map tags) { + return new ReaderNode(id, 0.0, 0.0, tags); + } + + // --------------------------------------------------------------------------- + // Parameterized: isBarrierNode return value per tag combination + // + // Covers: blockByDefault barriers, passByDefault barriers, access overrides, + // and nodes with no barrier tag at all. + // --------------------------------------------------------------------------- + + static Stream barrierNodeExpectations() { + return Stream.of( + // blocked by default (blockByDefaultBarriers list in CarFlagEncoder) + Arguments.of("bollard - always blocked", Map.of("barrier", "bollard"), true), + Arguments.of("fence - always blocked", Map.of("barrier", "fence"), true), + Arguments.of("stile - always blocked", Map.of("barrier", "stile"), true), + Arguments.of("cycle_barrier - always blocked", Map.of("barrier", "cycle_barrier"), true), + // blocked by default but access=yes overrides + Arguments.of("bollard + access=yes - explicitly pass", Map.of("barrier", "bollard", "access", "yes"), false), + // passable by default (passByDefaultBarriers list in CarFlagEncoder) + Arguments.of("gate - passes by default", Map.of("barrier", "gate"), false), + Arguments.of("lift_gate - passes by default", Map.of("barrier", "lift_gate"), false), + Arguments.of("cattle_grid - passes by default", Map.of("barrier", "cattle_grid"), false), + // passable by default but access=no overrides + Arguments.of("gate + access=yes - explicitly pass", Map.of("barrier", "gate", "access", "yes"), false), + Arguments.of("gate + access=no - explicitly blocked", Map.of("barrier", "gate", "access", "no"), true), + // no barrier tag - base class returns false, counters must not increment + Arguments.of("highway=crossing - not a barrier", Map.of("highway", "crossing"), false), + Arguments.of("name tag only - not a barrier", Map.of("name", "Main Street"), false) + ); + } + + @ParameterizedTest(name = "{0}") + @MethodSource("barrierNodeExpectations") + void isBarrierNode_matchesExpectedResult(String description, Map tags, boolean expected) { + assertEquals(expected, reader.isBarrierNode(makeNode(1L, tags)), + "isBarrierNode mismatch for: " + description); + } + + // --------------------------------------------------------------------------- + // Parameterized: counter state after processing a sequence of nodes + // + // Each scenario feeds a list of tag maps through isBarrierNode in order and + // asserts the resulting total / skipped counts. + // --------------------------------------------------------------------------- + + static Stream counterScenarios() { + return Stream.of( + Arguments.of( + "1 blocked + 1 passable → total=2 skipped=1", + List.of( + Map.of("barrier", "bollard"), // blocked + Map.of("barrier", "gate", "access", "yes") // passable + ), + 2, 1 + ), + Arguments.of( + "all blocked → total=3 skipped=0", + List.of( + Map.of("barrier", "bollard"), + Map.of("barrier", "fence"), + Map.of("barrier", "gate", "access", "no") + ), + 3, 0 + ), + Arguments.of( + "non-barrier nodes only → total=0 skipped=0", + List.of( + Map.of("highway", "crossing"), + Map.of("name", "Main Street") + ), + 0, 0 + ), + Arguments.of( + "all passable → total=3 skipped=3", + List.of( + Map.of("barrier", "gate"), + Map.of("barrier", "lift_gate"), + Map.of("barrier", "bollard", "access", "yes") + ), + 3, 3 + ) + ); + } + + @ParameterizedTest(name = "{0}") + @MethodSource("counterScenarios") + void counters_reflectBarrierDecisions(String description, + List> nodeTags, + int expectedTotal, + int expectedSkipped) { + for (int i = 0; i < nodeTags.size(); i++) { + reader.isBarrierNode(makeNode(i, nodeTags.get(i))); + } + assertEquals(expectedTotal, reader.getBarrierNodesTotal(), description + ": total mismatch"); + assertEquals(expectedSkipped, reader.getBarrierNodesSkipped(), description + ": skipped mismatch"); + } +} diff --git a/ors-engine/src/test/java/org/heigit/ors/routing/graphhopper/extensions/corelm/CoreLandmarkStorageTest.java b/ors-engine/src/test/java/org/heigit/ors/routing/graphhopper/extensions/corelm/CoreLandmarkStorageTest.java index 7f90655e9b..37edf1f646 100644 --- a/ors-engine/src/test/java/org/heigit/ors/routing/graphhopper/extensions/corelm/CoreLandmarkStorageTest.java +++ b/ors-engine/src/test/java/org/heigit/ors/routing/graphhopper/extensions/corelm/CoreLandmarkStorageTest.java @@ -36,6 +36,7 @@ import java.util.Arrays; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertEquals; /** @@ -166,6 +167,33 @@ void testOneSubnetwork() { assertEquals("[6, 2]", Arrays.toString(storage.getLandmarks(1))); } + @Test + void testAllComponentsBelowMinimumNodes_noExceptionThrown() { + // Regression test for the ORS-LM-WARN crash scenario: + // If ALL core graph components fall below minimumNodes (e.g. island partitions + // after + // aggressive Core contraction, or barrier micro-subgraphs), createLandmarks() + // must complete with a warning rather than throwing IllegalStateException. + CoreTestEdgeFilter restrictedEdges = new CoreTestEdgeFilter(); + for (int i = 0; i <= 12; i++) + restrictedEdges.add(i); + + createMediumGraph(); + contractGraph(restrictedEdges); + + CoreLMConfig coreLMConfig = new CoreLMConfig(encoder.toString(), weighting) + .setEdgeFilter(new LMEdgeFilterSequence()); + CoreLandmarkStorage storage = new CoreLandmarkStorage(dir, graph, routingCHGraph, coreLMConfig, 2); + // Setting minimumNodes much higher than the core graph node count simulates an + // island/ + // partition scenario where no component qualifies — factor stays <= 0 without + // the guard. + storage.setMinimumNodes(10000); + + assertDoesNotThrow(storage::createLandmarks, + "createLandmarks() must not throw when all components are below minimumNodes"); + } + @Test void testTwoSubnetworks() { // All edges in medium graph are part of core. Test if landmarks are built From 1980d9c1384e7732ad5a57c51feef4ef84c661cb Mon Sep 17 00:00:00 2001 From: Julian Psotta Date: Mon, 1 Jun 2026 19:41:26 +0200 Subject: [PATCH 05/13] fix: reduce logging for subnetwork processing in CoreLandmarkStorage --- .../extensions/core/CoreLandmarkStorage.java | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/ors-engine/src/main/java/org/heigit/ors/routing/graphhopper/extensions/core/CoreLandmarkStorage.java b/ors-engine/src/main/java/org/heigit/ors/routing/graphhopper/extensions/core/CoreLandmarkStorage.java index b17ddc40cb..7dfa963dc1 100644 --- a/ors-engine/src/main/java/org/heigit/ors/routing/graphhopper/extensions/core/CoreLandmarkStorage.java +++ b/ors-engine/src/main/java/org/heigit/ors/routing/graphhopper/extensions/core/CoreLandmarkStorage.java @@ -196,11 +196,16 @@ public void createLandmarks() { logger.debug(configName() + "init landmarks for subnetworks with node count greater than " + minimumNodes + " with factor:" + factor + additionalInfo); int nodes = 0; + int skippedSubnetworks = 0; + int processedSubnetworks = 0; for (IntArrayList subnetworkIds : graphComponents) { nodes += subnetworkIds.size(); - logger.info(String.format("[ORS-LM-DIAG] %s eval subnetwork size=%d minimumNodes=%d", configName(), subnetworkIds.size(), minimumNodes)); - if (subnetworkIds.size() < minimumNodes) + if (subnetworkIds.size() < minimumNodes) { + skippedSubnetworks++; continue; + } + processedSubnetworks++; + logger.debug(String.format("[ORS-LM-DIAG] %s processing subnetwork size=%d minimumNodes=%d", configName(), subnetworkIds.size(), minimumNodes)); if (factor <= 0) throw new IllegalStateException("factor wasn't initialized " + factor + ", subnetworks:" + graphComponents.size() + ", minimumNodes:" + minimumNodes + ", current size:" + subnetworkIds.size()); @@ -223,6 +228,9 @@ public void createLandmarks() { logger.warn("next start node not found in big enough network of size " + subnetworkIds.size() + ", first element is " + subnetworkIds.get(0) + ", " + createPoint(graph, subnetworkIds.get(0))); } + logger.info(String.format("[ORS-LM-DIAG] %s subnetwork summary: processed=%d skipped(size Date: Mon, 1 Jun 2026 21:15:51 +0200 Subject: [PATCH 06/13] fix: update Dockerfile to set default JVM options for Java application --- Dockerfile | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/Dockerfile b/Dockerfile index c43b059414..f24a081090 100644 --- a/Dockerfile +++ b/Dockerfile @@ -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 From e8c365d48c5320b02ce2865c2abc6686500d38a1 Mon Sep 17 00:00:00 2001 From: Julian Psotta Date: Tue, 2 Jun 2026 15:51:55 +0200 Subject: [PATCH 07/13] fix: enhance logging for build stages in ORSGraphHopper, ORSOSMReader, and CoreLMPreparationHandler --- .../extensions/ORSGraphHopper.java | 57 ++++++++++++++----- .../graphhopper/extensions/ORSOSMReader.java | 6 ++ .../core/CoreLMPreparationHandler.java | 17 ++++++ 3 files changed, 66 insertions(+), 14 deletions(-) diff --git a/ors-engine/src/main/java/org/heigit/ors/routing/graphhopper/extensions/ORSGraphHopper.java b/ors-engine/src/main/java/org/heigit/ors/routing/graphhopper/extensions/ORSGraphHopper.java index 797698b4e8..0f1a20317c 100644 --- a/ors-engine/src/main/java/org/heigit/ors/routing/graphhopper/extensions/ORSGraphHopper.java +++ b/ors-engine/src/main/java/org/heigit/ors/routing/graphhopper/extensions/ORSGraphHopper.java @@ -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; @@ -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); @@ -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")); @@ -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; } @@ -278,6 +286,8 @@ 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 @@ -285,10 +295,11 @@ protected void postProcessing(boolean closeEarly) { 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); @@ -296,7 +307,10 @@ protected void postProcessing(boolean 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()) { @@ -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 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 @@ -437,21 +463,24 @@ private List createCoreLMConfigs(List 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()); } } diff --git a/ors-engine/src/main/java/org/heigit/ors/routing/graphhopper/extensions/ORSOSMReader.java b/ors-engine/src/main/java/org/heigit/ors/routing/graphhopper/extensions/ORSOSMReader.java index bb2b657df2..0316715e80 100644 --- a/ors-engine/src/main/java/org/heigit/ors/routing/graphhopper/extensions/ORSOSMReader.java +++ b/ors-engine/src/main/java/org/heigit/ors/routing/graphhopper/extensions/ORSOSMReader.java @@ -442,7 +442,13 @@ public void readGraph() throws IOException { barrierNodesTotal.get(), barrierNodesTotal.get() - barrierNodesSkipped.get(), barrierNodesSkipped.get())); + int builderCount = procCntx.getStorageBuilders() != null ? procCntx.getStorageBuilders().size() : 0; + LOGGER.info(String.format("[ORS-READER-STAGE] profile=%s stage=storage_builder_finish action=start builder_count=%d", + ghStorage.getBaseGraph().toString(), builderCount)); + long t0 = System.currentTimeMillis(); procCntx.finish(); + LOGGER.info(String.format("[ORS-READER-STAGE] profile=%s stage=storage_builder_finish action=end took=%ds", + ghStorage.getBaseGraph().toString(), (System.currentTimeMillis() - t0) / 1000)); } @Override diff --git a/ors-engine/src/main/java/org/heigit/ors/routing/graphhopper/extensions/core/CoreLMPreparationHandler.java b/ors-engine/src/main/java/org/heigit/ors/routing/graphhopper/extensions/core/CoreLMPreparationHandler.java index db36a14f62..9687a9981f 100644 --- a/ors-engine/src/main/java/org/heigit/ors/routing/graphhopper/extensions/core/CoreLMPreparationHandler.java +++ b/ors-engine/src/main/java/org/heigit/ors/routing/graphhopper/extensions/core/CoreLMPreparationHandler.java @@ -19,10 +19,16 @@ import com.graphhopper.routing.lm.PrepareLandmarks; import com.graphhopper.storage.GraphHopperStorage; import com.graphhopper.storage.RoutingCHGraph; +import com.graphhopper.storage.index.LocationIndex; +import com.graphhopper.util.StopWatch; import org.heigit.ors.routing.graphhopper.extensions.ORSGraphHopperConfig; import org.heigit.ors.routing.graphhopper.extensions.ORSGraphHopperStorage; import org.heigit.ors.routing.graphhopper.extensions.util.GraphUtils; import org.heigit.ors.routing.graphhopper.extensions.util.ORSParameters.CoreLandmark; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import static com.graphhopper.util.Helper.getMemInfo; import java.util.Arrays; import java.util.HashMap; @@ -38,6 +44,8 @@ * @author Andrzej Oles */ public class CoreLMPreparationHandler extends LMPreparationHandler { + private static final Logger LOGGER = LoggerFactory.getLogger(CoreLMPreparationHandler.class); + private final CoreLMOptions coreLMOptions = new CoreLMOptions(); public CoreLMPreparationHandler() { @@ -109,4 +117,13 @@ public CoreLMOptions getCoreLMOptions() { return coreLMOptions; } + @Override + public boolean loadOrDoWork(List lmConfigs, GraphHopperStorage ghStorage, LocationIndex locationIndex, boolean closeEarly) { + LOGGER.info("[ORS-CORE-LM-STAGE] action=start lm_config_count={} {}", lmConfigs.size(), getMemInfo()); + StopWatch sw = new StopWatch().start(); + boolean result = super.loadOrDoWork(lmConfigs, ghStorage, locationIndex, closeEarly); + LOGGER.info("[ORS-CORE-LM-STAGE] action=end took={}s {}", sw.stop().getSeconds(), getMemInfo()); + return result; + } + } From 23920d7cb02aca09ea545b38f310c41745fc9bab Mon Sep 17 00:00:00 2001 From: Julian Psotta Date: Wed, 3 Jun 2026 14:02:26 +0200 Subject: [PATCH 08/13] fix: add .worktrees/ to .gitignore to prevent tracking of worktree files --- .gitignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index ee35c479c1..e1bf30460d 100644 --- a/.gitignore +++ b/.gitignore @@ -36,4 +36,5 @@ docs/.vitepress/cache /.integration-scenarios/debian-12-jar-mvn/graphs_volume/ /.integration-scenarios/debian-12-jar-mvn/tmp/ -gatling-results/** \ No newline at end of file +gatling-results/** +.worktrees/ From 7a1bd590bf5c4c79287f6274888acd2520f675d5 Mon Sep 17 00:00:00 2001 From: Julian Psotta Date: Wed, 3 Jun 2026 19:39:44 +0200 Subject: [PATCH 09/13] fix: update barrier node handling in ORSOSMReader and enhance tests for multi-encoder scenarios --- .../graphhopper/extensions/ORSOSMReader.java | 15 ++-- .../extensions/ORSOSMReaderBarrierTest.java | 72 +++++++++++++++++-- 2 files changed, 79 insertions(+), 8 deletions(-) diff --git a/ors-engine/src/main/java/org/heigit/ors/routing/graphhopper/extensions/ORSOSMReader.java b/ors-engine/src/main/java/org/heigit/ors/routing/graphhopper/extensions/ORSOSMReader.java index 0316715e80..6d8fa6284b 100644 --- a/ors-engine/src/main/java/org/heigit/ors/routing/graphhopper/extensions/ORSOSMReader.java +++ b/ors-engine/src/main/java/org/heigit/ors/routing/graphhopper/extensions/ORSOSMReader.java @@ -458,14 +458,21 @@ protected boolean isBarrierNode(ReaderNode node) { barrierNodesTotal.incrementAndGet(); + // Split the topology if ANY encoder is blocked by this barrier. The barrier edge then + // carries per-encoder access (EncodingManager.handleNodeTags blocks only the encoders whose + // isBarrier() is true), so the encoders that can pass simply traverse a zero-length no-op + // edge. Skipping the split only when EVERY encoder can pass drops just the fully-passable + // no-op edges. The inverse ("skip if any can pass") would be a correctness bug on any + // multi-encoder graph — it would leak a blocked profile through. For ORS's single-encoder + // graphs the two are equivalent, but this formulation stays correct if that ever changes. for (FlagEncoder encoder : encodingManager.fetchEdgeEncoders()) { if (encoder instanceof AbstractFlagEncoder abstractEncoder - && !abstractEncoder.isBarrier(node)) { - barrierNodesSkipped.incrementAndGet(); - return false; // at least one encoder can pass → no topology split + && abstractEncoder.isBarrier(node)) { + return true; // at least one encoder is blocked → topology split is required } } - return true; // all encoders blocked → split is correct + barrierNodesSkipped.incrementAndGet(); + return false; // passable for every encoder → no-op barrier edge, safe to skip } public int getBarrierNodesTotal() { diff --git a/ors-engine/src/test/java/org/heigit/ors/routing/graphhopper/extensions/ORSOSMReaderBarrierTest.java b/ors-engine/src/test/java/org/heigit/ors/routing/graphhopper/extensions/ORSOSMReaderBarrierTest.java index 0b454cae8d..71e878e845 100644 --- a/ors-engine/src/test/java/org/heigit/ors/routing/graphhopper/extensions/ORSOSMReaderBarrierTest.java +++ b/ors-engine/src/test/java/org/heigit/ors/routing/graphhopper/extensions/ORSOSMReaderBarrierTest.java @@ -17,6 +17,7 @@ import com.graphhopper.routing.OSMReaderConfig; import com.graphhopper.routing.util.CarFlagEncoder; import com.graphhopper.routing.util.EncodingManager; +import com.graphhopper.routing.util.FootFlagEncoder; import com.graphhopper.storage.GraphBuilder; import com.graphhopper.storage.GraphHopperStorage; import org.junit.jupiter.api.BeforeEach; @@ -35,9 +36,14 @@ /** * Unit tests for {@link ORSOSMReader#isBarrierNode(ReaderNode)}. * - * Verifies that the barrier-edge fix restores pre-GH-4.13 semantics: - * a barrier edge is only created when ALL registered encoders block the node. - * If ANY encoder can pass it, no artificial barrier edge is created. + * Verifies the barrier-edge split policy: a topology split (and thus a barrier edge) is created + * when ANY registered encoder is blocked by the node. The split is skipped only when EVERY encoder + * can pass — those would be zero-length no-op edges. The barrier edge itself carries per-encoder + * access via {@link EncodingManager#handleNodeTags}, so encoders that can pass simply traverse it. + * + * The single-encoder cases below exercise the common ORS model (one flag encoder per graph). The + * {@code multiEncoder_*} cases guard the multi-encoder branch: a selective barrier (blocks one + * profile, passes another) MUST still split, otherwise the blocked profile would leak through. */ class ORSOSMReaderBarrierTest { @@ -47,13 +53,18 @@ class ORSOSMReaderBarrierTest { void setUp() { CarFlagEncoder carEncoder = new CarFlagEncoder(); EncodingManager encodingManager = new EncodingManager.Builder().add(carEncoder).build(); + reader = newReader(encodingManager); + } + + /** Builds an ORSOSMReader over a graph using the given encoding manager. */ + private static ORSOSMReader newReader(EncodingManager encodingManager) { GraphHopperStorage storage = new GraphBuilder(encodingManager).create(); GraphProcessContext mockCtx = Mockito.mock(GraphProcessContext.class); Mockito.when(mockCtx.getStorageBuilders()).thenReturn(Collections.emptyList()); Mockito.when(mockCtx.isUseSidewalks()).thenReturn(false); - reader = new ORSOSMReader(storage, new OSMReaderConfig(), mockCtx); + return new ORSOSMReader(storage, new OSMReaderConfig(), mockCtx); } private static ReaderNode makeNode(long id, Map tags) { @@ -154,4 +165,57 @@ void counters_reflectBarrierDecisions(String description, assertEquals(expectedTotal, reader.getBarrierNodesTotal(), description + ": total mismatch"); assertEquals(expectedSkipped, reader.getBarrierNodesSkipped(), description + ": skipped mismatch"); } + + // --------------------------------------------------------------------------- + // Multi-encoder branch: a graph with both car and foot encoders. + // + // Car blocks bollard/fence/stile by default; foot passes a bollard but blocks a fence. + // A selective barrier (bollard) MUST still split because car is blocked — otherwise the car + // restriction would be silently dropped. The split is skipped only when BOTH encoders pass. + // --------------------------------------------------------------------------- + + static Stream multiEncoderExpectations() { + return Stream.of( + // selective: car blocked, foot passes → must split (regression guard) + Arguments.of("bollard - car blocked, foot passes → split", Map.of("barrier", "bollard"), true), + // both blocked → split + Arguments.of("fence - both blocked → split", Map.of("barrier", "fence"), true), + // car blocked via access=no, foot blocked via access=no on a pass-by-default gate → split + Arguments.of("gate + access=no - both blocked → split", Map.of("barrier", "gate", "access", "no"), true), + // passable for both encoders → no split + Arguments.of("gate - both pass → no split", Map.of("barrier", "gate"), false), + Arguments.of("cattle_grid - both pass → no split", Map.of("barrier", "cattle_grid"), false), + // explicit access=yes lets both pass → no split + Arguments.of("bollard + access=yes - both pass → no split", Map.of("barrier", "bollard", "access", "yes"), false) + ); + } + + @ParameterizedTest(name = "{0}") + @MethodSource("multiEncoderExpectations") + void multiEncoder_selectiveBarrierStillSplits(String description, Map tags, boolean expected) { + EncodingManager carAndFoot = new EncodingManager.Builder() + .add(new CarFlagEncoder()) + .add(new FootFlagEncoder()) + .build(); + ORSOSMReader multiReader = newReader(carAndFoot); + + assertEquals(expected, multiReader.isBarrierNode(makeNode(1L, tags)), + "multi-encoder isBarrierNode mismatch for: " + description); + } + + @ParameterizedTest(name = "{0}") + @MethodSource("multiEncoderExpectations") + void multiEncoder_skippedCountReflectsAllPass(String description, Map tags, boolean expected) { + EncodingManager carAndFoot = new EncodingManager.Builder() + .add(new CarFlagEncoder()) + .add(new FootFlagEncoder()) + .build(); + ORSOSMReader multiReader = newReader(carAndFoot); + + multiReader.isBarrierNode(makeNode(1L, tags)); + + // A node is "skipped" only when no encoder is blocked, i.e. when it does NOT split. + assertEquals(1, multiReader.getBarrierNodesTotal(), description + ": total mismatch"); + assertEquals(expected ? 0 : 1, multiReader.getBarrierNodesSkipped(), description + ": skipped mismatch"); + } } From afcd60e5466ca2b36800cb06fcfcbcbb25a36989 Mon Sep 17 00:00:00 2001 From: Julian Psotta Date: Fri, 5 Jun 2026 10:33:11 +0200 Subject: [PATCH 10/13] fix: Add a more detailed log line for LM --- .../extensions/core/CoreLandmarkStorage.java | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/ors-engine/src/main/java/org/heigit/ors/routing/graphhopper/extensions/core/CoreLandmarkStorage.java b/ors-engine/src/main/java/org/heigit/ors/routing/graphhopper/extensions/core/CoreLandmarkStorage.java index 7dfa963dc1..7b18964327 100644 --- a/ors-engine/src/main/java/org/heigit/ors/routing/graphhopper/extensions/core/CoreLandmarkStorage.java +++ b/ors-engine/src/main/java/org/heigit/ors/routing/graphhopper/extensions/core/CoreLandmarkStorage.java @@ -122,6 +122,7 @@ public void createLandmarks() { boolean logDetails = LOGGER.isDebugEnabled(); SubnetworkStorage subnetworkStorage = getSubnetworkStorage(); int coreNodes = getBaseNodes(); + logger.info(String.format("[ORS-LM-DIAG] %s action=start coreNodes=%d", configName(), coreNodes)); // fill 'from' and 'to' weights with maximum value long maxBytes = (long) coreNodes * LM_ROW_LENGTH; @@ -167,8 +168,7 @@ public void createLandmarks() { StopWatch sw = new StopWatch().start(); TarjansCoreSCCAlgorithm tarjanAlgo = new TarjansCoreSCCAlgorithm(graph, core, accessFilter, false); List graphComponents = tarjanAlgo.findComponents(); - if (logDetails) - logger.debug(configName() + "Calculated " + graphComponents.size() + " subnetworks via tarjan in " + sw.stop().getSeconds() + "s, " + Helper.getMemInfo()); + logger.info(String.format("[ORS-LM-DIAG] %s tarjan_scc took=%ss subnetworks=%d %s", configName(), sw.stop().getSeconds(), graphComponents.size(), Helper.getMemInfo())); createCoreNodeIdDA(); String additionalInfo = ""; @@ -179,8 +179,10 @@ public void createLandmarks() { // see estimateMaxWeight. If we pick the distance too big for small areas this could lead to (slightly) // suboptimal routes as there will be too big rounding errors. But picking it too small is bad for performance // e.g. for Germany at least 1500km is very important otherwise speed is at least twice as slow e.g. for 1000km + StopWatch swEst = new StopWatch().start(); double maxWeight = estimateMaxWeight(graphComponents, accessFilter); setMaximumWeight(maxWeight); + logger.info(String.format("[ORS-LM-DIAG] %s factor_estimation took=%ss maxWeight=%.1f", configName(), swEst.stop().getSeconds(), maxWeight)); additionalInfo = ", maxWeight:" + maxWeight + " from quick estimation"; } @@ -198,6 +200,7 @@ public void createLandmarks() { int nodes = 0; int skippedSubnetworks = 0; int processedSubnetworks = 0; + StopWatch swLoop = new StopWatch().start(); for (IntArrayList subnetworkIds : graphComponents) { nodes += subnetworkIds.size(); if (subnetworkIds.size() < minimumNodes) { @@ -205,6 +208,10 @@ public void createLandmarks() { continue; } processedSubnetworks++; + if (processedSubnetworks == 1) { + logger.info(String.format("[ORS-LM-DIAG] %s skip_loop took=%ss skipped=%d main_subnetwork_size=%d", + configName(), swLoop.stop().getSeconds(), skippedSubnetworks, subnetworkIds.size())); + } logger.debug(String.format("[ORS-LM-DIAG] %s processing subnetwork size=%d minimumNodes=%d", configName(), subnetworkIds.size(), minimumNodes)); if (factor <= 0) throw new IllegalStateException("factor wasn't initialized " + factor + ", subnetworks:" @@ -212,6 +219,7 @@ public void createLandmarks() { subnetworkNodes = new IntHashSet(subnetworkIds); int index = subnetworkIds.size() - 1; + StopWatch swFindLm = new StopWatch().start(); for (; index >= 0; index--) { int nextStartNode = subnetworkIds.get(index); if (subnetworks[getIndex(nextStartNode)] == UNSET_SUBNETWORK) { @@ -224,6 +232,8 @@ public void createLandmarks() { break; } } + logger.info(String.format("[ORS-LM-DIAG] %s find_landmarks took=%ss subnetwork_size=%d", + configName(), swFindLm.stop().getSeconds(), subnetworkIds.size())); if (index < 0) logger.warn("next start node not found in big enough network of size " + subnetworkIds.size() + ", first element is " + subnetworkIds.get(0) + ", " + createPoint(graph, subnetworkIds.get(0))); } From 2749325fe086e1fb6bcf118f870c963daedb4de8 Mon Sep 17 00:00:00 2001 From: Julian Psotta Date: Fri, 5 Jun 2026 10:33:38 +0200 Subject: [PATCH 11/13] fix: Make maximumLmWeight configurable through the ors config --- .../org/heigit/ors/config/profile/PreparationProperties.java | 4 +++- .../routing/graphhopper/extensions/ORSGraphHopperConfig.java | 5 ++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/ors-engine/src/main/java/org/heigit/ors/config/profile/PreparationProperties.java b/ors-engine/src/main/java/org/heigit/ors/config/profile/PreparationProperties.java index b124b1f471..43fc4246e9 100644 --- a/ors-engine/src/main/java/org/heigit/ors/config/profile/PreparationProperties.java +++ b/ors-engine/src/main/java/org/heigit/ors/config/profile/PreparationProperties.java @@ -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); } } diff --git a/ors-engine/src/main/java/org/heigit/ors/routing/graphhopper/extensions/ORSGraphHopperConfig.java b/ors-engine/src/main/java/org/heigit/ors/routing/graphhopper/extensions/ORSGraphHopperConfig.java index b1c543e675..c25215da57 100644 --- a/ors-engine/src/main/java/org/heigit/ors/routing/graphhopper/extensions/ORSGraphHopperConfig.java +++ b/ors-engine/src/main/java/org/heigit/ors/routing/graphhopper/extensions/ORSGraphHopperConfig.java @@ -151,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); From 77483c1e29666a14e8aa74136c7436710c28d999 Mon Sep 17 00:00:00 2001 From: Julian Psotta Date: Fri, 5 Jun 2026 12:32:11 +0200 Subject: [PATCH 12/13] fix: Add more detailed logs for contractable core nodes --- .../routing/graphhopper/extensions/core/PrepareCore.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/ors-engine/src/main/java/org/heigit/ors/routing/graphhopper/extensions/core/PrepareCore.java b/ors-engine/src/main/java/org/heigit/ors/routing/graphhopper/extensions/core/PrepareCore.java index 526e6ef063..abb81f9395 100644 --- a/ors-engine/src/main/java/org/heigit/ors/routing/graphhopper/extensions/core/PrepareCore.java +++ b/ors-engine/src/main/java/org/heigit/ors/routing/graphhopper/extensions/core/PrepareCore.java @@ -175,9 +175,11 @@ protected IntContainer contractNode(int node, int level) { @Override public void finishContractionHook() { - int coreNodes = sortedNodes.size() + restrictedNodesCount; + int remaining = sortedNodes.size(); + int coreNodes = remaining + restrictedNodesCount; chStore.setCoreNodes(coreNodes); - logger.info("[ORS-CORE-DIAG] Core contraction finished. core_node_count={}", coreNodes); + logger.info("[ORS-CORE-DIAG] Core contraction finished. core_node_count={} restricted_nodes={} remaining_contractable={}", + coreNodes, restrictedNodesCount, remaining); // insert shortcuts connected to core nodes CoreNodeContractor coreNodeContractor = (CoreNodeContractor) nodeContractor; From 89b5edffcea7cb466c8a3914157d3858f53bfd20 Mon Sep 17 00:00:00 2001 From: Julian Psotta Date: Fri, 5 Jun 2026 17:02:41 +0200 Subject: [PATCH 13/13] feat: Add graph management logging --- .../org/heigit/ors/routing/RoutingProfile.java | 17 +++++++++++++++++ .../manage/local/ORSGraphFileManager.java | 5 +++++ 2 files changed, 22 insertions(+) diff --git a/ors-engine/src/main/java/org/heigit/ors/routing/RoutingProfile.java b/ors-engine/src/main/java/org/heigit/ors/routing/RoutingProfile.java index 7c4de2f64d..c2b63e83ea 100644 --- a/ors-engine/src/main/java/org/heigit/ors/routing/RoutingProfile.java +++ b/ors-engine/src/main/java/org/heigit/ors/routing/RoutingProfile.java @@ -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)) { @@ -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())); diff --git a/ors-engine/src/main/java/org/heigit/ors/routing/graphhopper/extensions/manage/local/ORSGraphFileManager.java b/ors-engine/src/main/java/org/heigit/ors/routing/graphhopper/extensions/manage/local/ORSGraphFileManager.java index ca4fef90fc..9ff790e457 100644 --- a/ors-engine/src/main/java/org/heigit/ors/routing/graphhopper/extensions/manage/local/ORSGraphFileManager.java +++ b/ors-engine/src/main/java/org/heigit/ors/routing/graphhopper/extensions/manage/local/ORSGraphFileManager.java @@ -335,8 +335,13 @@ public void extractDownloadedGraph() { try { LOGGER.info("[%s] Extracting downloaded graph file to %s".formatted(getProfileDescriptiveName(), extractionDirectoryAbsPath)); long start = System.currentTimeMillis(); + double compressedMB = graphDownloadFile.length() / (1024.0 * 1024.0); (new Unzipper()).unzip(graphDownloadFileAbsPath, extractionDirectoryAbsPath, true); long end = System.currentTimeMillis(); + double elapsedS = (end - start) / 1000.0; + double throughputMBs = elapsedS > 0 ? compressedMB / elapsedS : 0; + LOGGER.info("[ORS-UNPACKING-DIAG] Extracted %s (%.1f MB) in %.1fs (%.1f MB/s) using sequential Unzipper.".formatted( + graphDownloadFile.getName(), compressedMB, elapsedS, throughputMBs)); LOGGER.debug("[%s] Extraction of downloaded graph file finished after %d ms, deleting downloaded graph file %s".formatted( getProfileDescriptiveName(),