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.43_2025-01-03";
}

/*  -----------------------
 *
 *       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|coach|share_taxi|trolleybus|train|light_rail|subway|tram|monorail|funicular|ferry)$/ =~ /^(yes|no)$/][!aerialway] {
  set PtVehicle;
}

*[/^(bus|coach|share_taxi|trolleybus|train|light_rail|subway|tram|monorail|funicular|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].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";
}

/* platform */
/* access tags are only useful on stop_position */
area[public_transport=platform][highway].PtVehicle {
  throwWarning: tr("`Problematic access tag on {0} together with {1}. Only stop positions need it, according to the wiki.", "{0.tag}", "{1.tag}");
  group: tr("Public Transport GTFS: suspicious tag combination");
  assertMatch:   "way public_transport=platform highway=platform bus=yes";
  assertNoMatch: "way public_transport=platform bus=yes";
}
area[public_transport=platform][!highway].PtVehicle {
  throwWarning: tr("`Access tag on {0}. Only stop positions need it, according to the wiki.", "{0.tag}");
  group: tr("Public Transport GTFS: suspicious tag combination");
  assertMatch:   "way public_transport=platform bus=yes";
  assertNoMatch: "way public_transport=platform";
}

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

/* platform */
/* access tags are only useful on stop_position */
node[public_transport=platform].PtVehicle {
  throwOther: tr("`Serving vehicle type specified on {0}. Only stop positions need it, according to the wiki.", "{0.tag}");
  group: tr("Public Transport GTFS: suspicious tag combination");
  assertMatch:   "node public_transport=platform bus=yes";
  assertNoMatch: "node public_transport=platform";
}

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

/* stop_position, platform */
node[public_transport=stop_position][train=yes][railway!=stop],
node[public_transport=stop_position][train=yes][!ref],
*[public_transport=platform][railway=platform][!ref] {
  throwWarning: tr("`{0}` with `{1}` but without `{2}`.", "{0.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) */

/* 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]a?-[0-9]+(;[ ]?[0-9]{1,2}-[A-Z]?[0-9]{1,3}[A-Z]?(-[0-9A-Z])?-[js]?[1-9][0-9j]a?-[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]a?-[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]a?-[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]a?-[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]a?-[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]a?-[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]a?-[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 */
  assertNoMatch: "relation gtfs:route_id=5-20-21a-1";                   /* DE-BW-bodo */
  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 */
  assertNoMatch: "relation gtfs:shape_id=5-20-21a-1.40.H";              /* DE-BW-bodo */
  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 */
  assertNoMatch: "relation gtfs:trip_id=21.T0.5-20-21a-1.40.H";         /* DE-BW-bodo */
}
/* 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(2[0-5])-(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";
  assertNoMatch: "relation gtfs:release_date=2024-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";
}

/* release_date in url */
relation.PtRouteMaster[gtfs:release_date][url     =~ /ptna\.openstreetmap\.de\/gtfs.+network=([a-zA-Z]+[-]){2,}\d{4}(-\d\d){2}/][!(tag("gtfs:release_date") == get(regexp_match(".+network\\=.+-(\\d{4}(-\\d{2}){2}).+", tag("url")), 1))], 
relation.PtRouteMaster[gtfs:release_date][website =~ /ptna\.openstreetmap\.de\/gtfs.+network=([a-zA-Z]+[-]){2,}\d{4}(-\d\d){2}/][!(tag("gtfs:release_date") == get(regexp_match(".+network\\=.+-(\\d{4}(-\\d{2}){2}).+", tag("website")), 1))],
relation.PtRouteMaster[gtfs:release_date][source  =~ /ptna\.openstreetmap\.de\/gtfs.+network=([a-zA-Z]+[-]){2,}\d{4}(-\d\d){2}/][!(tag("gtfs:release_date") == get(regexp_match(".+network\\=.+-(\\d{4}(-\\d{2}){2}).+", tag("source")), 1))],
relation.PtRouteMaster[gtfs:release_date][url     =~ /ptna\.openstreetmap\.de\/gtfs.+release_date=\d{4}(-\d\d){2}/][!(tag("gtfs:release_date") == get(regexp_match(".+release_date\\=(\\d{4}(-\\d{2}){2}).+", tag("url")), 1))], 
relation.PtRouteMaster[gtfs:release_date][website =~ /ptna\.openstreetmap\.de\/gtfs.+release_date=\d{4}(-\d\d){2}/][!(tag("gtfs:release_date") == get(regexp_match(".+release_date\\=(\\d{4}(-\\d{2}){2}).+", tag("website")), 1))],
relation.PtRouteMaster[gtfs:release_date][source  =~ /ptna\.openstreetmap\.de\/gtfs.+release_date=\d{4}(-\d\d){2}/][!(tag("gtfs:release_date") == get(regexp_match(".+release_date\\=(\\d{4}(-\\d{2}){2}).+", tag("source")), 1))],
relation.PtRoute[gtfs:release_date][url     =~ /ptna\.openstreetmap\.de\/gtfs.+network=([a-zA-Z]+[-]){2,}\d{4}(-\d\d){2}/][!(tag("gtfs:release_date") == get(regexp_match(".+network\\=.+-(\\d{4}(-\\d{2}){2}).+", tag("url")), 1))], 
relation.PtRoute[gtfs:release_date][website =~ /ptna\.openstreetmap\.de\/gtfs.+network=([a-zA-Z]+[-]){2,}\d{4}(-\d\d){2}/][!(tag("gtfs:release_date") == get(regexp_match(".+network\\=.+-(\\d{4}(-\\d{2}){2}).+", tag("website")), 1))],
relation.PtRoute[gtfs:release_date][source  =~ /ptna\.openstreetmap\.de\/gtfs.+network=([a-zA-Z]+[-]){2,}\d{4}(-\d\d){2}/][!(tag("gtfs:release_date") == get(regexp_match(".+network\\=.+-(\\d{4}(-\\d{2}){2}).+", tag("source")), 1))],
relation.PtRoute[gtfs:release_date][url     =~ /ptna\.openstreetmap\.de\/gtfs.+release_date=\d{4}(-\d\d){2}/][!(tag("gtfs:release_date") == get(regexp_match(".+release_date\\=(\\d{4}(-\\d{2}){2}).+", tag("url")), 1))], 
relation.PtRoute[gtfs:release_date][website =~ /ptna\.openstreetmap\.de\/gtfs.+release_date=\d{4}(-\d\d){2}/][!(tag("gtfs:release_date") == get(regexp_match(".+release_date\\=(\\d{4}(-\\d{2}){2}).+", tag("website")), 1))],
relation.PtRoute[gtfs:release_date][source  =~ /ptna\.openstreetmap\.de\/gtfs.+release_date=\d{4}(-\d\d){2}/][!(tag("gtfs:release_date") == get(regexp_match(".+release_date\\=(\\d{4}(-\\d{2}){2}).+", tag("source")), 1))] {
  throwWarning: tr("{0} and {1} in {2} differ", "{1.tag}", "{1.key}", "{2.key}");
  group: tr("Public Transport GTFS: conflicting tags");
  assertMatch: "relation type=route route=train gtfs:release_date=2020-09-28 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\"";
  assertNoMatch: "relation type=route route=train gtfs:release_date=2020-07-28 source=\"https://ptna.openstreetmap.de/gtfs/DE/trips.php?network=DE-BW-VAG-2020-07-28&route_id=10-10-I-j20-1\"";
  assertNoMatch: "relation type=route route=train gtfs:release_date=2020-07-28 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\"";
}

/* 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";
 */
}
