From 09ec844192b0774c4d8ffc608bf6a6bd60570b07 Mon Sep 17 00:00:00 2001 From: Leonard Ehrenfried Date: Thu, 23 Apr 2026 10:07:35 +0200 Subject: [PATCH 01/17] Replace streams in ExactMatchSpecifier --- .../specifier/ExactMatchSpecifier.java | 23 +++++++++++++------ 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/application/src/main/java/org/opentripplanner/osm/wayproperty/specifier/ExactMatchSpecifier.java b/application/src/main/java/org/opentripplanner/osm/wayproperty/specifier/ExactMatchSpecifier.java index 64c95c1149e..7a4ca83f18b 100644 --- a/application/src/main/java/org/opentripplanner/osm/wayproperty/specifier/ExactMatchSpecifier.java +++ b/application/src/main/java/org/opentripplanner/osm/wayproperty/specifier/ExactMatchSpecifier.java @@ -27,7 +27,7 @@ public class ExactMatchSpecifier implements OsmSpecifier { */ public static final int MATCH_MULTIPLIER = 200; public static final int NO_MATCH_SCORE = 0; - private final List conditions; + private final Condition[] conditions; private final int bestMatchScore; public ExactMatchSpecifier(String spec) { @@ -35,8 +35,8 @@ public ExactMatchSpecifier(String spec) { } public ExactMatchSpecifier(Condition... conditions) { - this.conditions = Arrays.asList(conditions); - bestMatchScore = this.conditions.size() * MATCH_MULTIPLIER; + this.conditions = conditions; + bestMatchScore = this.conditions.length * MATCH_MULTIPLIER; } @Override @@ -46,19 +46,28 @@ public int matchScore(OsmEntity way, TraverseDirection direction) { @Override public String toDocString() { - return conditions.stream().map(Object::toString).collect(Collectors.joining("; ")); + return Arrays.stream(conditions).map(Object::toString).collect(Collectors.joining("; ")); } public boolean allTagsMatch(OsmEntity way) { - return conditions.stream().allMatch(o -> o.isMatch(way)); + for (var c : conditions) { + if (!c.isMatch(way)) return false; + } + return true; } public boolean allBackwardTagsMatch(OsmEntity way) { - return conditions.stream().allMatch(c -> c.isBackwardMatch(way)); + for (var c : conditions) { + if (!c.isBackwardMatch(way)) return false; + } + return true; } public boolean allForwardTagsMatch(OsmEntity way) { - return conditions.stream().allMatch(c -> c.isForwardMatch(way)); + for (var c : conditions) { + if (!c.isForwardMatch(way)) return false; + } + return true; } private boolean allTagsMatch(OsmEntity way, TraverseDirection direction) { From 28fd5a699bcfe173b57b5c1a5dcdd06c2c220c8b Mon Sep 17 00:00:00 2001 From: Leonard Ehrenfried Date: Thu, 23 Apr 2026 13:11:22 +0200 Subject: [PATCH 02/17] Cache some tag look ups # Conflicts: # application/src/main/java/org/opentripplanner/osm/model/OsmEntity.java --- .../opentripplanner/osm/model/OsmEntity.java | 44 ++++++++++++------- .../opentripplanner/osm/model/OsmNode.java | 4 +- .../osm/wayproperty/specifier/Condition.java | 2 +- .../specifier/ExactMatchSpecifier.java | 13 ++++-- .../core/model/i18n/TranslatedString.java | 5 ++- 5 files changed, 44 insertions(+), 24 deletions(-) diff --git a/application/src/main/java/org/opentripplanner/osm/model/OsmEntity.java b/application/src/main/java/org/opentripplanner/osm/model/OsmEntity.java index 1999f0442d5..e187da4a3d0 100644 --- a/application/src/main/java/org/opentripplanner/osm/model/OsmEntity.java +++ b/application/src/main/java/org/opentripplanner/osm/model/OsmEntity.java @@ -133,6 +133,24 @@ public abstract class OsmEntity { */ protected static final Set CHECKED_MODES = Set.of("foot", "bicycle", "motorcar"); + private static final Set WALK_ONLY_HIGHWAY_VALUES = Set.of("footway", "step", "corridor"); + private static final Set NO_ACCESS_VALUES = Set.of("no", "license", "dismount"); + private static final Set HIGHWAY_BOARDING_LOCATION_VALUES = Set.of( + "platform", + "bus_stop" + ); + private static final Set RAILWAY_BOARDING_LOCATION_VALUES = Set.of( + "tram_stop", + "station", + "halt" + ); + private static final Set AMENITY_BOARDING_LOCATION_VALUES = Set.of( + "bus_station", + "amenity", + "ferry_terminal" + ); + private static final Set RAILWAY_PLATFORM_VALUES = Set.of("platform", "platform_edge"); + /** * Mapping for the fallback key for checking access restrictions for each access mode in OSM * However, access is not included because we are skeptical of access=yes tags. @@ -145,8 +163,6 @@ public abstract class OsmEntity { "bicycle", "vehicle" ); - private static final Set WALK_ONLY_HIGHWAYS = Set.of("footway", "step", "corridor"); - private static final Set NO_ACCESS_TAGS = Set.of("no", "license", "dismount"); private static final Map OSM_TAGS_FOR_TRAVERSAL_PERMISSION = Map.of( StreetTraversalPermission.CAR, @@ -160,6 +176,10 @@ public abstract class OsmEntity { private final Map tags; protected final long id; + private static final Pattern I18N_PATTERN = Pattern.compile("\\{(.*?)}"); + + /* To save memory this is only created when an entity actually has tags. */ + private Map tags; private final OsmProvider osmProvider; @@ -524,7 +544,7 @@ public I18NString getAssumedName() { public Map generateI18NForPattern(String pattern) { Map i18n = new HashMap<>(); i18n.put(null, new StringBuffer()); - Matcher matcher = Pattern.compile("\\{(.*?)}").matcher(pattern); + Matcher matcher = I18N_PATTERN.matcher(pattern); int lastEnd = 0; while (matcher.find()) { @@ -621,7 +641,7 @@ public Optional isOneWay(@Nullable String mode) { return Optional.empty(); } - if ("foot".equals(mode) && !isOneOfTags("highway", WALK_ONLY_HIGHWAYS)) { + if ("foot".equals(mode) && !isOneOfTags("highway", WALK_ONLY_HIGHWAY_VALUES)) { return Optional.empty(); } @@ -706,13 +726,9 @@ public boolean isParkAndRide() { */ public boolean isBoardingLocation() { return ( - isTag("highway", "bus_stop") || - isTag("railway", "tram_stop") || - isTag("railway", "station") || - isTag("railway", "halt") || - isTag("amenity", "bus_station") || - isTag("amenity", "ferry_terminal") || - isTag("highway", "platform") || + isOneOfTags("highway", HIGHWAY_BOARDING_LOCATION_VALUES) || + isOneOfTags("railway", RAILWAY_BOARDING_LOCATION_VALUES) || + isOneOfTags("amenity", AMENITY_BOARDING_LOCATION_VALUES) || isPlatform() ); } @@ -726,9 +742,7 @@ public boolean isBoardingLocation() { **/ public boolean isPlatform() { var isPlatform = - isTag("public_transport", "platform") || - isTag("railway", "platform") || - isTag("railway", "platform_edge"); + isTag("public_transport", "platform") || isOneOfTags("railway", RAILWAY_PLATFORM_VALUES); return isPlatform && !isTag("usage", "tourism"); } @@ -891,7 +905,7 @@ public boolean isExplicitlyUnnamed() { * Returns true if this tag is explicitly access to this entity. */ private boolean isExplicitlyDenied(String key) { - return isOneOfTags(key, NO_ACCESS_TAGS); + return isOneOfTags(key, NO_ACCESS_VALUES); } public StreetTraversalPermission getPermission() { diff --git a/application/src/main/java/org/opentripplanner/osm/model/OsmNode.java b/application/src/main/java/org/opentripplanner/osm/model/OsmNode.java index 6d4e48ba138..8289f116ab2 100644 --- a/application/src/main/java/org/opentripplanner/osm/model/OsmNode.java +++ b/application/src/main/java/org/opentripplanner/osm/model/OsmNode.java @@ -38,11 +38,11 @@ public Coordinate getCoordinate() { } public boolean hasHighwayTrafficLight() { - return hasTag("highway") && "traffic_signals".equals(getTag("highway")); + return "traffic_signals".equals(getTag("highway")); } public boolean hasCrossingTrafficLight() { - return hasTag("crossing") && "traffic_signals".equals(getTag("crossing")); + return "traffic_signals".equals(getTag("crossing")); } /** diff --git a/application/src/main/java/org/opentripplanner/osm/wayproperty/specifier/Condition.java b/application/src/main/java/org/opentripplanner/osm/wayproperty/specifier/Condition.java index ed8a856e600..8bee22bab8e 100644 --- a/application/src/main/java/org/opentripplanner/osm/wayproperty/specifier/Condition.java +++ b/application/src/main/java/org/opentripplanner/osm/wayproperty/specifier/Condition.java @@ -133,7 +133,7 @@ public String toString() { record Equals(String key, String value) implements Condition { @Override public boolean isExtendedKeyMatch(OsmEntity way, String exKey) { - return way.hasTag(exKey) && way.isTag(exKey, value); + return way.isTag(exKey, value); } @Override diff --git a/application/src/main/java/org/opentripplanner/osm/wayproperty/specifier/ExactMatchSpecifier.java b/application/src/main/java/org/opentripplanner/osm/wayproperty/specifier/ExactMatchSpecifier.java index 7a4ca83f18b..221c8e98d3c 100644 --- a/application/src/main/java/org/opentripplanner/osm/wayproperty/specifier/ExactMatchSpecifier.java +++ b/application/src/main/java/org/opentripplanner/osm/wayproperty/specifier/ExactMatchSpecifier.java @@ -1,7 +1,6 @@ package org.opentripplanner.osm.wayproperty.specifier; import java.util.Arrays; -import java.util.List; import java.util.stream.Collectors; import org.opentripplanner.osm.model.OsmEntity; import org.opentripplanner.osm.model.TraverseDirection; @@ -51,21 +50,27 @@ public String toDocString() { public boolean allTagsMatch(OsmEntity way) { for (var c : conditions) { - if (!c.isMatch(way)) return false; + if (!c.isMatch(way)) { + return false; + } } return true; } public boolean allBackwardTagsMatch(OsmEntity way) { for (var c : conditions) { - if (!c.isBackwardMatch(way)) return false; + if (!c.isBackwardMatch(way)) { + return false; + } } return true; } public boolean allForwardTagsMatch(OsmEntity way) { for (var c : conditions) { - if (!c.isForwardMatch(way)) return false; + if (!c.isForwardMatch(way)) { + return false; + } } return true; } diff --git a/domain-core/src/main/java/org/opentripplanner/core/model/i18n/TranslatedString.java b/domain-core/src/main/java/org/opentripplanner/core/model/i18n/TranslatedString.java index bcb05dc8b98..4986eb9eddd 100644 --- a/domain-core/src/main/java/org/opentripplanner/core/model/i18n/TranslatedString.java +++ b/domain-core/src/main/java/org/opentripplanner/core/model/i18n/TranslatedString.java @@ -114,8 +114,9 @@ static I18NString getI18NString( if (translations.isEmpty()) { throw new IllegalArgumentException("At least one translation must be provided"); } - if (TRANSLATION_CACHE.containsKey(translations)) { - return TRANSLATION_CACHE.get(translations); + var t = TRANSLATION_CACHE.get(translations); + if (t != null) { + return t; } else { I18NString ret; // Check if we only have one name, even under multiple languages From f34d68242cc85907d8373354066dbc76b267bebb Mon Sep 17 00:00:00 2001 From: Leonard Ehrenfried Date: Thu, 23 Apr 2026 13:37:32 +0200 Subject: [PATCH 03/17] Speed up getMulitTagValues --- .../opentripplanner/osm/model/OsmEntity.java | 24 ++++++++++++------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/application/src/main/java/org/opentripplanner/osm/model/OsmEntity.java b/application/src/main/java/org/opentripplanner/osm/model/OsmEntity.java index e187da4a3d0..5ccb223b6fe 100644 --- a/application/src/main/java/org/opentripplanner/osm/model/OsmEntity.java +++ b/application/src/main/java/org/opentripplanner/osm/model/OsmEntity.java @@ -13,6 +13,7 @@ import java.util.Arrays; import java.util.Collections; import java.util.HashMap; +import java.util.HashSet; import java.util.Map; import java.util.Objects; import java.util.Optional; @@ -21,7 +22,6 @@ import java.util.function.Consumer; import java.util.regex.Matcher; import java.util.regex.Pattern; -import java.util.stream.Collectors; import javax.annotation.Nullable; import org.opentripplanner.core.model.accessibility.Accessibility; import org.opentripplanner.core.model.i18n.I18NString; @@ -789,14 +789,20 @@ public String url() { * Values are split by semicolons. */ public Set getMultiTagValues(Set refTags) { - return refTags - .stream() - .map(this::getTag) - .filter(Objects::nonNull) - .flatMap(v -> Arrays.stream(v.split(";"))) - .map(String::strip) - .filter(v -> !v.isBlank()) - .collect(Collectors.toUnmodifiableSet()); + Set result = HashSet.newHashSet(2); + for (var tag : refTags) { + var value = getTag(tag); + if (value == null) { + continue; + } + for (var part : value.split(";")) { + var stripped = part.strip(); + if (!stripped.isBlank()) { + result.add(stripped); + } + } + } + return result; } public OsmProvider getOsmProvider() { From 50af0311502d4d556279a0ae102f6fbb2e5657e2 Mon Sep 17 00:00:00 2001 From: Leonard Ehrenfried Date: Thu, 23 Apr 2026 13:48:35 +0200 Subject: [PATCH 04/17] Speed up boarding locations --- .../opentripplanner/graph_builder/module/osm/OsmModule.java | 5 ++++- .../main/java/org/opentripplanner/osm/model/OsmEntity.java | 5 +++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/application/src/main/java/org/opentripplanner/graph_builder/module/osm/OsmModule.java b/application/src/main/java/org/opentripplanner/graph_builder/module/osm/OsmModule.java index a522b471ace..5b0c3362624 100644 --- a/application/src/main/java/org/opentripplanner/graph_builder/module/osm/OsmModule.java +++ b/application/src/main/java/org/opentripplanner/graph_builder/module/osm/OsmModule.java @@ -554,7 +554,10 @@ private void buildBarrierEdges(VertexGenerator vertexGenerator) { private Optional getPlatform(OsmDatabase osmdb, OsmWay way) { var references = way.getMultiTagValues(params.boardingAreaRefTags()); - if (way.isBoardingLocation() && !references.isEmpty()) { + if (!references.isEmpty()) { + if (!way.isBoardingLocation()) { + return Optional.empty(); + } var nodeRefs = way.getNodeRefs(); var size = nodeRefs.size(); var nodes = new Coordinate[size]; diff --git a/application/src/main/java/org/opentripplanner/osm/model/OsmEntity.java b/application/src/main/java/org/opentripplanner/osm/model/OsmEntity.java index 5ccb223b6fe..549d8de721f 100644 --- a/application/src/main/java/org/opentripplanner/osm/model/OsmEntity.java +++ b/application/src/main/java/org/opentripplanner/osm/model/OsmEntity.java @@ -862,10 +862,11 @@ public boolean isElevator() { * of other information. */ public boolean isWheelchairAccessible() { - if (isTagTrue("wheelchair")) { + var wheelchairValue = getTag("wheelchair"); + if (isTrue(wheelchairValue)) { return true; } - if (isTagFalse("wheelchair")) { + if (isFalse(wheelchairValue)) { return false; } if (isOneOfTags("barrier", WHEELCHAIR_INACCESSIBLE_BARRIERS)) { From 078e5e424030964654c87cd2f8c4972d7ccfd260 Mon Sep 17 00:00:00 2001 From: Leonard Ehrenfried Date: Thu, 30 Apr 2026 14:38:23 +0200 Subject: [PATCH 05/17] Fix after rebase --- .../main/java/org/opentripplanner/osm/model/OsmEntity.java | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/application/src/main/java/org/opentripplanner/osm/model/OsmEntity.java b/application/src/main/java/org/opentripplanner/osm/model/OsmEntity.java index 549d8de721f..270f7852bad 100644 --- a/application/src/main/java/org/opentripplanner/osm/model/OsmEntity.java +++ b/application/src/main/java/org/opentripplanner/osm/model/OsmEntity.java @@ -10,7 +10,6 @@ import java.time.Duration; import java.time.format.DateTimeParseException; -import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; @@ -37,6 +36,7 @@ */ public abstract class OsmEntity { + private static final Pattern I18N_PATTERN = Pattern.compile("\\{(.*?)}"); /** * highway=* values that we don't want to even consider when building the graph. */ @@ -176,10 +176,6 @@ public abstract class OsmEntity { private final Map tags; protected final long id; - private static final Pattern I18N_PATTERN = Pattern.compile("\\{(.*?)}"); - - /* To save memory this is only created when an entity actually has tags. */ - private Map tags; private final OsmProvider osmProvider; From f60dfebef329b3992c09a3ae0d2495298c0c0bbb Mon Sep 17 00:00:00 2001 From: Leonard Ehrenfried Date: Thu, 30 Apr 2026 15:05:12 +0200 Subject: [PATCH 06/17] Skip some tag lookups --- .../main/java/org/opentripplanner/osm/model/OsmEntity.java | 5 ++++- .../main/java/org/opentripplanner/osm/model/OsmNode.java | 7 +++++++ .../java/org/opentripplanner/osm/model/OsmNodeBuilder.java | 2 +- 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/application/src/main/java/org/opentripplanner/osm/model/OsmEntity.java b/application/src/main/java/org/opentripplanner/osm/model/OsmEntity.java index 270f7852bad..00b3df45a8c 100644 --- a/application/src/main/java/org/opentripplanner/osm/model/OsmEntity.java +++ b/application/src/main/java/org/opentripplanner/osm/model/OsmEntity.java @@ -173,7 +173,7 @@ public abstract class OsmEntity { "foot" ); - private final Map tags; + protected final Map tags; protected final long id; @@ -326,6 +326,9 @@ protected boolean isExplicitlyAllowed(String key) { */ @Nullable public String getTag(String tag) { + if(this.tags.isEmpty()){ + return null; + } tag = tag.toLowerCase(); return tags.get(tag); } diff --git a/application/src/main/java/org/opentripplanner/osm/model/OsmNode.java b/application/src/main/java/org/opentripplanner/osm/model/OsmNode.java index 8289f116ab2..8f2a9ead555 100644 --- a/application/src/main/java/org/opentripplanner/osm/model/OsmNode.java +++ b/application/src/main/java/org/opentripplanner/osm/model/OsmNode.java @@ -51,6 +51,10 @@ public boolean hasCrossingTrafficLight() { * @return true if it does */ public boolean isBarrier() { + // the majority of nodes have no tags at all, so this yields a good speed-up + if(this.tags.isEmpty()){ + return false; + } return overridePermissions(ALL) != ALL; } @@ -69,6 +73,9 @@ public boolean isStationEntrance() { * @return True if this entity provides an entrance to a platform or similar entity */ public boolean isEntrance() { + if(this.tags.isEmpty()){ + return false; + } return ( (isStationEntrance() || isTag("entrance", "yes") || isTag("entrance", "main")) && !isTag("access", "private") && diff --git a/application/src/main/java/org/opentripplanner/osm/model/OsmNodeBuilder.java b/application/src/main/java/org/opentripplanner/osm/model/OsmNodeBuilder.java index a55cb3dd2c1..f90d7b87886 100644 --- a/application/src/main/java/org/opentripplanner/osm/model/OsmNodeBuilder.java +++ b/application/src/main/java/org/opentripplanner/osm/model/OsmNodeBuilder.java @@ -10,7 +10,7 @@ public class OsmNodeBuilder { private long id; private double lat; private double lon; - // many nodes don't have any tags so we start with an empty immutable map + // the vast majority of nodes don't have any tags, so we start with an empty immutable map private Map tags = EMPTY_TAGS; private OsmProvider osmProvider; From fd19c8ecfcfe265750c481bbcbd29215c232a0de Mon Sep 17 00:00:00 2001 From: Leonard Ehrenfried Date: Thu, 30 Apr 2026 17:45:18 +0200 Subject: [PATCH 07/17] Use null instead of empty map --- .../opentripplanner/osm/model/OsmEntity.java | 18 +++++++++++++----- .../org/opentripplanner/osm/model/OsmNode.java | 4 ++-- .../osm/model/OsmNodeBuilder.java | 2 +- 3 files changed, 16 insertions(+), 8 deletions(-) diff --git a/application/src/main/java/org/opentripplanner/osm/model/OsmEntity.java b/application/src/main/java/org/opentripplanner/osm/model/OsmEntity.java index 00b3df45a8c..c642f255def 100644 --- a/application/src/main/java/org/opentripplanner/osm/model/OsmEntity.java +++ b/application/src/main/java/org/opentripplanner/osm/model/OsmEntity.java @@ -225,9 +225,16 @@ public Map getTags() { * Is the tag defined? */ public boolean hasTag(String tag) { + if(this.isTagLess()){ + return false; + } return getTag(tag) != null; } + boolean isTagLess() { + return this.tags == null; + } + /** * Determines if a tag contains a false value. 'no', 'false', and '0' are considered false. */ @@ -326,7 +333,7 @@ protected boolean isExplicitlyAllowed(String key) { */ @Nullable public String getTag(String tag) { - if(this.tags.isEmpty()){ + if(this.isTagLess()){ return null; } tag = tag.toLowerCase(); @@ -490,7 +497,7 @@ public OptionalInt parseIntOrBoolean(String tag, Consumer errorHandler) * Checks if a tag contains the specified value. */ public boolean isTag(String tag, String value) { - return value != null && value.equals(tags.get(tag.toLowerCase())); + return !isTagLess() && value != null && value.equals(tags.get(tag.toLowerCase())); } /** @@ -511,7 +518,7 @@ public boolean isOneOfTags(String key, Set oneOfTags) { */ @Nullable public I18NString getAssumedName() { - if (tags.containsKey("name")) { + if (hasTag("name")) { return TranslatedString.getDeduplicatedI18NString( this.generateI18NForPattern("{name}"), false @@ -525,8 +532,9 @@ public I18NString getAssumedName() { if (creativeName != null) { return this.creativeName; } - if (tags.containsKey("ref")) { - return new NonLocalizedString(tags.get("ref")); + var ref = getTag("ref"); + if (ref != null) { + return new NonLocalizedString(ref); } return null; } diff --git a/application/src/main/java/org/opentripplanner/osm/model/OsmNode.java b/application/src/main/java/org/opentripplanner/osm/model/OsmNode.java index 8f2a9ead555..71f058cb0cd 100644 --- a/application/src/main/java/org/opentripplanner/osm/model/OsmNode.java +++ b/application/src/main/java/org/opentripplanner/osm/model/OsmNode.java @@ -52,7 +52,7 @@ public boolean hasCrossingTrafficLight() { */ public boolean isBarrier() { // the majority of nodes have no tags at all, so this yields a good speed-up - if(this.tags.isEmpty()){ + if(this.isTagLess()){ return false; } return overridePermissions(ALL) != ALL; @@ -73,7 +73,7 @@ public boolean isStationEntrance() { * @return True if this entity provides an entrance to a platform or similar entity */ public boolean isEntrance() { - if(this.tags.isEmpty()){ + if(this.isTagLess()){ return false; } return ( diff --git a/application/src/main/java/org/opentripplanner/osm/model/OsmNodeBuilder.java b/application/src/main/java/org/opentripplanner/osm/model/OsmNodeBuilder.java index f90d7b87886..b5d68a194ea 100644 --- a/application/src/main/java/org/opentripplanner/osm/model/OsmNodeBuilder.java +++ b/application/src/main/java/org/opentripplanner/osm/model/OsmNodeBuilder.java @@ -6,7 +6,7 @@ public class OsmNodeBuilder { - private static final Map EMPTY_TAGS = Map.of(); + private static final Map EMPTY_TAGS = null; private long id; private double lat; private double lon; From 2ac5c55ec3bd244c4e1c1296aafacb7aa5be183f Mon Sep 17 00:00:00 2001 From: Leonard Ehrenfried Date: Thu, 30 Apr 2026 18:09:38 +0200 Subject: [PATCH 08/17] Add another shortcut --- .../src/main/java/org/opentripplanner/osm/model/OsmEntity.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/application/src/main/java/org/opentripplanner/osm/model/OsmEntity.java b/application/src/main/java/org/opentripplanner/osm/model/OsmEntity.java index c642f255def..651c616d484 100644 --- a/application/src/main/java/org/opentripplanner/osm/model/OsmEntity.java +++ b/application/src/main/java/org/opentripplanner/osm/model/OsmEntity.java @@ -293,6 +293,9 @@ protected Optional checkModePermission(String mode) { * or a parent mode, either with a directional suffix or not, empty if it is not specified. */ protected Optional checkModePermission(String mode, TraverseDirection direction) { + if(isTagLess()){ + return Optional.empty(); + } // check if the exact directional tag allows or denies access if (direction != DIRECTIONLESS) { if (isExplicitlyAllowed(mode + direction.tagSuffix())) { From e21b49296389d1851e998c46bb3d253f732ca458 Mon Sep 17 00:00:00 2001 From: Leonard Ehrenfried Date: Mon, 4 May 2026 11:07:51 +0200 Subject: [PATCH 09/17] Add nullable annotation, use TDoubleArrayList --- .../graph_builder/module/osm/OsmDatabase.java | 2 -- .../graph_builder/module/osm/OsmModule.java | 16 +++++----- .../opentripplanner/osm/model/OsmEntity.java | 30 +++++++++++-------- .../opentripplanner/osm/model/OsmNode.java | 4 +-- .../opentripplanner/osm/model/OsmWayTest.java | 9 ------ 5 files changed, 28 insertions(+), 33 deletions(-) diff --git a/application/src/main/java/org/opentripplanner/graph_builder/module/osm/OsmDatabase.java b/application/src/main/java/org/opentripplanner/graph_builder/module/osm/OsmDatabase.java index 2b34157bf3e..9460857bece 100644 --- a/application/src/main/java/org/opentripplanner/graph_builder/module/osm/OsmDatabase.java +++ b/application/src/main/java/org/opentripplanner/graph_builder/module/osm/OsmDatabase.java @@ -772,8 +772,6 @@ private void processMultipolygonRelations() { innerWays.add(way); } else if (member.hasRoleOuter()) { outerWays.add(way); - } else { - LOG.warn("Unexpected role '{}' in multipolygon", member.getRole()); } } processedAreas.add(relation); diff --git a/application/src/main/java/org/opentripplanner/graph_builder/module/osm/OsmModule.java b/application/src/main/java/org/opentripplanner/graph_builder/module/osm/OsmModule.java index 5b0c3362624..857ad989e8a 100644 --- a/application/src/main/java/org/opentripplanner/graph_builder/module/osm/OsmModule.java +++ b/application/src/main/java/org/opentripplanner/graph_builder/module/osm/OsmModule.java @@ -9,6 +9,7 @@ import com.google.common.collect.ImmutableMultimap; import com.google.common.collect.Multimap; import gnu.trove.iterator.TLongIterator; +import gnu.trove.list.array.TDoubleArrayList; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; @@ -372,7 +373,7 @@ private void buildBasicGraph( IntersectionVertex fromVertex = null; IntersectionVertex toVertex = null; - ArrayList segmentCoordinates = new ArrayList<>(); + TDoubleArrayList segmentCoordinates = new TDoubleArrayList(100); /* * Traverse through all the nodes of this edge. For nodes which are not shared with any other edge, do not create endpoints -- just @@ -412,7 +413,8 @@ private void buildBasicGraph( * the only processing we do on other nodes is to accumulate their geometry */ if (segmentCoordinates.isEmpty()) { - segmentCoordinates.add(osmStartNode.getCoordinate()); + segmentCoordinates.add(osmStartNode.lon); + segmentCoordinates.add(osmStartNode.lat); } if ( @@ -425,14 +427,14 @@ private void buildBasicGraph( osmEndNode.isEntrance() || vertexGenerator.nodesInBarrierWays().containsKey(osmEndNode) ) { - segmentCoordinates.add(osmEndNode.getCoordinate()); + segmentCoordinates.add(osmEndNode.lon); + segmentCoordinates.add(osmEndNode.lat); - geometry = GeometryUtils.getGeometryFactory().createLineString( - segmentCoordinates.toArray(new Coordinate[0]) - ); + geometry = GeometryUtils.makeLineString(segmentCoordinates.toArray()); segmentCoordinates.clear(); } else { - segmentCoordinates.add(osmEndNode.getCoordinate()); + segmentCoordinates.add(osmEndNode.lon); + segmentCoordinates.add(osmEndNode.lat); continue; } diff --git a/application/src/main/java/org/opentripplanner/osm/model/OsmEntity.java b/application/src/main/java/org/opentripplanner/osm/model/OsmEntity.java index 651c616d484..e99b079af54 100644 --- a/application/src/main/java/org/opentripplanner/osm/model/OsmEntity.java +++ b/application/src/main/java/org/opentripplanner/osm/model/OsmEntity.java @@ -173,7 +173,8 @@ public abstract class OsmEntity { "foot" ); - protected final Map tags; + @Nullable + private final Map tags; protected final long id; @@ -191,7 +192,7 @@ public abstract class OsmEntity { /** * Constructor for immutable OsmEntity */ - protected OsmEntity(long id, Map tags, OsmProvider osmProvider) { + protected OsmEntity(long id, @Nullable Map tags, OsmProvider osmProvider) { this.id = id; // calling Map.copyOf here costs about 10% of parsing performance, so we use // Collections.unmodifiableMap in the getter @@ -218,19 +219,26 @@ public long getId() { * The tags of an entity (immutable). */ public Map getTags() { - return Collections.unmodifiableMap(tags); + if (this.tags == null) { + return Map.of(); + } else { + return Collections.unmodifiableMap(tags); + } } /** * Is the tag defined? */ public boolean hasTag(String tag) { - if(this.isTagLess()){ + if (this.isTagLess()) { return false; } return getTag(tag) != null; } + /** + * Does the entity contain any tags at all? + */ boolean isTagLess() { return this.tags == null; } @@ -262,13 +270,6 @@ public boolean isTagTrue(String tag) { return isTrue(getTag(tag)); } - /** - * Returns true if bicycle dismounts are forced. - */ - public boolean isBicycleDismountForced() { - return isTag("bicycle", "dismount"); - } - public boolean isSidewalk() { return isTag("footway", "sidewalk") && isTag("highway", "footway"); } @@ -293,7 +294,7 @@ protected Optional checkModePermission(String mode) { * or a parent mode, either with a directional suffix or not, empty if it is not specified. */ protected Optional checkModePermission(String mode, TraverseDirection direction) { - if(isTagLess()){ + if (isTagLess()) { return Optional.empty(); } // check if the exact directional tag allows or denies access @@ -336,7 +337,7 @@ protected boolean isExplicitlyAllowed(String key) { */ @Nullable public String getTag(String tag) { - if(this.isTagLess()){ + if (this.isTagLess()) { return null; } tag = tag.toLowerCase(); @@ -735,6 +736,9 @@ public boolean isParkAndRide() { * @return whether the node is a place used to board a public transport vehicle */ public boolean isBoardingLocation() { + if (isTagLess()) { + return false; + } return ( isOneOfTags("highway", HIGHWAY_BOARDING_LOCATION_VALUES) || isOneOfTags("railway", RAILWAY_BOARDING_LOCATION_VALUES) || diff --git a/application/src/main/java/org/opentripplanner/osm/model/OsmNode.java b/application/src/main/java/org/opentripplanner/osm/model/OsmNode.java index 71f058cb0cd..99c3dbd848c 100644 --- a/application/src/main/java/org/opentripplanner/osm/model/OsmNode.java +++ b/application/src/main/java/org/opentripplanner/osm/model/OsmNode.java @@ -52,7 +52,7 @@ public boolean hasCrossingTrafficLight() { */ public boolean isBarrier() { // the majority of nodes have no tags at all, so this yields a good speed-up - if(this.isTagLess()){ + if (this.isTagLess()) { return false; } return overridePermissions(ALL) != ALL; @@ -73,7 +73,7 @@ public boolean isStationEntrance() { * @return True if this entity provides an entrance to a platform or similar entity */ public boolean isEntrance() { - if(this.isTagLess()){ + if (this.isTagLess()) { return false; } return ( diff --git a/application/src/test/java/org/opentripplanner/osm/model/OsmWayTest.java b/application/src/test/java/org/opentripplanner/osm/model/OsmWayTest.java index 870a313c74c..151f0deb1ef 100644 --- a/application/src/test/java/org/opentripplanner/osm/model/OsmWayTest.java +++ b/application/src/test/java/org/opentripplanner/osm/model/OsmWayTest.java @@ -22,15 +22,6 @@ void lowerCaseKeys() { assertEquals("baz", entity.getTag("foo")); } - @Test - void testIsBicycleDismountForced() { - OsmWay way = OsmWay.of().build(); - assertFalse(way.isBicycleDismountForced()); - - way = OsmWay.of().withTag("bicycle", "dismount").build(); - assertTrue(way.isBicycleDismountForced()); - } - @Test void testAreaMustContain3Nodes() { OsmWay way = OsmWay.of().withTag("area", "yes").build(); From a89f768ab64f2ff52d39ea1b0927db67edcc7bb6 Mon Sep 17 00:00:00 2001 From: Leonard Ehrenfried Date: Mon, 4 May 2026 11:23:43 +0200 Subject: [PATCH 10/17] Make hot methods final --- .../org/opentripplanner/osm/model/OsmEntity.java | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/application/src/main/java/org/opentripplanner/osm/model/OsmEntity.java b/application/src/main/java/org/opentripplanner/osm/model/OsmEntity.java index e99b079af54..93c3bb527f3 100644 --- a/application/src/main/java/org/opentripplanner/osm/model/OsmEntity.java +++ b/application/src/main/java/org/opentripplanner/osm/model/OsmEntity.java @@ -229,7 +229,7 @@ public Map getTags() { /** * Is the tag defined? */ - public boolean hasTag(String tag) { + public final boolean hasTag(String tag) { if (this.isTagLess()) { return false; } @@ -239,14 +239,14 @@ public boolean hasTag(String tag) { /** * Does the entity contain any tags at all? */ - boolean isTagLess() { + final boolean isTagLess() { return this.tags == null; } /** * Determines if a tag contains a false value. 'no', 'false', and '0' are considered false. */ - public boolean isTagFalse(String tag) { + public final boolean isTagFalse(String tag) { return isFalse(getTag(tag)); } @@ -266,7 +266,7 @@ public Accessibility explicitWheelchairAccessibility() { /** * Determines if a tag contains a true value. 'yes', 'true', and '1' are considered true. */ - public boolean isTagTrue(String tag) { + public final boolean isTagTrue(String tag) { return isTrue(getTag(tag)); } @@ -336,7 +336,7 @@ protected boolean isExplicitlyAllowed(String key) { * Returns null if tag is not present. */ @Nullable - public String getTag(String tag) { + public final String getTag(String tag) { if (this.isTagLess()) { return null; } @@ -500,7 +500,7 @@ public OptionalInt parseIntOrBoolean(String tag, Consumer errorHandler) /** * Checks if a tag contains the specified value. */ - public boolean isTag(String tag, String value) { + public final boolean isTag(String tag, String value) { return !isTagLess() && value != null && value.equals(tags.get(tag.toLowerCase())); } From e1c070e28301578665014b4a4b9c14e44ade9eef Mon Sep 17 00:00:00 2001 From: Leonard Ehrenfried Date: Tue, 5 May 2026 07:24:42 +0200 Subject: [PATCH 11/17] Improve Javadoc --- .../java/org/opentripplanner/osm/model/OsmNodeBuilder.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/application/src/main/java/org/opentripplanner/osm/model/OsmNodeBuilder.java b/application/src/main/java/org/opentripplanner/osm/model/OsmNodeBuilder.java index b5d68a194ea..9deb5889552 100644 --- a/application/src/main/java/org/opentripplanner/osm/model/OsmNodeBuilder.java +++ b/application/src/main/java/org/opentripplanner/osm/model/OsmNodeBuilder.java @@ -10,7 +10,8 @@ public class OsmNodeBuilder { private long id; private double lat; private double lon; - // the vast majority of nodes don't have any tags, so we start with an empty immutable map + // the vast majority of nodes don't have any tags, so we start with null, because that has no + // allocations at all private Map tags = EMPTY_TAGS; private OsmProvider osmProvider; From 96ea12dfad9e2a080a1d2f83a78b618d48527759 Mon Sep 17 00:00:00 2001 From: Leonard Ehrenfried Date: Wed, 6 May 2026 10:00:20 +0200 Subject: [PATCH 12/17] Review: Rename method --- .../org/opentripplanner/osm/model/OsmEntity.java | 15 ++++++--------- .../org/opentripplanner/osm/model/OsmNode.java | 4 ++-- 2 files changed, 8 insertions(+), 11 deletions(-) diff --git a/application/src/main/java/org/opentripplanner/osm/model/OsmEntity.java b/application/src/main/java/org/opentripplanner/osm/model/OsmEntity.java index 93c3bb527f3..42892a6fdfa 100644 --- a/application/src/main/java/org/opentripplanner/osm/model/OsmEntity.java +++ b/application/src/main/java/org/opentripplanner/osm/model/OsmEntity.java @@ -230,16 +230,13 @@ public Map getTags() { * Is the tag defined? */ public final boolean hasTag(String tag) { - if (this.isTagLess()) { - return false; - } - return getTag(tag) != null; + return !this.isTagless() && getTag(tag) != null; } /** * Does the entity contain any tags at all? */ - final boolean isTagLess() { + final boolean isTagless() { return this.tags == null; } @@ -294,7 +291,7 @@ protected Optional checkModePermission(String mode) { * or a parent mode, either with a directional suffix or not, empty if it is not specified. */ protected Optional checkModePermission(String mode, TraverseDirection direction) { - if (isTagLess()) { + if (isTagless()) { return Optional.empty(); } // check if the exact directional tag allows or denies access @@ -337,7 +334,7 @@ protected boolean isExplicitlyAllowed(String key) { */ @Nullable public final String getTag(String tag) { - if (this.isTagLess()) { + if (this.isTagless()) { return null; } tag = tag.toLowerCase(); @@ -501,7 +498,7 @@ public OptionalInt parseIntOrBoolean(String tag, Consumer errorHandler) * Checks if a tag contains the specified value. */ public final boolean isTag(String tag, String value) { - return !isTagLess() && value != null && value.equals(tags.get(tag.toLowerCase())); + return !isTagless() && value != null && value.equals(tags.get(tag.toLowerCase())); } /** @@ -736,7 +733,7 @@ public boolean isParkAndRide() { * @return whether the node is a place used to board a public transport vehicle */ public boolean isBoardingLocation() { - if (isTagLess()) { + if (isTagless()) { return false; } return ( diff --git a/application/src/main/java/org/opentripplanner/osm/model/OsmNode.java b/application/src/main/java/org/opentripplanner/osm/model/OsmNode.java index 99c3dbd848c..32a64186a4d 100644 --- a/application/src/main/java/org/opentripplanner/osm/model/OsmNode.java +++ b/application/src/main/java/org/opentripplanner/osm/model/OsmNode.java @@ -52,7 +52,7 @@ public boolean hasCrossingTrafficLight() { */ public boolean isBarrier() { // the majority of nodes have no tags at all, so this yields a good speed-up - if (this.isTagLess()) { + if (this.isTagless()) { return false; } return overridePermissions(ALL) != ALL; @@ -73,7 +73,7 @@ public boolean isStationEntrance() { * @return True if this entity provides an entrance to a platform or similar entity */ public boolean isEntrance() { - if (this.isTagLess()) { + if (this.isTagless()) { return false; } return ( From 8d3c6852e8f81e2b6c0835798484f392e1327ad2 Mon Sep 17 00:00:00 2001 From: Leonard Ehrenfried Date: Wed, 6 May 2026 10:01:32 +0200 Subject: [PATCH 13/17] Small clean up --- .../src/main/java/org/opentripplanner/osm/model/OsmEntity.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/application/src/main/java/org/opentripplanner/osm/model/OsmEntity.java b/application/src/main/java/org/opentripplanner/osm/model/OsmEntity.java index 42892a6fdfa..b37b49e6aa5 100644 --- a/application/src/main/java/org/opentripplanner/osm/model/OsmEntity.java +++ b/application/src/main/java/org/opentripplanner/osm/model/OsmEntity.java @@ -219,7 +219,7 @@ public long getId() { * The tags of an entity (immutable). */ public Map getTags() { - if (this.tags == null) { + if (this.isTagless()) { return Map.of(); } else { return Collections.unmodifiableMap(tags); @@ -800,6 +800,7 @@ public String url() { * Values are split by semicolons. */ public Set getMultiTagValues(Set refTags) { + // we try to keep the allocations low, so only allocate a small hash set by default Set result = HashSet.newHashSet(2); for (var tag : refTags) { var value = getTag(tag); From 5cd160d15e321ed562bae235d5ff23a29ebe36c1 Mon Sep 17 00:00:00 2001 From: Leonard Ehrenfried Date: Wed, 6 May 2026 10:10:09 +0200 Subject: [PATCH 14/17] Review: Simplify if/else --- .../graph_builder/module/osm/OsmModule.java | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/application/src/main/java/org/opentripplanner/graph_builder/module/osm/OsmModule.java b/application/src/main/java/org/opentripplanner/graph_builder/module/osm/OsmModule.java index 857ad989e8a..cc48af22fe7 100644 --- a/application/src/main/java/org/opentripplanner/graph_builder/module/osm/OsmModule.java +++ b/application/src/main/java/org/opentripplanner/graph_builder/module/osm/OsmModule.java @@ -556,10 +556,9 @@ private void buildBarrierEdges(VertexGenerator vertexGenerator) { private Optional getPlatform(OsmDatabase osmdb, OsmWay way) { var references = way.getMultiTagValues(params.boardingAreaRefTags()); - if (!references.isEmpty()) { - if (!way.isBoardingLocation()) { - return Optional.empty(); - } + if (!way.isBoardingLocation() || references.isEmpty()) { + return Optional.empty(); + } else { var nodeRefs = way.getNodeRefs(); var size = nodeRefs.size(); var nodes = new Coordinate[size]; @@ -578,8 +577,6 @@ private Optional getPlatform(OsmDatabase osmdb, OsmWay way) { references ) ); - } else { - return Optional.empty(); } } From 31dc1119026f0aa81de7398a34968e6eedf86a5f Mon Sep 17 00:00:00 2001 From: Leonard Ehrenfried Date: Thu, 7 May 2026 10:36:40 +0200 Subject: [PATCH 15/17] Review: Remove else --- .../graph_builder/module/osm/OsmModule.java | 29 ++++++++----------- 1 file changed, 12 insertions(+), 17 deletions(-) diff --git a/application/src/main/java/org/opentripplanner/graph_builder/module/osm/OsmModule.java b/application/src/main/java/org/opentripplanner/graph_builder/module/osm/OsmModule.java index cc48af22fe7..adca987149b 100644 --- a/application/src/main/java/org/opentripplanner/graph_builder/module/osm/OsmModule.java +++ b/application/src/main/java/org/opentripplanner/graph_builder/module/osm/OsmModule.java @@ -558,26 +558,21 @@ private Optional getPlatform(OsmDatabase osmdb, OsmWay way) { var references = way.getMultiTagValues(params.boardingAreaRefTags()); if (!way.isBoardingLocation() || references.isEmpty()) { return Optional.empty(); - } else { - var nodeRefs = way.getNodeRefs(); - var size = nodeRefs.size(); - var nodes = new Coordinate[size]; - for (int i = 0; i < size; i++) { - nodes[i] = osmdb.getNode(nodeRefs.get(i)).getCoordinate(); - } + } + var nodeRefs = way.getNodeRefs(); + var size = nodeRefs.size(); + var nodes = new Coordinate[size]; + for (int i = 0; i < size; i++) { + nodes[i] = osmdb.getNode(nodeRefs.get(i)).getCoordinate(); + } - var geometryFactory = GeometryUtils.getGeometryFactory(); + var geometryFactory = GeometryUtils.getGeometryFactory(); - var geometry = geometryFactory.createLineString(nodes); + var geometry = geometryFactory.createLineString(nodes); - return Optional.of( - new Platform( - params.edgeNamer().getName(way, "platform " + way.getId()), - geometry, - references - ) - ); - } + return Optional.of( + new Platform(params.edgeNamer().getName(way, "platform " + way.getId()), geometry, references) + ); } private void validateBarriers() { From 17bb76ae3673c38c1daf27534365c541bb0172ea Mon Sep 17 00:00:00 2001 From: Leonard Ehrenfried Date: Thu, 7 May 2026 10:38:19 +0200 Subject: [PATCH 16/17] Add Javadoc --- .../main/java/org/opentripplanner/osm/model/OsmEntity.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/application/src/main/java/org/opentripplanner/osm/model/OsmEntity.java b/application/src/main/java/org/opentripplanner/osm/model/OsmEntity.java index b37b49e6aa5..72561a7700d 100644 --- a/application/src/main/java/org/opentripplanner/osm/model/OsmEntity.java +++ b/application/src/main/java/org/opentripplanner/osm/model/OsmEntity.java @@ -173,6 +173,10 @@ public abstract class OsmEntity { "foot" ); + /// This is nullable for performance reasons. + /// + /// You could use an empty map, but using null allows you to skip the lower-casing and hash lookup + /// per tag, which is the hottest path during OSM processing. @Nullable private final Map tags; From 741db14e085052749829f48dd20df87d903f29bd Mon Sep 17 00:00:00 2001 From: Leonard Ehrenfried Date: Fri, 8 May 2026 09:46:56 +0200 Subject: [PATCH 17/17] Add more comments about isTagless --- .../src/main/java/org/opentripplanner/osm/model/OsmEntity.java | 1 + .../src/main/java/org/opentripplanner/osm/model/OsmNode.java | 1 + 2 files changed, 2 insertions(+) diff --git a/application/src/main/java/org/opentripplanner/osm/model/OsmEntity.java b/application/src/main/java/org/opentripplanner/osm/model/OsmEntity.java index 72561a7700d..b00a2d64f51 100644 --- a/application/src/main/java/org/opentripplanner/osm/model/OsmEntity.java +++ b/application/src/main/java/org/opentripplanner/osm/model/OsmEntity.java @@ -737,6 +737,7 @@ public boolean isParkAndRide() { * @return whether the node is a place used to board a public transport vehicle */ public boolean isBoardingLocation() { + // the majority of nodes have no tags at all, so this yields a good speed-up if (isTagless()) { return false; } diff --git a/application/src/main/java/org/opentripplanner/osm/model/OsmNode.java b/application/src/main/java/org/opentripplanner/osm/model/OsmNode.java index 32a64186a4d..7a4af6b4b51 100644 --- a/application/src/main/java/org/opentripplanner/osm/model/OsmNode.java +++ b/application/src/main/java/org/opentripplanner/osm/model/OsmNode.java @@ -73,6 +73,7 @@ public boolean isStationEntrance() { * @return True if this entity provides an entrance to a platform or similar entity */ public boolean isEntrance() { + // the majority of nodes have no tags at all, so this yields a good speed-up if (this.isTagless()) { return false; }