@@ -471,6 +471,163 @@ public void testStopTimeProxies() throws IOException {
471471 }
472472 }
473473
474+ /**
475+ * Reproduction for issue #409: merging two feeds that share a stop_id but disagree on
476+ * location_type (one is location_type=0 "stop", the other location_type=1 "station") should never
477+ * leave a StopTime referencing the location_type=1 entity, since GTFS trips may only visit
478+ * individual stops, not stations.
479+ *
480+ * <p>Input order: station feed listed first (_oldGtfs), platform feed listed second (_newGtfs).
481+ * GtfsMerger processes the LAST listed feed first (see GtfsMerger#run), so the platform feed's
482+ * stop (location_type=0) is registered as the target stop first. When the station feed's stop
483+ * (location_type=1) is processed, StopMergeStrategy#rejectDuplicateOverDifferences rejects it as
484+ * a duplicate due to the location_type mismatch, so it is kept as a separate, renamed Stop
485+ * instead of being merged.
486+ */
487+ @ Test
488+ public void testLocationTypeMismatch_StationFeedFirstPlatformFeedSecond () throws IOException {
489+ _oldGtfs .putLines (
490+ "agency.txt" ,
491+ "agency_id,agency_name,agency_url,agency_timezone" ,
492+ "1,Metro,http://metro.gov/,America/Los_Angeles" );
493+ _oldGtfs .putLines (
494+ "stops.txt" ,
495+ "stop_id,stop_name,stop_lat,stop_lon,location_type" ,
496+ "100,Stop 100,47.654403,-122.305211,1" );
497+ _oldGtfs .putLines ("routes.txt" , "route_id,route_short_name,route_long_name,route_type" , "" );
498+ _oldGtfs .putLines (
499+ "calendar.txt" ,
500+ "service_id,monday,tuesday,wednesday,thursday,friday,saturday,sunday,start_date,end_date" ,
501+ "" );
502+ _oldGtfs .putLines ("trips.txt" , "route_id,service_id,trip_id" , "" );
503+ _oldGtfs .putLines (
504+ "stop_times.txt" , "trip_id,stop_id,stop_sequence,arrival_time,departure_time" , "" );
505+
506+ _newGtfs .putLines (
507+ "agency.txt" ,
508+ "agency_id,agency_name,agency_url,agency_timezone" ,
509+ "1,Metro,http://metro.gov/,America/Los_Angeles" );
510+ _newGtfs .putLines (
511+ "stops.txt" ,
512+ "stop_id,stop_name,stop_lat,stop_lon,location_type" ,
513+ "100,Stop 100,47.654403,-122.305211,0" );
514+ _newGtfs .putLines (
515+ "routes.txt" , "route_id,route_short_name,route_long_name,route_type" , "R1,1,Route One,3" );
516+ _newGtfs .putLines (
517+ "calendar.txt" ,
518+ "service_id,monday,tuesday,wednesday,thursday,friday,saturday,sunday,start_date,end_date" ,
519+ "sid0,1,1,1,1,1,0,0,20120101,20121231" );
520+ _newGtfs .putLines ("trips.txt" , "route_id,service_id,trip_id" , "R1,sid0,T1" );
521+ _newGtfs .putLines (
522+ "stop_times.txt" ,
523+ "trip_id,stop_id,stop_sequence,arrival_time,departure_time" ,
524+ "T1,100,0,08:00:00,08:00:00" );
525+
526+ StopMergeStrategy stopStrategy = new StopMergeStrategy ();
527+ stopStrategy .setDuplicateDetectionStrategy (EDuplicateDetectionStrategy .IDENTITY );
528+ _merger .setStopStrategy (stopStrategy );
529+
530+ GtfsRelationalDao dao = merge ();
531+
532+ assertEquals (
533+ 2 ,
534+ dao .getAllStops ().size (),
535+ "stops with incompatible location_type must not collapse into one Stop; the station stop"
536+ + " should be kept as a separate, renamed entity" );
537+
538+ boolean foundStopTime = false ;
539+ for (Trip trip : dao .getAllTrips ()) {
540+ for (StopTime st : dao .getStopTimesForTrip (trip )) {
541+ foundStopTime = true ;
542+ Stop mergedStop = (Stop ) st .getStop ();
543+ assertEquals (
544+ 0 ,
545+ mergedStop .getLocationType (),
546+ "GTFS spec: trips must reference location_type=0 stops, not stations (location_type="
547+ + mergedStop .getLocationType ()
548+ + ")" );
549+ }
550+ }
551+ assertTrue (foundStopTime , "expected at least one merged stop_time" );
552+ }
553+
554+ /**
555+ * Same reproduction as {@link #testLocationTypeMismatch_StationFeedFirstPlatformFeedSecond}, with
556+ * input order reversed: platform feed listed first (_oldGtfs), station feed listed second
557+ * (_newGtfs). GtfsMerger processes the LAST listed feed first, so the station feed's stop
558+ * (location_type=1) is registered as the target stop first. When the platform feed's stop
559+ * (location_type=0, which owns the StopTime) is processed,
560+ * StopMergeStrategy#rejectDuplicateOverDifferences rejects it as a duplicate due to the
561+ * location_type mismatch, so it is kept as a separate, renamed Stop and the StopTime keeps
562+ * referencing it rather than being repointed onto the station.
563+ */
564+ @ Test
565+ public void testLocationTypeMismatch_PlatformFeedFirstStationFeedSecond () throws IOException {
566+ _oldGtfs .putLines (
567+ "agency.txt" ,
568+ "agency_id,agency_name,agency_url,agency_timezone" ,
569+ "1,Metro,http://metro.gov/,America/Los_Angeles" );
570+ _oldGtfs .putLines (
571+ "stops.txt" ,
572+ "stop_id,stop_name,stop_lat,stop_lon,location_type" ,
573+ "100,Stop 100,47.654403,-122.305211,0" );
574+ _oldGtfs .putLines (
575+ "routes.txt" , "route_id,route_short_name,route_long_name,route_type" , "R1,1,Route One,3" );
576+ _oldGtfs .putLines (
577+ "calendar.txt" ,
578+ "service_id,monday,tuesday,wednesday,thursday,friday,saturday,sunday,start_date,end_date" ,
579+ "sid0,1,1,1,1,1,0,0,20120101,20121231" );
580+ _oldGtfs .putLines ("trips.txt" , "route_id,service_id,trip_id" , "R1,sid0,T1" );
581+ _oldGtfs .putLines (
582+ "stop_times.txt" ,
583+ "trip_id,stop_id,stop_sequence,arrival_time,departure_time" ,
584+ "T1,100,0,08:00:00,08:00:00" );
585+
586+ _newGtfs .putLines (
587+ "agency.txt" ,
588+ "agency_id,agency_name,agency_url,agency_timezone" ,
589+ "1,Metro,http://metro.gov/,America/Los_Angeles" );
590+ _newGtfs .putLines (
591+ "stops.txt" ,
592+ "stop_id,stop_name,stop_lat,stop_lon,location_type" ,
593+ "100,Stop 100,47.654403,-122.305211,1" );
594+ _newGtfs .putLines ("routes.txt" , "route_id,route_short_name,route_long_name,route_type" , "" );
595+ _newGtfs .putLines (
596+ "calendar.txt" ,
597+ "service_id,monday,tuesday,wednesday,thursday,friday,saturday,sunday,start_date,end_date" ,
598+ "" );
599+ _newGtfs .putLines ("trips.txt" , "route_id,service_id,trip_id" , "" );
600+ _newGtfs .putLines (
601+ "stop_times.txt" , "trip_id,stop_id,stop_sequence,arrival_time,departure_time" , "" );
602+
603+ StopMergeStrategy stopStrategy = new StopMergeStrategy ();
604+ stopStrategy .setDuplicateDetectionStrategy (EDuplicateDetectionStrategy .IDENTITY );
605+ _merger .setStopStrategy (stopStrategy );
606+
607+ GtfsRelationalDao dao = merge ();
608+
609+ assertEquals (
610+ 2 ,
611+ dao .getAllStops ().size (),
612+ "stops with incompatible location_type must not collapse into one Stop; the platform stop"
613+ + " should be kept as a separate, renamed entity" );
614+
615+ boolean foundStopTime = false ;
616+ for (Trip trip : dao .getAllTrips ()) {
617+ for (StopTime st : dao .getStopTimesForTrip (trip )) {
618+ foundStopTime = true ;
619+ Stop mergedStop = (Stop ) st .getStop ();
620+ assertEquals (
621+ 0 ,
622+ mergedStop .getLocationType (),
623+ "GTFS spec: trips must reference location_type=0 stops, not stations (location_type="
624+ + mergedStop .getLocationType ()
625+ + ")" );
626+ }
627+ }
628+ assertTrue (foundStopTime , "expected at least one merged stop_time" );
629+ }
630+
474631 private GtfsRelationalDao merge () throws IOException {
475632 List <File > paths = new ArrayList <>();
476633 paths .add (_oldGtfs .getPath ());
0 commit comments