wiki:Rules/PublicTransportGtfs

Version 36 (modified by skyper, 3 weeks ago) (diff)

"s" is a year separator in ids, too.

Public Transport GTFS

These rules add additional checks for public transportation tagging. One major part are tags in use with GTFS and PTNA (OSM-Wiki, website).

See Public Transport GTFS preset for a corresponding preset.

Help needed

Please, feel free to:

Notes

  • Really beta so far and under development. Expect false positives and lots of changes.
  • The tagging schema is still under heavy development and therefore the rules might change quite often.

Supported Tags

So far checks in the categories Missing tags, Conflicting tags and Value syntax for the following tags exists.

Rules source code

meta {
  title: "Public Transport GTFS";
  link: "https://josm.openstreetmap.de/wiki/Rules/PublicTransportGtfs";
  description: "Special rules for Public transportation supporting GTFS and tags for PTNA. ";
  author: "skyper";
  version: "0.0.36_2021-02-19";
}

/*  -----------------------
 *
 *       Classes
 */

relation[type=route_master][route_master =~ /^(bus|coach|share_taxi|trolleybus|train|light_rail|subway|tram|monorail|funicular|ferry|aerialway|school_bus|walking_bus)$/] {
  set PtRouteMaster;
}
/* Note: Only relations, routes on way are not supported  */
relation[type=route              ][route        =~ /^(bus|coach|share_taxi|trolleybus|train|light_rail|subway|tram|monorail|funicular|ferry|aerialway|school_bus|walking_bus)$/] {
  set PtRoute;
}

relation[gtfs:route_id][type=route_master] > * {
  set ChildMasterRoute_id;
}

*[bus!=yes][coach!=yes][share_taxi!=yes][trolleybus!=yes][train!=yes][light_rail!=yes][subway!=yes][tram!=yes][monorail!=yes][funicular!=yes][ferry!=yes][!aerialway] {
  set noPtVehicle;
}

*[public_transport =~ /^(platform|stop_area|stop_position)$/] {
  set PtStop;
}

/*  -----------------------
 *
 *        Rules
 */

/*  -----------------------
 *
 *  missing tag
 */

/* {0.tag} without {1.key} (error level) */

/* route_master and route relations */

/* {0.tag} without {1.key} (warning level) */

/* stop_area, stop_position, platform */
*[public_transport =~ /^(platform|stop_position)$/][public_transport][!local_ref],
*.PtStop[public_transport][!network] {
  throwWarning: tr("`{0}` without `{1}*`.", "{1.tag}", "{2.tag}");
  group: tr("Public Transport GTFS: missing tag");
  assertMatch:   "node public_transport=stop_position";
  assertMatch:   "way public_transport=platform";
  assertNoMatch: "node public_transport=stop_position network=a local_ref=2";
}

/* route_master and route relations */
relation.PtRouteMaster[type=route_master][!colour],
relation.PtRouteMaster[type=route_master][!fee],
relation.PtRouteMaster[type=route_master][!gtfs:name],
relation.PtRouteMaster[type=route_master][!gtfs:route_id],
relation.PtRouteMaster[type=route_master][!network],
relation.PtRouteMaster[type=route_master][!operator],
relation.PtRouteMaster[type=route_master][!ref],
relation.PtRouteMaster[type=route_master][!source],
relation.PtRouteMaster[type=route_master][!website],
relation.PtRoute[type=route][!colour],
relation.PtRoute[type=route][!duration]["public_transport:version"=2],
relation.PtRoute[type=route][!interval]["public_transport:version"=2],
relation.PtRoute[type=route][!fee],
relation.PtRoute[type=route][!from]["public_transport:version"=2],
relation.PtRoute[type=route][!to]["public_transport:version"=2],
relation.PtRoute[type=route][!gtfs:name]["public_transport:version"=1],
relation.PtRoute[type=route][!gtfs:route_id],
relation.PtRoute[type=route][!network],
relation.PtRoute[type=route][!opening_hours],
relation.PtRoute[type=route][!operator],
relation.PtRoute[type=route][!ref],
relation.PtRoute[type=route][!source],
relation.PtRoute[type=route][!website] {
  throwWarning: tr("Public transport {0} relation without `{1}*`.", "{1.value}", "{2.tag}");
  group: tr("Public Transport GTFS: missing tag");
  assertMatch:   "relation type=route route=train";
  assertMatch:   "relation type=route_master route_master=bus";
/* Does not make sense with that many keys above */
/*  assertNoMatch: "relation type=route route=train network:short=MVV";
 *  assertNoMatch: "relation type=route_master route_master=bus colour=green";
 */
}

/* {0.tag} without {1.key} (info level) */

/* route_master and route relations */
relation.PtRouteMaster[type=route_master][!network:short][network],
relation.PtRouteMaster[type=route_master][!operator:short][operator],
relation.PtRoute[type=route][!network:short][network],
relation.PtRoute[type=route][!operator:short][operator] {
  throwOther: tr("Public transport {0} relation without `{1}*`.", "{1.value}", "{2.tag}");
  group: tr("Public Transport GTFS: missing tag");
  assertMatch:   "relation type=route route=train operator=A";
  assertMatch:   "relation type=route_master route_master=bus operator=A";
  assertNoMatch: "relation type=route route=train";
  assertNoMatch: "relation type=route_master route_master=bus";

}

/* {0.tag} without {1.key} or {2.key} (warning level) */

/* stop_area, stop_position, platform */
*.PtStop[!ref:IFOPT][!gtfs:stop_id][public_transport] {
  throwWarning: tr("`{0}` without `{1}*` or `{2}*`.", "{3.tag}", "{1.tag}", "{2.tag}");
  group: tr("Public Transport GTFS: missing tag");
  assertMatch:   "node public_transport=stop_position train=yes railway=halt";
  assertNoMatch: "node public_transport=stop_position train=yes railway=stop ref:IFOPT=ch:06666:4";
}

/* {0.key} without {1.key}, {2.key} or {3.key} (warning level) */

/* stop_area, stop_position, platform */
*.PtStop[ref:IFOPT   ][!gtfs:feed][!network:guid][!operator:guid],
*.PtStop[gtfs:stop_id][!gtfs:feed][!network:guid][!operator:guid] {
  throwWarning: tr("Public transport stop with `{0}=*` but none of `{1}*`, `{2}*` or `{3}*`.", "{1.key}", "{2.tag}", "{3.tag}", "{4.tag}");
  group: tr("Public Transport GTFS: missing tag");
  assertMatch:   "node public_transport=stop_position ref:IFOPT=A";
  assertNoMatch: "node public_transport=stop_position ref:IFOPT=A gtfs:feed=A";
}

/* route_master and route relations */
relation["public_transport:version"=2].PtRoute[!gtfs:shape_id][!gtfs:trip_id][!gtfs:trip_id:sample],
relation[gtfs:route_id].PtRouteMaster[!gtfs:feed][!network:guid][!operator:guid],
relation[gtfs:route_id      ].PtRoute[!gtfs:feed][!network:guid][!operator:guid],
relation[gtfs:shape_id      ].PtRoute[!gtfs:feed][!network:guid][!operator:guid],
relation[gtfs:trip_id       ].PtRoute[!gtfs:feed][!network:guid][!operator:guid],
relation[gtfs:trip_id:sample].PtRoute[!gtfs:feed][!network:guid][!operator:guid] {
  throwWarning: tr("Public transport relation with `{0}` but none of `{1}*`, `{2}*` or `{3}*`.", "{0.tag}", "{2.tag}", "{3.tag}", "{4.tag}");
  group: tr("Public Transport GTFS: missing tag");
  assertMatch:   "relation type=route_master route_master=train gtfs:route_id=0-7-242-19j-1";
  assertNoMatch: "relation type=route_master route_master=train gtfs:route_id=0-7-242-19j-1 network:guid=DE-BY-MVV";
  assertMatch:   "relation type=route route=bus gtfs:shape_id=0-7-242-19j-1.26.R";
  assertNoMatch: "relation type=route_master route=bus";
}

/* {0.tag} without any (warning level) */

/* stop_position */
/* ToDo: Check against parent relation vehicle type */
/* ToDo: Extend to check for parent relation value and offer fix */
/* *[public_transport =~ /^(platform|stop_position)$/][highway!=bus_stop][public_transport].noPtVehicle { */
/* needs check the other way around as access tags are problematic with ways */
node[public_transport=stop_position][highway!=bus_stop].noPtVehicle {
  throwWarning: tr("`{0}` without serving vehicle type specified.", "{0.tag}");
  group: tr("Public Transport GTFS: missing tag");
  assertMatch:   "node public_transport=stop_position";
  assertNoMatch: "node public_transport=stop_position bus=yes";
  assertNoMatch: "node public_transport=stop_position highway=bus_stop";
} 

/* {0.tag} without any (info level) */

/* platform */
/* node[public_transport=platform][highway!=bus_stop].noPtVehicle {
  throwInfo: tr("`{0}` without serving vehicle type specified.", "{0.tag}");
  group: tr("Public Transport GTFS: missing tag");
  assertMatch:   "node public_transport=platform";
  assertNoMatch: "node public_transport=platform bus=yes";
  assertNoMatch: "node public_transport=platform highway=bus_stop";
} */

/* {0.tag} and {1.tag} without {2.key} (warning level) */

/* stop_position, platform */
node[public_transport=stop_position][train=yes][railway!=stop][public_transport],
*[public_transport =~ /^(platform|stop_position)$/][train=yes][!ref][public_transport] {
  throwWarning: tr("`{0}` with `{1}` but without `{2}`.", "{3.tag}", "{1.tag}", "{2.tag}");
  group: tr("Public Transport GTFS: missing tag");
  assertMatch:   "node public_transport=stop_position train=yes railway=halt";
  assertNoMatch: "node public_transport=stop_position train=yes railway=stop ref=2";
}

/* {0.tag} and {1.tag} without {2.key}, {3.key} or {4.key} (warning level) */

/* platform */
*[highway=bus_stop][public_transport =~ /^(platform|stop_position)$/][bus!=yes][share_taxi!=yes][trolleybus!=yes][public_transport] {
  throwWarning: tr("`{0}` with `{1}` but none of `{2}*`, `{3}*` or `{4}*`.", "{5.tag}", "{0.tag}", "{2.tag}", "{3.tag}", "{4.tag}");
  group: tr("Public Transport GTFS: missing tag");
  assertMatch:   "node public_transport=platform highway=bus_stop";
  assertNoMatch: "node public_transport=platform highway=bus_stop bus=yes";
}

/* One of many {0.key} but no {1.key} */

/* gtfs:release_date */
*[/(^gtfs:|:gtfs|^ref:IFOPT)/][!gtfs:release_date] {
  throwWarning: tr("GTFS tag without `{0}*`.", "{1.tag}");
  group: tr("Public Transport GTFS: missing tag");
  assertMatch:   "node gtfs:route_id=1";
  assertMatch:   "node note:gtfs=A";
  assertMatch:   "node ref:IFOPT=1";
  assertNoMatch: "node ref:IFOPT=1 gtfs:release_date=2019";
} 

/*  -----------------------
 *  value syntax 
 */

/* route_id, shape_id, trip_id */
/* FIXME: Split checks per feed instead of one global regex.
/* FIXME: Get proper syntax as regex displayed. */
*[gtfs:route_id      ][gtfs:route_id       !~                           /^[0-9]{1,2}-[A-Z]?[0-9]{1,3}[A-Z]?(-[0-9A-Z])?-[js][1-9][0-9j]-[0-9]+(;[ ]?[0-9]{1,2}-[A-Z]?[0-9]{1,3}[A-Z]?(-[0-9A-Z])?-[js][1-9][0-9j]-[0-9]+)*$/],
*[gtfs:shape_id      ][gtfs:shape_id       !~                           /^[0-9]{1,2}-[A-Z]?[0-9]{1,3}[A-Z]?(-[0-9A-Z])?-[js][1-9][0-9j]-[0-9]+\.[1-9][0-9]{0,2}\.[HR](;[ ]?[0-9]{1,2}-[A-Z]?[0-9]{1,3}[A-Z]?(-[0-9A-Z])?-[js][1-9][0-9j]-[0-9]+\.[1-9][0-9]{0,2}\.[HR])*$/],
*[gtfs:trip_id       ][gtfs:trip_id        !~ /^[1-9][0-9]{0,3}\.T[023A]\.[0-9]{1,2}-[A-Z]?[0-9]{1,3}[A-Z]?(-[0-9A-Z])?-[js][1-9][0-9j]-[0-9]+\.[1-9][0-9]{0,2}\.[HR](;[ ]?[1-9][0-9]{0,3}\.T[023A]\.[0-9]{1,2}-[A-Z]?[0-9]{1,3}[A-Z]?(-[0-9A-Z])?-[js][1-9][0-9j]-[0-9]+\.[1-9][0-9]{0,2}\.[HR])*$/],
*[gtfs:trip_id:sample][gtfs:trip_id:sample !~ /^[1-9][0-9]{0,3}\.T[023A]\.[0-9]{1,2}-[A-Z]?[0-9]{1,3}[A-Z]?(-[0-9A-Z])?-[js][1-9][0-9j]-[0-9]+\.[1-9][0-9]{0,2}\.[HR](;[ ]?[1-9][0-9]{0,3}\.T[023A]\.[0-9]{1,2}-[A-Z]?[0-9]{1,3}[A-Z]?(-[0-9A-Z])?-[js][1-9][0-9j]-[0-9]+\.[1-9][0-9]{0,2}\.[HR])*$/] {
  throwError: tr("Value `{0}` for `{1}=*` does not match value syntax.", "{0.value}", "{0.key}");
/*  throwError: tr("Value `{0}` for `{1}=*` does not match value syntax `{2}`.", "{0.value}", "{0.key}", "{1.value}"); */
  group: tr("Public Transport GTFS: value syntax");
  set GtfsIdSyntax;
  assertMatch:   "relation gtfs:route_id=7-342-j1j-1.H";
  assertNoMatch: "relation gtfs:route_id=92-731-2-j21-1";               /* CH */
  assertNoMatch: "relation gtfs:route_id=7-342-j1j-1";                  /* DE-BW-SBG */
  assertNoMatch: "relation gtfs:route_id=7-342-j1j-1;7-342-j1j-5";
  assertNoMatch: "relation gtfs:route_id=90-742-B-j20-1";               /* DE-BW-SWEG */
  assertNoMatch: "relation gtfs:route_id=10-11-I-j20-1";                /* DE-BW-VAG */
  assertMatch:   "relation gtfs:shape_id=11-4-I-j20-1.23.";
  assertNoMatch: "relation gtfs:shape_id=11-4-I-j20-1.23.H";            /* DE-BW-VAG */
  assertNoMatch: "relation gtfs:shape_id=92-R01-F-j20-1.117.R";         /* DE-SPNV */
  assertNoMatch: "relation gtfs:shape_id=0-S3-E-j20-2.1.R";             /* DE-BW-SWEG */
  assertMatch:   "relation gtfs:trip_id=1108.T2.11-4-I-j20-1.10.";
  assertNoMatch: "relation gtfs:trip_id=1108.T2.11-4-I-j20-1.10.H";     /* DE-BW-VAG */
  assertNoMatch: "relation gtfs:trip_id=29.T0.7-342-j1j-1.50.H";        /* DE-BW-SBG */
  assertNoMatch: "relation gtfs:trip_id=2.TA.90-742-B-j20-1.4.R";       /* DE-BW-SWEG */
}
/* gtfs:feed, *:guid, gtfs:release_date */
/* FIXME: Get proper list of valid values. */
/* FIXME: Get proper syntax as regex displayed. */
*[gtfs:feed    ][gtfs:feed     !~ /^(([A-Z]{2}-){2}[a-zA-Z]{2}.*|[A-Z]{2}-(Alle|Flixbus)|BO-C-Cochabamba|(CH|LU)-[a-zA-Z]{3,}|CO-BOY-[A-Z].+|DE-SPNV|DE-S-und-U-Bahnen|ES-AR-Z-[A-Z].+|FR-IDF-(r|[a-zA-Z]{3,}.*)|MG-T-Antananarivo)$/],
*[network:guid ][network:guid  !~ /^(([A-Z]{2}-){2}[a-zA-Z]{2}.*|[A-Z]{2}-Flixbus|BO-C-Cochabamba|(CH|LU)-[a-zA-Z]{3,}|CO-BOY-[A-Z].+|ES-AR-Z-[A-Z].+|FR-IDF-(r|[a-zA-Z]{3,}.*)|MG-T-Antananarivo)$/],
*[operator:guid][operator:guid !~ /^(([A-Z]{2}-){2}[a-zA-Z]{2}.*|[A-Z]{2}-Flixbus|BO-C-Cochabamba|(CH|LU)-[a-zA-Z]{3,}|CO-BOY-[A-Z].+|ES-AR-Z-[A-Z].+|FR-IDF-(r|[a-zA-Z]{3,}.*)|MG-T-Antananarivo)$/],
*[gtfs:release_date][gtfs:release_date !~ /^20(1[7-9]|2[01])-(0[1-9]|1[0-2])-(3[01]|[12][0-9]|0[1-9])$/] {
  throwError: tr("Value `{0}` for `{1}=*` does not match value syntax.", "{0.value}", "{0.key}");
/*  throwError: tr("Value `{0}` for `{1}=*` does not match value syntax `{2}`.", "{0.value}", "{0.key}", "{1.value}"); */
  group: tr("Public Transport GTFS: value syntax");
  assertMatch:   "relation gtfs:feed=DE-SH.NAH";
  assertMatch:   "relation gtfs:feed=df-SL-saarVV";
  assertNoMatch: "relation gtfs:feed=DE-SH-NAH.SH";
  assertNoMatch: "relation gtfs:feed=DE-BY-VVM-Mittelschwaben";
  assertNoMatch: "relation gtfs:feed=AU-SA-Adelaide-Metro";
  assertNoMatch: "relation gtfs:feed=DE-SL-saarVV";
  assertMatch:   "relation gtfs:release_date=2016-09-30";
  assertMatch:   "relation gtfs:release_date=2020-9-3";
  assertMatch:   "relation gtfs:release_date=2016-09-30";
  assertNoMatch: "relation gtfs:release_date=2020-09-30";
}
/* ref:IFOPT, gtfs:stop_id */
/* FIXME: Get proper syntax as regex displayed. */
/* *[ref:IFOPT   ][ref:IFOPT    !~ /^(gen:[0-9]{4}|[a-z]{2}:[0-9]{5}):[1-9][0-9]{0,4}(:[0-9]{1,2}(:([A-Z]+[ ]?)?[1-9][0-9]{0,2}[A-Z]?)?)?$/], */
/* *[gtfs:stop_id][gtfs:stop_id !~ /^(gen:[0-9]{4}|[a-z]{2}:[0-9]{4,5}):[1-9][0-9]{0,4}(:[0-9]{1,2}(:([A-Z]+[ ]?)?[1-9][0-9]{0,2}[A-Z]?)?)?$/] { */
*[ref:IFOPT   ][ref:IFOPT    !~ /^(gen:[0-9]{4}|[a-z]{2}:[0-9]{5}):[1-9][0-9]{0,4}(:[0-9]{1,2}(:.+)?)?$/],
*[gtfs:stop_id][gtfs:stop_id !~ /^(gen:[0-9]{4}|[a-z]{2}:[0-9]{4,5}):[1-9][0-9]{0,4}(:[0-9]{1,2}(:.+)?)?$/] {
  throwError: tr("Value `{0}` for `{1}=*` does not match value syntax.", "{0.value}", "{0.key}");
/*  throwError: tr("Value `{0}` for `{1}=*` does not match value syntax `{2}`.", "{0.value}", "{0.key}", "{1.value}"); */
  group: tr("Public Transport GTFS: value syntax");
  set ErrorSyntaxIFOPT;
  assertMatch:   "node ref:IFOPT=+1";
  assertMatch:   "node ref:IFOPT=aa:aa:09";
  assertMatch:   "node ref:IFOPT=ch:3001:64883";
  assertMatch:   "node ref:IFOPT=gen:91888:5599:0:956R";
  assertNoMatch: "node ref:IFOPT=ch:23001:64883";
  assertNoMatch: "node ref:IFOPT=de:08315:6504:0:14";
  assertNoMatch: "node ref:IFOPT=gen:9188:5599:0:956R";
  assertNoMatch: "node ref:IFOPT=de:09162:1179:3:KIF 1";
  assertNoMatch: "node ref:IFOPT=ch:23005:6";
  assertNoMatch: "node gtfs:stop_id=ch:3001:64883";
}

/* route_id, shape_id, trip_id */
*[gtfs:route_id      ][count(split(";", tag("gtfs:route_id"))) > 1],
*[gtfs:shape_id      ][count(split(";", tag("gtfs:shape_id"))) > 1],
*[gtfs:trip_id       ][count(split(";", tag("gtfs:trip_id"))) > 1],
*[gtfs:trip_id:sample][count(split(";", tag("gtfs:trip_id:sample"))) > 1] {
  throwOther: tr("Multiple values `{0}` for `{1}=*`.", "{0.value}", "{0.key}");
  group: tr("Public Transport GTFS: value syntax");
  set MultipleID;
  assertMatch:   "relation gtfs:route_id=7-342-j1j-1;7-342-j1j-5";
  assertNoMatch: "relation gtfs:route_id=7-342-j1j-1.H";
}

/* old syntax ref:IFOPT */
*[ref:IFOPT   ][ref:IFOPT    =~ /^gen:[0-9]{4}:[1-9][0-9]{0,4}(:[0-9]{1,2}(:.+)?)?$/],
*[gtfs:stop_id][gtfs:stop_id =~ /^gen:[0-9]{4}:[1-9][0-9]{0,4}(:[0-9]{1,2}(:.+)?)?$/] {
  throwOther: tr("Value of `{0}` is in old format.", "{0.key}");
  group: tr("Public Transport GTFS: value syntax");
  assertMatch:   "node ref:IFOPT=gen:9188:5599:0:956R";
  assertNoMatch: "node ref:IFOPT=de:09162:1179:3:KIF 1";
}

/* short ref:IFOPT */
node[public_transport=stop_position][ref:IFOPT   ]!.ErrorSyntaxIFOPT[ref:IFOPT        !~ /^.+(:.+){4}$/],
node[public_transport=stop_position][gtfs:stop_id]!.ErrorSyntaxIFOPT[gtfs:stop_id     !~ /^.+(:.+){4}$/],
node[public_transport=platform     ][ref:IFOPT   ]!.ErrorSyntaxIFOPT[ref:IFOPT        !~ /^.+(:.+){4}$/],
node[public_transport=platform     ][gtfs:stop_id]!.ErrorSyntaxIFOPT[gtfs:stop_id     !~ /^.+(:.+){4}$/],
way[public_transport=platform      ][ref:IFOPT   ]!.ErrorSyntaxIFOPT[ref:IFOPT         !~ /^.+(:.+){3}$/],
way[public_transport=platform      ][gtfs:stop_id]!.ErrorSyntaxIFOPT[gtfs:stop_id      !~ /^.+(:.+){3}$/],
relation[public_transport=platform ][ref:IFOPT   ]!.ErrorSyntaxIFOPT[ref:IFOPT    !~ /^.+(:.+){3}$/],
relation[public_transport=platform ][gtfs:stop_id]!.ErrorSyntaxIFOPT[gtfs:stop_id !~ /^.+(:.+){3}$/] {
  throwWarning: tr("Incomplete `{0}` on `{1}`.", "{1.tag}", "{0.tag}");
  group: tr("Public Transport GTFS: value syntax");
  assertMatch:   "node public_transport=platform ref:IFOPT=de:09162:1179:31";
  assertNoMatch: "node public_transport=platform ref:IFOPT=de:09162:1179:53:KIF 1";
  assertNoMatch: "node public_transport=platform ref:IFOPT=DDDD:09162:1179:3:KIF 1";
  assertMatch:   "relation public_transport=platform ref:IFOPT=de:09162:1179";
  assertNoMatch: "relation public_transport=platform ref:IFOPT=de:09162:1179:30";
}
way[public_transport=platform      ][ref:IFOPT   ]!.ErrorSyntaxIFOPT[ref:IFOPT         !~ /^.+(:.+){4}$/],
way[public_transport=platform      ][gtfs:stop_id]!.ErrorSyntaxIFOPT[gtfs:stop_id      !~ /^.+(:.+){4}$/],
relation[public_transport=platform ][ref:IFOPT   ]!.ErrorSyntaxIFOPT[ref:IFOPT    !~ /^.+(:.+){4}$/],
relation[public_transport=platform ][gtfs:stop_id]!.ErrorSyntaxIFOPT[gtfs:stop_id !~ /^.+(:.+){4}$/],
relation[public_transport=stop_area][ref:IFOPT   ]!.ErrorSyntaxIFOPT[ref:IFOPT    !~ /^.+(:.+){3}$/],
relation[public_transport=stop_area][gtfs:stop_id]!.ErrorSyntaxIFOPT[gtfs:stop_id !~ /^.+(:.+){3}$/] {
  throwOther: tr("Possibly, incomplete `{0}` on `{1}`.", "{1.tag}", "{0.tag}");
  group: tr("Public Transport GTFS: value syntax");
  assertMatch:   "way public_transport=platform ref:IFOPT=de:09162:1179:21";
  assertNoMatch: "way public_transport=platform ref:IFOPT=de:09162:1179:21:0";
  assertNoMatch: "relation public_transport=platform ref:IFOPT=de:09162:1179:30:KIF 1";
  assertMatch:   "relation public_transport=stop_area ref:IFOPT=de:09162:1179";
  assertNoMatch: "relation public_transport=stop_area ref:IFOPT=de:09162:1179:30";
}

/* abbreviatons */

/* network */
*[public_transport   ][network][!network:short][network =~ /^[A-Z0-9]+$/],
relation.PtRouteMaster[network][!network:short][network =~ /^[A-Z0-9]+$/],
relation.PtRoute[network      ][!network:short][network =~ /^[A-Z0-9]+$/] {
  throwWarning: tr(" `{0}` looks like an abbreviation", "{1.tag}");
  group: tr("Public Transport GTFS: abbreviation as value");
  suggestAlternative: "`network:short=*` and a longer form for `network=*`"; 
  fixChangeKey: "network=>network:short";
  assertMatch:   "relation public_transport=stop_area network=AA0AA";
  assertNoMatch: "relation public_transport=stop_area network=AA0AA network:short=A";
  assertNoMatch: "relation public_transport=stop_area network=Anna";
}

/* operator */
*[public_transport   ][operator][!operator:short][operator =~ /^[A-Z0-9]+$/],
relation.PtRouteMaster[operator][!operator:short][operator =~ /^[A-Z0-9]+$/],
relation.PtRoute[operator      ][!operator:short][operator =~ /^[A-Z0-9]+$/] {
  throwWarning: tr(" `{0}` looks like an abbreviation", "{1.tag}");
  group: tr("Public Transport GTFS: abbreviation as value");
  suggestAlternative: "`operator:short=*` and a longer form for `operator=*`"; 
  fixChangeKey: "operator=>operator:short";
  assertMatch:   "relation public_transport=stop_area operator=AA0AA";
  assertNoMatch: "relation public_transport=stop_area operator=AA0AA operator:short=A";
  assertNoMatch: "relation public_transport=stop_area operator=Anna";
}

/* Permanet links to PTNA GTFS */
/* *[JOSM_search("ptna.openstreetmap.de/gtfs")][!JOSM_search("^.*network=([a-zA-Z]+[-]){2,}\\d{4}(-\\d\\d){2}.*$")] { */
*[url     =~ /^.*ptna\.openstreetmap\.de\/gtfs.+$/][url     !~ /^.*(network=([a-zA-Z]+[-]){2,}|release_date=)\d{4}(-\d\d){2}.*$/], 
*[website =~ /^.*ptna\.openstreetmap\.de\/gtfs.+$/][website !~ /^.*(network=([a-zA-Z]+[-]){2,}|release_date=)\d{4}(-\d\d){2}.*$/],
*[source  =~ /^.*ptna\.openstreetmap\.de\/gtfs.+$/][source  !~ /^.*(network=([a-zA-Z]+[-]){2,}|release_date=)\d{4}(-\d\d){2}.*$/] {
  throwWarning: tr("Relative GTFS url in `{0}=*`. Add `{1}` behind the feed or network value.", "{0.key}", "&release_date=*");
  assertMatch:   "node source=\"https://ptna.openstreetmap.de/gtfs/19\"";
  assertNoMatch: "node source=\"https://ptna.openstreetmap.de/gtfs/DE/trips.php?network=DE-BW-VAG-2020-07-28&route_id=10-10-I-j20-1\"";
  assertNoMatch: "node source=\"https://ptna.openstreetmap.de/gtfs/DE/trips.php?feed=DE-BW-VAG&release_date=2020-07-28&route_id=10-10-I-j20-1\"";
}

/*  -----------------------
 *  conflicting tags
 */

/* ref:IFOPT, gtfs:stop_id, local_ref */
/* *[public_transport][public_transport =~ /^(platform|stop_position)$/][!(tag("local_ref") == get(regexp_match("^(.+:){4}([A-Z]+[ ]?)?([1-9][0-9]{0,2}[A-Z]?)", tag("ref:IFOPT")), 3))][local_ref][ref:IFOPT], */
/* *[public_transport][public_transport =~ /^(platform|stop_position)$/][!(tag("local_ref") == get(regexp_match("^(.+:){4}([A-Z]+[ ]?)?([1-9][0-9]{0,2}[A-Z]?)", tag("gtfs:stop_id")), 3))][local_ref][ref:IFOPT] { */
*[local_ref][ref:IFOPT]!.ErrorSyntaxIFOPT[public_transport =~ /^(platform|stop_position)$/][!(tag("local_ref") == get(regexp_match("^(.+:){4}(.+)", tag("ref:IFOPT")), 2))],
*[local_ref][ref:IFOPT]!.ErrorSyntaxIFOPT[public_transport =~ /^(platform|stop_position)$/][!(tag("local_ref") == get(regexp_match("^(.+:){4}(.+)", tag("gtfs:stop_id")), 2))] {
  throwWarning:  tr("`{0}` conflicts with `{1}`.", "{0.tag}", "{1.tag}");
  group: tr("Public Transport GTFS: conflicting tags");
  assertMatch:   "node public_transport=platform local_ref=14 ref:IFOPT=de:08315:6504:0:1";
  assertMatch:   "node public_transport=platform local_ref=56R ref:IFOPT=gen:9188:5599:0:956R";
  assertNoMatch: "node public_transport=platform local_ref=14 ref:IFOPT=de:08315:6504:0:14";
  assertNoMatch: "node public_transport=platform local_ref=14 ref:IFOPT=de:08315:6504:14";
  assertNoMatch: "node public_transport=platform local_ref=956R ref:IFOPT=gen:9188:5599:0:956R";
  assertNoMatch: "node public_transport=platform local_ref=1 ref:IFOPT=de:09162:1179:3:KIF 1";
}

/* route_id, shape_id, trip_id */
/* FIXME: How to check proper syntax with multiple values */
*[gtfs:trip_id][gtfs:trip_id:sample][!(tag("gtfs:trip_id") == tag("gtfs:trip_id:sample"))] {
throwError: tr("`{1}` and `{0}` are not equal.", "{0.tag}", "{1.tag}");
  group: tr("Public Transport GTFS: conflicting tags");
  assertMatch:   "relation gtfs:trip_id:sample=1108.T5.11-4-I-j20-1.10.H gtfs:trip_id=1108.T2.11-4-I-j20-1.10.H";
  assertNoMatch: "relation gtfs:trip_id:sample=1108.T2.11-4-I-j20-1.10.H gtfs:trip_id=1108.T2.11-4-I-j20-1.10.H";
}
*[gtfs:route_id][gtfs:shape_id      ]!.MultipleID!.GtfsIdSyntax[!(tag("gtfs:route_id") == get(regexp_match("^(.+)\\.[0-9]+\\.[HR]$", tag("gtfs:shape_id")), 1))],
*[gtfs:route_id][gtfs:trip_id       ]!.MultipleID!.GtfsIdSyntax[!(tag("gtfs:route_id") == get(regexp_match("^[1-9][0-9]{0,3}\\.T[023A]\\.(.+)\\.[0-9]+\\.[HR]$", tag("gtfs:trip_id")), 1))],
*[gtfs:route_id][gtfs:trip_id:sample]!.MultipleID!.GtfsIdSyntax[!(tag("gtfs:route_id") == get(regexp_match("^[1-9][0-9]{0,3}\\.T[023A]\\.(.+)\\.[0-9]+\\.[HR]$", tag("gtfs:trip_id:sample")), 3))],
*[gtfs:shape_id][gtfs:trip_id       ]!.MultipleID!.GtfsIdSyntax[!(tag("gtfs:shape_id") == get(regexp_match("^[1-9][0-9]{0,3}\\.T[023A]\\.(.+)$", tag("gtfs:trip_id")), 1))],
*[gtfs:shape_id][gtfs:trip_id:sample]!.MultipleID!.GtfsIdSyntax[!(tag("gtfs:shape_id") == get(regexp_match("^[1-9][0-9]{0,3}\\.T[023A]\\.(.+)$", tag("gtfs:trip_id:sample")), 1))] {
  throwError: tr("`{1}` is not a substring of `{0}`.", "{0.tag}", "{1.tag}"); 
  group: tr("Public Transport GTFS: conflicting tags");
  assertMatch:   "relation gtfs:route_id=7-342-j1j-1 gtfs:shape_id=7-352-j1j-1.17.H";
  assertNoMatch: "relation gtfs:route_id=7-342-j1j-1 gtfs:shape_id=7-342-j1j-1.17.H";
  assertMatch:   "relation gtfs:route_id=11-4-I-j20-1 gtfs:trip_id=1108.T2.11-5-I-j20-1.10.H";
  assertNoMatch: "relation gtfs:route_id=11-4-I-j20-1 gtfs:trip_id=1108.TB2.11-5-I-j20-1.10.H";
  assertNoMatch: "relation gtfs:route_id=11-4-I-j20-1 gtfs:trip_id=1108.T2.11-4-I-j20-1.10.H";
}

/* route_id parent */
/* FIXME: How to ckeck with multiple values? */
/* FIXME: How to display the key-value of the parent? */
relation!.MultipleID[count(split(";", parent_tag("gtfs:route_id"))) == 1] {
  set NoMultiId;
}
relation[gtfs:route_id      ][!(parent_tag("gtfs:route_id") == tag("gtfs:route_id"))].NoMultiId,
relation[gtfs:shape_id      ][!gtfs:route_id][!(parent_tag("gtfs:route_id") == get(regexp_match("(.+)\\.\\d+\\.[HR]$", tag("gtfs:shape_id")), 1))].NoMultiId,
relation[gtfs:trip_id       ][!gtfs:route_id][!gtfs:shape_id][!(parent_tag("gtfs:route_id") == get(regexp_match("^\\d+\\.T[023A]\\.(.+)\\.\\d+\\.[HR]$", tag("gtfs:trip_id")), 1))].NoMultiId,
relation[gtfs:trip_id:sample][!gtfs:route_id][!gtfs:shape_id][!(parent_tag("gtfs:route_id") == get(regexp_match("^\\d+\\.T[023A]\\.(.+)\\.\\d+\\.[HR]$", tag("gtfs:trip_id:sample")), 1))].NoMultiId {
  throwWarning:  tr("`{0}` conflicts with `{1}` of the `route_master` relation.", "{0.tag}", "gtfs:route_id=*");
/*  throwWarning:  tr("`{0}` differs to `route_id={1}` of the `route_master` relation.", "{0.tag}", "{1.value}"); */
  group: tr("Public Transport GTFS: conflicting tags");
/*  assertMatch:   "relation gtfs:shape_id=10-20-I-j20-1.1.R parent_tag(\"type\")=route_master parent_tag(\"gtfs:route_id\")=10-19-I-j20-1";
  assertNoMatch: "relation gtfs:shape_id=10-19-I-j20-1.1.R parent_tag(\"type\")=route_master parent_tag(\"gtfs:route_id\")=10-19-I-j20-1"; */
}


/*  -----------------------
 *  Geometry
 */

/* stop_position */
/* FIXME: How to use assert with :class and evalexpression? */
node[public_transport=stop_position]:unconnected:in-downloaded-area,
node[public_transport=stop_position]:unconnected:new {
  throwError:  tr("`{0}` is not part of any way.", "{0.tag}");
  assertMatch:   "node public_transport=stop_position ";
  assertNoMatch: "node public_transport=stop_position :unconnected !:new !:in-downloaded-area";
  assertNoMatch: "node public_transport=platform :unconnected :new";
/*  assertMatch:   "node public_transport=stop_position :unconnected :in-downloaded-area";
 *  assertMatch:   "node public_transport=stop_position :unconnected :new";
 *  assertNoMatch: "node public_transport=stop_position :unconnected !:new !:in-downloaded-area";
 *  assertNoMatch: "node public_transport=platform :unconnected :new";
 */
}

Rules_PublicTransportGtfs.validator.mapcss, Rules_PublicTransportGtfs.zip