1010
1111import java .time .Duration ;
1212import java .time .format .DateTimeParseException ;
13- import java .util .Arrays ;
1413import java .util .Collections ;
1514import java .util .HashMap ;
15+ import java .util .HashSet ;
1616import java .util .Map ;
1717import java .util .Objects ;
1818import java .util .Optional ;
2121import java .util .function .Consumer ;
2222import java .util .regex .Matcher ;
2323import java .util .regex .Pattern ;
24- import java .util .stream .Collectors ;
2524import javax .annotation .Nullable ;
2625import org .opentripplanner .core .model .accessibility .Accessibility ;
2726import org .opentripplanner .core .model .i18n .I18NString ;
3736 */
3837public abstract class OsmEntity {
3938
39+ private static final Pattern I18N_PATTERN = Pattern .compile ("\\ {(.*?)}" );
4040 /**
4141 * highway=* values that we don't want to even consider when building the graph.
4242 */
@@ -133,6 +133,24 @@ public abstract class OsmEntity {
133133 */
134134 protected static final Set <String > CHECKED_MODES = Set .of ("foot" , "bicycle" , "motorcar" );
135135
136+ private static final Set <String > WALK_ONLY_HIGHWAY_VALUES = Set .of ("footway" , "step" , "corridor" );
137+ private static final Set <String > NO_ACCESS_VALUES = Set .of ("no" , "license" , "dismount" );
138+ private static final Set <String > HIGHWAY_BOARDING_LOCATION_VALUES = Set .of (
139+ "platform" ,
140+ "bus_stop"
141+ );
142+ private static final Set <String > RAILWAY_BOARDING_LOCATION_VALUES = Set .of (
143+ "tram_stop" ,
144+ "station" ,
145+ "halt"
146+ );
147+ private static final Set <String > AMENITY_BOARDING_LOCATION_VALUES = Set .of (
148+ "bus_station" ,
149+ "amenity" ,
150+ "ferry_terminal"
151+ );
152+ private static final Set <String > RAILWAY_PLATFORM_VALUES = Set .of ("platform" , "platform_edge" );
153+
136154 /**
137155 * Mapping for the fallback key for checking access restrictions for each access mode in OSM
138156 * However, access is not included because we are skeptical of access=yes tags.
@@ -145,8 +163,6 @@ public abstract class OsmEntity {
145163 "bicycle" ,
146164 "vehicle"
147165 );
148- private static final Set <String > WALK_ONLY_HIGHWAYS = Set .of ("footway" , "step" , "corridor" );
149- private static final Set <String > NO_ACCESS_TAGS = Set .of ("no" , "license" , "dismount" );
150166 private static final Map <StreetTraversalPermission , String > OSM_TAGS_FOR_TRAVERSAL_PERMISSION =
151167 Map .of (
152168 StreetTraversalPermission .CAR ,
@@ -157,6 +173,11 @@ public abstract class OsmEntity {
157173 "foot"
158174 );
159175
176+ /// This is nullable for performance reasons.
177+ ///
178+ /// You could use an empty map, but using null allows you to skip the lower-casing and hash lookup
179+ /// per tag, which is the hottest path during OSM processing.
180+ @ Nullable
160181 private final Map <String , String > tags ;
161182
162183 protected final long id ;
@@ -175,7 +196,7 @@ public abstract class OsmEntity {
175196 /**
176197 * Constructor for immutable OsmEntity
177198 */
178- protected OsmEntity (long id , Map <String , String > tags , OsmProvider osmProvider ) {
199+ protected OsmEntity (long id , @ Nullable Map <String , String > tags , OsmProvider osmProvider ) {
179200 this .id = id ;
180201 // calling Map.copyOf here costs about 10% of parsing performance, so we use
181202 // Collections.unmodifiableMap in the getter
@@ -202,20 +223,31 @@ public long getId() {
202223 * The tags of an entity (immutable).
203224 */
204225 public Map <String , String > getTags () {
205- return Collections .unmodifiableMap (tags );
226+ if (this .isTagless ()) {
227+ return Map .of ();
228+ } else {
229+ return Collections .unmodifiableMap (tags );
230+ }
206231 }
207232
208233 /**
209234 * Is the tag defined?
210235 */
211- public boolean hasTag (String tag ) {
212- return getTag (tag ) != null ;
236+ public final boolean hasTag (String tag ) {
237+ return !this .isTagless () && getTag (tag ) != null ;
238+ }
239+
240+ /**
241+ * Does the entity contain any tags at all?
242+ */
243+ final boolean isTagless () {
244+ return this .tags == null ;
213245 }
214246
215247 /**
216248 * Determines if a tag contains a false value. 'no', 'false', and '0' are considered false.
217249 */
218- public boolean isTagFalse (String tag ) {
250+ public final boolean isTagFalse (String tag ) {
219251 return isFalse (getTag (tag ));
220252 }
221253
@@ -235,17 +267,10 @@ public Accessibility explicitWheelchairAccessibility() {
235267 /**
236268 * Determines if a tag contains a true value. 'yes', 'true', and '1' are considered true.
237269 */
238- public boolean isTagTrue (String tag ) {
270+ public final boolean isTagTrue (String tag ) {
239271 return isTrue (getTag (tag ));
240272 }
241273
242- /**
243- * Returns true if bicycle dismounts are forced.
244- */
245- public boolean isBicycleDismountForced () {
246- return isTag ("bicycle" , "dismount" );
247- }
248-
249274 public boolean isSidewalk () {
250275 return isTag ("footway" , "sidewalk" ) && isTag ("highway" , "footway" );
251276 }
@@ -270,6 +295,9 @@ protected Optional<Permission> checkModePermission(String mode) {
270295 * or a parent mode, either with a directional suffix or not, empty if it is not specified.
271296 */
272297 protected Optional <Permission > checkModePermission (String mode , TraverseDirection direction ) {
298+ if (isTagless ()) {
299+ return Optional .empty ();
300+ }
273301 // check if the exact directional tag allows or denies access
274302 if (direction != DIRECTIONLESS ) {
275303 if (isExplicitlyAllowed (mode + direction .tagSuffix ())) {
@@ -309,7 +337,10 @@ protected boolean isExplicitlyAllowed(String key) {
309337 * Returns null if tag is not present.
310338 */
311339 @ Nullable
312- public String getTag (String tag ) {
340+ public final String getTag (String tag ) {
341+ if (this .isTagless ()) {
342+ return null ;
343+ }
313344 tag = tag .toLowerCase ();
314345 return tags .get (tag );
315346 }
@@ -470,8 +501,8 @@ public OptionalInt parseIntOrBoolean(String tag, Consumer<String> errorHandler)
470501 /**
471502 * Checks if a tag contains the specified value.
472503 */
473- public boolean isTag (String tag , String value ) {
474- return value != null && value .equals (tags .get (tag .toLowerCase ()));
504+ public final boolean isTag (String tag , String value ) {
505+ return ! isTagless () && value != null && value .equals (tags .get (tag .toLowerCase ()));
475506 }
476507
477508 /**
@@ -492,7 +523,7 @@ public boolean isOneOfTags(String key, Set<String> oneOfTags) {
492523 */
493524 @ Nullable
494525 public I18NString getAssumedName () {
495- if (tags . containsKey ("name" )) {
526+ if (hasTag ("name" )) {
496527 return TranslatedString .getDeduplicatedI18NString (
497528 this .generateI18NForPattern ("{name}" ),
498529 false
@@ -506,8 +537,9 @@ public I18NString getAssumedName() {
506537 if (creativeName != null ) {
507538 return this .creativeName ;
508539 }
509- if (tags .containsKey ("ref" )) {
510- return new NonLocalizedString (tags .get ("ref" ));
540+ var ref = getTag ("ref" );
541+ if (ref != null ) {
542+ return new NonLocalizedString (ref );
511543 }
512544 return null ;
513545 }
@@ -524,7 +556,7 @@ public I18NString getAssumedName() {
524556 public Map <String , String > generateI18NForPattern (String pattern ) {
525557 Map <String , StringBuffer > i18n = new HashMap <>();
526558 i18n .put (null , new StringBuffer ());
527- Matcher matcher = Pattern . compile ( " \\ {(.*?)}" ) .matcher (pattern );
559+ Matcher matcher = I18N_PATTERN .matcher (pattern );
528560
529561 int lastEnd = 0 ;
530562 while (matcher .find ()) {
@@ -621,7 +653,7 @@ public Optional<TraverseDirection> isOneWay(@Nullable String mode) {
621653 return Optional .empty ();
622654 }
623655
624- if ("foot" .equals (mode ) && !isOneOfTags ("highway" , WALK_ONLY_HIGHWAYS )) {
656+ if ("foot" .equals (mode ) && !isOneOfTags ("highway" , WALK_ONLY_HIGHWAY_VALUES )) {
625657 return Optional .empty ();
626658 }
627659
@@ -705,14 +737,14 @@ public boolean isParkAndRide() {
705737 * @return whether the node is a place used to board a public transport vehicle
706738 */
707739 public boolean isBoardingLocation () {
740+ // the majority of nodes have no tags at all, so this yields a good speed-up
741+ if (isTagless ()) {
742+ return false ;
743+ }
708744 return (
709- isTag ("highway" , "bus_stop" ) ||
710- isTag ("railway" , "tram_stop" ) ||
711- isTag ("railway" , "station" ) ||
712- isTag ("railway" , "halt" ) ||
713- isTag ("amenity" , "bus_station" ) ||
714- isTag ("amenity" , "ferry_terminal" ) ||
715- isTag ("highway" , "platform" ) ||
745+ isOneOfTags ("highway" , HIGHWAY_BOARDING_LOCATION_VALUES ) ||
746+ isOneOfTags ("railway" , RAILWAY_BOARDING_LOCATION_VALUES ) ||
747+ isOneOfTags ("amenity" , AMENITY_BOARDING_LOCATION_VALUES ) ||
716748 isPlatform ()
717749 );
718750 }
@@ -726,9 +758,7 @@ public boolean isBoardingLocation() {
726758 **/
727759 public boolean isPlatform () {
728760 var isPlatform =
729- isTag ("public_transport" , "platform" ) ||
730- isTag ("railway" , "platform" ) ||
731- isTag ("railway" , "platform_edge" );
761+ isTag ("public_transport" , "platform" ) || isOneOfTags ("railway" , RAILWAY_PLATFORM_VALUES );
732762 return isPlatform && !isTag ("usage" , "tourism" );
733763 }
734764
@@ -775,14 +805,21 @@ public String url() {
775805 * Values are split by semicolons.
776806 */
777807 public Set <String > getMultiTagValues (Set <String > refTags ) {
778- return refTags
779- .stream ()
780- .map (this ::getTag )
781- .filter (Objects ::nonNull )
782- .flatMap (v -> Arrays .stream (v .split (";" )))
783- .map (String ::strip )
784- .filter (v -> !v .isBlank ())
785- .collect (Collectors .toUnmodifiableSet ());
808+ // we try to keep the allocations low, so only allocate a small hash set by default
809+ Set <String > result = HashSet .newHashSet (2 );
810+ for (var tag : refTags ) {
811+ var value = getTag (tag );
812+ if (value == null ) {
813+ continue ;
814+ }
815+ for (var part : value .split (";" )) {
816+ var stripped = part .strip ();
817+ if (!stripped .isBlank ()) {
818+ result .add (stripped );
819+ }
820+ }
821+ }
822+ return result ;
786823 }
787824
788825 public OsmProvider getOsmProvider () {
@@ -842,10 +879,11 @@ public boolean isElevator() {
842879 * of other information.
843880 */
844881 public boolean isWheelchairAccessible () {
845- if (isTagTrue ("wheelchair" )) {
882+ var wheelchairValue = getTag ("wheelchair" );
883+ if (isTrue (wheelchairValue )) {
846884 return true ;
847885 }
848- if (isTagFalse ( "wheelchair" )) {
886+ if (isFalse ( wheelchairValue )) {
849887 return false ;
850888 }
851889 if (isOneOfTags ("barrier" , WHEELCHAIR_INACCESSIBLE_BARRIERS )) {
@@ -891,7 +929,7 @@ public boolean isExplicitlyUnnamed() {
891929 * Returns true if this tag is explicitly access to this entity.
892930 */
893931 private boolean isExplicitlyDenied (String key ) {
894- return isOneOfTags (key , NO_ACCESS_TAGS );
932+ return isOneOfTags (key , NO_ACCESS_VALUES );
895933 }
896934
897935 public StreetTraversalPermission getPermission () {
0 commit comments